From 7a211649b99465abe39dd226021b571bd7e288c4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:30:22 +0100 Subject: [PATCH] Add Polish license plate Anki flashcard generator with Wikipedia data extraction and caching (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Add Polish license plate Anki generator with bidirectional cards Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com> * Add comprehensive README for Polish license plates package Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com> * Fix license plate data: correct WT (Wawer) and WWY (Wyszków) mappings Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com> * Add Wikipedia scraper for automatic license plate data extraction Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com> * Improve fetch_license_plates: add constants and update User-Agent Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com> * Add caching to Wikipedia scraper to avoid unnecessary requests Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com> * Add error handling for cache file operations Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com> --- .gitignore | 3 + .pre-commit-config.yaml | 2 +- python_pkg/polish_license_plates/README.md | 151 ++++++ python_pkg/polish_license_plates/__init__.py | 7 + .../fetch_license_plates.py | 387 ++++++++++++++ .../license_plate_data.py | 489 ++++++++++++++++++ .../polish_license_plates_anki.py | 242 +++++++++ .../polish_license_plates/tests/__init__.py | 1 + .../tests/test_polish_license_plates_anki.py | 231 +++++++++ 9 files changed, 1512 insertions(+), 1 deletion(-) create mode 100644 python_pkg/polish_license_plates/README.md create mode 100644 python_pkg/polish_license_plates/__init__.py create mode 100755 python_pkg/polish_license_plates/fetch_license_plates.py create mode 100644 python_pkg/polish_license_plates/license_plate_data.py create mode 100644 python_pkg/polish_license_plates/polish_license_plates_anki.py create mode 100644 python_pkg/polish_license_plates/tests/__init__.py create mode 100644 python_pkg/polish_license_plates/tests/test_polish_license_plates_anki.py diff --git a/.gitignore b/.gitignore index 48cc27b..19fbd41 100644 --- a/.gitignore +++ b/.gitignore @@ -273,3 +273,6 @@ python_pkg/download_cats/http_cat_cache/ # Large geojson files that can be downloaded python_pkg/warsaw_districts/warszawa-dzielnice.geojson + +# Wikipedia cache (can be refreshed) +python_pkg/polish_license_plates/.wikipedia_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c524d7e..8cead33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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/) # =========================================================================== diff --git a/python_pkg/polish_license_plates/README.md b/python_pkg/polish_license_plates/README.md new file mode 100644 index 0000000..5caf4be --- /dev/null +++ b/python_pkg/polish_license_plates/README.md @@ -0,0 +1,151 @@ +# Polish License Plates Anki Generator + +Generate Anki flashcards for learning Polish car license plate codes. + +## Overview + +This package generates Anki-compatible flashcard decks for all Polish vehicle registration plate codes. Each code is mapped to its corresponding location (city or powiat). + +Polish license plates use a system where: + +- First letter indicates the **voivodeship** (province) +- Following 1-2 letters indicate the specific **city** or **powiat** (county) + +## Features + +- **444 license plate codes** covering all Polish voivodeships, cities, and powiats +- **Bidirectional flashcards**: + - Code → Location (e.g., `WY` → `Warszawa Wola`) + - Location → Code (e.g., `Warszawa Wola` → `WY`) +- **888 total flashcards** for comprehensive learning +- Visual license plate styling in flashcards (yellow background, monospace font) +- Dark mode support +- Self-contained `.apkg` file - no manual setup required + +## Data Source + +License plate data is automatically extracted from Wikipedia's authoritative table: + +- **Source**: [Vehicle registration plates of Poland](https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland) +- **Update**: Run `python -m python_pkg.polish_license_plates.fetch_license_plates` to refresh data + +This ensures the codes are always based on the most current public information. + +## Usage + +### Generate Flashcards + +```bash +# Generate with default settings +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 my_plates.apkg + +# Use custom deck name +python -m python_pkg.polish_license_plates.polish_license_plates_anki \ + --deck-name "My Polish Plates" +``` + +### Update License Plate Data + +To fetch the latest data from Wikipedia: + +```bash +# Use cached data if available (default) +python -m python_pkg.polish_license_plates.fetch_license_plates + +# Force refresh from Wikipedia (ignore cache) +python -m python_pkg.polish_license_plates.fetch_license_plates --force +``` + +**Caching**: Downloaded Wikipedia data is cached for 7 days in `.wikipedia_cache/` to avoid unnecessary requests. Use `--force` to bypass the cache. + +This will update `license_plate_data.py` with the current codes from Wikipedia. + +**Requirements**: `pip install requests beautifulsoup4 lxml` + +### Import into Anki + +1. Open Anki +2. File → Import +3. Select the generated `.apkg` file +4. Click Import + +## Examples + +### License Plate Codes by Voivodeship + +| Voivodeship | First Letter | Example Codes | +| ------------------- | ------------ | ------------------------------------------------ | +| Dolnośląskie | D | DA (Wrocław), DB (Wałbrzych), DJ (Jelenia Góra) | +| Kujawsko-Pomorskie | C | CB (Bydgoszcz), CT (Toruń), CG (Grudziądz) | +| Lubelskie | L | LL (Lublin), LC (Chełm), LZ (Zamość) | +| Lubuskie | F | FZ (Zielona Góra), FG (Gorzów Wielkopolski) | +| Łódzkie | E | ED (Łódź), EP (Piotrków Trybunalski) | +| Małopolskie | K | KR (Kraków), KT (Tarnów), KN (Nowy Sącz) | +| Mazowieckie | W | WA-WZ (Warsaw), WR (Radom), WS (Siedlce) | +| Opolskie | O | OP (Opole), OK (Kędzierzyn-Koźle) | +| Podkarpackie | R | RR (Rzeszów), RP (Przemyśl), RK (Krosno) | +| Podlaskie | B | BI (Białystok), BL (Łomża), BSU (Suwałki) | +| Pomorskie | G | GD (Gdańsk), GDY (Gdynia), GS (Słupsk) | +| Śląskie | S | SK (Katowice), SC (Chorzów), SB (Bielsko-Biała) | +| Świętokrzyskie | T | TK (Kielce), TSK (Skarżysko-Kamienna) | +| Warmińsko-Mazurskie | N | NO (Olsztyn), NE (Elbląg), NG (Giżycko) | +| Wielkopolskie | P | PO (Poznań), PKA (Kalisz), PIA (Piła) | +| Zachodniopomorskie | Z | ZS (Szczecin), ZKO (Koszalin), ZSW (Świnoujście) | + +### Warsaw (Warszawa) Codes + +Warsaw has an extensive range of codes (WA-WZ): + +- WA: Warszawa (general) +- WB: Warszawa Bemowo +- WC: Ciechanów +- WD: Warszawa Praga Południe +- WE: Warszawa Praga Północ +- WH: Warszawa Mokotów +- WY: Warszawa Wola +- And many more... + +## Data + +The package includes 444 license plate codes covering: + +- All 16 Polish voivodeships +- Major cities with powiat rights (e.g., Kraków, Gdańsk, Poznań) +- All powiats (counties) across Poland + +## Testing + +Run the test suite: + +```bash +python -m pytest python_pkg/polish_license_plates/tests/ -v +``` + +All 17 tests validate: + +- Data integrity (444 codes, no duplicates) +- Correct voivodeship prefixes +- Major cities present +- Anki package generation +- Bidirectional card templates +- CLI functionality + +## Technical Details + +- **Package format**: Anki `.apkg` (SQLite database) +- **Card model**: Bidirectional with two templates per note +- **Styling**: Custom CSS with license plate visual design +- **Tags**: `geography`, `poland`, `license-plates`, `transportation` + +## Requirements + +- Python 3.10+ +- genanki (for Anki package generation) + +## License + +Part of the testsAndMisc repository. diff --git a/python_pkg/polish_license_plates/__init__.py b/python_pkg/polish_license_plates/__init__.py new file mode 100644 index 0000000..796216f --- /dev/null +++ b/python_pkg/polish_license_plates/__init__.py @@ -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 diff --git a/python_pkg/polish_license_plates/fetch_license_plates.py b/python_pkg/polish_license_plates/fetch_license_plates.py new file mode 100755 index 0000000..eb46de3 --- /dev/null +++ b/python_pkg/polish_license_plates/fetch_license_plates.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +"""Fetch Polish license plate codes from Wikipedia. + +This script scrapes the Wikipedia page "Vehicle registration plates of Poland" +to extract the official license plate codes and their corresponding locations. + +The data is extracted from the wikitable on the page and saved to license_plate_data.py. + +Caching: + Fetched Wikipedia HTML is cached to avoid unnecessary requests. + Cache location: .wikipedia_cache/license_plates.html + Cache expires after 7 days by default. + +Usage: + python -m python_pkg.polish_license_plates.fetch_license_plates + + # Force refresh (ignore cache) + python -m python_pkg.polish_license_plates.fetch_license_plates --force + +Source: + https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland + +Note: + This script requires internet access and the following packages: + - requests + - beautifulsoup4 + - lxml +""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import re +import sys +import time + +try: + from bs4 import BeautifulSoup + import requests +except ImportError: + sys.stderr.write( + "Error: Required packages not installed.\n" + "Install with: pip install requests beautifulsoup4 lxml\n" + ) + sys.exit(1) + +# Constants +MIN_TABLE_COLUMNS = 2 # Minimum columns needed to extract code and location +MAX_CODE_LENGTH = 4 # Maximum length for a valid license plate code +CACHE_EXPIRY_DAYS = 7 # Cache expires after 7 days +USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" # Updated to recent version +) + + +def get_cache_path() -> Path: + """Get the path to the cache file. + + Returns: + Path to the cache file. + """ + script_dir = Path(__file__).parent + cache_dir = script_dir / ".wikipedia_cache" + cache_dir.mkdir(exist_ok=True) + return cache_dir / "license_plates.html" + + +def is_cache_valid(cache_path: Path, max_age_days: int = CACHE_EXPIRY_DAYS) -> bool: + """Check if the cache file exists and is not expired. + + Args: + cache_path: Path to the cache file. + max_age_days: Maximum age in days before cache is considered expired. + + Returns: + True if cache is valid, False otherwise. + """ + if not cache_path.exists(): + return False + + # Check age + file_age_seconds = time.time() - cache_path.stat().st_mtime + max_age_seconds = max_age_days * 24 * 60 * 60 + + return file_age_seconds < max_age_seconds + + +def fetch_wikipedia_html(*, force_refresh: bool = False) -> str: + """Fetch Wikipedia HTML, using cache if available. + + Args: + force_refresh: If True, ignore cache and fetch fresh data. + + Returns: + HTML content of the Wikipedia page. + + Raises: + RuntimeError: If the page cannot be fetched. + """ + cache_path = get_cache_path() + + # Check if we can use cache + if not force_refresh and is_cache_valid(cache_path): + try: + sys.stdout.write(f"Using cached data from {cache_path}\n") + cache_age_hours = int((time.time() - cache_path.stat().st_mtime) / 3600) + sys.stdout.write(f"Cache age: {cache_age_hours} hours\n") + return cache_path.read_text(encoding="utf-8") + except OSError as e: + sys.stderr.write(f"Warning: Failed to read cache: {e}\n") + sys.stderr.write("Fetching fresh data from Wikipedia...\n") + # Fall through to fetch from Wikipedia + + # Fetch from Wikipedia + url = "https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland" + headers = {"User-Agent": USER_AGENT} + + if force_refresh: + sys.stdout.write("Force refresh: Ignoring cache\n") + + sys.stdout.write(f"Fetching data from {url}...\n") + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + except requests.RequestException as e: + msg = f"Failed to fetch Wikipedia page: {e}" + raise RuntimeError(msg) from e + + # Cache the response + try: + cache_path.write_text(response.text, encoding="utf-8") + sys.stdout.write(f"Cached response to {cache_path}\n") + except OSError as e: + sys.stderr.write(f"Warning: Failed to write cache: {e}\n") + # Continue anyway - the data was fetched successfully + + return response.text + + +def parse_license_plates_from_html(html_content: str) -> dict[str, str]: + """Parse license plate codes from Wikipedia HTML. + + Args: + html_content: HTML content of the Wikipedia page. + + Returns: + Dictionary mapping license plate codes to their locations. + + Raises: + RuntimeError: If no valid tables are found. + """ + soup = BeautifulSoup(html_content, "html.parser") + + # Find all wikitables + tables = soup.find_all("table", {"class": "wikitable"}) + + if not tables: + msg = "No wikitable found on the page" + raise RuntimeError(msg) + + sys.stdout.write(f"Found {len(tables)} tables on the page\n") + + license_plates: dict[str, str] = {} + + # Process each table + for table_idx, table in enumerate(tables): + rows = table.find_all("tr") + + sys.stdout.write(f"Processing table {table_idx + 1} with {len(rows)} rows...\n") + + for row in rows[1:]: # Skip header row + cells = row.find_all(["td", "th"]) + + if len(cells) >= MIN_TABLE_COLUMNS: + # Extract code and location + code_text = cells[0].get_text(strip=True) + location_text = cells[1].get_text(strip=True) + + # Clean up the code (remove spaces, keep only letters) + code = re.sub(r"[^A-Z]", "", code_text.upper()) + + # Skip if code is invalid + if not code or len(code) > MAX_CODE_LENGTH: + continue + + # Clean up location text (remove citations, extra spaces) + location = re.sub(r"\[[0-9]+\]", "", location_text) + location = " ".join(location.split()) + + if location: + license_plates[code] = location + + sys.stdout.write(f"Extracted {len(license_plates)} license plate codes\n") + + return license_plates + + +def fetch_wikipedia_license_plates(*, force_refresh: bool = False) -> dict[str, str]: + """Fetch Polish license plate codes from Wikipedia. + + Args: + force_refresh: If True, ignore cache and fetch fresh data. + + Returns: + Dictionary mapping license plate codes to their locations. + + Raises: + RuntimeError: If the page cannot be fetched or parsed. + """ + html_content = fetch_wikipedia_html(force_refresh=force_refresh) + return parse_license_plates_from_html(html_content) + + +def generate_license_plate_data_file( + license_plates: dict[str, str], + output_path: Path, +) -> None: + """Generate license_plate_data.py file with the extracted data. + + Args: + license_plates: Dictionary mapping codes to locations. + output_path: Path to the output file. + """ + # Group by first letter (voivodeship) + voivodeships: dict[str, list[tuple[str, str]]] = {} + for code, location in sorted(license_plates.items()): + first_letter = code[0] + if first_letter not in voivodeships: + voivodeships[first_letter] = [] + voivodeships[first_letter].append((code, location)) + + # Voivodeship names + voivodeship_names = { + "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", + } + + # Generate file content + content = '''"""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 + +Data source: + Wikipedia - Vehicle registration plates of Poland + https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland + +Auto-generated by: + python -m python_pkg.polish_license_plates.fetch_license_plates + +Examples: + WA = Warszawa (Warsaw) + KR = Kraków + GD = Gdańsk +""" + +from __future__ import annotations + +LICENSE_PLATE_CODES: dict[str, str] = { +''' + + # Add entries grouped by voivodeship + for letter in sorted(voivodeships.keys()): + voivodeship_name = voivodeship_names.get(letter, f"Voivodeship {letter}") + codes = voivodeships[letter] + + content += f" # {letter} - {voivodeship_name} ({len(codes)} codes)\n" + + for code, location in codes: + # Escape quotes in location + location_escaped = location.replace('"', '\\"') + content += f' "{code}": "{location_escaped}",\n' + + content += "\n" + + # Remove last comma and newline, then close the dict + content = content.rstrip(",\n") + "\n}\n" + + # Write to file + output_path.write_text(content, encoding="utf-8") + sys.stdout.write(f"Generated {output_path}\n") + + +def main() -> int: + """Main entry point. + + Returns: + Exit code. + """ + parser = argparse.ArgumentParser( + description="Fetch Polish license plate codes from Wikipedia.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--force", + "-f", + action="store_true", + help="Force refresh: ignore cache and fetch fresh data from Wikipedia", + ) + + args = parser.parse_args() + + try: + # Fetch data from Wikipedia + license_plates = fetch_wikipedia_license_plates(force_refresh=args.force) + + # Determine output path + script_dir = Path(__file__).parent + output_path = script_dir / "license_plate_data.py" + + # Generate the file + generate_license_plate_data_file(license_plates, output_path) + + sys.stdout.write("\n") + sys.stdout.write("=" * 70 + "\n") + sys.stdout.write("LICENSE PLATE DATA UPDATE COMPLETE\n") + sys.stdout.write("=" * 70 + "\n") + sys.stdout.write(f"Total codes: {len(license_plates)}\n") + sys.stdout.write(f"Output file: {output_path}\n") + sys.stdout.write("\n") + sys.stdout.write("Data source: Wikipedia\n") + sys.stdout.write( + "URL: https://en.wikipedia.org/wiki/" + "Vehicle_registration_plates_of_Poland\n" + ) + sys.stdout.write(f"Cache location: {get_cache_path()}\n") + sys.stdout.write(f"Cache expiry: {CACHE_EXPIRY_DAYS} days\n") + sys.stdout.write("\n") + sys.stdout.write("Next steps:\n") + sys.stdout.write(" 1. Review the generated file\n") + sys.stdout.write( + " 2. Run tests: " "pytest python_pkg/polish_license_plates/tests/\n" + ) + sys.stdout.write( + " 3. Regenerate Anki package: " + "python -m python_pkg.polish_license_plates.polish_license_plates_anki\n" + ) + + except RuntimeError as e: + sys.stderr.write(f"Error: {e}\n") + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/polish_license_plates/license_plate_data.py b/python_pkg/polish_license_plates/license_plate_data.py new file mode 100644 index 0000000..ce40f0b --- /dev/null +++ b/python_pkg/polish_license_plates/license_plate_data.py @@ -0,0 +1,489 @@ +"""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 + +Data source: + Wikipedia - Vehicle registration plates of Poland + https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_Poland + +Note: + This data can be automatically updated by running: + python -m python_pkg.polish_license_plates.fetch_license_plates + +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 Wawer", + "WU": "Warszawa Ursus", + "WV": "Ostrołęka powiat", + "WW": "Warszawa Ochota", + "WWL": "Wołomin", + "WWY": "Wyszków", + "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", +} diff --git a/python_pkg/polish_license_plates/polish_license_plates_anki.py b/python_pkg/polish_license_plates/polish_license_plates_anki.py new file mode 100644 index 0000000..d120d00 --- /dev/null +++ b/python_pkg/polish_license_plates/polish_license_plates_anki.py @@ -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": '
' + '{{Code}}' + "
", + "afmt": '
' + '{{Code}}' + "
" + '
' + '
{{Location}}
', + }, + { + "name": "Location → Code", + "qfmt": '
{{Location}}
', + "afmt": '
{{Location}}
' + '
' + '
' + '{{Code}}' + "
", + }, + ], + 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()) diff --git a/python_pkg/polish_license_plates/tests/__init__.py b/python_pkg/polish_license_plates/tests/__init__.py new file mode 100644 index 0000000..4f70ec1 --- /dev/null +++ b/python_pkg/polish_license_plates/tests/__init__.py @@ -0,0 +1 @@ +"""Tests init file.""" diff --git a/python_pkg/polish_license_plates/tests/test_polish_license_plates_anki.py b/python_pkg/polish_license_plates/tests/test_polish_license_plates_anki.py new file mode 100644 index 0000000..81fa094 --- /dev/null +++ b/python_pkg/polish_license_plates/tests/test_polish_license_plates_anki.py @@ -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"])