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>
518 lines
22 KiB
Python
518 lines
22 KiB
Python
"""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
|
|
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():
|
|
# 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
|
|
|
|
# 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,
|
|
"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())
|