782 lines
21 KiB
Python
782 lines
21 KiB
Python
"""
|
|
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': '''
|
|
<div class="question">{{Question}}</div>
|
|
<hr>
|
|
<div class="hint-text" id="hintText"></div>
|
|
<div class="options-container" id="options">{{OptionsHTML}}</div>
|
|
<div class="button-container">
|
|
<button id="checkBtn" onclick="checkAnswer()">Antwort prüfen ✓</button>
|
|
</div>
|
|
|
|
<script>
|
|
var correctAnswer = "{{Answer}}";
|
|
var isMultiple = "{{IsMultiple}}" === "true";
|
|
|
|
// Toggle option when clicking on the entire box
|
|
function toggleOption(event, element) {
|
|
var input = element.querySelector('input');
|
|
if (input.type === 'checkbox') {
|
|
input.checked = !input.checked;
|
|
} else {
|
|
input.checked = true;
|
|
}
|
|
}
|
|
|
|
// Set hint text based on number of answers
|
|
(function() {
|
|
var hintDiv = document.getElementById('hintText');
|
|
if (isMultiple) {
|
|
var numAnswers = correctAnswer.split(',').length;
|
|
hintDiv.innerHTML = '💡 Wähle ' + numAnswers + ' Antworten aus';
|
|
hintDiv.style.display = 'block';
|
|
}
|
|
})();
|
|
|
|
function getSelectedAnswers() {
|
|
var selected = [];
|
|
var inputs = document.querySelectorAll('input[name="answer"]:checked');
|
|
inputs.forEach(function(input) {
|
|
selected.push(input.value);
|
|
});
|
|
return selected.sort().join(", ");
|
|
}
|
|
|
|
function checkAnswer() {
|
|
var selected = getSelectedAnswers();
|
|
|
|
if (selected === "") {
|
|
alert("Bitte wähle mindestens eine Antwort aus!");
|
|
return;
|
|
}
|
|
|
|
// Store the result for the back side
|
|
var normalizedCorrect = correctAnswer.replace(/\\s/g, "").toUpperCase().split(",").sort().join(",");
|
|
var normalizedSelected = selected.replace(/\\s/g, "").toUpperCase().split(",").sort().join(",");
|
|
var isCorrect = normalizedSelected === normalizedCorrect;
|
|
|
|
// Store result in sessionStorage for back side
|
|
sessionStorage.setItem('lastAnswer', selected);
|
|
sessionStorage.setItem('wasCorrect', isCorrect ? 'true' : 'false');
|
|
|
|
// Trigger Anki to show the answer
|
|
if (typeof pycmd !== 'undefined') {
|
|
pycmd('ans');
|
|
} else if (typeof AnkiDroidJS !== 'undefined') {
|
|
showAnswer();
|
|
}
|
|
}
|
|
</script>
|
|
''',
|
|
'afmt': '''
|
|
<div class="question">{{Question}}</div>
|
|
<hr>
|
|
<div class="options-container answered" id="options">{{OptionsHTML}}</div>
|
|
|
|
<div id="result" class="result"></div>
|
|
<div class="correct-answer">
|
|
<strong>Richtige Antwort:</strong> {{Answer}}
|
|
</div>
|
|
|
|
<hr>
|
|
<div class="source">
|
|
<em>Quelle: {{Source}}</em>
|
|
</div>
|
|
|
|
<div class="rating-container">
|
|
<p class="rating-hint" id="ratingHint">Bewerte deine Antwort:</p>
|
|
<div class="rating-buttons">
|
|
<button class="rating-btn again" onclick="answerCard(1)">Nochmal<br><span class="rating-sub"><1min</span></button>
|
|
<button class="rating-btn hard" onclick="answerCard(2)">Schwer<br><span class="rating-sub"><6min</span></button>
|
|
<button class="rating-btn good" onclick="answerCard(3)">Gut<br><span class="rating-sub"><10min</span></button>
|
|
<button class="rating-btn easy" onclick="answerCard(4)">Einfach<br><span class="rating-sub">4 Tage</span></button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var correctAnswer = "{{Answer}}";
|
|
|
|
// Cross-platform answer function for desktop Anki and AnkiDroid
|
|
function answerCard(ease) {
|
|
// AnkiDroid uses direct functions
|
|
if (typeof buttonAnswerEase1 !== 'undefined') {
|
|
switch(ease) {
|
|
case 1: buttonAnswerEase1(); break;
|
|
case 2: buttonAnswerEase2(); break;
|
|
case 3: buttonAnswerEase3(); break;
|
|
case 4: buttonAnswerEase4(); break;
|
|
}
|
|
} else if (typeof AnkiDroidJS !== 'undefined') {
|
|
// Fallback for older AnkiDroid versions
|
|
AnkiDroidJS.ankiAnswerCard(ease);
|
|
} else if (typeof pycmd !== 'undefined') {
|
|
// Desktop Anki
|
|
pycmd('ease' + ease);
|
|
}
|
|
}
|
|
|
|
(function() {
|
|
var selected = sessionStorage.getItem('lastAnswer') || '';
|
|
var wasCorrect = sessionStorage.getItem('wasCorrect') === 'true';
|
|
var resultDiv = document.getElementById('result');
|
|
var ratingHint = document.getElementById('ratingHint');
|
|
|
|
// Highlight options
|
|
var options = document.querySelectorAll('.option-item');
|
|
var correctLetters = correctAnswer.replace(/\\s/g, "").toUpperCase().split(",");
|
|
var selectedLetters = selected.replace(/\\s/g, "").toUpperCase().split(",");
|
|
|
|
options.forEach(function(opt) {
|
|
var input = opt.querySelector('input');
|
|
if (input) {
|
|
var letter = input.value.toUpperCase();
|
|
input.disabled = true;
|
|
|
|
if (correctLetters.includes(letter)) {
|
|
opt.classList.add('correct-option');
|
|
input.checked = true;
|
|
}
|
|
if (selectedLetters.includes(letter) && !correctLetters.includes(letter)) {
|
|
opt.classList.add('incorrect-option');
|
|
input.checked = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Show result
|
|
if (selected) {
|
|
if (wasCorrect) {
|
|
resultDiv.innerHTML = "✅ Richtig!";
|
|
resultDiv.className = "result correct";
|
|
ratingHint.innerHTML = "🎉 Super! Wie gut hast du es gewusst?";
|
|
} else {
|
|
resultDiv.innerHTML = "❌ Falsch! Deine Antwort: " + selected;
|
|
resultDiv.className = "result incorrect";
|
|
ratingHint.innerHTML = "📚 Für nächstes Mal merken:";
|
|
}
|
|
}
|
|
|
|
// Clear storage
|
|
sessionStorage.removeItem('lastAnswer');
|
|
sessionStorage.removeItem('wasCorrect');
|
|
})();
|
|
</script>
|
|
''',
|
|
},
|
|
],
|
|
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*<details|\Z)'
|
|
option_matches = re.findall(option_pattern, block, re.DOTALL)
|
|
|
|
for letter, text in option_matches:
|
|
options.append(f"{letter}. {text.strip()}")
|
|
|
|
# Extract answer from <details> 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'''<div class="option-item" onclick="toggleOption(event, this)">
|
|
<input type="{input_type}" name="answer" value="{letter}" id="opt_{letter}" onclick="event.stopPropagation()">
|
|
<label for="opt_{letter}"><strong>{letter}.</strong> {text}</label>
|
|
</div>'''
|
|
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'''<div class="option-item" onclick="toggleOption(event, this)">
|
|
<input type="{input_type}" name="answer" value="{letter}" id="opt_{letter}" onclick="event.stopPropagation()">
|
|
<label for="opt_{letter}"><strong>{letter}.</strong> {text}</label>
|
|
</div>'''
|
|
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()
|