# Amateurfunk — BNetzA Question Catalog → Anki Decks A small two-stage Python pipeline that downloads the German amateur-radio exam question catalog ("Fragenkatalog") published by the Bundesnetzagentur (BNetzA) and turns it into Anki decks. The full source-discovery notes, JSON schema, exam-structure details, and per-stage design decisions live in `DESIGN.md`. This file is a short orientation for anyone (human or agent) opening the project. ## What the catalog is - Official German amateur-radio exam questions for classes **N, E, A** (German license tiers). - Published by the Bundesnetzagentur under the **DL-DE→BY-2.0** open data license (free reuse, attribution required). - Distributed as a single ZIP containing one JSON file with the full question tree, plus a `svgs/` folder with figures referenced by individual questions. - Current edition at time of writing: **3. Auflage, März 2024** (issued 2024-03-20, valid from 2024-06-24, ~1750 questions). ## Canonical source - ZIP (machine-readable): `https://www.bundesnetzagentur.de/SharedDocs/Downloads/DE/Sachgebiete/Telekommunikation/Unternehmen_Institutionen/Frequenzen/Amateurfunk/Fragenkatalog/PruefungsfragenZIP.zip?__blob=publicationFile` - Landing page (short link): `https://www.bnetza.de/amateurfunk-fragenkatalog` - PDF (human-readable, not used by this pipeline): same path with `Pruefungsfragen.pdf` instead of `PruefungsfragenZIP.zip`. The ZIP URL is stable across editions — BNetzA replaces the file in-place. The `Last-Modified` HTTP header is reliable for change detection. The filename inside the ZIP (`fragenkatalog3b.json`) encodes the edition (`3b` = 3rd edition, revision b) and will change on new editions, so we discover it from the archive rather than hard-coding. ## Pipeline overview ``` BNetzA ZIP ──[Stage 1: amateurfunk_fetch.py]──► data// ├── fragenkatalog*.json ├── svgs/ ├── README.txt └── manifest.json data/ ──[Stage 2: amateurfunk_anki.py]──► anki/ ├── amateurfunk-technische-kenntnisse-n.apkg ├── amateurfunk-technische-kenntnisse-e.apkg ├── amateurfunk-technische-kenntnisse-a.apkg ├── amateurfunk-betriebliche-kenntnisse.apkg └── amateurfunk-kenntnisse-von-vorschriften.apkg ``` ### Stage 1 — `amateurfunk_fetch.py` 1. Download the ZIP from the canonical URL. 2. Verify it is a valid ZIP and contains the expected JSON + SVG files. 3. Extract to a target directory (default: `./data//`). 4. Emit a small `manifest.json` next to the data: source URL, fetched-at timestamp, `Last-Modified` from the server, JSON edition metadata, sha256 of the ZIP. 5. Be idempotent — re-running without an upstream change is a no-op. The skip key is the HTTP `Last-Modified` header recorded on the previous manifest; the ZIP is deleted by default after extraction, so the recorded sha256 is provenance, not a re-verification target. See `DESIGN.md` §4 for the full idempotency contract. ### Stage 2 — `amateurfunk_anki.py` 1. Read the latest edition from `data/` (following `manifest-latest.json` to a per-edition directory). 2. Split the catalog into five categories. Betriebliche and Vorschriften get one deck each (shared across every candidate). Technische is additionally fanned out per license class into three decks (N / E / A) using a strict equality split on the question's `class` field. The `klasse-N|E|A` tag is still emitted on every note for inside-Anki filtering. 3. Render every question as an Anki note: shuffled A/B/C/D choices on the front, the displayed position of the correct answer on the back. Inline `$...$` LaTeX is converted to MathJax `\(...\)` delimiters; the catalog's safe inline markup (`...`) is preserved. If the question's number has an entry in `explanations.json` (see EXPLANATIONS.md), an English explanation block is appended to the back; a "low confidence" badge shows for entries with `confidence < 7`. 4. Hand-roll the v11 Anki collection (SQLite + JSON config) and package it as a `.apkg` ZIP with deterministic timestamps. By default each build mints a fresh shuffle seed, so answers are reshuffled every run. Pass `--seed` (and `--epoch`) for the reproducible-build contract: same catalog + same seed + same timestamp → byte-identical output across runs. The Anki design decisions (shuffle seeding, deterministic build epoch, SVG dark-mode handling, schema choices) live in `DESIGN.md` §7. ## Repo conventions - Python 3.11+, standard library only. No third-party dependencies in either stage. - Single-file scripts: `amateurfunk_fetch.py`, `amateurfunk_anki.py`. No frameworks, no CLI library beyond `argparse`. - Style: section banners, commented constants, docstrings on every function, inline comments at decision points. The two scripts intentionally read the same way. - Outputs are build artifacts: kept under `data/` and `anki/`, both gitignored. - License attribution string (required by DL-DE→BY-2.0) is preserved verbatim from the upstream `README.txt` whenever we redistribute the data. ## Working on this repo - `EXPLANATIONS.md` — the editorial contract for agents asked to add or improve per-question explanations. The schema, the workflows ("explain everything unexplained", "improve everything below confidence 7"), and the source/confidence guidance live there. `explanations.json` is an empty `{}` until agents populate it. - Start from `DESIGN.md` — it has the JSON schema, the question/answer conventions (answer A is always correct upstream → consumers shuffle before display), the LaTeX-in-questions caveat, the exam-structure rationale for the Anki package layout (Betriebliche, Vorschriften, and Technische split per license class), and per-stage design notes. - Do not invent new download URLs; the ones in `DESIGN.md` were verified against the live BNetzA site. - When BNetzA publishes a new edition, expect a new `fragenkatalog.json` filename inside the ZIP. The fetcher must not hard-code the current name. - Both stages have a fixture-driven test suite. Run with `python3 -m unittest test_amateurfunk_fetch test_amateurfunk_anki`. Network access is only needed for the manual smoke test of Stage 1.