waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ data-pipeline glossar_zusammenfuehren.py

5,5 KB Rohdatei
data-pipeline/glossar_zusammenfuehren.py — 160 Zeilen
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())