"""Baut ``content/themen.json`` – die Feingliederung der Kapitel II bis IV. Warum es diese Datei gibt ------------------------- Der amtliche Katalog gliedert **nur Kapitel I** in Abschnitte. Für 230 Fragen aus den Kapiteln II, III und IV steht ``abschnitt`` auf ``null`` – und alles, was auf Bereichen aufsetzt (Kapitelwahl, Lernstand je Bereich, Themenanalyse der Prüfungsauswertung, Fragenlisten-Druck), arbeitet für sie zwangsläufig auf Kapitelebene. Wer in der Simulation an Munitionskunde scheitert, sieht nur „Kapitel II schwach“ und kann nur das ganze Kapitel nachüben: 92 Fragen. Diese Gliederung ist **redaktionell**, nicht amtlich. Sie steht deshalb in einer eigenen Datei und nicht im Katalog – genau wie Erklärungen und Glossar. Der amtliche Katalog bleibt unangetastet. Was das Skript garantiert ------------------------- 1. **Vollständigkeit.** Jede Frage der Kapitel II bis IV liegt in genau einer Gruppe. Eine unvollständige Gliederung wäre schlimmer als keine: Themen zu üben hieße dann, Fragen stillschweigend zu übergehen. 2. **Keine Doppelung.** Keine Frage steht in zwei Gruppen. 3. **Kapiteltreue.** Keine Gruppe mischt Kapitel. 4. **Mindestgröße.** Eine Gruppe mit einer Frage ist kein Thema. Scheitert eine dieser Prüfungen, wird nichts geschrieben. Aufruf ------ python data-pipeline/themen_bauen.py python data-pipeline/themen_bauen.py --selbsttest """ from __future__ import annotations import json import sys from datetime import date from pathlib import Path WURZEL = Path(__file__).resolve().parent.parent KATALOG = WURZEL / 'content' / 'katalog' / 'katalog.json' ZIEL = WURZEL / 'content' / 'themen.json' #: Kapitel ohne amtliche Abschnittsgliederung. Kapitel I bleibt außen vor – #: dort gibt es die Abschnitte des Bundesverwaltungsamtes, und eine zweite #: Gliederung daneben wäre eine Zahl, die auseinanderläuft. KAPITEL = ('II', 'III', 'IV') #: Weniger als so viele Fragen sind kein Thema, sondern ein Einzelfall. MINDESTGROESSE = 3 HINWEIS = ( 'Redaktionelle Feingliederung dieser Software, nicht Teil des amtlichen ' 'Fragenkatalogs des Bundesverwaltungsamtes. Der amtliche Katalog gliedert nur ' 'Kapitel I in Abschnitte; fuer die 230 Fragen der Kapitel II bis IV gibt es dort ' 'keine Untergliederung. Nicht von Hand aendern - neu erzeugen mit: ' 'python data-pipeline/themen_bauen.py' ) def n(kapitel: str, *nummern: int) -> list[str]: """Fragen-IDs aus Nummern – die Kapitel II bis IV zählen zweistellig.""" return [f'{kapitel}-{nummer:02d}' for nummer in nummern] #: Die Gliederung. Von Hand geschrieben und bewusst grob: Zwölf Gruppen für #: 92 Fragen sind lernbare Portionen; dreißig wären ein zweites Inhaltsverzeichnis. GRUPPEN: list[dict] = [ # ── Kapitel II: Waffentechnik ───────────────────────────────────────── { 'id': 'II.1', 'titel': 'Waffenarten und Funktionsprinzipien', 'fragen': n('II', 1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 16, 18, 19, 45, 46, 47, 56, 58), }, { 'id': 'II.2', 'titel': 'Wesentliche Teile und Baugruppen', 'fragen': n('II', 5, 13, 14, 15, 55, 88, 89), }, { 'id': 'II.3', 'titel': 'Lauf, Züge und Felder, Zubehör', 'fragen': n('II', 10, 37, 38, 39, 49, 50, 51, 57), }, { 'id': 'II.4', 'titel': 'Schalldämpfer und Schallentstehung', 'fragen': n('II', 33, 34, 35, 36, 86, 87), }, {'id': 'II.5', 'titel': 'Sicherungen', 'fragen': n('II', 52, 53, 54)}, { 'id': 'II.6', 'titel': 'Munition: Aufbau und Zündung', 'fragen': n('II', 20, 21, 22, 23, 25, 26, 48), }, { 'id': 'II.7', 'titel': 'Kaliber und Munitionsbezeichnung', 'fragen': n('II', 17, 24, 27, 28, 29, 30, 31, 32, 40, 41, 42, 43, 44), }, { 'id': 'II.8', 'titel': 'Ballistik: Begriffe und Teilgebiete', 'fragen': n('II', 60, 61, 62, 74, 75, 76, 79, 80, 85), }, { 'id': 'II.9', 'titel': 'Flugbahn, Drall und Streuung', 'fragen': n('II', 59, 77, 78, 81, 82, 83, 84), }, { 'id': 'II.10', 'titel': 'Reichweite und Gefährdungsbereich', 'fragen': n('II', 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73), }, {'id': 'II.11', 'titel': 'Unbrauchbarmachung', 'fragen': n('II', 90, 91, 92)}, # ── Kapitel III: Handhabung von Schusswaffen ────────────────────────── { 'id': 'III.1', 'titel': 'Grundregeln im Umgang mit Schusswaffen', 'fragen': n('III', 1, 8, 46), }, { 'id': 'III.2', 'titel': 'Übergeben und Annehmen', 'fragen': n('III', 2, 9, 10, 13, 20), }, { 'id': 'III.3', 'titel': 'Laden, Entladen und Schussbereitschaft', 'fragen': n('III', 11, 12, 27, 37, 38, 39, 40, 43, 45), }, { 'id': 'III.4', 'titel': 'Ablegen und Tragen auf dem Stand', 'fragen': n('III', 18, 19, 21, 26, 33, 42, 44), }, { 'id': 'III.5', 'titel': 'Schießstätte: Ordnung, Aufsicht und Gefahrenbereich', 'fragen': n('III', 15, 16, 17, 22, 23, 24, 25, 30, 31, 32, 34, 35, 48, 49), }, { 'id': 'III.6', 'titel': 'Schießen außerhalb von Schießstätten', 'fragen': n('III', 3, 14, 28, 29), }, { 'id': 'III.7', 'titel': 'Munition auf der Schießstätte', 'fragen': n('III', 4, 5, 6, 7, 36, 41, 47), }, # ── Kapitel IV: Not- und Seenotsignalmittel ─────────────────────────── { 'id': 'IV.1', 'titel': 'Begriffe und Arten der Signalmittel', 'fragen': n('IV', 1, 2, 3, 9, 41, 42), }, { 'id': 'IV.2', 'titel': 'Verwendung im Notfall', 'fragen': n('IV', 4, 6, 7, 8, 10, 11, 12, 13, 43, 44, 45, 53, 59, 83), }, { 'id': 'IV.3', 'titel': 'Technische Daten und Aufbau', 'fragen': n('IV', 5, 48, 51, 52, 55, 57, 58), }, { 'id': 'IV.4', 'titel': 'Kennzeichnung, Zulassung und Klassen', 'fragen': n('IV', 39, 40, 47, 49, 50, 54, 56, 89), }, { 'id': 'IV.5', 'titel': 'Haltbarkeit, Lagerung und Entsorgung', 'fragen': n('IV', 14, 15, 16, 17, 18, 19, 25, 26, 46), }, { 'id': 'IV.6', 'titel': 'Aufbewahrung und Transport', 'fragen': n('IV', 20, 21, 22, 23, 24, 75, 76, 77), }, { 'id': 'IV.7', 'titel': 'Erlaubnisse, Bedürfnis und Dokumente', 'fragen': n('IV', 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 80, 85, 86, 87, 88), }, {'id': 'IV.8', 'titel': 'Überlassen und Führen', 'fragen': n('IV', 78, 79, 82)}, { 'id': 'IV.9', 'titel': 'Rechtsgrundlagen und Zuständigkeiten', 'fragen': n('IV', 61, 63, 64, 65, 66, 67, 68, 69, 70, 71), }, {'id': 'IV.10', 'titel': 'Störungen und Versager', 'fragen': n('IV', 72, 73, 74)}, { 'id': 'IV.11', 'titel': 'Missbrauch und Meldepflichten', 'fragen': n('IV', 60, 62, 81, 84), }, ] def pruefen(gruppen: list[dict], fragen: dict[str, dict]) -> list[str]: """Alle Zusagen dieser Datei. Leere Liste heißt: in Ordnung.""" befunde: list[str] = [] kennungen = [gruppe['id'] for gruppe in gruppen] if len(set(kennungen)) != len(kennungen): befunde.append('Doppelte Gruppenkennung') zugeordnet: dict[str, str] = {} for gruppe in gruppen: if not gruppe['titel'].strip(): befunde.append(f'{gruppe["id"]}: Titel fehlt') if len(gruppe['fragen']) < MINDESTGROESSE: befunde.append( f'{gruppe["id"]}: nur {len(gruppe["fragen"])} Fragen ' f'(mindestens {MINDESTGROESSE})' ) kapitel_der_gruppe = set() for frage_id in gruppe['fragen']: frage = fragen.get(frage_id) if frage is None: befunde.append(f'{gruppe["id"]}: {frage_id} gibt es im Katalog nicht') continue kapitel_der_gruppe.add(frage['kapitel']) if frage_id in zugeordnet: befunde.append( f'{frage_id} steht in {zugeordnet[frage_id]} und in {gruppe["id"]}' ) zugeordnet[frage_id] = gruppe['id'] if len(kapitel_der_gruppe) > 1: befunde.append(f'{gruppe["id"]}: mischt Kapitel {sorted(kapitel_der_gruppe)}') # Die wichtigste Zusage: Vollständigkeit. Eine luecken­hafte Gliederung # hiesse, dass „Thema ueben" Fragen stillschweigend uebergeht. for frage_id, frage in fragen.items(): if frage['kapitel'] in KAPITEL and frage_id not in zugeordnet: befunde.append(f'{frage_id} ({frage["kapitel"]}) liegt in keiner Gruppe') if frage['kapitel'] not in KAPITEL and frage_id in zugeordnet: befunde.append(f'{frage_id} gehoert zu Kapitel I und braucht keine Gruppe') return befunde def selbsttest(fragen: dict[str, dict]) -> int: """Prüft die Prüfung: Jede Zusage wird einmal absichtlich verletzt.""" fehler = 0 beispiel = next(f for f in fragen.values() if f['kapitel'] == 'II')['id'] zweites = next( f for f in fragen.values() if f['kapitel'] == 'II' and f['id'] != beispiel )['id'] drittes = next( f for f in fragen.values() if f['kapitel'] == 'II' and f['id'] not in (beispiel, zweites) )['id'] faelle = [ ('Luecke', [{'id': 'X', 'titel': 'T', 'fragen': [beispiel, zweites, drittes]}], 'liegt in keiner Gruppe'), ('erfundene Frage', [{'id': 'X', 'titel': 'T', 'fragen': ['II-99', beispiel, zweites]}], 'gibt es im Katalog nicht'), ('zu kleine Gruppe', [{'id': 'X', 'titel': 'T', 'fragen': [beispiel]}], 'mindestens'), ('Titel fehlt', [{'id': 'X', 'titel': ' ', 'fragen': [beispiel, zweites, drittes]}], 'Titel fehlt'), ] for name, gruppen, erwartet in faelle: befunde = pruefen(gruppen, fragen) if any(erwartet in b for b in befunde): print(f' erkannt: {name}') else: print(f' DURCHGELASSEN: {name}') fehler += 1 # Doppelte Zuordnung braucht zwei Gruppen. doppelt = pruefen( [ {'id': 'A', 'titel': 'T', 'fragen': [beispiel, zweites, drittes]}, {'id': 'B', 'titel': 'T', 'fragen': [beispiel, zweites, drittes]}, ], fragen, ) if any('steht in' in b for b in doppelt): print(' erkannt: doppelte Zuordnung') else: print(' DURCHGELASSEN: doppelte Zuordnung') fehler += 1 return fehler def main() -> int: katalog = json.loads(KATALOG.read_text(encoding='utf-8')) fragen = {f['id']: f for f in katalog['fragen']} if '--selbsttest' in sys.argv: fehler = selbsttest(fragen) print('Selbsttest bestanden.' if fehler == 0 else f'{fehler} Fehler.') return 1 if fehler else 0 befunde = pruefen(GRUPPEN, fragen) if befunde: for befund in befunde: print(f' {befund}') print(f'ABBRUCH: {len(befunde)} Beanstandungen - es wird nichts geschrieben.') return 1 gruppen = [ { 'id': gruppe['id'], 'kapitel': fragen[gruppe['fragen'][0]]['kapitel'], 'titel': gruppe['titel'], 'fragen': gruppe['fragen'], } for gruppe in GRUPPEN ] daten = { 'meta': {'version': 1, 'stand': date.today().isoformat(), 'hinweis': HINWEIS}, 'gruppen': gruppen, } ZIEL.write_text(json.dumps(daten, ensure_ascii=False, indent=1) + '\n', encoding='utf-8', # LF wie im Archiv verlangt, nicht das CRLF von Windows. newline='\n', ) je_kapitel: dict[str, int] = {} for gruppe in gruppen: je_kapitel[gruppe['kapitel']] = je_kapitel.get(gruppe['kapitel'], 0) + len( gruppe['fragen'] ) verteilung = ', '.join(f'{k}: {v}' for k, v in sorted(je_kapitel.items())) print(f'{len(gruppen)} Gruppen, {sum(je_kapitel.values())} Fragen ({verteilung}).') print(f'Geschrieben: {ZIEL.relative_to(WURZEL)}') return 0 if __name__ == '__main__': raise SystemExit(main())