waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ data-pipeline glossar_zusammenfuehren.py
| 1 | """Führt die Begriffsgruppen zu `content/glossar.json` zusammen. |
| 2 | |
| 3 | Das Glossar entsteht gruppenweise – Umgangsarten, Waffenarten, Erlaubniswesen |
| 4 | und so fort. Dieses Skript legt die Teilstücke zusammen, sortiert alphabetisch |
| 5 | nach deutscher Regel und setzt den Kopf mit dem tatsächlichen Gesetzesstand. |
| 6 | |
| 7 | Ein Teilstück ist eine JSON-Datei, entweder mit Umschlag:: |
| 8 | |
| 9 | { "eintraege": [ { "begriff": "Führen", … } ] } |
| 10 | |
| 11 | oder unmittelbar als Liste:: |
| 12 | |
| 13 | [ { "begriff": "Führen", … } ] |
| 14 | |
| 15 | Aufruf |
| 16 | ------ |
| 17 | python data-pipeline/glossar_zusammenfuehren.py <datei-oder-verzeichnis> … |
| 18 | |
| 19 | Anschließend prüfen – ohne bestandene Prüfung wird nicht ausgeliefert:: |
| 20 | |
| 21 | python data-pipeline/pruefe_glossar.py |
| 22 | """ |
| 23 | |
| 24 | from __future__ import annotations |
| 25 | |
| 26 | import argparse |
| 27 | import io |
| 28 | import json |
| 29 | import locale |
| 30 | import sys |
| 31 | from datetime import date |
| 32 | from pathlib import Path |
| 33 | |
| 34 | WURZEL = Path(__file__).resolve().parent.parent |
| 35 | GESETZE = WURZEL / 'content' / 'gesetze' / 'index.json' |
| 36 | ZIEL = WURZEL / 'content' / 'glossar.json' |
| 37 | |
| 38 | HINWEIS = ( |
| 39 | 'Eigener redaktioneller Inhalt. Wo das Gesetz einen Begriff selbst bestimmt, ' |
| 40 | 'gibt der Eintrag diese Bestimmung wieder; Gesetzestexte sind nach § 5 Abs. 1 ' |
| 41 | 'UrhG gemeinfrei. Zusammenstellung und Auswahl sind nicht Teil des amtlichen ' |
| 42 | 'Fragenkatalogs. Jede Fundstelle wird von data-pipeline/pruefe_glossar.py ' |
| 43 | 'gegen den amtlichen Gesetzestext geprueft.' |
| 44 | ) |
| 45 | |
| 46 | #: Felder einer Fundstelle, die als Zeichenkette vorliegen müssen. |
| 47 | FUNDSTELLENFELDER = ('gesetz', 'norm', 'absatz', 'nummer', 'buchstabe', 'satz', 'stelle') |
| 48 | |
| 49 | |
| 50 | def teilstuecke_einlesen(orte: list[str]) -> list[dict]: |
| 51 | dateien: list[Path] = [] |
| 52 | for ort in orte: |
| 53 | pfad = Path(ort) |
| 54 | if pfad.is_dir(): |
| 55 | dateien.extend(sorted(pfad.glob('*.json'))) |
| 56 | elif pfad.is_file(): |
| 57 | dateien.append(pfad) |
| 58 | else: |
| 59 | raise SystemExit(f'Nicht gefunden: {ort}') |
| 60 | |
| 61 | gesammelt: list[dict] = [] |
| 62 | for datei in dateien: |
| 63 | if datei.resolve() == ZIEL.resolve(): |
| 64 | continue |
| 65 | roh = json.load(io.open(datei, encoding='utf-8')) |
| 66 | eintraege = roh.get('eintraege') if isinstance(roh, dict) else roh |
| 67 | if not isinstance(eintraege, list): |
| 68 | raise SystemExit(f'{datei}: erwartet wird eine Liste von Eintraegen.') |
| 69 | gesammelt.extend(eintraege) |
| 70 | print(f' {datei.name}: {len(eintraege)} Eintraege') |
| 71 | |
| 72 | return gesammelt |
| 73 | |
| 74 | |
| 75 | def normalisieren(eintraege: list[dict]) -> int: |
| 76 | """Macht aus Zahlen Zeichenketten – wie bei den Erklaerungen. |
| 77 | |
| 78 | Der Lader in der Anwendung prueft den Typ. `"absatz": 2` ist eine |
| 79 | naheliegende Verwechslung, inhaltlich harmlos und wuerde dort dennoch die |
| 80 | ganze Datei verwerfen. |
| 81 | """ |
| 82 | geaendert = 0 |
| 83 | for eintrag in eintraege: |
| 84 | for fundstelle in eintrag.get('fundstellen') or []: |
| 85 | if not isinstance(fundstelle, dict): |
| 86 | continue |
| 87 | for feld in FUNDSTELLENFELDER: |
| 88 | wert = fundstelle.get(feld) |
| 89 | if isinstance(wert, bool): |
| 90 | continue |
| 91 | if isinstance(wert, (int, float)): |
| 92 | fundstelle[feld] = str(wert) |
| 93 | geaendert += 1 |
| 94 | return geaendert |
| 95 | |
| 96 | |
| 97 | def sortierschluessel(begriff: str) -> str: |
| 98 | """Deutsche Sortierung: Umlaute stehen bei ihren Grundbuchstaben.""" |
| 99 | ersatz = {'ä': 'a', 'ö': 'o', 'ü': 'u', 'ß': 'ss', 'Ä': 'A', 'Ö': 'O', 'Ü': 'U'} |
| 100 | return ''.join(ersatz.get(z, z) for z in begriff).casefold() |
| 101 | |
| 102 | |
| 103 | def main() -> int: |
| 104 | zerleger = argparse.ArgumentParser(description='Glossar zusammenfuehren.') |
| 105 | zerleger.add_argument('quellen', nargs='+', help='Dateien oder Verzeichnisse') |
| 106 | argumente = zerleger.parse_args() |
| 107 | |
| 108 | print('Teilstuecke:') |
| 109 | eintraege = teilstuecke_einlesen(argumente.quellen) |
| 110 | |
| 111 | geaendert = normalisieren(eintraege) |
| 112 | if geaendert: |
| 113 | print(f' {geaendert} Zahlangaben zu Zeichenketten gemacht.') |
| 114 | |
| 115 | # Doppelte Begriffe zusammenfuehren waere geraten; hier faellt es auf. |
| 116 | gesehen: dict[str, int] = {} |
| 117 | for eintrag in eintraege: |
| 118 | begriff = eintrag.get('begriff') |
| 119 | if isinstance(begriff, str): |
| 120 | gesehen[begriff] = gesehen.get(begriff, 0) + 1 |
| 121 | doppelt = sorted(b for b, n in gesehen.items() if n > 1) |
| 122 | if doppelt: |
| 123 | print(f'\nFEHLER: mehrfach vorhanden: {", ".join(doppelt)}') |
| 124 | return 1 |
| 125 | |
| 126 | eintraege.sort(key=lambda e: sortierschluessel(str(e.get('begriff', '')))) |
| 127 | |
| 128 | gesetze = json.load(io.open(GESETZE, encoding='utf-8'))['gesetze'] |
| 129 | zitiert = { |
| 130 | f.get('gesetz') |
| 131 | for e in eintraege |
| 132 | for f in (e.get('fundstellen') or []) |
| 133 | if isinstance(f, dict) |
| 134 | } |
| 135 | |
| 136 | ergebnis = { |
| 137 | 'meta': { |
| 138 | 'version': 1, |
| 139 | 'stand': date.today().isoformat(), |
| 140 | 'hinweis': HINWEIS, |
| 141 | 'gesetzesstand': {k: gesetze[k]['stand'] for k in gesetze if k in zitiert}, |
| 142 | }, |
| 143 | 'eintraege': eintraege, |
| 144 | } |
| 145 | |
| 146 | with io.open(ZIEL, 'w', encoding='utf-8', newline='\n') as datei: |
| 147 | json.dump(ergebnis, datei, ensure_ascii=False, indent=1) |
| 148 | datei.write('\n') |
| 149 | |
| 150 | begriffe = sum(1 for e in eintraege if e.get('art') == 'begriff') |
| 151 | kuerzel = sum(1 for e in eintraege if e.get('art') == 'abkuerzung') |
| 152 | print(f'\n{len(eintraege)} Eintraege ({begriffe} Begriffe, {kuerzel} Abkuerzungen)') |
| 153 | print(f'geschrieben: {ZIEL.name}') |
| 154 | print('\nJetzt pruefen mit: python data-pipeline/pruefe_glossar.py') |
| 155 | return 0 |
| 156 | |
| 157 | |
| 158 | if __name__ == '__main__': |
| 159 | sys.stdout.reconfigure(encoding='utf-8', errors='replace') # type: ignore[union-attr] |
| 160 | raise SystemExit(main()) |