"""Hessen (HE) — Beschlussprotokoll-Parser (#106 / #154, ADR 0009). **Limitierung:** Hessen publiziert nur kompakte Beschlussprotokolle (Tagesordnung + Status), KEINE Wortprotokolle mit Vote-Block. Daher liefert dieser Parser pro Drucksache: - Ergebnis-Status (angenommen/abgelehnt/ueberwiesen/...) - KEINE Fraktions-Vote-Detail (Vote-Listen bleiben leer) URL-Pattern (verifiziert WP21 Sitzungen 61, 62, 63): ``http://starweb.hessen.de/cache/hessen/landtag/Plenum/{wp}/Beschlussprotokoll_PL_{n}_{datum}.pdf`` Datum-Teil ``DD-MM-YYYY`` macht direkte URL-Vorhersage unmoeglich. Der Auto-Ingest-Cron muss die Index-Seite scrapen: ``https://starweb.hessen.de/starweb/LIS/Pd_Eingang.htm`` (analog HH). ## Format-Beispiel (verifiziert WP21 Sitzung 62) ``` 13. Dritte Lesung Gesetzentwurf Landesregierung Gesetz ueber die Feststellung des Haushaltsplans... – Drucks. 21/4047 zu Drucks. 21/3503 zu Drucks. 21/2971 – In dritter Lesung angenommen: Gesetz beschlossen ``` Pattern: - ``Drucks. {wp}/N`` (mit Punkt nach Drucks) - Naechstes Status-Wort: ``angenommen``, ``Abgelehnt``, ``Beschlussempfehlung angenommen`` - Vorangehende Lesungs-Phrase: ``In erster/zweiter/dritter Lesung`` ## Ergebnis-Mapping - ``angenommen`` → ergebnis="angenommen" - ``Abgelehnt`` → ergebnis="abgelehnt" - ``Nach (Aussprache|erster Lesung|zweiter Lesung) an [Ausschuss]`` → ergebnis="überwiesen" - ``Entgegengenommen``, ``Abgehalten`` → kind="info" (kein Vote-Beschluss) """ from __future__ import annotations import re from typing import Optional try: import fitz except ImportError: fitz = None ALLE_FRAKTIONEN_HE = ["CDU", "SPD", "GRÜNE", "AfD", "FDP", "LINKE"] # Anchor-Pattern: Drucks. {wp}/N + Status (mit beliebigem Text dazwischen) ENTRY_RE = re.compile( r"Drucks\.\s+(?P\d{1,2}/\d{2,5})" r"(?P[^–]{0,800}?)" r"–\s*(?P" r"(?:In (?:erster|zweiter|dritter) Lesung\s+)?angenommen" r"|(?:In (?:erster|zweiter|dritter) Lesung\s+)?Abgelehnt" r"|Beschlussempfehlung angenommen" r"|Beschlussempfehlung abgelehnt" r"|Nach (?:Aussprache|erster Lesung|zweiter Lesung|dritter Lesung) an [A-Z]+(?:\s*,\s*[A-Z]+)*" r"|Entgegengenommen(?:\s+und\s+besprochen)?" r"|Abgehalten" r"|Zur Kenntnis genommen" r")", re.DOTALL | re.IGNORECASE, ) def _classify_status(status: str) -> str: """Map HE-Status auf einheitliche ergebnis-Codes.""" s = status.lower() if "abgelehnt" in s: return "abgelehnt" if "nach" in s and "an " in s: return "überwiesen" if "angenommen" in s: return "angenommen" if "abgehalten" in s or "entgegengenommen" in s or "zur kenntnis genommen" in s: return "kenntnis" # eigene Kategorie ohne Vote-Bedeutung return "unbekannt" def _normalize_text(text: str) -> str: return re.sub(r"\s+", " ", text) def parse_protocol(pdf_path: str) -> list[dict]: """Parst HE-Beschlussprotokoll und liefert Status pro Drucksache. Vote-Listen bleiben leer (HE publiziert nur Status, kein Vote-Detail). """ if fitz is None: raise ImportError("PyMuPDF (fitz) ist erforderlich fuer den HE-Parser") doc = fitz.open(pdf_path) full = "".join(p.get_text() for p in doc) doc.close() full = _normalize_text(full) results = [] for m in ENTRY_RE.finditer(full): ds = m.group("ds") status = m.group("status").strip() ergebnis = _classify_status(status) if ergebnis in ("kenntnis", "unbekannt"): # Kein Vote-Beschluss — uebersprungen, sonst muellt es die DB voll continue results.append({ "drucksache": ds, "ergebnis": ergebnis, "einstimmig": False, # Beschlussprotokoll hat keine Vote-Detail "kind": "direct", "votes": {"ja": [], "nein": [], "enthaltung": []}, "anchor_pos": m.start(), "_he_raw_status": status, # debug-info, nicht persistiert }) # Dedup ueber (drucksache, anchor_pos) seen = set() deduped = [] for r in results: key = (r["drucksache"], r["anchor_pos"]) if key in seen: continue seen.add(key) deduped.append(r) return deduped