DIP-Drucksachen mit `herausgeber: 'BR'` (Bundesrat) haben Bundesländer als Antragsteller (z.B. SN, HE) statt Fraktionen. Variante b — explizite Behandlung statt nur ausschließen: - Drucksache-dataclass: neue Felder `is_bundesrat: bool`, `urheber_bundeslaender: list[str]`. Existierende Pfade unberührt. - BundestagAdapter._doc_to_drucksache: liest herausgeber + urheber-Liste, setzt Bundesländer-Codes (bezeichnung wie "SN") in urheber_bundeslaender. fraktionen bleibt leer fuer BR — verhindert dass Stimmverhalten-Aggregate verwirrt werden. - /api/search-landtag liefert is_bundesrat + urheber_bundeslaender im Response. - /api/analyze-drucksache (POST) lehnt BR-Drucksachen mit HTTP 400 + klarer Meldung ab statt crashen. - v2-Search-UI rendert grayen Bundesrat-Sticker mit BL-Codes statt Fraktionen, "Analysieren"-Button durch "nicht unterstuetzt" ersetzt. is_bundesrat_drucksache() in drucksache_typen.py als Format-Helper (N/M/JJ-Pattern) bleibt fuer Cases wo nur die Drucksache-ID ohne Adapter-Metadaten verfuegbar ist. Refs: #6 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
"""Drucksache-Typ-Normalisierung (#127).
|
|
|
|
Jeder Landtag hat eigene Bezeichnungen für Dokumenttypen. Dieses Modul
|
|
normalisiert sie auf einheitliche Kategorien und bestimmt ob eine
|
|
Drucksache abstimmbar ist (= GWÖ-Bewertung sinnvoll).
|
|
"""
|
|
|
|
# Normierte Kategorien
|
|
ANTRAG = "antrag"
|
|
GESETZENTWURF = "gesetzentwurf"
|
|
AENDERUNGSANTRAG = "aenderungsantrag"
|
|
DRINGLICHKEITSANTRAG = "dringlichkeitsantrag"
|
|
ENTSCHLIESSUNGSANTRAG = "entschliessungsantrag"
|
|
BESCHLUSSEMPFEHLUNG = "beschlussempfehlung"
|
|
KLEINE_ANFRAGE = "kleine_anfrage"
|
|
GROSSE_ANFRAGE = "grosse_anfrage"
|
|
UNTERRICHTUNG = "unterrichtung"
|
|
PETITION = "petition"
|
|
WAHLVORSCHLAG = "wahlvorschlag"
|
|
BERICHT = "bericht"
|
|
SONSTIGE = "sonstige"
|
|
|
|
ABSTIMMBARE_TYPEN = {
|
|
ANTRAG,
|
|
GESETZENTWURF,
|
|
AENDERUNGSANTRAG,
|
|
DRINGLICHKEITSANTRAG,
|
|
ENTSCHLIESSUNGSANTRAG,
|
|
}
|
|
|
|
# Übersetzungstabelle: Original-Typ (lowercase) → normierter Typ.
|
|
# Keys werden case-insensitive + substring-matched.
|
|
# Reihenfolge: spezifischere zuerst (z.B. "kleine anfrage" vor "anfrage").
|
|
_TYP_MAP = [
|
|
# Abstimmbar
|
|
("gesetzentwurf", GESETZENTWURF),
|
|
("änderungsantrag", AENDERUNGSANTRAG),
|
|
("aenderungsantrag", AENDERUNGSANTRAG),
|
|
("dringlichkeitsantrag", DRINGLICHKEITSANTRAG),
|
|
("entschließungsantrag", ENTSCHLIESSUNGSANTRAG),
|
|
("entschliessungsantrag", ENTSCHLIESSUNGSANTRAG),
|
|
("antrag gemäß", ANTRAG),
|
|
("antrag", ANTRAG),
|
|
# Nicht abstimmbar
|
|
("kleine anfrage", KLEINE_ANFRAGE),
|
|
("große anfrage", GROSSE_ANFRAGE),
|
|
("grosse anfrage", GROSSE_ANFRAGE),
|
|
("anfrage", KLEINE_ANFRAGE),
|
|
("beschlussempfehlung", BESCHLUSSEMPFEHLUNG),
|
|
("unterrichtung", UNTERRICHTUNG),
|
|
("bericht", BERICHT),
|
|
("mitteilung", UNTERRICHTUNG),
|
|
("vorlage", UNTERRICHTUNG),
|
|
("petition", PETITION),
|
|
("wahlvorschlag", WAHLVORSCHLAG),
|
|
("stellungnahme", SONSTIGE),
|
|
("drucksache", SONSTIGE),
|
|
]
|
|
|
|
|
|
def normalize_typ(original: str) -> str:
|
|
"""Normalisiert einen BL-spezifischen Typ-String auf eine Kategorie.
|
|
|
|
Case-insensitiv, Substring-Match, spezifischere Patterns zuerst.
|
|
"""
|
|
if not original:
|
|
return SONSTIGE
|
|
low = original.lower().strip()
|
|
for pattern, norm in _TYP_MAP:
|
|
if pattern in low:
|
|
return norm
|
|
return SONSTIGE
|
|
|
|
|
|
def ist_abstimmbar(typ_normiert: str) -> bool:
|
|
"""Prüft ob ein normierter Typ zur Abstimmung steht.
|
|
|
|
``sonstige`` wird durchgelassen (benefit of the doubt) — wenn der
|
|
Adapter den Typ nicht bestimmen kann (z.B. NRW liefert nur
|
|
"Drucksache"), wird der echte Check erst beim Analysieren gemacht
|
|
(aus dem Dokument-Text).
|
|
"""
|
|
return typ_normiert in ABSTIMMBARE_TYPEN or typ_normiert == SONSTIGE
|
|
|
|
|
|
def ist_abstimmbar_original(original: str) -> bool:
|
|
"""Convenience: prüft direkt am Original-Typ-String."""
|
|
return ist_abstimmbar(normalize_typ(original))
|
|
|
|
|
|
# Frage-Präfixe die typisch für Kleine Anfragen sind. Wird genutzt wenn der
|
|
# Adapter (z.B. NRW) den Typ nur als "Drucksache" liefert — wir versuchen
|
|
# anhand des Titels eine bessere Klassifikation, damit Search-Ergebnisse
|
|
# nicht voll mit nicht-abstimmbaren Anfragen sind.
|
|
_FRAGE_PRAEFIXE = (
|
|
"welche ", "wie viele ", "wieviel", "wie viel ", "wie hoch ", "wie ",
|
|
"wann ", "warum ", "weshalb ", "wo ", "wer ", "wie steht ", "wie weit ",
|
|
"ist es ", "ist der ", "ist die ", "ist das ", "sind ",
|
|
"trifft es ", "kann ", "wird ", "wieso ", "was ",
|
|
"hat ", "hat der ", "hat die ", "hat das ",
|
|
"haben ", "war ", "waren ",
|
|
)
|
|
|
|
|
|
def likely_kleine_anfrage_titel(title: str) -> bool:
|
|
"""Heuristik: erkennt Kleine Anfragen am Titel-Format.
|
|
|
|
Wenn der Titel mit einem typischen Frage-Präfix beginnt oder mit "?" endet,
|
|
behandeln wir die Drucksache als Kleine Anfrage. NRW-OPAL klassifiziert
|
|
alle Drucksachen als "Drucksache" → ohne diese Heuristik landen Anfragen
|
|
in den Search-Ergebnissen, was den User verwirrt (#149 Folge).
|
|
|
|
Args:
|
|
title: Drucksachen-Titel inkl. evtl. Nummer-Präfix wie "1Welche...".
|
|
|
|
Returns:
|
|
True wenn der Titel wie eine Kleine Anfrage aussieht.
|
|
"""
|
|
if not title:
|
|
return False
|
|
t = title.strip()
|
|
# Manche Adapter prefixen mit Nummerierung wie "1Welche..." — strippen
|
|
while t and (t[0].isdigit() or t[0] in " .-"):
|
|
t = t[1:]
|
|
t_low = t.lower()
|
|
if t_low.startswith(_FRAGE_PRAEFIXE):
|
|
return True
|
|
if t.rstrip().endswith("?"):
|
|
return True
|
|
return False
|
|
|
|
|
|
# Bundesrats-Drucksachen-Format erkennen.
|
|
# Bundestag: "21/9954" (Wahlperiode/Nummer, 2 Komponenten).
|
|
# Bundesrat: "186/3/26", "186/2/26" (Nummer/Sub/Jahr, 3 Komponenten).
|
|
import re as _re
|
|
|
|
_BUNDESRAT_PATTERN = _re.compile(r"^\d+/\d+/\d+$")
|
|
|
|
|
|
def is_bundesrat_drucksache(drucksache: str) -> bool:
|
|
"""Erkennt Bundesrats-Drucksachen am 3-Komponenten-Format ``N/M/JJ``.
|
|
|
|
Diese Drucksachen finden sich über die DIP-API (Bundestags-Backend
|
|
indexiert sie mit), haben aber **keine Fraktionen** als Antragsteller —
|
|
sondern Bundesländer. Der GWÖ-Analyzer erwartet Fraktionen und schlägt
|
|
bei Bundesrats-Drucksachen fehl. Folge: separate Behandlung im Such-
|
|
+ Analyse-Pfad.
|
|
"""
|
|
if not drucksache:
|
|
return False
|
|
return bool(_BUNDESRAT_PATTERN.match(drucksache.strip()))
|