gwoe-antragspruefer/app/programme.py
Dotty Dotter 445fcc90ca feat: Block 2.1 — NRW WP17 historische Wahlprogramme indiziert (Pilot)
5 Programme zur LTW NRW 14.05.2017 als historische Wahlprogramme im
Embeddings-Index — erster Datensatz für die zeitpunktige Bewertung
historischer Antraege:

- cdu-nrw-2017 (Laschet, 120 S., 172 chunks)
- spd-nrw-2017 (Kraft, 116 S., 169 chunks)
- gruene-nrw-2017 (131 S., 322 chunks)
- fdp-nrw-2017 (Lindner, 56 S., 92 chunks)
- afd-nrw-2017 (84 S., 78 chunks)

Geltungszeitraum 2017-05-14 (Wahltag WP17) bis 2022-05-15 (Wahltag
WP18, exklusiv). Eintraege liegen NUR in embeddings.PROGRAMME — die
WAHLPROGRAMME[NRW]-Struktur bleibt single-current (cdu-nrw-2022).

programme._migrate_from_legacy hat einen neuen Schritt 2b, der
typ=wahlprogramm-Eintraege aus embeddings.PROGRAMME mit explizitem
gueltig_ab/_bis als historische Wahlprogramme registriert. Damit
liefert wahlprogramm_zum_zeitpunkt() jetzt fuer NRW-Antraege aus dem
Zeitraum 2017-2022 das passende Programm.

Live-Verifikation auf gwoe-antragspruefer-dev:
- 2018-09-01 -> cdu-nrw-2017 (WP17)
- 2024-01-01 -> cdu-nrw-2022 (WP18)
- Grenze: 14.05.2022 -> WP17, 15.05.2022 -> WP18

Tests: 116 gruen, plus neue test_grenze_zwischen_wp17_und_wp18 und
angepasstes test_datum_vor_aktueller_wp_nrw_wp17.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:44:26 +02:00

555 lines
23 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 + Schritt 2b
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 2b: Historische Wahlprogramme aus embeddings.PROGRAMME.
# Wahlprogramme aus WAHLPROGRAMME (Schritt 1) sind die jeweils
# aktuelle Version pro (BL, Partei). Vorperioden-Programme sind nur
# in embeddings.PROGRAMME geführt (mit explizitem ``gueltig_ab`` und
# ``gueltig_bis`` als Eintrag-Felder) und werden hier nachregistriert.
# Voraussetzung: der embeddings-Eintrag hat ``gueltig_ab`` und/oder
# ``gueltig_bis`` gesetzt. Ohne das zählt er als "aktuelles Programm",
# wurde aber bereits via WAHLPROGRAMME migriert.
for pid, info in _EMB_PROGRAMME.items():
if info.get("typ") != "wahlprogramm":
continue
if pid in PROGRAMME:
continue
# Nur Einträge mit explizit gesetzter Geltung als historisch werten.
gueltig_ab = info.get("gueltig_ab")
gueltig_bis = info.get("gueltig_bis")
if gueltig_ab is None and gueltig_bis is None:
continue
bl = info.get("bundesland")
prog2b: Programm = {
"id": pid,
"titel": info.get("name", ""),
"name": info.get("name", ""),
"typ": "wahlprogramm",
"partei": info.get("partei", ""),
"bundesland": bl,
"beschluss": None,
"wahl": gueltig_ab,
"wp": info.get("wp"),
"gueltig_ab": gueltig_ab or "1900-01-01",
"gueltig_bis": gueltig_bis,
"pdf": info.get("pdf", ""),
"seiten": 0,
"hinweis": None,
}
PROGRAMME[pid] = prog2b
# 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())