mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 15:43:06 +02:00
Add Polish license plate Anki flashcard generator with Wikipedia data extraction and caching (#4)
* 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>
This commit is contained in:
parent
652e86c370
commit
7a211649b9
3
.gitignore
vendored
3
.gitignore
vendored
@ -273,3 +273,6 @@ python_pkg/download_cats/http_cat_cache/
|
|||||||
|
|
||||||
# Large geojson files that can be downloaded
|
# Large geojson files that can be downloaded
|
||||||
python_pkg/warsaw_districts/warszawa-dzielnice.geojson
|
python_pkg/warsaw_districts/warszawa-dzielnice.geojson
|
||||||
|
|
||||||
|
# Wikipedia cache (can be refreshed)
|
||||||
|
python_pkg/polish_license_plates/.wikipedia_cache/
|
||||||
|
|||||||
@ -162,7 +162,7 @@ repos:
|
|||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
args:
|
||||||
- --skip=*.json,*.lock,*.min.js,*.min.css,.git,__pycache__,.venv,*.txt
|
- --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/)
|
exclude: ^(Bash/ffmpeg-build/|LaTeX/|CPP/)
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|||||||
151
python_pkg/polish_license_plates/README.md
Normal file
151
python_pkg/polish_license_plates/README.md
Normal file
@ -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.
|
||||||
7
python_pkg/polish_license_plates/__init__.py
Normal file
7
python_pkg/polish_license_plates/__init__.py
Normal 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
|
||||||
387
python_pkg/polish_license_plates/fetch_license_plates.py
Executable file
387
python_pkg/polish_license_plates/fetch_license_plates.py
Executable file
@ -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())
|
||||||
489
python_pkg/polish_license_plates/license_plate_data.py
Normal file
489
python_pkg/polish_license_plates/license_plate_data.py
Normal file
@ -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",
|
||||||
|
}
|
||||||
242
python_pkg/polish_license_plates/polish_license_plates_anki.py
Normal file
242
python_pkg/polish_license_plates/polish_license_plates_anki.py
Normal 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())
|
||||||
1
python_pkg/polish_license_plates/tests/__init__.py
Normal file
1
python_pkg/polish_license_plates/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests init 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"])
|
||||||
Loading…
Reference in New Issue
Block a user