waffensachkunde

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

/ data-pipeline gesetz.py

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