"""Nachschlagewerkzeug für den amtlichen Gesetzestext. Beim Schreiben der Erklärungen darf nichts aus dem Gedächtnis zitiert werden. Dieses Werkzeug macht das Nachschlagen so bequem, dass es keinen Grund gibt, es zu unterlassen. Grundlage ist `content/gesetze/index.json`, erzeugt aus dem amtlichen XML von gesetze-im-internet.de. Aufrufe ------- Eine Norm im Volltext:: python data-pipeline/gesetz.py WaffG "§ 12" python data-pipeline/gesetz.py AWaffV "§ 13" --absatz 2 python data-pipeline/gesetz.py StGB "§ 32" In allen Gesetzen nach einem Begriff suchen:: python data-pipeline/gesetz.py --suche "Anscheinswaffe" Nur in einem Gesetz suchen – das Kürzel steht **vor** der Fahne, es ist ein Stellungsargument und keine Option:: python data-pipeline/gesetz.py WaffG --suche "Schalldämpfer" Innerhalb einer Anlage suchen (Anlagen sind lang):: python data-pipeline/gesetz.py WaffG "Anlage 1" --suche Kurzwaffen Die Gliederung einer Norm anzeigen – genau die Angaben, die eine Fundstelle enthalten darf:: python data-pipeline/gesetz.py WaffG "§ 12" --gliederung Übersicht aller Gesetze und ihres Standes:: python data-pipeline/gesetz.py --gesetze """ from __future__ import annotations import argparse import io import json import re import sys from pathlib import Path WURZEL = Path(__file__).resolve().parent.parent INDEX = WURZEL / 'content' / 'gesetze' / 'index.json' #: Zeichen Umfeld je Fundstelle bei der Volltextsuche. UMFELD = 320 def daten() -> dict: if not INDEX.exists(): raise SystemExit( f'Index fehlt: {INDEX}\nErst erzeugen mit: python data-pipeline/gesetze_index.py' ) return json.load(io.open(INDEX, encoding='utf-8'))['gesetze'] def gesetze_auflisten(gesetze: dict) -> int: for kuerzel, gesetz in gesetze.items(): anlagen = sum(1 for n in gesetz['normen'].values() if n['ist_anlage']) paragrafen = len(gesetz['normen']) - anlagen print(f'{kuerzel:12} {gesetz["langtitel"]}') print(f'{"":12} {paragrafen} Paragrafen, {anlagen} Anlagen') print(f'{"":12} Stand: {gesetz["stand"]}') print(f'{"":12} {gesetz["quelle"]}') print() return 0 def norm_zeigen(gesetz: dict, kuerzel: str, name: str, absatz: str | None) -> int: norm = gesetz['normen'].get(name) if norm is None: aehnlich = [b for b in gesetz['normen'] if b.replace(' ', '') == name.replace(' ', '')] hinweis = f'\nGemeint war vielleicht: {aehnlich[0]}' if aehnlich else '' print(f'{name} gibt es im {kuerzel} nicht.{hinweis}') return 1 print(f'=== {kuerzel} {name} – {norm["titel"]} ===') print(f'Stand: {gesetz["stand"]}\n') if absatz is not None: eintrag = norm['absaetze'].get(absatz) if eintrag is None: vorhanden = ', '.join(norm['absaetze']) or 'keine' print(f'Abs. {absatz} gibt es nicht. Vorhanden: {vorhanden}') return 1 print(f'({absatz}) {eintrag["text"]}') if eintrag['gliederung']: print(f'\nGliederung: {gliederung_text(eintrag["gliederung"])}') return 0 print(norm['text']) return 0 def gliederung_text(gliederung: dict[str, list[str]]) -> str: """Gliederung als Aufzaehlung. Die meisten Absaetze zaehlen mit Nummern auf, einige unmittelbar mit Buchstaben (etwa § 3a Abs. 2 SprengG). Die Bezeichnung richtet sich deshalb nach der Marke, nicht nach der Ebene. """ teile = [] for marke, unter in gliederung.items(): if not marke: # Eine leere Marke ist ein Fehler im Index, kein Grund # abzubrechen. Bis Fassung 0.24.1 stand hier `marke[0]`, und # `gesetz.py BeschG "§ 11" --gliederung` endete mitten in der # Ausgabe mit einem IndexError – ausgerechnet das Werkzeug, das # zeigen soll, welche Angaben eine Fundstelle tragen darf. teile.append('(unlesbare Marke im Index)') continue art = 'Nr.' if marke[0].isdigit() else 'Buchst.' teile.append(f'{art} {marke}' + (f' ({", ".join(unter)})' if unter else '')) return '; '.join(teile) def gliederung_zeigen(gesetz: dict, kuerzel: str, name: str) -> int: """Zeigt, welche Angaben eine Fundstelle auf diese Norm enthalten darf.""" norm = gesetz['normen'].get(name) if norm is None: print(f'{name} gibt es im {kuerzel} nicht.') return 1 print(f'=== {kuerzel} {name} – {norm["titel"]} ===') if norm['ist_anlage']: print('Anlage: keine Absatzgliederung. In einer Fundstelle „stelle" verwenden,') print('z. B. { "norm": "Anlage 1", "stelle": "Abschnitt 1 Unterabschnitt 1 Nr. 1.1" }') return 0 if not norm['absaetze']: print('Keine Absatzgliederung – die Norm wird als Ganzes zitiert.') return 0 for nummer, absatz in norm['absaetze'].items(): marke = gliederung_text(absatz['gliederung']) if absatz['gliederung'] else '(ohne Nummern)' print(f' Abs. {nummer}: {marke}') return 0 def suchen(gesetze: dict, begriff: str, nur: str | None, in_norm: str | None) -> int: muster = re.compile(re.escape(begriff), re.IGNORECASE) treffer = 0 for kuerzel, gesetz in gesetze.items(): if nur is not None and kuerzel != nur: continue for name, norm in gesetz['normen'].items(): if in_norm is not None and name != in_norm: continue for fund in muster.finditer(norm['text']): anfang = max(0, fund.start() - UMFELD) ende = min(len(norm['text']), fund.end() + UMFELD) auszug = norm['text'][anfang:ende].replace('\n', ' | ') print(f'--- {kuerzel} {name} ({norm["titel"][:60]}) ---') print(f'...{auszug}...\n') treffer += 1 if treffer >= 40: print('(weitere Treffer nicht angezeigt – Suchbegriff eingrenzen)') return 0 if treffer == 0: print(f'„{begriff}" kommt nicht vor' + (f' im {nur}' if nur else ' in keinem der Gesetze')) print('Das ist ein belastbares Ergebnis: Was hier fehlt, steht nicht im Gesetz.') return 0 def main() -> int: # allow_abbrev=False: Ohne das loest argparse jede eindeutige Abkuerzung # einer langen Option auf. `--gesetz WaffG` wurde dadurch zu `--gesetze` # (auflisten) plus dem Stellungsargument `WaffG` - der Suchzweig lief nie # an, und der Aufruf endete mit einer Gesetzesliste und Exit-Code 0. Bis # Fassung 0.27.2 stand genau dieser Aufruf im Kopf dieser Datei, und # CLAUDE.md macht das Nachschlagen mit diesem Werkzeug zur Pflicht: Wer # der Anleitung folgte, hielt ein leeres Suchergebnis fuer ein Ergebnis. # Jetzt bricht ein unbekannter Optionsname mit einer Meldung ab. zerleger = argparse.ArgumentParser( description='Amtlichen Gesetzestext nachschlagen.', epilog='Ohne Nachschlagen keine Fundstelle.', allow_abbrev=False, ) zerleger.add_argument('gesetz', nargs='?', help='Kürzel, z. B. WaffG') zerleger.add_argument('norm', nargs='?', help='„§ 12" oder „Anlage 1"') zerleger.add_argument('--absatz', help='Nur diesen Absatz zeigen') zerleger.add_argument('--gliederung', action='store_true', help='Nur die Gliederung zeigen') zerleger.add_argument('--suche', help='Volltextsuche') zerleger.add_argument('--gesetze', action='store_true', help='Alle Gesetze auflisten') argumente = zerleger.parse_args() gesetze = daten() if argumente.gesetze: return gesetze_auflisten(gesetze) if argumente.suche is not None and argumente.gesetz is None: return suchen(gesetze, argumente.suche, None, None) if argumente.gesetz is None: zerleger.print_help() return 1 if argumente.gesetz not in gesetze: print(f'Unbekanntes Gesetz: {argumente.gesetz}') print(f'Bekannt sind: {", ".join(gesetze)}') return 1 gesetz = gesetze[argumente.gesetz] if argumente.suche is not None: return suchen(gesetze, argumente.suche, argumente.gesetz, argumente.norm) if argumente.norm is None: print(f'{argumente.gesetz} – {gesetz["langtitel"]}, Stand: {gesetz["stand"]}') print(f'\nNormen: {", ".join(gesetz["normen"])}') return 0 if argumente.gliederung: return gliederung_zeigen(gesetz, argumente.gesetz, argumente.norm) return norm_zeigen(gesetz, argumente.gesetz, argumente.norm, argumente.absatz) if __name__ == '__main__': sys.stdout.reconfigure(encoding='utf-8', errors='replace') # type: ignore[union-attr] raise SystemExit(main())