Add Polish license plate Anki generator with bidirectional cards

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-18 12:03:31 +00:00
parent 99d967cc56
commit 63774f74d3
6 changed files with 963 additions and 1 deletions

View File

@ -162,7 +162,7 @@ repos:
- id: codespell
args:
- --skip=*.json,*.lock,*.min.js,*.min.css,.git,__pycache__,.venv,*.txt
- --ignore-words-list=ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive
- --ignore-words-list=ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe
exclude: ^(Bash/ffmpeg-build/|LaTeX/|CPP/)
# ===========================================================================

View File

@ -0,0 +1,7 @@
"""Polish license plate Anki flashcard generator."""
from __future__ import annotations
__all__ = ["LICENSE_PLATE_CODES"]
from python_pkg.polish_license_plates.license_plate_data import LICENSE_PLATE_CODES

View File

@ -0,0 +1,481 @@
"""Database of Polish car license plate registration codes.
This module contains a comprehensive mapping of Polish vehicle registration
plate codes to their corresponding locations (cities, powiats, voivodeships).
Polish license plates use a system where:
- First letter indicates the voivodeship (province)
- Following 1-2 letters indicate the specific city or powiat (county)
The database is organized by voivodeships in alphabetical order:
- B: Podlaskie
- C: Kujawsko-Pomorskie
- D: Dolnośląskie
- E: Łódzkie
- F: Lubuskie
- G: Pomorskie
- K: Małopolskie
- L: Lubelskie
- N: Warmińsko-Mazurskie
- O: Opolskie
- P: Wielkopolskie
- R: Podkarpackie
- S: Śląskie
- T: Świętokrzyskie
- W: Mazowieckie
- Z: Zachodniopomorskie
Examples:
WA = Warszawa (Warsaw)
KR = Kraków
GD = Gdańsk
"""
from __future__ import annotations
LICENSE_PLATE_CODES: dict[str, str] = {
"DA": "Wrocław Fabryczna",
"DB": "Wałbrzych",
"DC": "Wrocław Śródmieście",
"DD": "Dzierżoniów",
"DE": "Wrocław Psie Pole",
"DF": "Wrocław Krzyki",
"DG": "Głogów",
"DH": "Wrocław Stare Miasto",
"DJ": "Jelenia Góra",
"DK": "Kłodzko",
"DL": "Legnica",
"DLB": "Lubań",
"DLE": "Legnica powiat",
"DMI": "Milicz",
"DN": "Wrocław Nowy Dwór",
"DO": "Oława",
"DP": "Polkowice",
"DR": "Wrocław Krzyki",
"DS": "Świdnica",
"DSR": "Środa Śląska",
"DSW": "Świebodzice",
"DT": "Twardogóra",
"DTR": "Trzebnica",
"DW": "Wałbrzych powiat",
"DWL": "Wołów",
"DWR": "Wrocław",
"DZ": "Zgorzelec",
"DZA": "Ząbkowice Śląskie",
"DZG": "Zgorzelec powiat",
"CB": "Bydgoszcz",
"CBR": "Brodnica",
"CC": "Chełmno",
"CD": "Świecie",
"CE": "Inowrocław",
"CG": "Grudziądz",
"CH": "Chojnice",
"CI": "Inowrocław powiat",
"CL": "Lipno",
"CMG": "Mogilno",
"CN": "Nakło nad Notecią",
"CR": "Radziejów",
"CT": "Toruń",
"CTR": "Toruń powiat",
"CTU": "Tuchola",
"CW": "Włocławek",
"CWA": "Wąbrzeźno",
"CWL": "Włocławek powiat",
"CZ": "Żnin",
"LB": "Biała Podlaska",
"LBI": "Biłgoraj",
"LC": "Chełm",
"LCH": "Chełm powiat",
"LHR": "Hrubieszów",
"LI": "Janów Lubelski",
"LKR": "Kraśnik",
"LKS": "Krasnystaw",
"LL": "Lublin",
"LLE": "Łęczna",
"LLU": "Łuków",
"LM": "Biała Podlaska powiat",
"LOP": "Opole Lubelskie",
"LPA": "Parczew",
"LPU": "Puławy",
"LRA": "Radzyń Podlaski",
"LRY": "Ryki",
"LSI": "Świdnik",
"LT": "Tomaszów Lubelski",
"LU": "Lublin powiat",
"LWL": "Włodawa",
"LZ": "Zamość",
"LZA": "Zamość powiat",
"FG": "Gorzów Wielkopolski",
"FKR": "Krosno Odrzańskie",
"FMI": "Międzyrzecz",
"FNW": "Nowa Sól",
"FSD": "Strzelce-Drezdenko",
"FSL": "Słubice",
"FSU": "Sulęcin",
"FSW": "Świebodzin",
"FWS": "Wschowa",
"FZ": "Zielona Góra",
"FZG": "Zielona Góra powiat",
"FZI": "Żagań",
"FZY": "Żary",
"EA": "Bełchatów",
"EB": "Łódź Bałuty",
"EBE": "Bełchatów powiat",
"EBR": "Brzeziny",
"EC": "Łęczyca",
"ED": "Łódź Śródmieście",
"EE": "Łódź Górna",
"EG": "Głowno",
"EK": "Kutno",
"EKU": "Kutno powiat",
"EL": "Łask",
"ELA": "Łowicz",
"ELE": "Łęczyca powiat",
"ELW": "Łowicz powiat",
"EM": "Opoczno",
"EO": "Opoczno powiat",
"EP": "Piotrków Trybunalski",
"EPA": "Pajęczno",
"EPD": "Poddębice",
"EPI": "Piotrków Trybunalski powiat",
"ER": "Rawa Mazowiecka",
"ERA": "Radomsko",
"ERW": "Rawa Mazowiecka powiat",
"ES": "Sieradz",
"ESI": "Sieradz powiat",
"ESK": "Skierniewice",
"ESR": "Skierniewice powiat",
"ET": "Tomaszów Mazowiecki",
"EW": "Wieluń",
"EWI": "Wieluń powiat",
"EZ": "Zduńska Wola",
"EZD": "Zgierz",
"KA": "Kraków Krowodrza",
"KB": "Bochnia",
"KBC": "Brzesko",
"KC": "Chrzanów",
"KCH": "Chrzanów powiat",
"KD": "Kraków Nowa Huta",
"KDA": "Dąbrowa Tarnowska",
"KE": "Kraków Śródmieście",
"KG": "Gorlice",
"KH": "Kraków Podgórze",
"KI": "Miechów",
"KK": "Kraków Śródmieście",
"KL": "Limanowa",
"KLI": "Limanowa powiat",
"KM": "Myślenice",
"KN": "Nowy Sącz",
"KNS": "Nowy Sącz powiat",
"KNT": "Nowy Targ",
"KO": "Olkusz",
"KOL": "Olkusz powiat",
"KOS": "Oświęcim",
"KP": "Proszowice",
"KR": "Kraków",
"KRA": "Kraków powiat",
"KS": "Sucha Beskidzka",
"KT": "Tarnów",
"KTA": "Tarnów powiat",
"KTT": "Tatry",
"KW": "Wadowice",
"KWA": "Wadowice powiat",
"WA": "Warszawa",
"WB": "Warszawa Bemowo",
"WBR": "Białobrzegi",
"WC": "Ciechanów",
"WCI": "Ciechanów powiat",
"WD": "Warszawa Praga Południe",
"WE": "Warszawa Praga Północ",
"WF": "Garwolin",
"WG": "Grodzisk Mazowiecki",
"WGM": "Grójec",
"WGO": "Gostynin",
"WGR": "Garwolin powiat",
"WH": "Warszawa Mokotów",
"WI": "Pruszków",
"WJ": "Józefów",
"WK": "Kozienice",
"WL": "Legionowo",
"WLI": "Lipsko",
"WLS": "Łosice",
"WM": "Mińsk Mazowiecki",
"WMA": "Maków Mazowiecki",
"WML": "Mława",
"WN": "Warszawa Białołęka",
"WND": "Nowy Dwór Mazowiecki",
"WO": "Otwock",
"WOR": "Ostrołęka",
"WOS": "Ostrów Mazowiecka",
"WOT": "Otwock powiat",
"WP": "Piaseczno",
"WPI": "Płońsk",
"WPL": "Płock",
"WPN": "Przasnysz",
"WPR": "Przysucha",
"WPU": "Pułtusk",
"WPY": "Płońsk powiat",
"WPZ": "Przasnysz powiat",
"WR": "Radom",
"WRA": "Radom powiat",
"WS": "Siedlce",
"WSC": "Sokołów Podlaski",
"WSE": "Siedlce powiat",
"WSI": "Sierpc",
"WSK": "Sochaczew",
"WSZ": "Szydłowiec",
"WT": "Warszawa Wola",
"WU": "Warszawa Ursus",
"WV": "Ostrołęka powiat",
"WW": "Warszawa Ochota",
"WWL": "Wołomin",
"WWY": "Wyszkó",
"WX": "Warszawa Ursynów",
"WY": "Warszawa Wola",
"WZ": "Żyrardów",
"WZW": "Zwoleń",
"OA": "Brzeg",
"OB": "Namysłów",
"OGL": "Głubczyce",
"OK": "Kędzierzyn-Koźle",
"OKL": "Kluczbork",
"OKR": "Krapkowice",
"OL": "Nysa",
"ONA": "Namysłów powiat",
"ONY": "Nysa powiat",
"OP": "Opole",
"OO": "Opole powiat",
"OOL": "Olesno",
"OPO": "Prudnik",
"OST": "Strzelce Opolskie",
"RB": "Brzozów",
"RBI": "Biłgoraj",
"RC": "Rzeszów Centrum",
"RD": "Dębica",
"RDE": "Dębica powiat",
"RJ": "Jarosław",
"RJA": "Jarosław powiat",
"RJS": "Jasło",
"RK": "Krosno",
"RKL": "Kolbuszowa",
"RKR": "Krosno powiat",
"RL": "Leżajsk",
"RLE": "Lesko",
"RLS": "Lubaczów",
"RLU": "Łańcut",
"RM": "Mielec",
"RMI": "Mielec powiat",
"RN": "Nisko",
"RP": "Przemyśl",
"RPR": "Przemyśl powiat",
"RPZ": "Przeworsk",
"RR": "Rzeszów",
"RRS": "Ropczyce-Sędziszów",
"RRZ": "Rzeszów powiat",
"RSA": "Sanok",
"RSN": "Sanok powiat",
"RSR": "Stalowa Wola",
"RST": "Strzyżów",
"RTA": "Tarnobrzeg",
"RZ": "Rzeszów",
"BA": "Augustów",
"BBI": "Białystok",
"BC": "Hajnówka",
"BD": "Bielsk Podlaski",
"BE": "Wysokie Mazowieckie",
"BG": "Grajewo",
"BGR": "Grajewo powiat",
"BH": "Hajnówka powiat",
"BHA": "Hajnówka",
"BI": "Białystok",
"BIA": "Białystok powiat",
"BJ": "Kolno",
"BK": "Kolno powiat",
"BKL": "Kolno",
"BL": "Łomża",
"BLM": "Łomża powiat",
"BLS": "Łomża",
"BM": "Mońki",
"BMN": "Mońki powiat",
"BO": "Sokółka",
"BP": "Zambrów",
"BPI": "Piątnica",
"BR": "Siemiatycze",
"BS": "Sokółka powiat",
"BSE": "Sejny",
"BSI": "Siemiatycze powiat",
"BSK": "Sokółka",
"BSU": "Suwałki",
"BT": "Suwałki powiat",
"BWM": "Wysokie Mazowieckie powiat",
"BZA": "Zambrów powiat",
"GA": "Gdańsk",
"GB": "Bytów",
"GBY": "Bytów powiat",
"GC": "Chojnice",
"GCH": "Chojnice powiat",
"GCZ": "Człuchów",
"GD": "Gdańsk",
"GDA": "Gdańsk powiat",
"GDY": "Gdynia",
"GI": "Kościerzyna",
"GKA": "Kartuzy",
"GKS": "Kościerzyna powiat",
"GKW": "Kwidzyn",
"GL": "Lębork",
"GLE": "Lębork powiat",
"GMB": "Malbork",
"GND": "Nowy Dwór Gdański",
"GP": "Puck",
"GPU": "Puck powiat",
"GS": "Słupsk",
"GSL": "Słupsk powiat",
"GSP": "Starogard Gdański",
"GST": "Sztum",
"GT": "Tczew",
"GTB": "Tczew powiat",
"GW": "Wejherowo",
"GWE": "Wejherowo powiat",
"SA": "Sosnowiec",
"SB": "Bielsko-Biała",
"SBB": "Bielsko-Biała powiat",
"SBE": "Będzin",
"SBI": "Bieruń-Lędziny",
"SC": "Chorzów",
"SCH": "Cieszyn",
"SCI": "Cieszyn powiat",
"SD": "Dąbrowa Górnicza",
"SF": "Racibórz",
"SG": "Gliwice",
"SGI": "Gliwice powiat",
"SH": "Chorzów",
"SI": "Siemianowice Śląskie",
"SJ": "Jastrzębie-Zdrój",
"SJZ": "Jastrzębie-Zdrój",
"SK": "Katowice",
"SKA": "Katowice powiat",
"SKL": "Kłobuck",
"SKT": "Lubliniec",
"SL": "Rybnik",
"SLU": "Lubliniec powiat",
"SM": "Mysłowice",
"SMI": "Mikołów",
"SML": "Myszków",
"SN": "Nowy Targ",
"SO": "Sosnowiec powiat",
"SP": "Piekary Śląskie",
"SPI": "Pszczyna",
"SPS": "Pszczyna powiat",
"SR": "Rybnik powiat",
"SRC": "Racibórz powiat",
"SRY": "Rybnik",
"SS": "Świętochłowice",
"ST": "Tychy",
"STA": "Tarnowskie Góry",
"STG": "Tarnowskie Góry powiat",
"SW": "Wodzisław Śląski",
"SWD": "Wodzisław Śląski powiat",
"SY": "Ruda Śląska",
"SZ": "Zabrze",
"SZA": "Zawiercie",
"SZO": "Żory",
"SZY": "Żywiec",
"TB": "Busko-Zdrój",
"TBU": "Busko-Zdrój powiat",
"TJE": "Jędrzejów",
"TK": "Kielce",
"TKA": "Kazimierza Wielka",
"TKI": "Kielce powiat",
"TKN": "Końskie",
"TKO": "Końskie powiat",
"TOS": "Ostrołęka",
"TPI": "Pińczów",
"TSA": "Sandomierz",
"TSK": "Skarżysko-Kamienna",
"TST": "Starachowice",
"TWL": "Włoszczowa",
"NBA": "Bartoszyce",
"NBR": "Braniewo",
"NDZ": "Działdowo",
"NE": "Elbląg",
"NEL": "Elbląg powiat",
"NEB": "Ełk",
"NEK": "Ełk powiat",
"NG": "Giżycko",
"NGI": "Giżycko powiat",
"NGO": "Gołdap",
"NI": "Iława",
"NKE": "Kętrzyn",
"NL": "Lidzbark Warmiński",
"NMR": "Mrągowo",
"NNI": "Nidzica",
"NO": "Olsztyn",
"NOE": "Olecko",
"NOL": "Olsztyn powiat",
"NOS": "Ostróda",
"NPI": "Pisz",
"NSZ": "Szczytno",
"NW": "Węgorzewo",
"PCD": "Czarnków-Trzcianka",
"PCH": "Chodzież",
"PGN": "Gniezno",
"PGO": "Gostyń",
"PGR": "Grodzisk Wielkopolski",
"PIA": "Piła",
"PJ": "Jarocin",
"PJA": "Jarocin powiat",
"PK": "Kępno",
"PKA": "Kalisz",
"PKL": "Kalisz powiat",
"PKN": "Koło",
"PKO": "Konin",
"PKS": "Kościan",
"PL": "Leszno",
"PLE": "Leszno powiat",
"PMI": "Międzychód",
"PNT": "Nowy Tomyśl",
"PO": "Poznań",
"POB": "Oborniki",
"POL": "Ostrów Wielkopolski",
"POP": "Opole",
"POS": "Ostrzeszów",
"POT": "Ostrów Wielkopolski powiat",
"PP": "Pleszew",
"PPI": "Piła powiat",
"PPL": "Pleszew powiat",
"PRA": "Poznań powiat",
"PRS": "Rawicz",
"PSE": "Śrem",
"PSL": "Słupca",
"PSR": "Środa Wielkopolska",
"PSZ": "Szamotuły",
"PT": "Turek",
"PTU": "Turek powiat",
"PW": "Wągrowiec",
"PWA": "Wągrowiec powiat",
"PWL": "Wolsztyn",
"PWR": "Września",
"PZ": "Poznań",
"PZL": "Złotów",
"ZBI": "Białogard",
"ZCH": "Choszczno",
"ZG": "Gryfice",
"ZGR": "Gryfino",
"ZI": "Stargard",
"ZK": "Kołobrzeg",
"ZKA": "Kamień Pomorski",
"ZKL": "Kołobrzeg powiat",
"ZKO": "Koszalin",
"ZKS": "Koszalin powiat",
"ZL": "Łobez",
"ZM": "Myślibórz",
"ZPL": "Pyrzyce",
"ZPO": "Police",
"ZS": "Szczecin",
"ZSL": "Sławno",
"ZST": "Stargard powiat",
"ZSW": "Świnoujście",
"ZSZ": "Szczecinek",
"ZW": "Wałcz",
"ZZ": "Szczecin powiat",
}

View File

@ -0,0 +1,242 @@
"""Anki flashcard generator for Polish car license plates.
Generates Anki-compatible flashcard decks with bidirectional cards for Polish
vehicle registration plate codes and their corresponding locations.
Creates two types of cards:
1. Code Location (e.g., WY Warszawa Wola)
2. Location Code (e.g., Warszawa Wola WY)
Usage:
# Generate Anki cards for all Polish license plates
python -m python_pkg.polish_license_plates.polish_license_plates_anki
# Specify custom output file
python -m python_pkg.polish_license_plates.polish_license_plates_anki \
--output plates.apkg
Output:
Creates a self-contained .apkg file that can be directly imported into Anki.
"""
from __future__ import annotations
import argparse
import hashlib
from pathlib import Path
import random
import sys
from typing import TYPE_CHECKING
import genanki
from python_pkg.polish_license_plates.license_plate_data import LICENSE_PLATE_CODES
if TYPE_CHECKING:
from collections.abc import Sequence
def generate_anki_package(
deck_name: str = "Polish License Plates",
) -> genanki.Package:
"""Generate Anki package (.apkg) for Polish license plates.
Creates two cards for each license plate code:
1. Code Location
2. Location Code
Args:
deck_name: Name for the Anki deck.
Returns:
genanki.Package object ready to be written to file.
"""
# Create unique model ID based on deck name
model_id_hash = hashlib.md5(
f"polish_license_plates_{deck_name}".encode(),
usedforsecurity=False,
)
model_id = int(model_id_hash.hexdigest()[:8], 16)
# Define the note model with centered styling and bidirectional templates
card_css = """
.card {
font-family: Arial, sans-serif;
font-size: 28px;
text-align: center;
color: #333;
background-color: #fff;
}
.card.night_mode {
color: #eee;
background-color: #2f2f2f;
}
.question {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
font-size: 48px;
font-weight: bold;
color: #2C3E50;
}
.card.night_mode .question {
color: #ECF0F1;
}
.answer {
font-size: 36px;
font-weight: bold;
margin-top: 20px;
color: #27AE60;
}
.card.night_mode .answer {
color: #2ECC71;
}
.plate-code {
font-family: 'Courier New', monospace;
background-color: #FFD700;
color: #000;
padding: 15px 30px;
border: 3px solid #000;
border-radius: 8px;
display: inline-block;
letter-spacing: 5px;
}
.card.night_mode .plate-code {
background-color: #FFA500;
}
"""
my_model = genanki.Model(
model_id,
"Polish License Plate Model",
fields=[
{"name": "Code"},
{"name": "Location"},
],
templates=[
{
"name": "Code → Location",
"qfmt": '<div class="question">'
'<span class="plate-code">{{Code}}</span>'
"</div>",
"afmt": '<div class="question">'
'<span class="plate-code">{{Code}}</span>'
"</div>"
'<hr id="answer">'
'<div class="answer">{{Location}}</div>',
},
{
"name": "Location → Code",
"qfmt": '<div class="question">{{Location}}</div>',
"afmt": '<div class="question">{{Location}}</div>'
'<hr id="answer">'
'<div class="answer">'
'<span class="plate-code">{{Code}}</span>'
"</div>",
},
],
css=card_css,
)
# Create unique deck ID
deck_id = random.randrange(1 << 30, 1 << 31) # noqa: S311
# Create the deck
my_deck = genanki.Deck(deck_id, deck_name)
# Generate notes for each license plate code
for code, location in sorted(LICENSE_PLATE_CODES.items()):
note = genanki.Note(
model=my_model,
fields=[code, location],
tags=["geography", "poland", "license-plates", "transportation"],
)
my_deck.add_note(note)
# Create package
return genanki.Package(my_deck)
def main(argv: Sequence[str] | None = None) -> int:
"""Main entry point.
Args:
argv: Command line arguments.
Returns:
Exit code.
"""
parser = argparse.ArgumentParser(
description="Generate Anki flashcards for Polish license plates.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--output",
"-o",
type=str,
default=None,
help="Output file path (default: polish_license_plates.apkg)",
)
parser.add_argument(
"--deck-name",
"-d",
type=str,
default="Polish License Plates",
help="Name for the Anki deck (default: 'Polish License Plates')",
)
args = parser.parse_args(argv)
# Determine output path
output_path = (
Path(args.output) if args.output else Path("polish_license_plates.apkg")
)
try:
num_codes = len(LICENSE_PLATE_CODES)
num_cards = num_codes * 2 # Two cards per code (bidirectional)
sys.stdout.write(
f"Generating flashcards for {num_codes} Polish license plate codes...\n"
)
sys.stdout.write(
"Each code will have 2 cards: Code → Location and Location → Code\n"
)
# Generate the package
package = generate_anki_package(args.deck_name)
# Write to file
package.write_to_file(str(output_path))
sys.stdout.write("\n")
sys.stdout.write("=" * 70 + "\n")
sys.stdout.write("FLASHCARD GENERATION COMPLETE\n")
sys.stdout.write("=" * 70 + "\n")
sys.stdout.write(f"License plate codes: {num_codes}\n")
sys.stdout.write(f"Total flashcards: {num_cards} (bidirectional)\n")
sys.stdout.write(f"Output file: {output_path.absolute()}\n")
sys.stdout.write("\n")
sys.stdout.write("Card types:\n")
sys.stdout.write(" 1. Code → Location (e.g., WY → Warszawa Wola)\n")
sys.stdout.write(" 2. Location → Code (e.g., Warszawa Wola → WY)\n")
sys.stdout.write("\n")
sys.stdout.write("To import into Anki:\n")
sys.stdout.write(" 1. Open Anki\n")
sys.stdout.write(" 2. File → Import\n")
sys.stdout.write(f" 3. Select: {output_path.absolute()}\n")
sys.stdout.write(" 4. Click Import\n")
sys.stdout.write("\n")
sys.stdout.write("You can now learn Polish license plates both ways!\n")
except (OSError, ValueError) as e:
sys.stderr.write(f"Error: {e}\n")
return 1
else:
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1 @@
"""Tests init file."""

View File

@ -0,0 +1,231 @@
"""Tests for the Polish license plates Anki generator."""
from __future__ import annotations
from pathlib import Path
import pytest
try:
from python_pkg.polish_license_plates.license_plate_data import (
LICENSE_PLATE_CODES,
)
from python_pkg.polish_license_plates.polish_license_plates_anki import (
generate_anki_package,
main,
)
except ImportError:
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from python_pkg.polish_license_plates.license_plate_data import (
LICENSE_PLATE_CODES,
)
from python_pkg.polish_license_plates.polish_license_plates_anki import (
generate_anki_package,
main,
)
class TestLicensePlateData:
"""Tests for license plate data."""
def test_has_codes(self) -> None:
"""Test that we have license plate codes."""
assert len(LICENSE_PLATE_CODES) > 0
def test_all_codes_are_uppercase(self) -> None:
"""Test that all codes are uppercase strings."""
for code in LICENSE_PLATE_CODES:
assert isinstance(code, str)
assert code.isupper()
assert len(code) >= 2
def test_all_locations_are_strings(self) -> None:
"""Test that all locations are non-empty strings."""
for location in LICENSE_PLATE_CODES.values():
assert isinstance(location, str)
assert len(location) > 0
def test_no_duplicate_codes(self) -> None:
"""Test that all codes are unique."""
codes = list(LICENSE_PLATE_CODES.keys())
assert len(codes) == len(set(codes))
def test_warsaw_codes_present(self) -> None:
"""Test that Warsaw codes are in the database."""
warsaw_codes = [
"WA",
"WB",
"WC",
"WD",
"WE",
"WF",
"WG",
"WH",
"WI",
"WJ",
"WK",
"WL",
"WM",
"WN",
"WO",
"WP",
"WR",
"WS",
"WT",
"WU",
"WW",
"WX",
"WY",
"WZ",
]
for code in warsaw_codes:
assert code in LICENSE_PLATE_CODES
def test_major_cities_present(self) -> None:
"""Test that major Polish cities have codes."""
major_cities = {
"WA": "Warszawa",
"KR": "Kraków",
"GD": "Gdańsk",
"PO": "Poznań",
"WR": "Radom",
"BI": "Białystok",
}
for code, city_part in major_cities.items():
assert code in LICENSE_PLATE_CODES
assert city_part.lower() in LICENSE_PLATE_CODES[code].lower()
def test_voivodeship_prefixes_present(self) -> None:
"""Test that all 16 voivodeship prefixes are represented."""
voivodeship_prefixes = {
"B",
"C",
"D",
"E",
"F",
"G",
"K",
"L",
"N",
"O",
"P",
"R",
"S",
"T",
"W",
"Z",
}
found_prefixes = {code[0] for code in LICENSE_PLATE_CODES}
assert voivodeship_prefixes.issubset(found_prefixes)
class TestGenerateAnkiPackage:
"""Tests for generating Anki package."""
def test_generates_package(self) -> None:
"""Test that output is a genanki Package."""
package = generate_anki_package("Test Deck")
assert package is not None
assert len(package.decks) == 1
def test_generates_notes_for_all_codes(self) -> None:
"""Test that package contains notes for all license plate codes."""
package = generate_anki_package()
deck = package.decks[0]
# Each code generates one note with two card templates
assert len(deck.notes) == len(LICENSE_PLATE_CODES)
def test_custom_deck_name(self) -> None:
"""Test that custom deck name is used."""
package = generate_anki_package("Custom Deck")
deck = package.decks[0]
assert deck.name == "Custom Deck"
def test_notes_have_correct_fields(self) -> None:
"""Test that notes have Code and Location fields."""
package = generate_anki_package()
deck = package.decks[0]
note = deck.notes[0]
# Note should have 2 fields: Code and Location
assert len(note.fields) == 2
# Fields should be non-empty strings
assert len(note.fields[0]) > 0
assert len(note.fields[1]) > 0
def test_notes_have_tags(self) -> None:
"""Test that notes have appropriate tags."""
package = generate_anki_package()
deck = package.decks[0]
note = deck.notes[0]
assert "geography" in note.tags
assert "poland" in note.tags
assert "license-plates" in note.tags
def test_model_has_bidirectional_templates(self) -> None:
"""Test that the model has two card templates (bidirectional)."""
package = generate_anki_package()
deck = package.decks[0]
model = deck.notes[0].model
# Should have 2 templates: Code → Location and Location → Code
assert len(model.templates) == 2
class TestMain:
"""Tests for the main CLI function."""
def test_creates_output_file(self, tmp_path: Path) -> None:
"""Test that main creates the output file."""
output_file = tmp_path / "test_output.apkg"
result = main(
[
"--output",
str(output_file),
]
)
assert result == 0
assert output_file.exists()
def test_custom_deck_name(self, tmp_path: Path) -> None:
"""Test that custom deck name is used."""
output_file = tmp_path / "test_output.apkg"
result = main(
[
"--output",
str(output_file),
"--deck-name",
"Custom Deck",
]
)
assert result == 0
assert output_file.exists()
def test_default_output_path(self) -> None:
"""Test that default output path is used when not specified."""
# Clean up any existing file
default_path = Path("polish_license_plates.apkg")
if default_path.exists():
default_path.unlink()
result = main([])
assert result == 0
assert default_path.exists()
# Clean up
default_path.unlink()
def test_help_flag(self) -> None:
"""Test that --help works."""
with pytest.raises(SystemExit) as exc_info:
main(["--help"])
assert exc_info.value.code == 0
if __name__ == "__main__":
pytest.main([__file__, "-v"])