Add persian-tutor: Gradio-based GCSE Persian language learning app

Vocabulary study with FSRS spaced repetition, AI tutoring (Ollama/Claude),
essay marking, idioms browser, Anki export, and dashboard. 918 vocabulary
entries across 39 categories. 41 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
local
2026-02-08 01:57:44 +00:00
parent 104da381fb
commit 2e8c2c11d0
22 changed files with 10664 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
"""Tests for anki_export.py — Anki .apkg generation."""
import os
import sys
import tempfile
import zipfile
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from anki_export import export_deck
SAMPLE_VOCAB = [
{
"id": "verb_go",
"section": "High-frequency language",
"category": "Common verbs",
"english": "to go",
"persian": "رفتن",
"finglish": "raftan",
},
{
"id": "verb_eat",
"section": "High-frequency language",
"category": "Common verbs",
"english": "to eat",
"persian": "خوردن",
"finglish": "khordan",
},
{
"id": "colour_red",
"section": "High-frequency language",
"category": "Colours",
"english": "red",
"persian": "قرمز",
"finglish": "ghermez",
},
]
def test_export_deck_creates_file(tmp_path):
"""export_deck should create a valid .apkg file."""
output = str(tmp_path / "test.apkg")
result = export_deck(SAMPLE_VOCAB, output_path=output)
assert result == output
assert os.path.exists(output)
assert os.path.getsize(output) > 0
def test_export_deck_is_valid_zip(tmp_path):
"""An .apkg file is a zip archive containing an Anki SQLite database."""
output = str(tmp_path / "test.apkg")
export_deck(SAMPLE_VOCAB, output_path=output)
assert zipfile.is_zipfile(output)
def test_export_deck_with_category_filter(tmp_path):
"""export_deck with category filter should only include matching entries."""
output = str(tmp_path / "test.apkg")
export_deck(SAMPLE_VOCAB, categories=["Colours"], output_path=output)
# File should exist and be smaller than unfiltered
assert os.path.exists(output)
size_filtered = os.path.getsize(output)
output2 = str(tmp_path / "test_all.apkg")
export_deck(SAMPLE_VOCAB, output_path=output2)
size_all = os.path.getsize(output2)
# Filtered deck should be smaller (fewer cards)
assert size_filtered <= size_all
def test_export_deck_empty_vocab(tmp_path):
"""export_deck with empty vocabulary should still create a valid file."""
output = str(tmp_path / "test.apkg")
export_deck([], output_path=output)
assert os.path.exists(output)
def test_export_deck_no_category_match(tmp_path):
"""export_deck with non-matching category filter should create empty deck."""
output = str(tmp_path / "test.apkg")
export_deck(SAMPLE_VOCAB, categories=["Nonexistent"], output_path=output)
assert os.path.exists(output)