gwoe-antragspruefer/app/programme.py

518 lines
22 KiB
Python
Raw Normal View History

"""Zentrale Programm-Registry — alle politischen Programm-Dokumente
(Wahlprogramme, Bundes-Grundsatzprogramme, Landes-Grundsatzprogramme),
historisch und aktuell.
Single Source of Truth für:
- ``embeddings.py`` (Indexer liest die PDFs aus dieser Liste)
- ``wahlprogramme.py`` (Compat-Shim, leitet die alte BL/Partei-API hierher)
- ``analyzer.py`` (sucht das zum Antrag passende Wahlprogramm)
- UI (zeigt Geltungszeitraum + zugeordnete Regierung pro Programm)
Siehe ``app/legislaturen.py`` für Wahlperioden + Regierungen.
"""
from __future__ import annotations
from pathlib import Path
from typing import Literal, Optional, TypedDict
# ─────────────────────────────────────────────────────────────────────────────
# Typ
# ─────────────────────────────────────────────────────────────────────────────
ProgrammTyp = Literal[
"wahlprogramm", # zur Wahl beschlossen, gilt für 1 Legislatur
"grundsatzprogramm-bund", # bundesweites Grundsatzprogramm
"grundsatzprogramm-land", # landesspezifisches Grundsatzprogramm
]
class Programm(TypedDict, total=False):
"""Single source of truth für ein politisches Programm-Dokument.
Pflichtfelder: id, titel, name, typ, partei, gueltig_ab, pdf, seiten.
Optional: bundesland, beschluss, wahl, wp, gueltig_bis, hinweis.
"""
id: str # eindeutiger Schlüssel, z.B. "cdu-nrw-2022"
titel: str # offizieller Titel ("Machen, worauf es ankommt")
name: str # voll-qualifiziert für Citation (z.B. "CDU NRW Wahlprogramm 2022")
typ: ProgrammTyp
partei: str # normalisierter Schlüssel (CDU, GRÜNE, FREIE WÄHLER, BSW, …)
bundesland: Optional[str] # BL-Code; None nur bei reinen Bundesgrundsatzprogrammen
beschluss: Optional[str] # ISO YYYY-MM-DD; bei grundsatz: Parteitags-Beschluss
wahl: Optional[str] # ISO YYYY-MM-DD; nur typ=wahlprogramm: Wahltag
wp: Optional[int] # Legislatur-Nummer; nur typ=wahlprogramm
gueltig_ab: str # ISO; bei wahl: regierungsbildung; bei grundsatz: beschluss
gueltig_bis: Optional[str] # ISO; None = Programm ist aktuell gültig
pdf: str # Dateiname in static/referenzen/
seiten: int
hinweis: Optional[str] # freier Text, z.B. "BSW hat kein Grundsatzprogramm — Wahlprogramm dient als Hauptquelle"
REFERENZEN_PATH = Path(__file__).parent / "static" / "referenzen"
KONTEXT_PATH = Path(__file__).parent / "kontext"
# ─────────────────────────────────────────────────────────────────────────────
# Programm-Registry
# Die Daten werden aus der bisherigen ``embeddings.PROGRAMME`` und
# ``wahlprogramme.WAHLPROGRAMME`` migriert. Historische Wahlprogramme +
# Landesgrundsatzprogramme werden ergänzt sobald die Recherche fertig ist.
# ─────────────────────────────────────────────────────────────────────────────
# Aktuelle Programme — gefüllt durch ``_register_initial_data()`` weiter unten,
# damit der Migrations-Pfad an einer Stelle zu sehen ist.
PROGRAMME: dict[str, Programm] = {}
# ─────────────────────────────────────────────────────────────────────────────
# Helper-API
# ─────────────────────────────────────────────────────────────────────────────
def _date_in_range(datum: str, ab: str, bis: Optional[str]) -> bool:
"""Liefert True, wenn ``datum`` (ISO) in [ab, bis) liegt."""
if datum < ab:
return False
if bis is None:
return True
return datum < bis
def get_programm(programm_id: str) -> Optional[Programm]:
"""Lookup nach ID."""
return PROGRAMME.get(programm_id)
def aktuelles_wahlprogramm(bundesland: str, partei: str) -> Optional[Programm]:
"""Aktuell gültiges Wahlprogramm einer Partei in einem Bundesland.
Es kann nur eines aktuell sein (gueltig_bis=None und typ=wahlprogramm).
"""
for prog in PROGRAMME.values():
if (
prog.get("typ") == "wahlprogramm"
and prog.get("bundesland") == bundesland
and prog.get("partei") == partei
and prog.get("gueltig_bis") is None
):
return prog
return None
def wahlprogramm_zum_zeitpunkt(
bundesland: str, partei: str, datum: str,
) -> Optional[Programm]:
"""Welches Wahlprogramm dieser Partei galt im Bundesland am gegebenen Datum?
``datum`` ist ISO-Datum. Es wird das Programm zurückgegeben, dessen
Geltungszeitraum [gueltig_ab, gueltig_bis) das Datum enthält.
Rückgabe ``None``, wenn die Partei zu dem Zeitpunkt nicht im Schema
erfasst ist (oder das Bundesland nicht).
"""
for prog in PROGRAMME.values():
if (
prog.get("typ") == "wahlprogramm"
and prog.get("bundesland") == bundesland
and prog.get("partei") == partei
and _date_in_range(datum, prog["gueltig_ab"], prog.get("gueltig_bis"))
):
return prog
return None
def grundsatzprogramm_zum_zeitpunkt(
partei: str,
datum: str,
bundesland: Optional[str] = None,
) -> Optional[Programm]:
"""Welches Grundsatzprogramm der Partei galt am gegebenen Datum?
Wenn ``bundesland`` gesetzt ist, wird zuerst nach einem
Landes-Grundsatzprogramm gesucht; falls keines existiert, fällt die
Suche auf das Bundes-Grundsatzprogramm zurück.
Wenn ``bundesland`` ``None`` ist, wird nur nach Bundes-Grundsatz gesucht.
"""
if bundesland is not None:
# Erst Land suchen
for prog in PROGRAMME.values():
if (
prog.get("typ") == "grundsatzprogramm-land"
and prog.get("partei") == partei
and prog.get("bundesland") == bundesland
and _date_in_range(datum, prog["gueltig_ab"], prog.get("gueltig_bis"))
):
return prog
# Bund als Fallback / oder primär
for prog in PROGRAMME.values():
if (
prog.get("typ") == "grundsatzprogramm-bund"
and prog.get("partei") == partei
and _date_in_range(datum, prog["gueltig_ab"], prog.get("gueltig_bis"))
):
return prog
return None
def parteien_mit_wahlprogramm(bundesland: str) -> list[str]:
"""Parteien mit einem aktuell gültigen Wahlprogramm in dem Bundesland.
Reihenfolge: nach Eintrags-Reihenfolge in PROGRAMME (deterministic).
"""
seen: list[str] = []
for prog in PROGRAMME.values():
if (
prog.get("typ") == "wahlprogramm"
and prog.get("bundesland") == bundesland
and prog.get("gueltig_bis") is None
):
partei = prog["partei"]
if partei not in seen:
seen.append(partei)
return seen
def alle_versionen(bundesland: str, partei: str) -> list[Programm]:
"""Alle Wahlprogramm-Versionen dieser Partei im Bundesland, sortiert
nach ``gueltig_ab`` aufsteigend."""
versions = [
prog for prog in PROGRAMME.values()
if prog.get("typ") == "wahlprogramm"
and prog.get("bundesland") == bundesland
and prog.get("partei") == partei
]
versions.sort(key=lambda p: p["gueltig_ab"])
return versions
# ─────────────────────────────────────────────────────────────────────────────
# Initiale Daten — wird über separate Daten-Datei eingespielt.
# Während des Migrationsfensters wird ``embeddings.PROGRAMME`` und
# ``wahlprogramme.WAHLPROGRAMME`` automatisch in PROGRAMME überführt.
# ─────────────────────────────────────────────────────────────────────────────
def _register(prog: Programm) -> None:
"""Add a Programm to the registry, validating uniqueness of id."""
if prog["id"] in PROGRAMME:
raise ValueError(f"Programm-id collision: {prog['id']}")
PROGRAMME[prog["id"]] = prog
# ─────────────────────────────────────────────────────────────────────────────
# Zusätzliche Programme — Reserve für Programme, die NICHT aus
# embeddings.PROGRAMME oder WAHLPROGRAMME migriert werden können (z.B.
# zukünftig: historische Vorgänger-Wahlprogramme aus früheren Legislaturen,
# falls deren PDFs eingebunden werden ohne dass embeddings.PROGRAMME sie
# kennt).
# ─────────────────────────────────────────────────────────────────────────────
_ADDITIONAL_PROGRAMME: list[Programm] = []
# Historische Skelett-Einträge (PDFs jetzt in embeddings.PROGRAMME) —
# wurden in einer früheren Iteration manuell als pdf-pending markiert,
# bevor die Beschaffung lief. Aktuelle Daten leben in embeddings.PROGRAMME
# und werden über _migrate_from_legacy() automatisch übernommen.
_ARCHIVED_SKELETONS: list[Programm] = [
# ─── CSU Bayern: aktualisiertes Grundsatzprogramm 2023 ───────────────
# "Die Ordnung 2016" (in embeddings.PROGRAMME als Bundes-Grundsatz)
# wird durch das 2023er ersetzt — wir tragen 2023 als aktuelles ein und
# setzen das 2016er auf gueltig_bis. Funktionell ist es ein Landes-
# grundsatzprogramm, weil CSU nur in Bayern existiert.
{
"id": "csu-grundsatz-2023",
"titel": "Für ein neues Miteinander — Grundsatzprogramm der CSU",
"name": "CSU Grundsatzprogramm 2023",
"typ": "grundsatzprogramm-land",
"partei": "CSU",
"bundesland": "BY",
"beschluss": "2023-05-06",
"wahl": None,
"wp": None,
"gueltig_ab": "2023-05-06",
"gueltig_bis": None,
"pdf": "csu-grundsatz-2023.pdf",
"seiten": 0, # nach Download via PyMuPDF setzen
"hinweis": "pdf-pending",
},
# ─── CDU NRW Landesgrundsatzprogramm 2015 ─────────────────────────────
{
"id": "cdu-grundsatz-nrw-2015",
"titel": "Aufstieg, Sicherheit, Perspektive — Das Nordrhein-Westfalen-Programm",
"name": "CDU NRW Grundsatzprogramm 2015",
"typ": "grundsatzprogramm-land",
"partei": "CDU",
"bundesland": "NRW",
"beschluss": "2015-06-13",
"wahl": None,
"wp": None,
"gueltig_ab": "2015-06-13",
"gueltig_bis": None,
"pdf": "cdu-grundsatz-nrw-2015.pdf",
"seiten": 0,
"hinweis": "pdf-pending",
},
# ─── CDU Sachsen Landesgrundsatzprogramm 2023 ─────────────────────────
{
"id": "cdu-grundsatz-sn-2023",
"titel": "Zukunftsplan für Sachsen",
"name": "CDU Sachsen Grundsatzprogramm 2023",
"typ": "grundsatzprogramm-land",
"partei": "CDU",
"bundesland": "SN",
"beschluss": "2023-11-20",
"wahl": None,
"wp": None,
"gueltig_ab": "2023-11-20",
"gueltig_bis": None,
"pdf": "cdu-grundsatz-sn-2023.pdf",
"seiten": 0,
"hinweis": "pdf-pending",
},
# ─── CDU Sachsen-Anhalt Landesgrundsatzprogramm 2023 ──────────────────
{
"id": "cdu-grundsatz-lsa-2023",
"titel": "Sachsen-Anhalt. Unsere Verantwortung. Unsere Zukunft.",
"name": "CDU Sachsen-Anhalt Grundsatzprogramm 2023",
"typ": "grundsatzprogramm-land",
"partei": "CDU",
"bundesland": "LSA",
"beschluss": "2023-09-30",
"wahl": None,
"wp": None,
"gueltig_ab": "2023-09-30",
"gueltig_bis": None,
"pdf": "cdu-grundsatz-lsa-2023.pdf",
"seiten": 0,
"hinweis": "pdf-pending",
},
# ─── SSW Schleswig-Holstein Rahmenprogramm 2016 ────────────────────────
# SSW ist ausschließlich in SH aktiv (Minderheitenpartei der dänisch-
# friesischen Volksgruppe). Das Rahmenprogramm tritt funktional an die
# Stelle eines Grundsatzprogramms.
{
"id": "ssw-grundsatz-sh-2016",
"titel": "SSW Rahmenprogramm",
"name": "SSW Rahmenprogramm 2016",
"typ": "grundsatzprogramm-land",
"partei": "SSW",
"bundesland": "SH",
"beschluss": "2016-04-16",
"wahl": None,
"wp": None,
"gueltig_ab": "2016-04-16",
"gueltig_bis": None,
"pdf": "ssw-grundsatz-sh-2016.pdf",
"seiten": 0,
"hinweis": "pdf-pending",
},
# ─── FREIE WÄHLER Bundesvereinigung Grundsatzprogramm ─────────────────
# FW sind nicht im Bundestag (BTW 2025 unter 5%), aber im Landtag in
# Bayern (Regierung) und Rheinland-Pfalz (Opposition). Bundesgrundsatz-
# programm gilt als bundesweite Referenz für alle Landesverbände.
{
"id": "fw-grundsatz-2012",
"titel": "FREIE WÄHLER Bundesgrundsatzprogramm",
"name": "FREIE WÄHLER Bundesgrundsatzprogramm",
"typ": "grundsatzprogramm-bund",
"partei": "FREIE WÄHLER",
"bundesland": None,
"beschluss": "2012-02-25", # erstes Bundesgrundsatzprogramm; mehrfach fortgeschrieben
"wahl": None,
"wp": None,
"gueltig_ab": "2012-02-25",
"gueltig_bis": None,
"pdf": "fw-grundsatz.pdf",
"seiten": 0,
"hinweis": "pdf-pending — FREIE WÄHLER sind nicht im Bundestag, "
"Programm gilt als bundesweite Referenz für FW Bayern + FW Rheinland-Pfalz",
},
]
def _migrate_from_legacy() -> None:
"""Migriere bestehende Daten aus ``wahlprogramme.WAHLPROGRAMME`` und
``embeddings.PROGRAMME`` in die neue Registry. Wird einmal beim
Modul-Import aufgerufen.
Reihenfolge:
1. Wahlprogramme aus WAHLPROGRAMME (autoritative Quelle für regierungs-
gebundene Geltungsdaten).
2. Grundsatzprogramme aus embeddings.PROGRAMME (typ=parteiprogramm).
3. _ADDITIONAL_PROGRAMME (neue Daten Landesgrundsatz, ggf. Updates).
Update-Logik: Wenn ein _ADDITIONAL Programm denselben ``partei``,
``bundesland`` und Typ wie ein bestehendes hat und neueres
``gueltig_ab`` besitzt, wird das Vorgänger-``gueltig_bis`` rückwirkend
auf das ``gueltig_ab`` des neuen gesetzt.
"""
# Zirkuläre Imports vermeiden — lazy import beim Migrationszeitpunkt.
from .wahlprogramme import WAHLPROGRAMME
from .embeddings import PROGRAMME as _EMB_PROGRAMME
feat(programme): Wahlprogramm-Geltung beginnt am Wahltag (B1+B2) Antwort auf B1 + B2 aus der Roadmap: - B1: Antrag VOR Regierungsbildung (z.B. NRW WP18-Antrag im Mai 2022, vor Vereidigung Wuest II am 29.06.2022) bekommt jetzt das passende Wahlprogramm zurueck — der Geltungsbeginn ist der Wahltag, nicht die Vereidigung. - B2: Opposition vs. Regierung wird einheitlich behandelt. Die fruehere Logik "Geltung ab Regierungsbildung" war fuer Regierungsfraktionen intuitiv (Koalitionsvertrag wird zu Politik), fuer Opposition aber willkuerlich. Programme werden zur Wahl beschlossen und sind Wahlversprechen ab dem Tag der Wahl. Implementation in programme._migrate_from_legacy: - gueltig_ab = aktuelle_legislatur(bl)["wahltermin"] (Fallback auf altes "regierungsbildung" fuer rueckwaerts-kompatible Eintraege) - ``wahl``-Feld auf Wahltag gesetzt - ``wp``-Feld aus aktuelle_legislatur ergaenzt Das ``regierungsbildung``-Feld in WAHLPROGRAMME bleibt erhalten und versorgt den Bewertungs-Kontext-Block weiterhin mit dem Anzeige-Wert "Regierung zur Antragszeit" (per legislaturen.regierung_zum_zeitpunkt laeuft das primaer ueber legislaturen.REGIERUNGEN). UI-Effekt: im Antrag-Detail liest sich z.B. ein BUND-Eintrag jetzt "gueltig seit 2025-02-23, 60 S." (BTW-Wahltag) statt "2025-05-06" (Vereidigung Merz I). Tests: 115 gruen (test_programme + test_legislaturen + test_wahlprogramme + test_embeddings). Tests test_bund_btw_2025_in_uebergangsphase und test_bund_btw_2025_vor_wahl neu, decken die geaenderte Geltungs-Logik explizit ab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:17:08 +02:00
from .legislaturen import aktuelle_legislatur
# Schritt 1: Wahlprogramme aus WAHLPROGRAMME.
# Geltungsbeginn = Wahltag (siehe ADR 0013 Folge-Entscheidung B1+B2):
# Programme werden zur Wahl beschlossen; Opposition muss nicht auf eine
# Regierungsbildung warten, an der sie nicht beteiligt ist. Der
# ``regierungsbildung``-Wert aus WAHLPROGRAMME bleibt erhalten und wird
# vom Bewertungs-Kontext-Block separat angezeigt ("Regierung zur
# Antragszeit"), aber die Programm-Geltung selbst startet am Wahltag.
for bundesland, parteien in WAHLPROGRAMME.items():
feat(programme): Wahlprogramm-Geltung beginnt am Wahltag (B1+B2) Antwort auf B1 + B2 aus der Roadmap: - B1: Antrag VOR Regierungsbildung (z.B. NRW WP18-Antrag im Mai 2022, vor Vereidigung Wuest II am 29.06.2022) bekommt jetzt das passende Wahlprogramm zurueck — der Geltungsbeginn ist der Wahltag, nicht die Vereidigung. - B2: Opposition vs. Regierung wird einheitlich behandelt. Die fruehere Logik "Geltung ab Regierungsbildung" war fuer Regierungsfraktionen intuitiv (Koalitionsvertrag wird zu Politik), fuer Opposition aber willkuerlich. Programme werden zur Wahl beschlossen und sind Wahlversprechen ab dem Tag der Wahl. Implementation in programme._migrate_from_legacy: - gueltig_ab = aktuelle_legislatur(bl)["wahltermin"] (Fallback auf altes "regierungsbildung" fuer rueckwaerts-kompatible Eintraege) - ``wahl``-Feld auf Wahltag gesetzt - ``wp``-Feld aus aktuelle_legislatur ergaenzt Das ``regierungsbildung``-Feld in WAHLPROGRAMME bleibt erhalten und versorgt den Bewertungs-Kontext-Block weiterhin mit dem Anzeige-Wert "Regierung zur Antragszeit" (per legislaturen.regierung_zum_zeitpunkt laeuft das primaer ueber legislaturen.REGIERUNGEN). UI-Effekt: im Antrag-Detail liest sich z.B. ein BUND-Eintrag jetzt "gueltig seit 2025-02-23, 60 S." (BTW-Wahltag) statt "2025-05-06" (Vereidigung Merz I). Tests: 115 gruen (test_programme + test_legislaturen + test_wahlprogramme + test_embeddings). Tests test_bund_btw_2025_in_uebergangsphase und test_bund_btw_2025_vor_wahl neu, decken die geaenderte Geltungs-Logik explizit ab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:17:08 +02:00
# Wahltag aus legislaturen lookup (aktuelle WP des BL).
leg = aktuelle_legislatur(bundesland)
wahltag = leg.get("wahltermin") if leg else None
for partei, info in parteien.items():
# ID ableiten aus PDF-Stem
pid = info["file"].rsplit(".", 1)[0]
if info.get("ist_grundsatz"):
# Bundes-Grundsatzprogramm; Eintrag erfolgt in Schritt 2.
continue
feat(programme): Wahlprogramm-Geltung beginnt am Wahltag (B1+B2) Antwort auf B1 + B2 aus der Roadmap: - B1: Antrag VOR Regierungsbildung (z.B. NRW WP18-Antrag im Mai 2022, vor Vereidigung Wuest II am 29.06.2022) bekommt jetzt das passende Wahlprogramm zurueck — der Geltungsbeginn ist der Wahltag, nicht die Vereidigung. - B2: Opposition vs. Regierung wird einheitlich behandelt. Die fruehere Logik "Geltung ab Regierungsbildung" war fuer Regierungsfraktionen intuitiv (Koalitionsvertrag wird zu Politik), fuer Opposition aber willkuerlich. Programme werden zur Wahl beschlossen und sind Wahlversprechen ab dem Tag der Wahl. Implementation in programme._migrate_from_legacy: - gueltig_ab = aktuelle_legislatur(bl)["wahltermin"] (Fallback auf altes "regierungsbildung" fuer rueckwaerts-kompatible Eintraege) - ``wahl``-Feld auf Wahltag gesetzt - ``wp``-Feld aus aktuelle_legislatur ergaenzt Das ``regierungsbildung``-Feld in WAHLPROGRAMME bleibt erhalten und versorgt den Bewertungs-Kontext-Block weiterhin mit dem Anzeige-Wert "Regierung zur Antragszeit" (per legislaturen.regierung_zum_zeitpunkt laeuft das primaer ueber legislaturen.REGIERUNGEN). UI-Effekt: im Antrag-Detail liest sich z.B. ein BUND-Eintrag jetzt "gueltig seit 2025-02-23, 60 S." (BTW-Wahltag) statt "2025-05-06" (Vereidigung Merz I). Tests: 115 gruen (test_programme + test_legislaturen + test_wahlprogramme + test_embeddings). Tests test_bund_btw_2025_in_uebergangsphase und test_bund_btw_2025_vor_wahl neu, decken die geaenderte Geltungs-Logik explizit ab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:17:08 +02:00
# Geltungsbeginn: bevorzugt expliziter ``wahltag``-Eintrag
# (für historische Programme aus Block 2 nötig), Fallback
# auf aktuellen Wahltag der Legislatur, letzter Fallback auf
# ``regierungsbildung`` (rückwärts-kompatibel).
gueltig_ab = (
info.get("wahltag")
or wahltag
or info.get("regierungsbildung")
or "1900-01-01"
)
prog: Programm = {
"id": pid,
"titel": info.get("titel", ""),
"name": f"{info.get('partei', partei)} Wahlprogramm {info.get('jahr', '')}".strip(),
"typ": "wahlprogramm",
"partei": partei,
"bundesland": bundesland,
"beschluss": None,
feat(programme): Wahlprogramm-Geltung beginnt am Wahltag (B1+B2) Antwort auf B1 + B2 aus der Roadmap: - B1: Antrag VOR Regierungsbildung (z.B. NRW WP18-Antrag im Mai 2022, vor Vereidigung Wuest II am 29.06.2022) bekommt jetzt das passende Wahlprogramm zurueck — der Geltungsbeginn ist der Wahltag, nicht die Vereidigung. - B2: Opposition vs. Regierung wird einheitlich behandelt. Die fruehere Logik "Geltung ab Regierungsbildung" war fuer Regierungsfraktionen intuitiv (Koalitionsvertrag wird zu Politik), fuer Opposition aber willkuerlich. Programme werden zur Wahl beschlossen und sind Wahlversprechen ab dem Tag der Wahl. Implementation in programme._migrate_from_legacy: - gueltig_ab = aktuelle_legislatur(bl)["wahltermin"] (Fallback auf altes "regierungsbildung" fuer rueckwaerts-kompatible Eintraege) - ``wahl``-Feld auf Wahltag gesetzt - ``wp``-Feld aus aktuelle_legislatur ergaenzt Das ``regierungsbildung``-Feld in WAHLPROGRAMME bleibt erhalten und versorgt den Bewertungs-Kontext-Block weiterhin mit dem Anzeige-Wert "Regierung zur Antragszeit" (per legislaturen.regierung_zum_zeitpunkt laeuft das primaer ueber legislaturen.REGIERUNGEN). UI-Effekt: im Antrag-Detail liest sich z.B. ein BUND-Eintrag jetzt "gueltig seit 2025-02-23, 60 S." (BTW-Wahltag) statt "2025-05-06" (Vereidigung Merz I). Tests: 115 gruen (test_programme + test_legislaturen + test_wahlprogramme + test_embeddings). Tests test_bund_btw_2025_in_uebergangsphase und test_bund_btw_2025_vor_wahl neu, decken die geaenderte Geltungs-Logik explizit ab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:17:08 +02:00
"wahl": gueltig_ab,
"wp": leg.get("wp") if leg else None,
"gueltig_ab": gueltig_ab,
"gueltig_bis": info.get("regierungsende"),
"pdf": info["file"],
"seiten": int(info.get("seiten", 0)),
"hinweis": None,
}
if pid not in PROGRAMME:
PROGRAMME[pid] = prog
# Schritt 2: Grundsatzprogramme aus embeddings.PROGRAMME.
# typ=parteiprogramm: ohne ``bundesland`` → Bundes-Grundsatz, mit
# ``bundesland`` → Landes-Grundsatzprogramm. Beide funktional gleich,
# nur unterscheidet sich der Geltungsbereich.
for pid, info in _EMB_PROGRAMME.items():
if info.get("typ") != "parteiprogramm":
continue # Wahlprogramme schon in Schritt 1
if pid in PROGRAMME:
continue
bl = info.get("bundesland")
prog2: Programm = {
"id": pid,
"titel": info.get("name", ""),
"name": info.get("name", ""),
"typ": "grundsatzprogramm-land" if bl else "grundsatzprogramm-bund",
"partei": info.get("partei", ""),
"bundesland": bl,
"beschluss": info.get("gueltig_ab"),
"wahl": None,
"wp": None,
"gueltig_ab": info.get("gueltig_ab") or "1900-01-01",
"gueltig_bis": info.get("gueltig_bis"),
"pdf": info.get("pdf", ""),
"seiten": 0,
"hinweis": None,
}
PROGRAMME[pid] = prog2
# Schritt 3: _ADDITIONAL_PROGRAMME — mit Vorgänger-bis-Update
for prog3 in _ADDITIONAL_PROGRAMME:
# Setze gueltig_bis von Vorgänger-Programmen rückwirkend.
for existing in list(PROGRAMME.values()):
if (
existing.get("typ") in ("grundsatzprogramm-bund",
"grundsatzprogramm-land")
and existing.get("partei") == prog3.get("partei")
and existing.get("bundesland") == prog3.get("bundesland")
and existing.get("gueltig_bis") is None
and existing.get("gueltig_ab", "9999-99-99") < prog3["gueltig_ab"]
):
# Wir mutieren ein TypedDict in der Registry — das ist OK,
# weil _migrate_from_legacy() einmal beim Import läuft.
existing["gueltig_bis"] = prog3["gueltig_ab"] # type: ignore[typeddict-item]
if prog3["id"] not in PROGRAMME:
PROGRAMME[prog3["id"]] = prog3
# Lazy initialisierung — erst beim ersten echten Zugriff. Dadurch vermeidet
# das Modul Import-Reihenfolge-Probleme mit ``embeddings.py`` (das viel mehr
# Initialisierung braucht: openai, fitz, etc.).
_INITIALIZED = False
def _ensure_initialized() -> None:
global _INITIALIZED
if _INITIALIZED:
return
_migrate_from_legacy()
_INITIALIZED = True
# Patch helper-API to ensure init runs on first call.
_get_programm = get_programm
_aktuelles_wahlprogramm = aktuelles_wahlprogramm
_wahlprogramm_zum_zeitpunkt = wahlprogramm_zum_zeitpunkt
_grundsatzprogramm_zum_zeitpunkt = grundsatzprogramm_zum_zeitpunkt
_parteien_mit_wahlprogramm = parteien_mit_wahlprogramm
_alle_versionen = alle_versionen
def get_programm(programm_id: str) -> Optional[Programm]: # type: ignore[no-redef]
_ensure_initialized()
return _get_programm(programm_id)
def aktuelles_wahlprogramm(bundesland: str, partei: str) -> Optional[Programm]: # type: ignore[no-redef]
_ensure_initialized()
return _aktuelles_wahlprogramm(bundesland, partei)
def wahlprogramm_zum_zeitpunkt( # type: ignore[no-redef]
bundesland: str, partei: str, datum: str,
) -> Optional[Programm]:
_ensure_initialized()
return _wahlprogramm_zum_zeitpunkt(bundesland, partei, datum)
def grundsatzprogramm_zum_zeitpunkt( # type: ignore[no-redef]
partei: str, datum: str, bundesland: Optional[str] = None,
) -> Optional[Programm]:
_ensure_initialized()
return _grundsatzprogramm_zum_zeitpunkt(partei, datum, bundesland)
def parteien_mit_wahlprogramm(bundesland: str) -> list[str]: # type: ignore[no-redef]
_ensure_initialized()
return _parteien_mit_wahlprogramm(bundesland)
def alle_versionen(bundesland: str, partei: str) -> list[Programm]: # type: ignore[no-redef]
_ensure_initialized()
return _alle_versionen(bundesland, partei)
def all_programme() -> list[Programm]:
"""Alle eingetragenen Programme (initialisiert, falls nötig)."""
_ensure_initialized()
return list(PROGRAMME.values())