waffensachkunde

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

/ data-pipeline gesetz.py

8,6 KB Rohdatei
data-pipeline/gesetz.py — 230 Zeilen
1 """Nachschlagewerkzeug für den amtlichen Gesetzestext.
2
3 Beim Schreiben der Erklärungen darf nichts aus dem Gedächtnis zitiert werden.
4 Dieses Werkzeug macht das Nachschlagen so bequem, dass es keinen Grund gibt,
5 es zu unterlassen. Grundlage ist `content/gesetze/index.json`, erzeugt aus dem
6 amtlichen XML von gesetze-im-internet.de.
7
8 Aufrufe
9 -------
10 Eine Norm im Volltext::
11
12 python data-pipeline/gesetz.py WaffG "§ 12"
13 python data-pipeline/gesetz.py AWaffV "§ 13" --absatz 2
14 python data-pipeline/gesetz.py StGB "§ 32"
15
16 In allen Gesetzen nach einem Begriff suchen::
17
18 python data-pipeline/gesetz.py --suche "Anscheinswaffe"
19
20 Nur in einem Gesetz suchen – das Kürzel steht **vor** der Fahne, es ist ein
21 Stellungsargument und keine Option::
22
23 python data-pipeline/gesetz.py WaffG --suche "Schalldämpfer"
24
25 Innerhalb einer Anlage suchen (Anlagen sind lang)::
26
27 python data-pipeline/gesetz.py WaffG "Anlage 1" --suche Kurzwaffen
28
29 Die Gliederung einer Norm anzeigen – genau die Angaben, die eine Fundstelle
30 enthalten darf::
31
32 python data-pipeline/gesetz.py WaffG "§ 12" --gliederung
33
34 Übersicht aller Gesetze und ihres Standes::
35
36 python data-pipeline/gesetz.py --gesetze
37 """
38
39 from __future__ import annotations
40
41 import argparse
42 import io
43 import json
44 import re
45 import sys
46 from pathlib import Path
47
48 WURZEL = Path(__file__).resolve().parent.parent
49 INDEX = WURZEL / 'content' / 'gesetze' / 'index.json'
50
51 #: Zeichen Umfeld je Fundstelle bei der Volltextsuche.
52 UMFELD = 320
53
54
55 def daten() -> dict:
56 if not INDEX.exists():
57 raise SystemExit(
58 f'Index fehlt: {INDEX}\nErst erzeugen mit: python data-pipeline/gesetze_index.py'
59 )
60 return json.load(io.open(INDEX, encoding='utf-8'))['gesetze']
61
62
63 def gesetze_auflisten(gesetze: dict) -> int:
64 for kuerzel, gesetz in gesetze.items():
65 anlagen = sum(1 for n in gesetz['normen'].values() if n['ist_anlage'])
66 paragrafen = len(gesetz['normen']) - anlagen
67 print(f'{kuerzel:12} {gesetz["langtitel"]}')
68 print(f'{"":12} {paragrafen} Paragrafen, {anlagen} Anlagen')
69 print(f'{"":12} Stand: {gesetz["stand"]}')
70 print(f'{"":12} {gesetz["quelle"]}')
71 print()
72 return 0
73
74
75 def norm_zeigen(gesetz: dict, kuerzel: str, name: str, absatz: str | None) -> int:
76 norm = gesetz['normen'].get(name)
77 if norm is None:
78 aehnlich = [b for b in gesetz['normen'] if b.replace(' ', '') == name.replace(' ', '')]
79 hinweis = f'\nGemeint war vielleicht: {aehnlich[0]}' if aehnlich else ''
80 print(f'{name} gibt es im {kuerzel} nicht.{hinweis}')
81 return 1
82
83 print(f'=== {kuerzel} {name} – {norm["titel"]} ===')
84 print(f'Stand: {gesetz["stand"]}\n')
85
86 if absatz is not None:
87 eintrag = norm['absaetze'].get(absatz)
88 if eintrag is None:
89 vorhanden = ', '.join(norm['absaetze']) or 'keine'
90 print(f'Abs. {absatz} gibt es nicht. Vorhanden: {vorhanden}')
91 return 1
92 print(f'({absatz}) {eintrag["text"]}')
93 if eintrag['gliederung']:
94 print(f'\nGliederung: {gliederung_text(eintrag["gliederung"])}')
95 return 0
96
97 print(norm['text'])
98 return 0
99
100
101 def gliederung_text(gliederung: dict[str, list[str]]) -> str:
102 """Gliederung als Aufzaehlung.
103
104 Die meisten Absaetze zaehlen mit Nummern auf, einige unmittelbar mit
105 Buchstaben (etwa § 3a Abs. 2 SprengG). Die Bezeichnung richtet sich
106 deshalb nach der Marke, nicht nach der Ebene.
107 """
108 teile = []
109 for marke, unter in gliederung.items():
110 if not marke:
111 # Eine leere Marke ist ein Fehler im Index, kein Grund
112 # abzubrechen. Bis Fassung 0.24.1 stand hier `marke[0]`, und
113 # `gesetz.py BeschG "§ 11" --gliederung` endete mitten in der
114 # Ausgabe mit einem IndexError – ausgerechnet das Werkzeug, das
115 # zeigen soll, welche Angaben eine Fundstelle tragen darf.
116 teile.append('(unlesbare Marke im Index)')
117 continue
118 art = 'Nr.' if marke[0].isdigit() else 'Buchst.'
119 teile.append(f'{art} {marke}' + (f' ({", ".join(unter)})' if unter else ''))
120 return '; '.join(teile)
121
122
123 def gliederung_zeigen(gesetz: dict, kuerzel: str, name: str) -> int:
124 """Zeigt, welche Angaben eine Fundstelle auf diese Norm enthalten darf."""
125 norm = gesetz['normen'].get(name)
126 if norm is None:
127 print(f'{name} gibt es im {kuerzel} nicht.')
128 return 1
129
130 print(f'=== {kuerzel} {name} – {norm["titel"]} ===')
131 if norm['ist_anlage']:
132 print('Anlage: keine Absatzgliederung. In einer Fundstelle „stelle" verwenden,')
133 print('z. B. { "norm": "Anlage 1", "stelle": "Abschnitt 1 Unterabschnitt 1 Nr. 1.1" }')
134 return 0
135
136 if not norm['absaetze']:
137 print('Keine Absatzgliederung – die Norm wird als Ganzes zitiert.')
138 return 0
139
140 for nummer, absatz in norm['absaetze'].items():
141 marke = gliederung_text(absatz['gliederung']) if absatz['gliederung'] else '(ohne Nummern)'
142 print(f' Abs. {nummer}: {marke}')
143 return 0
144
145
146 def suchen(gesetze: dict, begriff: str, nur: str | None, in_norm: str | None) -> int:
147 muster = re.compile(re.escape(begriff), re.IGNORECASE)
148 treffer = 0
149
150 for kuerzel, gesetz in gesetze.items():
151 if nur is not None and kuerzel != nur:
152 continue
153 for name, norm in gesetz['normen'].items():
154 if in_norm is not None and name != in_norm:
155 continue
156 for fund in muster.finditer(norm['text']):
157 anfang = max(0, fund.start() - UMFELD)
158 ende = min(len(norm['text']), fund.end() + UMFELD)
159 auszug = norm['text'][anfang:ende].replace('\n', ' | ')
160 print(f'--- {kuerzel} {name} ({norm["titel"][:60]}) ---')
161 print(f'...{auszug}...\n')
162 treffer += 1
163 if treffer >= 40:
164 print('(weitere Treffer nicht angezeigt – Suchbegriff eingrenzen)')
165 return 0
166
167 if treffer == 0:
168 print(f'„{begriff}" kommt nicht vor' + (f' im {nur}' if nur else ' in keinem der Gesetze'))
169 print('Das ist ein belastbares Ergebnis: Was hier fehlt, steht nicht im Gesetz.')
170 return 0
171
172
173 def main() -> int:
174 # allow_abbrev=False: Ohne das loest argparse jede eindeutige Abkuerzung
175 # einer langen Option auf. `--gesetz WaffG` wurde dadurch zu `--gesetze`
176 # (auflisten) plus dem Stellungsargument `WaffG` - der Suchzweig lief nie
177 # an, und der Aufruf endete mit einer Gesetzesliste und Exit-Code 0. Bis
178 # Fassung 0.27.2 stand genau dieser Aufruf im Kopf dieser Datei, und
179 # CLAUDE.md macht das Nachschlagen mit diesem Werkzeug zur Pflicht: Wer
180 # der Anleitung folgte, hielt ein leeres Suchergebnis fuer ein Ergebnis.
181 # Jetzt bricht ein unbekannter Optionsname mit einer Meldung ab.
182 zerleger = argparse.ArgumentParser(
183 description='Amtlichen Gesetzestext nachschlagen.',
184 epilog='Ohne Nachschlagen keine Fundstelle.',
185 allow_abbrev=False,
186 )
187 zerleger.add_argument('gesetz', nargs='?', help='Kürzel, z. B. WaffG')
188 zerleger.add_argument('norm', nargs='?', help='„§ 12" oder „Anlage 1"')
189 zerleger.add_argument('--absatz', help='Nur diesen Absatz zeigen')
190 zerleger.add_argument('--gliederung', action='store_true', help='Nur die Gliederung zeigen')
191 zerleger.add_argument('--suche', help='Volltextsuche')
192 zerleger.add_argument('--gesetze', action='store_true', help='Alle Gesetze auflisten')
193 argumente = zerleger.parse_args()
194
195 gesetze = daten()
196
197 if argumente.gesetze:
198 return gesetze_auflisten(gesetze)
199
200 if argumente.suche is not None and argumente.gesetz is None:
201 return suchen(gesetze, argumente.suche, None, None)
202
203 if argumente.gesetz is None:
204 zerleger.print_help()
205 return 1
206
207 if argumente.gesetz not in gesetze:
208 print(f'Unbekanntes Gesetz: {argumente.gesetz}')
209 print(f'Bekannt sind: {", ".join(gesetze)}')
210 return 1
211
212 gesetz = gesetze[argumente.gesetz]
213
214 if argumente.suche is not None:
215 return suchen(gesetze, argumente.suche, argumente.gesetz, argumente.norm)
216
217 if argumente.norm is None:
218 print(f'{argumente.gesetz} – {gesetz["langtitel"]}, Stand: {gesetz["stand"]}')
219 print(f'\nNormen: {", ".join(gesetz["normen"])}')
220 return 0
221
222 if argumente.gliederung:
223 return gliederung_zeigen(gesetz, argumente.gesetz, argumente.norm)
224
225 return norm_zeigen(gesetz, argumente.gesetz, argumente.norm, argumente.absatz)
226
227
228 if __name__ == '__main__':
229 sys.stdout.reconfigure(encoding='utf-8', errors='replace') # type: ignore[union-attr]
230 raise SystemExit(main())