Also make explanation section slightly more readable
6.6 KiB
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.pdfinstead ofPruefungsfragenZIP.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/<slug>/
├── 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
- Download the ZIP from the canonical URL.
- Verify it is a valid ZIP and contains the expected JSON + SVG files.
- Extract to a target directory (default:
./data/<edition>/). - Emit a small
manifest.jsonnext to the data: source URL, fetched-at timestamp,Last-Modifiedfrom the server, JSON edition metadata, sha256 of the ZIP. - Be idempotent — re-running without an upstream change is a no-op.
The skip key is the HTTP
Last-Modifiedheader recorded on the previous manifest; the ZIP is deleted by default after extraction, so the recorded sha256 is provenance, not a re-verification target. SeeDESIGN.md§4 for the full idempotency contract.
Stage 2 — amateurfunk_anki.py
- Read the latest edition from
data/(followingmanifest-latest.jsonto a per-edition directory). - 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
classfield. Theklasse-N|E|Atag is still emitted on every note for inside-Anki filtering. - 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 (<u>...</u>) is preserved. If the question's number has an entry inexplanations.json(see EXPLANATIONS.md), an English explanation block is appended to the back; a "low confidence" badge shows for entries withconfidence < 7. - Hand-roll the v11 Anki collection (SQLite + JSON config) and
package it as a
.apkgZIP 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 beyondargparse. - 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/andanki/, both gitignored. - License attribution string (required by DL-DE→BY-2.0) is preserved
verbatim from the upstream
README.txtwhenever 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.jsonis 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.mdwere verified against the live BNetzA site. - When BNetzA publishes a new edition, expect a new
fragenkatalog<N><rev>.jsonfilename 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.