Files
Amateurfunk-Anki/CLAUDE.md
T

119 lines
5.9 KiB
Markdown

# 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/<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`
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/<edition>/`).
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 (`<u>...</u>`) is
preserved.
4. Hand-roll the v11 Anki collection (SQLite + JSON config) and
package it as a `.apkg` ZIP with deterministic timestamps. Same
input → 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
- 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<N><rev>.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.