""" 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()