diff --git a/practice-exam/aws-ccp-all-practice-exams.apkg b/practice-exam/aws-ccp-all-practice-exams.apkg new file mode 100644 index 0000000..ffcdca8 Binary files /dev/null and b/practice-exam/aws-ccp-all-practice-exams.apkg differ diff --git a/practice-exam/generate_anki_deck.py b/practice-exam/generate_anki_deck.py new file mode 100644 index 0000000..54a8c03 --- /dev/null +++ b/practice-exam/generate_anki_deck.py @@ -0,0 +1,781 @@ +""" +AWS Certified Cloud Practitioner Practice Exam to Anki Deck Converter + +This script parses practice exam markdown files and generates Anki decks. +""" + +import re +import os +import glob +import random +import genanki + + +def generate_unique_id(): + """Generate a unique ID for Anki models and decks.""" + return random.randrange(1 << 30, 1 << 31) + + +# Define the Anki card model with interactive quiz functionality +AWS_MODEL = genanki.Model( + generate_unique_id(), + 'AWS Practice Exam Interactive Model', + fields=[ + {'name': 'Question'}, + {'name': 'OptionsHTML'}, + {'name': 'Answer'}, + {'name': 'Source'}, + {'name': 'IsMultiple'}, # "true" or "false" - whether multiple answers are expected + ], + templates=[ + { + 'name': 'Interactive Quiz Card', + 'qfmt': ''' +
{{Question}}
+
+
+
{{OptionsHTML}}
+
+ +
+ + + ''', + 'afmt': ''' +
{{Question}}
+
+
{{OptionsHTML}}
+ +
+
+ Richtige Antwort: {{Answer}} +
+ +
+
+ Quelle: {{Source}} +
+ +
+

Bewerte deine Antwort:

+
+ + + + +
+
+ + + ''', + }, + ], + css=''' +.card { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; + font-size: 16px; + text-align: left; + color: #333; + background-color: #f8f9fa; + padding: 20px; + max-width: 800px; + margin: 0 auto; +} + +.question { + font-weight: 600; + margin-bottom: 20px; + font-size: 18px; + color: #232f3e; + line-height: 1.5; +} + +.options-container { + margin: 15px 0; +} + +.option-item { + display: flex; + align-items: flex-start; + margin: 10px 0; + padding: 12px 15px; + background-color: #fff; + border-radius: 8px; + border: 2px solid #e0e0e0; + cursor: pointer; + transition: all 0.2s ease; +} + +.option-item:hover { + border-color: #ff9900; + background-color: #fff8f0; +} + +.options-container.answered .option-item:hover { + border-color: inherit; + background-color: inherit; +} + +.option-item input { + margin-right: 12px; + margin-top: 3px; + width: 18px; + height: 18px; + cursor: pointer; + accent-color: #ff9900; +} + +.option-item label { + cursor: pointer; + flex: 1; + line-height: 1.4; +} + +.option-item.correct-option { + border-color: #28a745; + background-color: #d4edda; +} + +.option-item.incorrect-option { + border-color: #dc3545; + background-color: #f8d7da; +} + +.hint-text { + display: none; + background-color: #e3f2fd; + color: #1565c0; + padding: 10px 15px; + border-radius: 8px; + margin-bottom: 15px; + font-weight: 500; +} + +.button-container { + margin: 20px 0; + text-align: center; +} + +button { + padding: 12px 30px; + font-size: 16px; + font-weight: 600; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + margin: 5px; +} + +#checkBtn { + background: linear-gradient(135deg, #ff9900, #ffad33); + color: #232f3e; +} + +#checkBtn:hover { + background: linear-gradient(135deg, #ec8b00, #ff9900); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(255, 153, 0, 0.4); +} + +.result { + padding: 15px 20px; + border-radius: 8px; + margin: 15px 0; + font-size: 18px; + font-weight: 600; + text-align: center; +} + +.result.correct { + background-color: #d4edda; + color: #155724; + border: 2px solid #28a745; +} + +.result.incorrect { + background-color: #f8d7da; + color: #721c24; + border: 2px solid #dc3545; +} + +.correct-answer { + padding: 15px 20px; + background-color: #e8f5e9; + border-radius: 8px; + border-left: 4px solid #28a745; + margin: 15px 0; + font-size: 16px; +} + +.source { + color: #666; + font-size: 12px; + margin-top: 10px; + text-align: right; +} + +.rating-container { + margin-top: 20px; + padding: 20px; + background-color: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.rating-hint { + text-align: center; + font-size: 16px; + margin-bottom: 15px; + color: #333; +} + +.rating-buttons { + display: flex; + justify-content: center; + gap: 10px; + flex-wrap: wrap; +} + +.rating-btn { + padding: 15px 20px; + min-width: 80px; + font-size: 14px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + line-height: 1.3; +} + +.rating-btn .rating-sub { + font-size: 11px; + opacity: 0.8; +} + +.rating-btn.again { + background: #dc3545; + color: white; +} + +.rating-btn.again:hover { + background: #c82333; + transform: translateY(-2px); +} + +.rating-btn.hard { + background: #fd7e14; + color: white; +} + +.rating-btn.hard:hover { + background: #e96b02; + transform: translateY(-2px); +} + +.rating-btn.good { + background: #28a745; + color: white; +} + +.rating-btn.good:hover { + background: #218838; + transform: translateY(-2px); +} + +.rating-btn.easy { + background: #17a2b8; + color: white; +} + +.rating-btn.easy:hover { + background: #138496; + transform: translateY(-2px); +} + +hr { + border: none; + border-top: 1px solid #dee2e6; + margin: 20px 0; +} + +/* Hide custom rating buttons on AnkiDroid/mobile (uses built-in buttons) */ +.mobile .rating-container, +.android .rating-container { + display: none !important; +} + +/* Dark Mode Styles */ +.night_mode .card { + background-color: #1e1e2e; + color: #cdd6f4; +} + +.night_mode .question { + color: #cdd6f4; +} + +.night_mode .option-item { + background-color: #313244; + border-color: #45475a; + color: #cdd6f4; +} + +.night_mode .option-item:hover { + border-color: #fab387; + background-color: #45475a; +} + +.night_mode .option-item label { + color: #cdd6f4; +} + +.night_mode .option-item.correct-option { + border-color: #a6e3a1; + background-color: rgba(166, 227, 161, 0.2); +} + +.night_mode .option-item.incorrect-option { + border-color: #f38ba8; + background-color: rgba(243, 139, 168, 0.2); +} + +.night_mode .hint-text { + background-color: rgba(137, 180, 250, 0.2); + color: #89b4fa; +} + +.night_mode #checkBtn { + background: linear-gradient(135deg, #fab387, #f9e2af); + color: #1e1e2e; +} + +.night_mode #checkBtn:hover { + background: linear-gradient(135deg, #f9e2af, #fab387); + box-shadow: 0 4px 12px rgba(250, 179, 135, 0.4); +} + +.night_mode .result.correct { + background-color: rgba(166, 227, 161, 0.2); + color: #a6e3a1; + border-color: #a6e3a1; +} + +.night_mode .result.incorrect { + background-color: rgba(243, 139, 168, 0.2); + color: #f38ba8; + border-color: #f38ba8; +} + +.night_mode .correct-answer { + background-color: rgba(166, 227, 161, 0.15); + border-left-color: #a6e3a1; + color: #cdd6f4; +} + +.night_mode .source { + color: #a6adc8; +} + +.night_mode .rating-container { + background-color: #313244; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.night_mode .rating-hint { + color: #cdd6f4; +} + +.night_mode hr { + border-top-color: #45475a; +} + ''' +) + + +def parse_markdown_file(filepath: str) -> list[dict]: + """ + Parse a practice exam markdown file and extract questions. + + Args: + filepath: Path to the markdown file + + Returns: + List of dictionaries containing question data + """ + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + questions = [] + + # Pattern to match each question block + # Questions start with a number followed by a period + question_pattern = r'(\d+)\.\s+(.+?)(?=\n\s*-\s+A\.)' + + # Split content by question numbers at the start of lines + question_blocks = re.split(r'\n(?=\d+\.\s)', content) + + for block in question_blocks: + if not block.strip(): + continue + + # Match question number and text + q_match = re.match(r'(\d+)\.\s+(.+?)(?=\n\s*-\s+A\.)', block, re.DOTALL) + if not q_match: + continue + + question_num = q_match.group(1) + question_text = q_match.group(2).strip() + + # Extract options (A, B, C, D, E) + options = [] + option_pattern = r'-\s+([A-E])\.(.+?)(?=(?:\n\s*-\s+[A-E]\.)|\n\s* block + answer_pattern = r'Correct answer:\s*([A-E](?:,\s*[A-E])*)' + answer_match = re.search(answer_pattern, block) + + if answer_match and options: + answer = answer_match.group(1).strip() + + # Check if multiple answers are expected + is_multiple = ',' in answer + + questions.append({ + 'number': question_num, + 'question': question_text, + 'options': options, + 'answer': answer, + 'is_multiple': is_multiple + }) + + return questions + + +def create_anki_deck(questions: list[dict], deck_name: str, source_file: str) -> genanki.Deck: + """ + Create an Anki deck from parsed questions. + + Args: + questions: List of question dictionaries + deck_name: Name for the Anki deck + source_file: Source file name for reference + + Returns: + genanki.Deck object + """ + deck = genanki.Deck(generate_unique_id(), deck_name) + + for q in questions: + # Format options as interactive HTML with checkboxes or radio buttons + is_multiple = q.get('is_multiple', False) + input_type = 'checkbox' if is_multiple else 'radio' + + options_html_parts = [] + for opt in q['options']: + letter = opt[0] # First character is the option letter + text = opt[3:] # Skip "X. " prefix + option_html = f'''
+ + +
''' + options_html_parts.append(option_html) + + options_html = '\n'.join(options_html_parts) + + note = genanki.Note( + model=AWS_MODEL, + fields=[ + q['question'], + options_html, + q['answer'], + f"{source_file} - Q{q['number']}", + "true" if is_multiple else "false" + ] + ) + deck.add_note(note) + + return deck + + +def process_single_file(filepath: str, output_dir: str = None): + """ + Process a single practice exam file and create an Anki deck. + + Args: + filepath: Path to the markdown file + output_dir: Output directory for the .apkg file (defaults to same as input) + """ + if output_dir is None: + output_dir = os.path.dirname(filepath) + + filename = os.path.basename(filepath) + deck_name = f"AWS CCP - {filename.replace('.md', '').replace('-', ' ').title()}" + + print(f"Processing: {filename}") + questions = parse_markdown_file(filepath) + print(f" Found {len(questions)} questions") + + if questions: + deck = create_anki_deck(questions, deck_name, filename) + output_path = os.path.join(output_dir, filename.replace('.md', '.apkg')) + genanki.Package(deck).write_to_file(output_path) + print(f" Created: {output_path}") + return deck + else: + print(f" No questions found in {filename}") + return None + + +def process_all_files(directory: str, output_dir: str = None, combined: bool = True): + """ + Process all practice exam files in a directory. + + Args: + directory: Directory containing practice exam markdown files + output_dir: Output directory for .apkg files + combined: If True, create a single combined deck; if False, create separate decks + """ + if output_dir is None: + output_dir = directory + + pattern = os.path.join(directory, 'practice-exam-*.md') + files = sorted(glob.glob(pattern)) + + if not files: + print(f"No practice exam files found matching: {pattern}") + return + + print(f"Found {len(files)} practice exam files") + + if combined: + # Create a combined deck + all_questions = [] + for filepath in files: + filename = os.path.basename(filepath) + print(f"Processing: {filename}") + questions = parse_markdown_file(filepath) + + # Add source file info to each question + for q in questions: + q['source_file'] = filename + + all_questions.extend(questions) + print(f" Found {len(questions)} questions") + + print(f"\nTotal questions: {len(all_questions)}") + + if all_questions: + deck = genanki.Deck( + generate_unique_id(), + 'AWS Certified Cloud Practitioner - All Practice Exams' + ) + + for q in all_questions: + # Format options as interactive HTML with checkboxes or radio buttons + is_multiple = q.get('is_multiple', False) + input_type = 'checkbox' if is_multiple else 'radio' + + options_html_parts = [] + for opt in q['options']: + letter = opt[0] # First character is the option letter + text = opt[3:] # Skip "X. " prefix + option_html = f'''
+ + +
''' + options_html_parts.append(option_html) + + options_html = '\n'.join(options_html_parts) + + note = genanki.Note( + model=AWS_MODEL, + fields=[ + q['question'], + options_html, + q['answer'], + f"{q['source_file']} - Q{q['number']}", + "true" if is_multiple else "false" + ] + ) + deck.add_note(note) + + output_path = os.path.join(output_dir, 'aws-ccp-all-practice-exams.apkg') + genanki.Package(deck).write_to_file(output_path) + print(f"\nCreated combined deck: {output_path}") + else: + # Create separate decks for each file + for filepath in files: + process_single_file(filepath, output_dir) + + +def main(): + """Main entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description='Convert AWS Practice Exam markdown files to Anki decks' + ) + parser.add_argument( + 'input', + nargs='?', + default='.', + help='Input file or directory (default: current directory)' + ) + parser.add_argument( + '-o', '--output', + help='Output directory for .apkg files (default: same as input)' + ) + parser.add_argument( + '-s', '--separate', + action='store_true', + help='Create separate decks for each file instead of a combined deck' + ) + parser.add_argument( + '-f', '--file', + help='Process a single specific file' + ) + + args = parser.parse_args() + + if args.file: + # Process single file + process_single_file(args.file, args.output) + elif os.path.isfile(args.input): + # Input is a file + process_single_file(args.input, args.output) + else: + # Input is a directory + process_all_files(args.input, args.output, combined=not args.separate) + + +if __name__ == '__main__': + main()