diff --git a/app/main.py b/app/main.py index 8dbb446..bcc103d 100644 --- a/app/main.py +++ b/app/main.py @@ -4055,7 +4055,7 @@ async def api_admin_wahlprogramm_fetch( wird sie nicht überschrieben — stattdessen wird ein 409-Fehler zurückgegeben. """ from .wahlprogramm_fetch import fetch_and_verify, suggest_candidates - from .wahlprogramme import WAHLPROGRAMME + from .programme import aktuelles_wahlprogramm body = await request.json() bl = body.get("bl", "").strip().upper() @@ -4075,10 +4075,10 @@ async def api_admin_wahlprogramm_fetch( ) url = candidates[0]["url"] - wp_info = WAHLPROGRAMME.get(bl, {}).get(partei) - if wp_info: + wp_info = aktuelles_wahlprogramm(bl, partei) + if wp_info and wp_info.get("pdf"): from pathlib import Path as _Path - dest = _Path(__file__).parent / "static" / "referenzen" / wp_info["file"] + dest = _Path(__file__).parent / "static" / "referenzen" / wp_info["pdf"] else: from pathlib import Path as _Path dest = _Path(__file__).parent / "static" / "referenzen" / f"{partei.lower()}-{bl.lower()}-neu.pdf" diff --git a/app/programme.py b/app/programme.py index d4f2409..68d1eb5 100644 --- a/app/programme.py +++ b/app/programme.py @@ -4,11 +4,13 @@ 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. +Siehe ``app/legislaturen.py`` für Wahlperioden + Regierungen — Programm- +Daten beschreiben das Dokument selbst, nicht die Regierung, die aus ihm +hervorging. Die Verbindung läuft über +``legislaturen.regierung_zum_zeitpunkt(bundesland, datum)``. """ from __future__ import annotations @@ -17,7 +19,7 @@ from typing import Literal, Optional, TypedDict # ───────────────────────────────────────────────────────────────────────────── -# Typ +# Schema # ───────────────────────────────────────────────────────────────────────────── @@ -31,23 +33,27 @@ ProgrammTyp = Literal[ 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. + Pflichtfelder: id, typ, partei, gueltig_ab, name, pdf. + Optional: bundesland, wp, gueltig_bis, titel, seiten. + + Was hier bewusst NICHT drin ist: + - ``regierungsbildung`` / ``regierungsende`` — gehört zu + ``legislaturen.REGIERUNGEN``. Verbindung Programm→Regierung läuft + über ``legislaturen.regierung_zum_zeitpunkt(bl, antrag_datum)``. + - ``partei`` Langform ("CDU NRW") — ableitbar via partei + bundesland. + - ``jahr`` — ``int(gueltig_ab[:4])`` reicht. """ 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 + partei: str # kanonisch (CDU, BiW, BÜNDNIS 90/DIE GRÜNEN, …) + bundesland: Optional[str] # BL-Code; None nur bei Bundesgrundsatzprogrammen 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 + gueltig_ab: str # ISO YYYY-MM-DD; bei wahl: Wahltag + gueltig_bis: Optional[str] # ISO; None = aktuell gültig + name: str # voll-qualifiziert für Citation, z.B. "CDU NRW Wahlprogramm 2022" + titel: Optional[str] # Slogan ("Machen, worauf es ankommt"); None wenn nicht erfasst pdf: str # Dateiname in static/referenzen/ - seiten: int - hinweis: Optional[str] # freier Text, z.B. "BSW hat kein Grundsatzprogramm — Wahlprogramm dient als Hauptquelle" + seiten: Optional[int] # PDF-Seitenzahl REFERENZEN_PATH = Path(__file__).parent / "static" / "referenzen" @@ -55,16 +61,348 @@ 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. +# Daten — alle 287 Programme (historisch + aktuell), sortiert nach +# (typ, bundesland, gueltig_ab, partei). Auto-generiert aus der bisherigen +# embeddings.PROGRAMME + WAHLPROGRAMME-Lazy-Migration via fitz für seiten. +# Generator: tools/build_programme_literal.py (siehe Commit-Historie). # ───────────────────────────────────────────────────────────────────────────── -# Aktuelle Programme — gefüllt durch ``_register_initial_data()`` weiter unten, -# damit der Migrations-Pfad an einer Stelle zu sehen ist. -PROGRAMME: dict[str, Programm] = {} +PROGRAMME: dict[str, Programm] = { + + # ─── grundsatzprogramm-bund · BUND ─── + "spd-grundsatz": {"id": "spd-grundsatz", "typ": "grundsatzprogramm-bund", "partei": "SPD", "bundesland": None, "wp": None, "gueltig_ab": "2007-10-28", "gueltig_bis": None, "name": "SPD Hamburger Programm 2007", "titel": None, "pdf": "spd-grundsatzprogramm.pdf", "seiten": 79}, + "linke-grundsatz": {"id": "linke-grundsatz", "typ": "grundsatzprogramm-bund", "partei": "LINKE", "bundesland": None, "wp": None, "gueltig_ab": "2011-10-23", "gueltig_bis": None, "name": "DIE LINKE Erfurter Programm 2011", "titel": None, "pdf": "linke-grundsatzprogramm.pdf", "seiten": 58}, + "fw-grundsatz": {"id": "fw-grundsatz", "typ": "grundsatzprogramm-bund", "partei": "FREIE WÄHLER", "bundesland": None, "wp": None, "gueltig_ab": "2012-02-25", "gueltig_bis": None, "name": "FREIE WÄHLER Bundesgrundsatzprogramm", "titel": None, "pdf": "fw-grundsatz.pdf", "seiten": 46}, + "fdp-grundsatz": {"id": "fdp-grundsatz", "typ": "grundsatzprogramm-bund", "partei": "FDP", "bundesland": None, "wp": None, "gueltig_ab": "2012-04-22", "gueltig_bis": None, "name": "FDP Karlsruher Freiheitsthesen 2012", "titel": None, "pdf": "fdp-grundsatzprogramm.pdf", "seiten": 118}, + "afd-grundsatz": {"id": "afd-grundsatz", "typ": "grundsatzprogramm-bund", "partei": "AfD", "bundesland": None, "wp": None, "gueltig_ab": "2016-05-01", "gueltig_bis": None, "name": "AfD Grundsatzprogramm 2016", "titel": None, "pdf": "afd-grundsatzprogramm.pdf", "seiten": 96}, + "gruene-grundsatz": {"id": "gruene-grundsatz", "typ": "grundsatzprogramm-bund", "partei": "GRÜNE", "bundesland": None, "wp": None, "gueltig_ab": "2020-11-22", "gueltig_bis": None, "name": "Grüne Grundsatzprogramm 2020", "titel": None, "pdf": "gruene-grundsatzprogramm.pdf", "seiten": 136}, + "cdu-grundsatz": {"id": "cdu-grundsatz", "typ": "grundsatzprogramm-bund", "partei": "CDU", "bundesland": None, "wp": None, "gueltig_ab": "2024-05-07", "gueltig_bis": None, "name": "CDU Grundsatzprogramm 2024", "titel": None, "pdf": "cdu-grundsatzprogramm.pdf", "seiten": 82}, + + # ─── grundsatzprogramm-land · BY ─── + "csu-grundsatz": {"id": "csu-grundsatz", "typ": "grundsatzprogramm-land", "partei": "CSU", "bundesland": "BY", "wp": None, "gueltig_ab": "2023-05-06", "gueltig_bis": None, "name": "CSU Grundsatzprogramm 2023", "titel": "Für ein neues Miteinander — Grundsatzprogramm der CSU", "pdf": "csu-grundsatz-2023.pdf", "seiten": 55}, + + # ─── grundsatzprogramm-land · LSA ─── + "cdu-grundsatz-lsa": {"id": "cdu-grundsatz-lsa", "typ": "grundsatzprogramm-land", "partei": "CDU", "bundesland": "LSA", "wp": None, "gueltig_ab": "2023-09-30", "gueltig_bis": None, "name": "CDU Sachsen-Anhalt Grundsatzprogramm 2023", "titel": "Sachsen-Anhalt. Unsere Verantwortung. Unsere Zukunft.", "pdf": "cdu-grundsatz-lsa-2023.pdf", "seiten": 70}, + + # ─── grundsatzprogramm-land · NRW ─── + "cdu-grundsatz-nrw": {"id": "cdu-grundsatz-nrw", "typ": "grundsatzprogramm-land", "partei": "CDU", "bundesland": "NRW", "wp": None, "gueltig_ab": "2015-06-13", "gueltig_bis": None, "name": "CDU NRW Grundsatzprogramm 2015", "titel": "Aufstieg, Sicherheit, Perspektive — Das Nordrhein-Westfalen-Programm", "pdf": "cdu-grundsatz-nrw-2015.pdf", "seiten": 48}, + + # ─── grundsatzprogramm-land · SH ─── + "ssw-grundsatz": {"id": "ssw-grundsatz", "typ": "grundsatzprogramm-land", "partei": "SSW", "bundesland": "SH", "wp": None, "gueltig_ab": "2016-04-16", "gueltig_bis": None, "name": "SSW Rahmenprogramm 2016", "titel": "SSW Rahmenprogramm", "pdf": "ssw-grundsatz-sh-2016.pdf", "seiten": 34}, + + # ─── grundsatzprogramm-land · SN ─── + "cdu-grundsatz-sn": {"id": "cdu-grundsatz-sn", "typ": "grundsatzprogramm-land", "partei": "CDU", "bundesland": "SN", "wp": None, "gueltig_ab": "2023-11-20", "gueltig_bis": None, "name": "CDU Sachsen Grundsatzprogramm 2023", "titel": "Zukunftsplan für Sachsen", "pdf": "cdu-grundsatz-sn-2023.pdf", "seiten": 54}, + + # ─── wahlprogramm · BB ─── + "afd-bb-2014": {"id": "afd-bb-2014", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BB", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-09-01", "name": "AfD BB Wahlprogramm 2014", "titel": None, "pdf": "afd-bb-2014.pdf", "seiten": 37}, + "bvb-fw-bb-2014": {"id": "bvb-fw-bb-2014", "typ": "wahlprogramm", "partei": "BVB/FREIE WÄHLER", "bundesland": "BB", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-09-01", "name": "BVB/FREIE WÄHLER BB Wahlprogramm 2014", "titel": None, "pdf": "bvb-fw-bb-2014.pdf", "seiten": 39}, + "cdu-bb-2014": {"id": "cdu-bb-2014", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BB", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-09-01", "name": "CDU BB Wahlprogramm 2014", "titel": None, "pdf": "cdu-bb-2014.pdf", "seiten": 20}, + "gruene-bb-2014": {"id": "gruene-bb-2014", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BB", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-09-01", "name": "Grüne BB Wahlprogramm 2014", "titel": None, "pdf": "gruene-bb-2014.pdf", "seiten": 150}, + "linke-bb-2014": {"id": "linke-bb-2014", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BB", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-09-01", "name": "DIE LINKE BB Wahlprogramm 2014", "titel": None, "pdf": "linke-bb-2014.pdf", "seiten": 60}, + "spd-bb-2014": {"id": "spd-bb-2014", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BB", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-09-01", "name": "SPD BB Wahlprogramm 2014", "titel": None, "pdf": "spd-bb-2014.pdf", "seiten": 26}, + "afd-bb-2019": {"id": "afd-bb-2019", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BB", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-22", "name": "AfD BB Wahlprogramm 2019", "titel": None, "pdf": "afd-bb-2019.pdf", "seiten": 42}, + "bvb-fw-bb-2019": {"id": "bvb-fw-bb-2019", "typ": "wahlprogramm", "partei": "BVB/FREIE WÄHLER", "bundesland": "BB", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-22", "name": "BVB/FREIE WÄHLER BB Wahlprogramm 2019", "titel": None, "pdf": "bvb-fw-bb-2019.pdf", "seiten": 37}, + "cdu-bb-2019": {"id": "cdu-bb-2019", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BB", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-22", "name": "CDU BB Wahlprogramm 2019", "titel": None, "pdf": "cdu-bb-2019.pdf", "seiten": 50}, + "gruene-bb-2019": {"id": "gruene-bb-2019", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BB", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-22", "name": "Grüne BB Wahlprogramm 2019", "titel": None, "pdf": "gruene-bb-2019.pdf", "seiten": 106}, + "linke-bb-2019": {"id": "linke-bb-2019", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BB", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-22", "name": "DIE LINKE BB Wahlprogramm 2019", "titel": None, "pdf": "linke-bb-2019.pdf", "seiten": 100}, + "spd-bb-2019": {"id": "spd-bb-2019", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BB", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-22", "name": "SPD BB Wahlprogramm 2019", "titel": None, "pdf": "spd-bb-2019.pdf", "seiten": 47}, + "afd-bb-2024": {"id": "afd-bb-2024", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BB", "wp": 8, "gueltig_ab": "2024-09-22", "gueltig_bis": None, "name": "AfD Brandenburg Wahlprogramm 2024", "titel": None, "pdf": "afd-bb-2024.pdf", "seiten": 100}, + "bsw-bb-2024": {"id": "bsw-bb-2024", "typ": "wahlprogramm", "partei": "BSW", "bundesland": "BB", "wp": 8, "gueltig_ab": "2024-09-22", "gueltig_bis": None, "name": "BSW Brandenburg Wahlprogramm 2024", "titel": None, "pdf": "bsw-bb-2024.pdf", "seiten": 50}, + "cdu-bb-2024": {"id": "cdu-bb-2024", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BB", "wp": 8, "gueltig_ab": "2024-09-22", "gueltig_bis": None, "name": "CDU Brandenburg Wahlprogramm 2024", "titel": None, "pdf": "cdu-bb-2024.pdf", "seiten": 100}, + "spd-bb-2024": {"id": "spd-bb-2024", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BB", "wp": 8, "gueltig_ab": "2024-09-22", "gueltig_bis": None, "name": "SPD Brandenburg Wahlprogramm 2024", "titel": None, "pdf": "spd-bb-2024.pdf", "seiten": 100}, + + # ─── wahlprogramm · BE ─── + "cdu-be-2011": {"id": "cdu-be-2011", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BE", "wp": 17, "gueltig_ab": "2011-09-18", "gueltig_bis": "2016-09-18", "name": "CDU BE Wahlprogramm 2011", "titel": None, "pdf": "cdu-be-2011.pdf", "seiten": 41}, + "gruene-be-2011": {"id": "gruene-be-2011", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BE", "wp": 17, "gueltig_ab": "2011-09-18", "gueltig_bis": "2016-09-18", "name": "Grüne BE Wahlprogramm 2011", "titel": None, "pdf": "gruene-be-2011.pdf", "seiten": 120}, + "linke-be-2011": {"id": "linke-be-2011", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BE", "wp": 17, "gueltig_ab": "2011-09-18", "gueltig_bis": "2016-09-18", "name": "DIE LINKE BE Wahlprogramm 2011", "titel": None, "pdf": "linke-be-2011.pdf", "seiten": 72}, + "piraten-be-2011": {"id": "piraten-be-2011", "typ": "wahlprogramm", "partei": "PIRATEN", "bundesland": "BE", "wp": 17, "gueltig_ab": "2011-09-18", "gueltig_bis": "2016-09-18", "name": "PIRATEN BE Wahlprogramm 2011", "titel": None, "pdf": "piraten-be-2011.pdf", "seiten": 51}, + "spd-be-2011": {"id": "spd-be-2011", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BE", "wp": 17, "gueltig_ab": "2011-09-18", "gueltig_bis": "2016-09-18", "name": "SPD BE Wahlprogramm 2011", "titel": None, "pdf": "spd-be-2011.pdf", "seiten": 60}, + "afd-be-2016": {"id": "afd-be-2016", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BE", "wp": 18, "gueltig_ab": "2016-09-18", "gueltig_bis": "2021-09-26", "name": "AfD BE Wahlprogramm 2016", "titel": None, "pdf": "afd-be-2016.pdf", "seiten": 40}, + "cdu-be-2016": {"id": "cdu-be-2016", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BE", "wp": 18, "gueltig_ab": "2016-09-18", "gueltig_bis": "2021-09-26", "name": "CDU BE Wahlprogramm 2016", "titel": None, "pdf": "cdu-be-2016.pdf", "seiten": 49}, + "fdp-be-2016": {"id": "fdp-be-2016", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BE", "wp": 18, "gueltig_ab": "2016-09-18", "gueltig_bis": "2021-09-26", "name": "FDP BE Wahlprogramm 2016", "titel": None, "pdf": "fdp-be-2016.pdf", "seiten": 57}, + "gruene-be-2016": {"id": "gruene-be-2016", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BE", "wp": 18, "gueltig_ab": "2016-09-18", "gueltig_bis": "2021-09-26", "name": "Grüne BE Wahlprogramm 2016", "titel": None, "pdf": "gruene-be-2016.pdf", "seiten": 104}, + "linke-be-2016": {"id": "linke-be-2016", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BE", "wp": 18, "gueltig_ab": "2016-09-18", "gueltig_bis": "2021-09-26", "name": "DIE LINKE BE Wahlprogramm 2016", "titel": None, "pdf": "linke-be-2016.pdf", "seiten": 100}, + "spd-be-2016": {"id": "spd-be-2016", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BE", "wp": 18, "gueltig_ab": "2016-09-18", "gueltig_bis": "2021-09-26", "name": "SPD BE Wahlprogramm 2016", "titel": None, "pdf": "spd-be-2016.pdf", "seiten": 99}, + "afd-be-2023": {"id": "afd-be-2023", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BE", "wp": 19, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "AfD Berlin Wahlprogramm 2021", "titel": "Wahlprogramm der AfD Berlin für die Wahl des Abgeordnetenhauses am 26. September 2021", "pdf": "afd-be-2023.pdf", "seiten": 166}, + "cdu-be-2023": {"id": "cdu-be-2023", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BE", "wp": 19, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "CDU Berlin Wahlprogramm 2021", "titel": "Unser Berlin. Mehr geht nur gemeinsam. — Berlin-Plan der CDU Berlin 2021–2026", "pdf": "cdu-be-2023.pdf", "seiten": 135}, + "gruene-be-2023": {"id": "gruene-be-2023", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BE", "wp": 19, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Berlin Wahlprogramm 2021", "titel": "Unser Plan für Berlin — Landeswahlprogramm BÜNDNIS 90/DIE GRÜNEN Berlin 2021", "pdf": "gruene-be-2023.pdf", "seiten": 280}, + "linke-be-2023": {"id": "linke-be-2023", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BE", "wp": 19, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "DIE LINKE Berlin Wahlprogramm 2021", "titel": "rot. radikal. realistisch. — Unser Programm für die soziale Stadt", "pdf": "linke-be-2023.pdf", "seiten": 130}, + "spd-be-2023": {"id": "spd-be-2023", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BE", "wp": 19, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "SPD Berlin Wahlprogramm 2021", "titel": "Ganz sicher Berlin — Wahlprogramm der SPD Berlin zur Abgeordnetenhauswahl 2021", "pdf": "spd-be-2023.pdf", "seiten": 86}, + + # ─── wahlprogramm · BUND ─── + "cdu-bund-2013": {"id": "cdu-bund-2013", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BUND", "wp": 18, "gueltig_ab": "2013-09-22", "gueltig_bis": "2017-09-24", "name": "CDU BUND Wahlprogramm 2013", "titel": None, "pdf": "cdu-bund-2013.pdf", "seiten": 81}, + "csu-bund-2013": {"id": "csu-bund-2013", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BUND", "wp": 18, "gueltig_ab": "2013-09-22", "gueltig_bis": "2017-09-24", "name": "CSU BUND Wahlprogramm 2013", "titel": None, "pdf": "csu-bund-2013.pdf", "seiten": 41}, + "gruene-bund-2013": {"id": "gruene-bund-2013", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BUND", "wp": 18, "gueltig_ab": "2013-09-22", "gueltig_bis": "2017-09-24", "name": "Grüne BUND Wahlprogramm 2013", "titel": None, "pdf": "gruene-bund-2013.pdf", "seiten": 337}, + "linke-bund-2013": {"id": "linke-bund-2013", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BUND", "wp": 18, "gueltig_ab": "2013-09-22", "gueltig_bis": "2017-09-24", "name": "DIE LINKE BUND Wahlprogramm 2013", "titel": None, "pdf": "linke-bund-2013.pdf", "seiten": 93}, + "spd-bund-2013": {"id": "spd-bund-2013", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BUND", "wp": 18, "gueltig_ab": "2013-09-22", "gueltig_bis": "2017-09-24", "name": "SPD BUND Wahlprogramm 2013", "titel": None, "pdf": "spd-bund-2013.pdf", "seiten": 120}, + "afd-bund-2017": {"id": "afd-bund-2017", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BUND", "wp": 19, "gueltig_ab": "2017-09-24", "gueltig_bis": "2021-09-26", "name": "AfD Bundestagswahlprogramm 2017", "titel": None, "pdf": "afd-bund-2017.pdf", "seiten": 76}, + "cdu-bund-2017": {"id": "cdu-bund-2017", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BUND", "wp": 19, "gueltig_ab": "2017-09-24", "gueltig_bis": "2021-09-26", "name": "CDU/CSU Wahlprogramm BTW 2017", "titel": None, "pdf": "cdu-bund-2017.pdf", "seiten": 76}, + "csu-bund-2017": {"id": "csu-bund-2017", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BUND", "wp": 19, "gueltig_ab": "2017-09-24", "gueltig_bis": "2021-09-26", "name": "CSU Bayernplan 2017", "titel": None, "pdf": "csu-bund-2017.pdf", "seiten": 31}, + "fdp-bund-2017": {"id": "fdp-bund-2017", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BUND", "wp": 19, "gueltig_ab": "2017-09-24", "gueltig_bis": "2021-09-26", "name": "FDP Wahlprogramm BTW 2017", "titel": None, "pdf": "fdp-bund-2017.pdf", "seiten": 158}, + "gruene-bund-2017": {"id": "gruene-bund-2017", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BUND", "wp": 19, "gueltig_ab": "2017-09-24", "gueltig_bis": "2021-09-26", "name": "Grüne Bundestagswahlprogramm 2017", "titel": None, "pdf": "gruene-bund-2017.pdf", "seiten": 248}, + "linke-bund-2017": {"id": "linke-bund-2017", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BUND", "wp": 19, "gueltig_ab": "2017-09-24", "gueltig_bis": "2021-09-26", "name": "DIE LINKE Bundestagswahlprogramm 2017", "titel": None, "pdf": "linke-bund-2017.pdf", "seiten": 144}, + "spd-bund-2017": {"id": "spd-bund-2017", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BUND", "wp": 19, "gueltig_ab": "2017-09-24", "gueltig_bis": "2021-09-26", "name": "SPD Zukunftsprogramm BTW 2017", "titel": None, "pdf": "spd-bund-2017.pdf", "seiten": 116}, + "afd-bund-2021": {"id": "afd-bund-2021", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BUND", "wp": 20, "gueltig_ab": "2021-09-26", "gueltig_bis": "2025-02-23", "name": "AfD Bundestagswahlprogramm 2021", "titel": None, "pdf": "afd-bund-2021.pdf", "seiten": 210}, + "cdu-bund-2021": {"id": "cdu-bund-2021", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BUND", "wp": 20, "gueltig_ab": "2021-09-26", "gueltig_bis": "2025-02-23", "name": "CDU/CSU Programm fuer Stabilitaet und Erneuerung 2021", "titel": None, "pdf": "cdu-bund-2021.pdf", "seiten": 140}, + "csu-bund-2021": {"id": "csu-bund-2021", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BUND", "wp": 20, "gueltig_ab": "2021-09-26", "gueltig_bis": "2025-02-23", "name": "CSU-Programm Bundestagswahl 2021", "titel": None, "pdf": "csu-bund-2021.pdf", "seiten": 18}, + "fdp-bund-2021": {"id": "fdp-bund-2021", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BUND", "wp": 20, "gueltig_ab": "2021-09-26", "gueltig_bis": "2025-02-23", "name": "FDP Wahlprogramm Bundestagswahl 2021", "titel": None, "pdf": "fdp-bund-2021.pdf", "seiten": 68}, + "gruene-bund-2021": {"id": "gruene-bund-2021", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BUND", "wp": 20, "gueltig_ab": "2021-09-26", "gueltig_bis": "2025-02-23", "name": "Gruene Bundestagswahlprogramm 2021", "titel": None, "pdf": "gruene-bund-2021.pdf", "seiten": 272}, + "linke-bund-2021": {"id": "linke-bund-2021", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BUND", "wp": 20, "gueltig_ab": "2021-09-26", "gueltig_bis": "2025-02-23", "name": "DIE LINKE Bundestagswahlprogramm 2021", "titel": None, "pdf": "linke-bund-2021.pdf", "seiten": 168}, + "spd-bund-2021": {"id": "spd-bund-2021", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BUND", "wp": 20, "gueltig_ab": "2021-09-26", "gueltig_bis": "2025-02-23", "name": "SPD Zukunftsprogramm Bundestagswahl 2021", "titel": None, "pdf": "spd-bund-2021.pdf", "seiten": 66}, + "afd-bund-2025": {"id": "afd-bund-2025", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BUND", "wp": 21, "gueltig_ab": "2025-02-23", "gueltig_bis": None, "name": "AfD Wahlprogramm 2025", "titel": "Zeit für Deutschland — AfD Bundestagswahlprogramm 2025", "pdf": "afd-bund-2025.pdf", "seiten": 177}, + "bsw-bund-2025": {"id": "bsw-bund-2025", "typ": "wahlprogramm", "partei": "BSW", "bundesland": "BUND", "wp": 21, "gueltig_ab": "2025-02-23", "gueltig_bis": None, "name": "BSW Wahlprogramm 2025", "titel": "Unser Land verdient mehr — BSW Wahlprogramm BTW 2025", "pdf": "bsw-bund-2025.pdf", "seiten": 45}, + "cdu-bund-2025": {"id": "cdu-bund-2025", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BUND", "wp": 21, "gueltig_ab": "2025-02-23", "gueltig_bis": None, "name": "CDU Wahlprogramm 2025", "titel": "Politikwechsel für Deutschland — Wahlprogramm CDU/CSU BTW 2025", "pdf": "cdu-bund-2025.pdf", "seiten": 82}, + "csu-bund-2025": {"id": "csu-bund-2025", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BUND", "wp": 21, "gueltig_ab": "2025-02-23", "gueltig_bis": None, "name": "CSU Wahlprogramm 2025", "titel": "Politikwechsel für Deutschland — Wahlprogramm CDU/CSU BTW 2025 (CSU)", "pdf": "csu-bund-2025.pdf", "seiten": 81}, + "fdp-bund-2025": {"id": "fdp-bund-2025", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BUND", "wp": 21, "gueltig_ab": "2025-02-23", "gueltig_bis": None, "name": "FDP Wahlprogramm 2025", "titel": "Alles lässt sich ändern — FDP Wahlprogramm BTW 2025", "pdf": "fdp-bund-2025.pdf", "seiten": 52}, + "gruene-bund-2025": {"id": "gruene-bund-2025", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BUND", "wp": 21, "gueltig_ab": "2025-02-23", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Wahlprogramm 2025", "titel": "Zusammen wachsen — Regierungsprogramm BÜNDNIS 90/DIE GRÜNEN BTW 2025", "pdf": "gruene-bund-2025.pdf", "seiten": 160}, + "linke-bund-2025": {"id": "linke-bund-2025", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BUND", "wp": 21, "gueltig_ab": "2025-02-23", "gueltig_bis": None, "name": "DIE LINKE Wahlprogramm 2025", "titel": "Alle wollen regieren. Wir wollen verändern. — DIE LINKE Wahlprogramm BTW 2025", "pdf": "linke-bund-2025.pdf", "seiten": 60}, + "spd-bund-2025": {"id": "spd-bund-2025", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BUND", "wp": 21, "gueltig_ab": "2025-02-23", "gueltig_bis": None, "name": "SPD Wahlprogramm 2025", "titel": "Mehr für Dich. Besser für Deutschland. — SPD Regierungsprogramm BTW 2025", "pdf": "spd-bund-2025.pdf", "seiten": 68}, + + # ─── wahlprogramm · BW ─── + "cdu-bw-2011": {"id": "cdu-bw-2011", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BW", "wp": 15, "gueltig_ab": "2011-03-27", "gueltig_bis": "2016-03-13", "name": "CDU BW Wahlprogramm 2011", "titel": None, "pdf": "cdu-bw-2011.pdf", "seiten": 57}, + "fdp-bw-2011": {"id": "fdp-bw-2011", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BW", "wp": 15, "gueltig_ab": "2011-03-27", "gueltig_bis": "2016-03-13", "name": "FDP BW Wahlprogramm 2011", "titel": None, "pdf": "fdp-bw-2011.pdf", "seiten": 137}, + "gruene-bw-2011": {"id": "gruene-bw-2011", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BW", "wp": 15, "gueltig_ab": "2011-03-27", "gueltig_bis": "2016-03-13", "name": "Grüne BW Wahlprogramm 2011", "titel": None, "pdf": "gruene-bw-2011.pdf", "seiten": 241}, + "spd-bw-2011": {"id": "spd-bw-2011", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BW", "wp": 15, "gueltig_ab": "2011-03-27", "gueltig_bis": "2016-03-13", "name": "SPD BW Wahlprogramm 2011", "titel": None, "pdf": "spd-bw-2011.pdf", "seiten": 65}, + "afd-bw-2016": {"id": "afd-bw-2016", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BW", "wp": 16, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "AfD BW Wahlprogramm 2016", "titel": None, "pdf": "afd-bw-2016.pdf", "seiten": 64}, + "cdu-bw-2016": {"id": "cdu-bw-2016", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BW", "wp": 16, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "CDU BW Wahlprogramm 2016", "titel": None, "pdf": "cdu-bw-2016.pdf", "seiten": 156}, + "fdp-bw-2016": {"id": "fdp-bw-2016", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BW", "wp": 16, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "FDP BW Wahlprogramm 2016", "titel": None, "pdf": "fdp-bw-2016.pdf", "seiten": 124}, + "gruene-bw-2016": {"id": "gruene-bw-2016", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BW", "wp": 16, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "Grüne BW Wahlprogramm 2016", "titel": None, "pdf": "gruene-bw-2016.pdf", "seiten": 249}, + "spd-bw-2016": {"id": "spd-bw-2016", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BW", "wp": 16, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "SPD BW Wahlprogramm 2016", "titel": None, "pdf": "spd-bw-2016.pdf", "seiten": 41}, + "afd-bw-2021": {"id": "afd-bw-2021", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BW", "wp": 17, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "AfD Baden-Württemberg Wahlprogramm 2021", "titel": "AfD Baden-Württemberg Landtagswahlprogramm 2021", "pdf": "afd-bw-2021.pdf", "seiten": 100}, + "cdu-bw-2021": {"id": "cdu-bw-2021", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BW", "wp": 17, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "CDU Baden-Württemberg Wahlprogramm 2021", "titel": "Neue Ideen für eine neue Zeit — Regierungsprogramm der CDU Baden-Württemberg zur Landtagswahl 2021", "pdf": "cdu-bw-2021.pdf", "seiten": 100}, + "fdp-bw-2021": {"id": "fdp-bw-2021", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BW", "wp": 17, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "FDP Baden-Württemberg Wahlprogramm 2021", "titel": "FDP Baden-Württemberg Landtagswahlprogramm 2021", "pdf": "fdp-bw-2021.pdf", "seiten": 100}, + "gruene-bw-2021": {"id": "gruene-bw-2021", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BW", "wp": 17, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Baden-Württemberg Wahlprogramm 2021", "titel": "Wachsen wir über uns hinaus — Landtagswahlprogramm BÜNDNIS 90/DIE GRÜNEN Baden-Württemberg 2021", "pdf": "gruene-bw-2021.pdf", "seiten": 100}, + "spd-bw-2021": {"id": "spd-bw-2021", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BW", "wp": 17, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "SPD Baden-Württemberg Wahlprogramm 2021", "titel": "SPD Baden-Württemberg Wahlprogramm zur Landtagswahl 2021", "pdf": "spd-bw-2021.pdf", "seiten": 100}, + + # ─── wahlprogramm · BY ─── + "csu-by-2013": {"id": "csu-by-2013", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BY", "wp": 17, "gueltig_ab": "2013-09-15", "gueltig_bis": "2018-10-14", "name": "CSU BY Wahlprogramm 2013", "titel": None, "pdf": "csu-by-2013.pdf", "seiten": 28}, + "fw-by-2013": {"id": "fw-by-2013", "typ": "wahlprogramm", "partei": "FREIE WÄHLER", "bundesland": "BY", "wp": 17, "gueltig_ab": "2013-09-15", "gueltig_bis": "2018-10-14", "name": "FREIE WÄHLER BY Wahlprogramm 2013", "titel": None, "pdf": "fw-by-2013.pdf", "seiten": 67}, + "gruene-by-2013": {"id": "gruene-by-2013", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BY", "wp": 17, "gueltig_ab": "2013-09-15", "gueltig_bis": "2018-10-14", "name": "Grüne BY Wahlprogramm 2013", "titel": None, "pdf": "gruene-by-2013.pdf", "seiten": 87}, + "spd-by-2013": {"id": "spd-by-2013", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BY", "wp": 17, "gueltig_ab": "2013-09-15", "gueltig_bis": "2018-10-14", "name": "SPD BY Wahlprogramm 2013", "titel": None, "pdf": "spd-by-2013.pdf", "seiten": 150}, + "afd-by-2018": {"id": "afd-by-2018", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BY", "wp": 18, "gueltig_ab": "2018-10-14", "gueltig_bis": "2023-10-08", "name": "AfD BY Wahlprogramm 2018", "titel": None, "pdf": "afd-by-2018.pdf", "seiten": 100}, + "csu-by-2018": {"id": "csu-by-2018", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BY", "wp": 18, "gueltig_ab": "2018-10-14", "gueltig_bis": "2023-10-08", "name": "CSU BY Wahlprogramm 2018", "titel": None, "pdf": "csu-by-2018.pdf", "seiten": 24}, + "fdp-by-2018": {"id": "fdp-by-2018", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BY", "wp": 18, "gueltig_ab": "2018-10-14", "gueltig_bis": "2023-10-08", "name": "FDP BY Wahlprogramm 2018", "titel": None, "pdf": "fdp-by-2018.pdf", "seiten": 78}, + "fw-by-2018": {"id": "fw-by-2018", "typ": "wahlprogramm", "partei": "FREIE WÄHLER", "bundesland": "BY", "wp": 18, "gueltig_ab": "2018-10-14", "gueltig_bis": "2023-10-08", "name": "FREIE WÄHLER BY Wahlprogramm 2018", "titel": None, "pdf": "fw-by-2018.pdf", "seiten": 43}, + "gruene-by-2018": {"id": "gruene-by-2018", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BY", "wp": 18, "gueltig_ab": "2018-10-14", "gueltig_bis": "2023-10-08", "name": "Grüne BY Wahlprogramm 2018", "titel": None, "pdf": "gruene-by-2018.pdf", "seiten": 107}, + "spd-by-2018": {"id": "spd-by-2018", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BY", "wp": 18, "gueltig_ab": "2018-10-14", "gueltig_bis": "2023-10-08", "name": "SPD BY Wahlprogramm 2018", "titel": None, "pdf": "spd-by-2018.pdf", "seiten": 69}, + "afd-by-2023": {"id": "afd-by-2023", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BY", "wp": 19, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "AfD Bayern Wahlprogramm 2023", "titel": None, "pdf": "afd-by-2023.pdf", "seiten": 100}, + "csu-by-2023": {"id": "csu-by-2023", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BY", "wp": 19, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "CSU Bayern Wahlprogramm 2023", "titel": "CSU Bayern Bayernplan 2023", "pdf": "csu-by-2023.pdf", "seiten": 100}, + "fw-by-2023": {"id": "fw-by-2023", "typ": "wahlprogramm", "partei": "FREIE WÄHLER", "bundesland": "BY", "wp": 19, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "FREIE WÄHLER Bayern Wahlprogramm 2023", "titel": None, "pdf": "fw-by-2023.pdf", "seiten": 80}, + "gruene-by-2023": {"id": "gruene-by-2023", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BY", "wp": 19, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Bayern Wahlprogramm 2023", "titel": "BÜNDNIS 90/DIE GRÜNEN Bayern Regierungsprogramm 2023", "pdf": "gruene-by-2023.pdf", "seiten": 100}, + "spd-by-2023": {"id": "spd-by-2023", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BY", "wp": 19, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "SPD Bayern Wahlprogramm 2023", "titel": "SPD Bayern Zukunftsprogramm 2023", "pdf": "spd-by-2023.pdf", "seiten": 100}, + + # ─── wahlprogramm · HB ─── + "afd-hb-2015": {"id": "afd-hb-2015", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "HB", "wp": 19, "gueltig_ab": "2015-05-10", "gueltig_bis": "2019-05-26", "name": "AfD HB Wahlprogramm 2015", "titel": None, "pdf": "afd-hb-2015.pdf", "seiten": 49}, + "biw-hb-2015": {"id": "biw-hb-2015", "typ": "wahlprogramm", "partei": "BiW", "bundesland": "HB", "wp": 19, "gueltig_ab": "2015-05-10", "gueltig_bis": "2019-05-26", "name": "BIW Bremen HB Wahlprogramm 2015", "titel": None, "pdf": "biw-hb-2015.pdf", "seiten": 32}, + "cdu-hb-2015": {"id": "cdu-hb-2015", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "HB", "wp": 19, "gueltig_ab": "2015-05-10", "gueltig_bis": "2019-05-26", "name": "CDU HB Wahlprogramm 2015", "titel": None, "pdf": "cdu-hb-2015.pdf", "seiten": 58}, + "fdp-hb-2015": {"id": "fdp-hb-2015", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "HB", "wp": 19, "gueltig_ab": "2015-05-10", "gueltig_bis": "2019-05-26", "name": "FDP HB Wahlprogramm 2015", "titel": None, "pdf": "fdp-hb-2015.pdf", "seiten": 80}, + "gruene-hb-2015": {"id": "gruene-hb-2015", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "HB", "wp": 19, "gueltig_ab": "2015-05-10", "gueltig_bis": "2019-05-26", "name": "Grüne HB Wahlprogramm 2015", "titel": None, "pdf": "gruene-hb-2015.pdf", "seiten": 140}, + "linke-hb-2015": {"id": "linke-hb-2015", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "HB", "wp": 19, "gueltig_ab": "2015-05-10", "gueltig_bis": "2019-05-26", "name": "DIE LINKE HB Wahlprogramm 2015", "titel": None, "pdf": "linke-hb-2015.pdf", "seiten": 82}, + "spd-hb-2015": {"id": "spd-hb-2015", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "HB", "wp": 19, "gueltig_ab": "2015-05-10", "gueltig_bis": "2019-05-26", "name": "SPD HB Wahlprogramm 2015", "titel": None, "pdf": "spd-hb-2015.pdf", "seiten": 67}, + "afd-hb-2019": {"id": "afd-hb-2019", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "HB", "wp": 20, "gueltig_ab": "2019-05-26", "gueltig_bis": "2023-05-14", "name": "AfD HB Wahlprogramm 2019", "titel": None, "pdf": "afd-hb-2019.pdf", "seiten": 27}, + "biw-hb-2019": {"id": "biw-hb-2019", "typ": "wahlprogramm", "partei": "BiW", "bundesland": "HB", "wp": 20, "gueltig_ab": "2019-05-26", "gueltig_bis": "2023-05-14", "name": "BIW Bremen Wahlprogramm 2019", "titel": None, "pdf": "biw-hb-2019.pdf", "seiten": 22}, + "cdu-hb-2019": {"id": "cdu-hb-2019", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "HB", "wp": 20, "gueltig_ab": "2019-05-26", "gueltig_bis": "2023-05-14", "name": "CDU HB Wahlprogramm 2019", "titel": None, "pdf": "cdu-hb-2019.pdf", "seiten": 66}, + "fdp-hb-2019": {"id": "fdp-hb-2019", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "HB", "wp": 20, "gueltig_ab": "2019-05-26", "gueltig_bis": "2023-05-14", "name": "FDP HB Wahlprogramm 2019", "titel": None, "pdf": "fdp-hb-2019.pdf", "seiten": 89}, + "gruene-hb-2019": {"id": "gruene-hb-2019", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "HB", "wp": 20, "gueltig_ab": "2019-05-26", "gueltig_bis": "2023-05-14", "name": "Grüne HB Wahlprogramm 2019", "titel": None, "pdf": "gruene-hb-2019.pdf", "seiten": 190}, + "linke-hb-2019": {"id": "linke-hb-2019", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "HB", "wp": 20, "gueltig_ab": "2019-05-26", "gueltig_bis": "2023-05-14", "name": "DIE LINKE HB Wahlprogramm 2019", "titel": None, "pdf": "linke-hb-2019.pdf", "seiten": 64}, + "spd-hb-2019": {"id": "spd-hb-2019", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "HB", "wp": 20, "gueltig_ab": "2019-05-26", "gueltig_bis": "2023-05-14", "name": "SPD HB Wahlprogramm 2019", "titel": None, "pdf": "spd-hb-2019.pdf", "seiten": 132}, + "biw-hb-2023": {"id": "biw-hb-2023", "typ": "wahlprogramm", "partei": "BiW", "bundesland": "HB", "wp": 21, "gueltig_ab": "2023-05-14", "gueltig_bis": None, "name": "BiW Bremen Wahlprogramm 2023", "titel": "BÜRGER IN WUT — Programm für die Bürgerschaftswahl 2023", "pdf": "biw-hb-2023.pdf", "seiten": 26}, + "cdu-hb-2023": {"id": "cdu-hb-2023", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "HB", "wp": 21, "gueltig_ab": "2023-05-14", "gueltig_bis": None, "name": "CDU Bremen Wahlprogramm 2023", "titel": "CDU Bremen Wahlprogramm Bürgerschaftswahl 2023", "pdf": "cdu-hb-2023.pdf", "seiten": 100}, + "gruene-hb-2023": {"id": "gruene-hb-2023", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "HB", "wp": 21, "gueltig_ab": "2023-05-14", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Bremen Wahlprogramm 2023", "titel": None, "pdf": "gruene-hb-2023.pdf", "seiten": 100}, + "linke-hb-2023": {"id": "linke-hb-2023", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "HB", "wp": 21, "gueltig_ab": "2023-05-14", "gueltig_bis": None, "name": "DIE LINKE Bremen Wahlprogramm 2023", "titel": "DIE LINKE Bremen Wahlprogramm Bürgerschaftswahl 2023", "pdf": "linke-hb-2023.pdf", "seiten": 100}, + "spd-hb-2023": {"id": "spd-hb-2023", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "HB", "wp": 21, "gueltig_ab": "2023-05-14", "gueltig_bis": None, "name": "SPD Bremen Wahlprogramm 2023", "titel": "SPD Bremen Wahlprogramm Bürgerschaftswahl 2023", "pdf": "spd-hb-2023.pdf", "seiten": 100}, + + # ─── wahlprogramm · HE ─── + "cdu-he-2013": {"id": "cdu-he-2013", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "HE", "wp": 19, "gueltig_ab": "2013-09-22", "gueltig_bis": "2019-01-18", "name": "CDU HE Wahlprogramm 2013", "titel": None, "pdf": "cdu-he-2013.pdf", "seiten": 72}, + "fdp-he-2013": {"id": "fdp-he-2013", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "HE", "wp": 19, "gueltig_ab": "2013-09-22", "gueltig_bis": "2019-01-18", "name": "FDP HE Wahlprogramm 2013", "titel": None, "pdf": "fdp-he-2013.pdf", "seiten": 197}, + "gruene-he-2013": {"id": "gruene-he-2013", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "HE", "wp": 19, "gueltig_ab": "2013-09-22", "gueltig_bis": "2019-01-18", "name": "Grüne HE Wahlprogramm 2013", "titel": None, "pdf": "gruene-he-2013.pdf", "seiten": 110}, + "linke-he-2013": {"id": "linke-he-2013", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "HE", "wp": 19, "gueltig_ab": "2013-09-22", "gueltig_bis": "2019-01-18", "name": "DIE LINKE HE Wahlprogramm 2013", "titel": None, "pdf": "linke-he-2013.pdf", "seiten": 60}, + "spd-he-2013": {"id": "spd-he-2013", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "HE", "wp": 19, "gueltig_ab": "2013-09-22", "gueltig_bis": "2019-01-18", "name": "SPD HE Wahlprogramm 2013", "titel": None, "pdf": "spd-he-2013.pdf", "seiten": 112}, + "afd-he-2018": {"id": "afd-he-2018", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "HE", "wp": 20, "gueltig_ab": "2018-10-28", "gueltig_bis": "2023-10-08", "name": "AfD HE Wahlprogramm 2018", "titel": None, "pdf": "afd-he-2018.pdf", "seiten": 90}, + "cdu-he-2018": {"id": "cdu-he-2018", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "HE", "wp": 20, "gueltig_ab": "2018-10-28", "gueltig_bis": "2023-10-08", "name": "CDU HE Wahlprogramm 2018", "titel": None, "pdf": "cdu-he-2018.pdf", "seiten": 132}, + "fdp-he-2018": {"id": "fdp-he-2018", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "HE", "wp": 20, "gueltig_ab": "2018-10-28", "gueltig_bis": "2023-10-08", "name": "FDP HE Wahlprogramm 2018", "titel": None, "pdf": "fdp-he-2018.pdf", "seiten": 112}, + "gruene-he-2018": {"id": "gruene-he-2018", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "HE", "wp": 20, "gueltig_ab": "2018-10-28", "gueltig_bis": "2023-10-08", "name": "Grüne HE Wahlprogramm 2018", "titel": None, "pdf": "gruene-he-2018.pdf", "seiten": 140}, + "linke-he-2018": {"id": "linke-he-2018", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "HE", "wp": 20, "gueltig_ab": "2018-10-28", "gueltig_bis": "2023-10-08", "name": "DIE LINKE HE Wahlprogramm 2018", "titel": None, "pdf": "linke-he-2018.pdf", "seiten": 90}, + "spd-he-2018": {"id": "spd-he-2018", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "HE", "wp": 20, "gueltig_ab": "2018-10-28", "gueltig_bis": "2023-10-08", "name": "SPD HE Wahlprogramm 2018", "titel": None, "pdf": "spd-he-2018.pdf", "seiten": 175}, + "afd-he-2023": {"id": "afd-he-2023", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "HE", "wp": 21, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "AfD Hessen Wahlprogramm 2023", "titel": None, "pdf": "afd-he-2023.pdf", "seiten": 100}, + "cdu-he-2023": {"id": "cdu-he-2023", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "HE", "wp": 21, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "CDU Hessen Wahlprogramm 2023", "titel": "CDU Hessen Regierungsprogramm 2023", "pdf": "cdu-he-2023.pdf", "seiten": 100}, + "fdp-he-2023": {"id": "fdp-he-2023", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "HE", "wp": 21, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "FDP Hessen Wahlprogramm 2023", "titel": None, "pdf": "fdp-he-2023.pdf", "seiten": 100}, + "gruene-he-2023": {"id": "gruene-he-2023", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "HE", "wp": 21, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Hessen Wahlprogramm 2023", "titel": None, "pdf": "gruene-he-2023.pdf", "seiten": 100}, + "spd-he-2023": {"id": "spd-he-2023", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "HE", "wp": 21, "gueltig_ab": "2023-10-08", "gueltig_bis": None, "name": "SPD Hessen Wahlprogramm 2023", "titel": None, "pdf": "spd-he-2023.pdf", "seiten": 100}, + + # ─── wahlprogramm · HH ─── + "afd-hh-2015": {"id": "afd-hh-2015", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "HH", "wp": 21, "gueltig_ab": "2015-02-15", "gueltig_bis": "2020-02-23", "name": "AfD HH Wahlprogramm 2015", "titel": None, "pdf": "afd-hh-2015.pdf", "seiten": 28}, + "cdu-hh-2015": {"id": "cdu-hh-2015", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "HH", "wp": 21, "gueltig_ab": "2015-02-15", "gueltig_bis": "2020-02-23", "name": "CDU HH Wahlprogramm 2015", "titel": None, "pdf": "cdu-hh-2015.pdf", "seiten": 60}, + "fdp-hh-2015": {"id": "fdp-hh-2015", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "HH", "wp": 21, "gueltig_ab": "2015-02-15", "gueltig_bis": "2020-02-23", "name": "FDP HH Wahlprogramm 2015", "titel": None, "pdf": "fdp-hh-2015.pdf", "seiten": 63}, + "gruene-hh-2015": {"id": "gruene-hh-2015", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "HH", "wp": 21, "gueltig_ab": "2015-02-15", "gueltig_bis": "2020-02-23", "name": "Grüne HH Wahlprogramm 2015", "titel": None, "pdf": "gruene-hh-2015.pdf", "seiten": 237}, + "linke-hh-2015": {"id": "linke-hh-2015", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "HH", "wp": 21, "gueltig_ab": "2015-02-15", "gueltig_bis": "2020-02-23", "name": "DIE LINKE HH Wahlprogramm 2015", "titel": None, "pdf": "linke-hh-2015.pdf", "seiten": 44}, + "spd-hh-2015": {"id": "spd-hh-2015", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "HH", "wp": 21, "gueltig_ab": "2015-02-15", "gueltig_bis": "2020-02-23", "name": "SPD HH Wahlprogramm 2015", "titel": None, "pdf": "spd-hh-2015.pdf", "seiten": 70}, + "afd-hh-2020": {"id": "afd-hh-2020", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "HH", "wp": 22, "gueltig_ab": "2020-02-23", "gueltig_bis": "2025-03-02", "name": "AfD HH Wahlprogramm 2020", "titel": None, "pdf": "afd-hh-2020.pdf", "seiten": 40}, + "cdu-hh-2020": {"id": "cdu-hh-2020", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "HH", "wp": 22, "gueltig_ab": "2020-02-23", "gueltig_bis": "2025-03-02", "name": "CDU HH Wahlprogramm 2020", "titel": None, "pdf": "cdu-hh-2020.pdf", "seiten": 84}, + "fdp-hh-2020": {"id": "fdp-hh-2020", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "HH", "wp": 22, "gueltig_ab": "2020-02-23", "gueltig_bis": "2025-03-02", "name": "FDP HH Wahlprogramm 2020", "titel": None, "pdf": "fdp-hh-2020.pdf", "seiten": 97}, + "gruene-hh-2020": {"id": "gruene-hh-2020", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "HH", "wp": 22, "gueltig_ab": "2020-02-23", "gueltig_bis": "2025-03-02", "name": "Grüne HH Wahlprogramm 2020", "titel": None, "pdf": "gruene-hh-2020.pdf", "seiten": 146}, + "linke-hh-2020": {"id": "linke-hh-2020", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "HH", "wp": 22, "gueltig_ab": "2020-02-23", "gueltig_bis": "2025-03-02", "name": "DIE LINKE HH Wahlprogramm 2020", "titel": None, "pdf": "linke-hh-2020.pdf", "seiten": 37}, + "spd-hh-2020": {"id": "spd-hh-2020", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "HH", "wp": 22, "gueltig_ab": "2020-02-23", "gueltig_bis": "2025-03-02", "name": "SPD HH Wahlprogramm 2020", "titel": None, "pdf": "spd-hh-2020.pdf", "seiten": 90}, + "afd-hh-2025": {"id": "afd-hh-2025", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "HH", "wp": 23, "gueltig_ab": "2025-03-02", "gueltig_bis": None, "name": "AfD Hamburg Wahlprogramm 2025", "titel": "AfD Hamburg Wahlprogramm Bürgerschaftswahl 2025", "pdf": "afd-hh-2025.pdf", "seiten": 100}, + "cdu-hh-2025": {"id": "cdu-hh-2025", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "HH", "wp": 23, "gueltig_ab": "2025-03-02", "gueltig_bis": None, "name": "CDU Hamburg Wahlprogramm 2025", "titel": "CDU Hamburg Wahlprogramm Bürgerschaftswahl 2025", "pdf": "cdu-hh-2025.pdf", "seiten": 100}, + "gruene-hh-2025": {"id": "gruene-hh-2025", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "HH", "wp": 23, "gueltig_ab": "2025-03-02", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Hamburg Wahlprogramm 2025", "titel": "Gute Gründe für Grün — Regierungsprogramm BÜNDNIS 90/DIE GRÜNEN Hamburg 2025", "pdf": "gruene-hh-2025.pdf", "seiten": 100}, + "linke-hh-2025": {"id": "linke-hh-2025", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "HH", "wp": 23, "gueltig_ab": "2025-03-02", "gueltig_bis": None, "name": "DIE LINKE Hamburg Wahlprogramm 2025", "titel": "DIE LINKE Hamburg Wahlprogramm Bürgerschaftswahl 2025", "pdf": "linke-hh-2025.pdf", "seiten": 100}, + "spd-hh-2025": {"id": "spd-hh-2025", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "HH", "wp": 23, "gueltig_ab": "2025-03-02", "gueltig_bis": None, "name": "SPD Hamburg Wahlprogramm 2025", "titel": "SPD Hamburg Wahlprogramm Bürgerschaftswahl 2025", "pdf": "spd-hh-2025.pdf", "seiten": 100}, + + # ─── wahlprogramm · LSA ─── + "cdu-lsa-2011": {"id": "cdu-lsa-2011", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "LSA", "wp": 6, "gueltig_ab": "2011-03-20", "gueltig_bis": "2016-04-13", "name": "CDU LSA Wahlprogramm 2011", "titel": None, "pdf": "cdu-lsa-2011.pdf", "seiten": 66}, + "gruene-lsa-2011": {"id": "gruene-lsa-2011", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "LSA", "wp": 6, "gueltig_ab": "2011-03-20", "gueltig_bis": "2016-04-13", "name": "Grüne LSA Wahlprogramm 2011", "titel": None, "pdf": "gruene-lsa-2011.pdf", "seiten": 94}, + "linke-lsa-2011": {"id": "linke-lsa-2011", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "LSA", "wp": 6, "gueltig_ab": "2011-03-20", "gueltig_bis": "2016-04-13", "name": "DIE LINKE LSA Wahlprogramm 2011", "titel": None, "pdf": "linke-lsa-2011.pdf", "seiten": 36}, + "spd-lsa-2011": {"id": "spd-lsa-2011", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "LSA", "wp": 6, "gueltig_ab": "2011-03-20", "gueltig_bis": "2016-04-13", "name": "SPD LSA Wahlprogramm 2011", "titel": None, "pdf": "spd-lsa-2011.pdf", "seiten": 38}, + "afd-lsa-2016": {"id": "afd-lsa-2016", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "LSA", "wp": 7, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-06-06", "name": "AfD LSA Wahlprogramm 2016", "titel": None, "pdf": "afd-lsa-2016.pdf", "seiten": 68}, + "cdu-lsa-2016": {"id": "cdu-lsa-2016", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "LSA", "wp": 7, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-06-06", "name": "CDU LSA Wahlprogramm 2016", "titel": None, "pdf": "cdu-lsa-2016.pdf", "seiten": 64}, + "fdp-lsa-2016": {"id": "fdp-lsa-2016", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "LSA", "wp": 7, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-06-06", "name": "FDP LSA Wahlprogramm 2016", "titel": None, "pdf": "fdp-lsa-2016.pdf", "seiten": 51}, + "gruene-lsa-2016": {"id": "gruene-lsa-2016", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "LSA", "wp": 7, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-06-06", "name": "Grüne LSA Wahlprogramm 2016", "titel": None, "pdf": "gruene-lsa-2016.pdf", "seiten": 129}, + "linke-lsa-2016": {"id": "linke-lsa-2016", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "LSA", "wp": 7, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-06-06", "name": "DIE LINKE LSA Wahlprogramm 2016", "titel": None, "pdf": "linke-lsa-2016.pdf", "seiten": 59}, + "spd-lsa-2016": {"id": "spd-lsa-2016", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "LSA", "wp": 7, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-06-06", "name": "SPD LSA Wahlprogramm 2016", "titel": None, "pdf": "spd-lsa-2016.pdf", "seiten": 72}, + "afd-lsa-2021": {"id": "afd-lsa-2021", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "LSA", "wp": 8, "gueltig_ab": "2021-06-06", "gueltig_bis": None, "name": "AfD Sachsen-Anhalt Wahlprogramm 2021", "titel": "Alles für unsere Heimat! Programm der AfD Sachsen-Anhalt zur Landtagswahl 2021", "pdf": "afd-lsa-2021.pdf", "seiten": 64}, + "cdu-lsa-2021": {"id": "cdu-lsa-2021", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "LSA", "wp": 8, "gueltig_ab": "2021-06-06", "gueltig_bis": None, "name": "CDU Sachsen-Anhalt Wahlprogramm 2021", "titel": "Unsere Heimat. Unsere Verantwortung.", "pdf": "cdu-lsa-2021.pdf", "seiten": 82}, + "fdp-lsa-2021": {"id": "fdp-lsa-2021", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "LSA", "wp": 8, "gueltig_ab": "2021-06-06", "gueltig_bis": None, "name": "FDP Sachsen-Anhalt Wahlprogramm 2021", "titel": "Wahlprogramm der FDP Sachsen-Anhalt zur Landtagswahl 2021", "pdf": "fdp-lsa-2021.pdf", "seiten": 76}, + "gruene-lsa-2021": {"id": "gruene-lsa-2021", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "LSA", "wp": 8, "gueltig_ab": "2021-06-06", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Sachsen-Anhalt Wahlprogramm 2021", "titel": "Verlässlich für Sachsen-Anhalt", "pdf": "gruene-lsa-2021.pdf", "seiten": 164}, + "linke-lsa-2021": {"id": "linke-lsa-2021", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "LSA", "wp": 8, "gueltig_ab": "2021-06-06", "gueltig_bis": None, "name": "DIE LINKE Sachsen-Anhalt Wahlprogramm 2021", "titel": "Wahlprogramm zur Landtagswahl 2021", "pdf": "linke-lsa-2021.pdf", "seiten": 88}, + "spd-lsa-2021": {"id": "spd-lsa-2021", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "LSA", "wp": 8, "gueltig_ab": "2021-06-06", "gueltig_bis": None, "name": "SPD Sachsen-Anhalt Wahlprogramm 2021", "titel": "Zusammenhalt und neue Chancen. Politik fürs ganze Land", "pdf": "spd-lsa-2021.pdf", "seiten": 77}, + + # ─── wahlprogramm · MV ─── + "cdu-mv-2011": {"id": "cdu-mv-2011", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "MV", "wp": 6, "gueltig_ab": "2011-09-04", "gueltig_bis": "2016-10-04", "name": "CDU MV Wahlprogramm 2011", "titel": None, "pdf": "cdu-mv-2011.pdf", "seiten": 34}, + "gruene-mv-2011": {"id": "gruene-mv-2011", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "MV", "wp": 6, "gueltig_ab": "2011-09-04", "gueltig_bis": "2016-10-04", "name": "Grüne MV Wahlprogramm 2011", "titel": None, "pdf": "gruene-mv-2011.pdf", "seiten": 144}, + "linke-mv-2011": {"id": "linke-mv-2011", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "MV", "wp": 6, "gueltig_ab": "2011-09-04", "gueltig_bis": "2016-10-04", "name": "DIE LINKE MV Wahlprogramm 2011", "titel": None, "pdf": "linke-mv-2011.pdf", "seiten": 33}, + "spd-mv-2011": {"id": "spd-mv-2011", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "MV", "wp": 6, "gueltig_ab": "2011-09-04", "gueltig_bis": "2016-10-04", "name": "SPD MV Wahlprogramm 2011", "titel": None, "pdf": "spd-mv-2011.pdf", "seiten": 50}, + "afd-mv-2016": {"id": "afd-mv-2016", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "MV", "wp": 7, "gueltig_ab": "2016-09-04", "gueltig_bis": "2021-09-26", "name": "AfD MV Wahlprogramm 2016", "titel": None, "pdf": "afd-mv-2016.pdf", "seiten": 22}, + "cdu-mv-2016": {"id": "cdu-mv-2016", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "MV", "wp": 7, "gueltig_ab": "2016-09-04", "gueltig_bis": "2021-09-26", "name": "CDU MV Wahlprogramm 2016", "titel": None, "pdf": "cdu-mv-2016.pdf", "seiten": 27}, + "gruene-mv-2016": {"id": "gruene-mv-2016", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "MV", "wp": 7, "gueltig_ab": "2016-09-04", "gueltig_bis": "2021-09-26", "name": "Grüne MV Wahlprogramm 2016", "titel": None, "pdf": "gruene-mv-2016.pdf", "seiten": 100}, + "linke-mv-2016": {"id": "linke-mv-2016", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "MV", "wp": 7, "gueltig_ab": "2016-09-04", "gueltig_bis": "2021-09-26", "name": "DIE LINKE MV Wahlprogramm 2016", "titel": None, "pdf": "linke-mv-2016.pdf", "seiten": 49}, + "spd-mv-2016": {"id": "spd-mv-2016", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "MV", "wp": 7, "gueltig_ab": "2016-09-04", "gueltig_bis": "2021-09-26", "name": "SPD MV Wahlprogramm 2016", "titel": None, "pdf": "spd-mv-2016.pdf", "seiten": 48}, + "afd-mv-2021": {"id": "afd-mv-2021", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "MV", "wp": 8, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "AfD Mecklenburg-Vorpommern Wahlprogramm 2021", "titel": "Landeswahlprogramm der AfD Mecklenburg-Vorpommern 2021", "pdf": "afd-mv-2021.pdf", "seiten": 84}, + "cdu-mv-2021": {"id": "cdu-mv-2021", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "MV", "wp": 8, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "CDU Mecklenburg-Vorpommern Wahlprogramm 2021", "titel": "Zusammen. Den Blick nach vorn. Gemeinsam die Zukunft meistern", "pdf": "cdu-mv-2021.pdf", "seiten": 56}, + "fdp-mv-2021": {"id": "fdp-mv-2021", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "MV", "wp": 8, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "FDP Mecklenburg-Vorpommern Wahlprogramm 2021", "titel": "Wahlprogramm der Freien Demokraten Mecklenburg-Vorpommern zur Landtagswahl 2021", "pdf": "fdp-mv-2021.pdf", "seiten": 120}, + "gruene-mv-2021": {"id": "gruene-mv-2021", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "MV", "wp": 8, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Mecklenburg-Vorpommern Wahlprogramm 2021", "titel": "Für Klima, Land und ein besseres Miteinander", "pdf": "gruene-mv-2021.pdf", "seiten": 88}, + "linke-mv-2021": {"id": "linke-mv-2021", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "MV", "wp": 8, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "DIE LINKE Mecklenburg-Vorpommern Wahlprogramm 2021", "titel": "Das ist links! — Zukunftsprogramm für Mecklenburg-Vorpommern", "pdf": "linke-mv-2021.pdf", "seiten": 82}, + "spd-mv-2021": {"id": "spd-mv-2021", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "MV", "wp": 8, "gueltig_ab": "2021-09-26", "gueltig_bis": None, "name": "SPD Mecklenburg-Vorpommern Wahlprogramm 2021", "titel": "Verantwortung für heute und morgen — Regierungsprogramm 2021–2026", "pdf": "spd-mv-2021.pdf", "seiten": 95}, + + # ─── wahlprogramm · NI ─── + "cdu-ni-2013": {"id": "cdu-ni-2013", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "NI", "wp": 17, "gueltig_ab": "2013-01-20", "gueltig_bis": "2017-11-14", "name": "CDU NI Wahlprogramm 2013", "titel": None, "pdf": "cdu-ni-2013.pdf", "seiten": 86}, + "fdp-ni-2013": {"id": "fdp-ni-2013", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "NI", "wp": 17, "gueltig_ab": "2013-01-20", "gueltig_bis": "2017-11-14", "name": "FDP NI Wahlprogramm 2013", "titel": None, "pdf": "fdp-ni-2013.pdf", "seiten": 65}, + "gruene-ni-2013": {"id": "gruene-ni-2013", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "NI", "wp": 17, "gueltig_ab": "2013-01-20", "gueltig_bis": "2017-11-14", "name": "Grüne NI Wahlprogramm 2013", "titel": None, "pdf": "gruene-ni-2013.pdf", "seiten": 92}, + "spd-ni-2013": {"id": "spd-ni-2013", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "NI", "wp": 17, "gueltig_ab": "2013-01-20", "gueltig_bis": "2017-11-14", "name": "SPD NI Wahlprogramm 2013", "titel": None, "pdf": "spd-ni-2013.pdf", "seiten": 70}, + "afd-ni-2017": {"id": "afd-ni-2017", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "NI", "wp": 18, "gueltig_ab": "2017-10-15", "gueltig_bis": "2022-10-09", "name": "AfD NI Wahlprogramm 2017", "titel": None, "pdf": "afd-ni-2017.pdf", "seiten": 93}, + "cdu-ni-2017": {"id": "cdu-ni-2017", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "NI", "wp": 18, "gueltig_ab": "2017-10-15", "gueltig_bis": "2022-10-09", "name": "CDU NI Wahlprogramm 2017", "titel": None, "pdf": "cdu-ni-2017.pdf", "seiten": 116}, + "fdp-ni-2017": {"id": "fdp-ni-2017", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "NI", "wp": 18, "gueltig_ab": "2017-10-15", "gueltig_bis": "2022-10-09", "name": "FDP NI Wahlprogramm 2017", "titel": None, "pdf": "fdp-ni-2017.pdf", "seiten": 74}, + "gruene-ni-2017": {"id": "gruene-ni-2017", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "NI", "wp": 18, "gueltig_ab": "2017-10-15", "gueltig_bis": "2022-10-09", "name": "Grüne NI Wahlprogramm 2017", "titel": None, "pdf": "gruene-ni-2017.pdf", "seiten": 101}, + "spd-ni-2017": {"id": "spd-ni-2017", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "NI", "wp": 18, "gueltig_ab": "2017-10-15", "gueltig_bis": "2022-10-09", "name": "SPD NI Wahlprogramm 2017", "titel": None, "pdf": "spd-ni-2017.pdf", "seiten": 154}, + "afd-ni-2022": {"id": "afd-ni-2022", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "NI", "wp": 19, "gueltig_ab": "2022-10-09", "gueltig_bis": None, "name": "AfD Niedersachsen Wahlprogramm 2022", "titel": None, "pdf": "afd-ni-2022.pdf", "seiten": 100}, + "cdu-ni-2022": {"id": "cdu-ni-2022", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "NI", "wp": 19, "gueltig_ab": "2022-10-09", "gueltig_bis": None, "name": "CDU Niedersachsen Wahlprogramm 2022", "titel": "CDU Niedersachsen Regierungsprogramm 2022", "pdf": "cdu-ni-2022.pdf", "seiten": 100}, + "gruene-ni-2022": {"id": "gruene-ni-2022", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "NI", "wp": 19, "gueltig_ab": "2022-10-09", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Niedersachsen Wahlprogramm 2022", "titel": None, "pdf": "gruene-ni-2022.pdf", "seiten": 100}, + "spd-ni-2022": {"id": "spd-ni-2022", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "NI", "wp": 19, "gueltig_ab": "2022-10-09", "gueltig_bis": None, "name": "SPD Niedersachsen Wahlprogramm 2022", "titel": "SPD Niedersachsen Regierungsprogramm 2022", "pdf": "spd-ni-2022.pdf", "seiten": 100}, + + # ─── wahlprogramm · NRW ─── + "cdu-nrw-2012": {"id": "cdu-nrw-2012", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "NRW", "wp": 16, "gueltig_ab": "2012-05-13", "gueltig_bis": "2017-06-01", "name": "CDU NRW Wahlprogramm 2012", "titel": None, "pdf": "cdu-nrw-2012.pdf", "seiten": 17}, + "fdp-nrw-2012": {"id": "fdp-nrw-2012", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "NRW", "wp": 16, "gueltig_ab": "2012-05-13", "gueltig_bis": "2017-06-01", "name": "FDP NRW Wahlprogramm 2012", "titel": None, "pdf": "fdp-nrw-2012.pdf", "seiten": 6}, + "gruene-nrw-2012": {"id": "gruene-nrw-2012", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "NRW", "wp": 16, "gueltig_ab": "2012-05-13", "gueltig_bis": "2017-06-01", "name": "Grüne NRW Zukunftsplan Update 2012", "titel": None, "pdf": "gruene-nrw-2012.pdf", "seiten": 52}, + "piraten-nrw-2012": {"id": "piraten-nrw-2012", "typ": "wahlprogramm", "partei": "PIRATEN", "bundesland": "NRW", "wp": 16, "gueltig_ab": "2012-05-13", "gueltig_bis": "2017-06-01", "name": "PIRATEN NRW Wahlprogramm 2012", "titel": None, "pdf": "piraten-nrw-2012.pdf", "seiten": 76}, + "spd-nrw-2012": {"id": "spd-nrw-2012", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "NRW", "wp": 16, "gueltig_ab": "2012-05-13", "gueltig_bis": "2017-06-01", "name": "SPD NRW Wahlprogramm 2012", "titel": None, "pdf": "spd-nrw-2012.pdf", "seiten": 22}, + "afd-nrw-2017": {"id": "afd-nrw-2017", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "NRW", "wp": 17, "gueltig_ab": "2017-05-14", "gueltig_bis": "2022-05-15", "name": "AfD NRW Wahlprogramm 2017", "titel": None, "pdf": "afd-nrw-2017.pdf", "seiten": 84}, + "cdu-nrw-2017": {"id": "cdu-nrw-2017", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "NRW", "wp": 17, "gueltig_ab": "2017-05-14", "gueltig_bis": "2022-05-15", "name": "CDU NRW Regierungsprogramm 2017", "titel": None, "pdf": "cdu-nrw-2017.pdf", "seiten": 120}, + "fdp-nrw-2017": {"id": "fdp-nrw-2017", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "NRW", "wp": 17, "gueltig_ab": "2017-05-14", "gueltig_bis": "2022-05-15", "name": "FDP NRW Wahlprogramm 2017", "titel": None, "pdf": "fdp-nrw-2017.pdf", "seiten": 56}, + "gruene-nrw-2017": {"id": "gruene-nrw-2017", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "NRW", "wp": 17, "gueltig_ab": "2017-05-14", "gueltig_bis": "2022-05-15", "name": "Grüne NRW Wahlprogramm 2017", "titel": None, "pdf": "gruene-nrw-2017.pdf", "seiten": 131}, + "spd-nrw-2017": {"id": "spd-nrw-2017", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "NRW", "wp": 17, "gueltig_ab": "2017-05-14", "gueltig_bis": "2022-05-15", "name": "SPD NRW Regierungsprogramm 2017", "titel": None, "pdf": "spd-nrw-2017.pdf", "seiten": 116}, + "afd-nrw-2022": {"id": "afd-nrw-2022", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "NRW", "wp": 18, "gueltig_ab": "2022-05-15", "gueltig_bis": None, "name": "AfD NRW Wahlprogramm 2022", "titel": "Wer sonst.", "pdf": "afd-nrw-2022.pdf", "seiten": 68}, + "cdu-nrw-2022": {"id": "cdu-nrw-2022", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "NRW", "wp": 18, "gueltig_ab": "2022-05-15", "gueltig_bis": None, "name": "CDU NRW Wahlprogramm 2022", "titel": "Machen, worauf es ankommt", "pdf": "cdu-nrw-2022.pdf", "seiten": 109}, + "fdp-nrw-2022": {"id": "fdp-nrw-2022", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "NRW", "wp": 18, "gueltig_ab": "2022-05-15", "gueltig_bis": None, "name": "FDP NRW Wahlprogramm 2022", "titel": "Nie gab es mehr zu tun", "pdf": "fdp-nrw-2022.pdf", "seiten": 96}, + "gruene-nrw-2022": {"id": "gruene-nrw-2022", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "NRW", "wp": 18, "gueltig_ab": "2022-05-15", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN NRW Wahlprogramm 2022", "titel": "Von hier an Zukunft", "pdf": "gruene-nrw-2022.pdf", "seiten": 100}, + "spd-nrw-2022": {"id": "spd-nrw-2022", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "NRW", "wp": 18, "gueltig_ab": "2022-05-15", "gueltig_bis": None, "name": "SPD NRW Wahlprogramm 2022", "titel": "Unser Land von morgen", "pdf": "spd-nrw-2022.pdf", "seiten": 116}, + + # ─── wahlprogramm · RP ─── + "cdu-rp-2011": {"id": "cdu-rp-2011", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "RP", "wp": 16, "gueltig_ab": "2011-03-27", "gueltig_bis": "2016-05-18", "name": "CDU RP Wahlprogramm 2011", "titel": None, "pdf": "cdu-rp-2011.pdf", "seiten": 84}, + "gruene-rp-2011": {"id": "gruene-rp-2011", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "RP", "wp": 16, "gueltig_ab": "2011-03-27", "gueltig_bis": "2016-05-18", "name": "Grüne RP Wahlprogramm 2011", "titel": None, "pdf": "gruene-rp-2011.pdf", "seiten": 110}, + "spd-rp-2011": {"id": "spd-rp-2011", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "RP", "wp": 16, "gueltig_ab": "2011-03-27", "gueltig_bis": "2016-05-18", "name": "SPD RP Wahlprogramm 2011", "titel": None, "pdf": "spd-rp-2011.pdf", "seiten": 65}, + "afd-rp-2016": {"id": "afd-rp-2016", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "RP", "wp": 17, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "AfD RP Wahlprogramm 2016", "titel": None, "pdf": "afd-rp-2016.pdf", "seiten": 17}, + "cdu-rp-2016": {"id": "cdu-rp-2016", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "RP", "wp": 17, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "CDU RP Wahlprogramm 2016", "titel": None, "pdf": "cdu-rp-2016.pdf", "seiten": 102}, + "fdp-rp-2016": {"id": "fdp-rp-2016", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "RP", "wp": 17, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "FDP RP Wahlprogramm 2016", "titel": None, "pdf": "fdp-rp-2016.pdf", "seiten": 84}, + "gruene-rp-2016": {"id": "gruene-rp-2016", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "RP", "wp": 17, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "Grüne RP Wahlprogramm 2016", "titel": None, "pdf": "gruene-rp-2016.pdf", "seiten": 57}, + "spd-rp-2016": {"id": "spd-rp-2016", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "RP", "wp": 17, "gueltig_ab": "2016-03-13", "gueltig_bis": "2021-03-14", "name": "SPD RP Wahlprogramm 2016", "titel": None, "pdf": "spd-rp-2016.pdf", "seiten": 64}, + "afd-rp-2021": {"id": "afd-rp-2021", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "RP", "wp": 18, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "AfD Rheinland-Pfalz Wahlprogramm 2021", "titel": None, "pdf": "afd-rp-2021.pdf", "seiten": 100}, + "cdu-rp-2021": {"id": "cdu-rp-2021", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "RP", "wp": 18, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "CDU Rheinland-Pfalz Wahlprogramm 2021", "titel": "Regierungsprogramm der CDU RLP 2021–26", "pdf": "cdu-rp-2021.pdf", "seiten": 100}, + "fdp-rp-2021": {"id": "fdp-rp-2021", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "RP", "wp": 18, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "FDP Rheinland-Pfalz Wahlprogramm 2021", "titel": "FDP Rheinland-Pfalz Landtagswahlprogramm 2021", "pdf": "fdp-rp-2021.pdf", "seiten": 100}, + "fw-rp-2021": {"id": "fw-rp-2021", "typ": "wahlprogramm", "partei": "FREIE WÄHLER", "bundesland": "RP", "wp": 18, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "FREIE WÄHLER Rheinland-Pfalz Wahlprogramm 2021", "titel": None, "pdf": "fw-rp-2021.pdf", "seiten": 80}, + "gruene-rp-2021": {"id": "gruene-rp-2021", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "RP", "wp": 18, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Rheinland-Pfalz Wahlprogramm 2021", "titel": "BÜNDNIS 90/DIE GRÜNEN Rheinland-Pfalz Landtagswahlprogramm 2021", "pdf": "gruene-rp-2021.pdf", "seiten": 100}, + "spd-rp-2021": {"id": "spd-rp-2021", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "RP", "wp": 18, "gueltig_ab": "2021-03-14", "gueltig_bis": None, "name": "SPD Rheinland-Pfalz Wahlprogramm 2021", "titel": "Wir mit Ihr — Regierungsprogramm der SPD Rheinland-Pfalz 2021–2026", "pdf": "spd-rp-2021.pdf", "seiten": 100}, + + # ─── wahlprogramm · SH ─── + "cdu-sh-2012": {"id": "cdu-sh-2012", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "SH", "wp": 18, "gueltig_ab": "2012-05-06", "gueltig_bis": "2017-06-06", "name": "CDU SH Wahlprogramm 2012", "titel": None, "pdf": "cdu-sh-2012.pdf", "seiten": 150}, + "fdp-sh-2012": {"id": "fdp-sh-2012", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "SH", "wp": 18, "gueltig_ab": "2012-05-06", "gueltig_bis": "2017-06-06", "name": "FDP SH Wahlprogramm 2012", "titel": None, "pdf": "fdp-sh-2012.pdf", "seiten": 99}, + "gruene-sh-2012": {"id": "gruene-sh-2012", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SH", "wp": 18, "gueltig_ab": "2012-05-06", "gueltig_bis": "2017-06-06", "name": "Grüne SH Wahlprogramm 2012", "titel": None, "pdf": "gruene-sh-2012.pdf", "seiten": 80}, + "piraten-sh-2012": {"id": "piraten-sh-2012", "typ": "wahlprogramm", "partei": "PIRATEN", "bundesland": "SH", "wp": 18, "gueltig_ab": "2012-05-06", "gueltig_bis": "2017-06-06", "name": "PIRATEN SH Wahlprogramm 2012", "titel": None, "pdf": "piraten-sh-2012.pdf", "seiten": 80}, + "spd-sh-2012": {"id": "spd-sh-2012", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SH", "wp": 18, "gueltig_ab": "2012-05-06", "gueltig_bis": "2017-06-06", "name": "SPD SH Wahlprogramm 2012", "titel": None, "pdf": "spd-sh-2012.pdf", "seiten": 36}, + "ssw-sh-2012": {"id": "ssw-sh-2012", "typ": "wahlprogramm", "partei": "SSW", "bundesland": "SH", "wp": 18, "gueltig_ab": "2012-05-06", "gueltig_bis": "2017-06-06", "name": "SSW SH Wahlprogramm 2012", "titel": None, "pdf": "ssw-sh-2012.pdf", "seiten": 84}, + "afd-sh-2017": {"id": "afd-sh-2017", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "SH", "wp": 19, "gueltig_ab": "2017-05-07", "gueltig_bis": "2022-05-08", "name": "AfD SH Wahlprogramm 2017", "titel": None, "pdf": "afd-sh-2017.pdf", "seiten": 56}, + "cdu-sh-2017": {"id": "cdu-sh-2017", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "SH", "wp": 19, "gueltig_ab": "2017-05-07", "gueltig_bis": "2022-05-08", "name": "CDU SH Wahlprogramm 2017", "titel": None, "pdf": "cdu-sh-2017.pdf", "seiten": 96}, + "fdp-sh-2017": {"id": "fdp-sh-2017", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "SH", "wp": 19, "gueltig_ab": "2017-05-07", "gueltig_bis": "2022-05-08", "name": "FDP SH Wahlprogramm 2017", "titel": None, "pdf": "fdp-sh-2017.pdf", "seiten": 117}, + "gruene-sh-2017": {"id": "gruene-sh-2017", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SH", "wp": 19, "gueltig_ab": "2017-05-07", "gueltig_bis": "2022-05-08", "name": "Grüne SH Wahlprogramm 2017", "titel": None, "pdf": "gruene-sh-2017.pdf", "seiten": 95}, + "spd-sh-2017": {"id": "spd-sh-2017", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SH", "wp": 19, "gueltig_ab": "2017-05-07", "gueltig_bis": "2022-05-08", "name": "SPD SH Wahlprogramm 2017", "titel": None, "pdf": "spd-sh-2017.pdf", "seiten": 66}, + "ssw-sh-2017": {"id": "ssw-sh-2017", "typ": "wahlprogramm", "partei": "SSW", "bundesland": "SH", "wp": 19, "gueltig_ab": "2017-05-07", "gueltig_bis": "2022-05-08", "name": "SSW SH Wahlprogramm 2017", "titel": None, "pdf": "ssw-sh-2017.pdf", "seiten": 60}, + "cdu-sh-2022": {"id": "cdu-sh-2022", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "SH", "wp": 20, "gueltig_ab": "2022-05-08", "gueltig_bis": None, "name": "CDU Schleswig-Holstein Wahlprogramm 2022", "titel": None, "pdf": "cdu-sh-2022.pdf", "seiten": 100}, + "fdp-sh-2022": {"id": "fdp-sh-2022", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "SH", "wp": 20, "gueltig_ab": "2022-05-08", "gueltig_bis": None, "name": "FDP Schleswig-Holstein Wahlprogramm 2022", "titel": None, "pdf": "fdp-sh-2022.pdf", "seiten": 100}, + "gruene-sh-2022": {"id": "gruene-sh-2022", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SH", "wp": 20, "gueltig_ab": "2022-05-08", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Schleswig-Holstein Wahlprogramm 2022", "titel": None, "pdf": "gruene-sh-2022.pdf", "seiten": 100}, + "spd-sh-2022": {"id": "spd-sh-2022", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SH", "wp": 20, "gueltig_ab": "2022-05-08", "gueltig_bis": None, "name": "SPD Schleswig-Holstein Wahlprogramm 2022", "titel": None, "pdf": "spd-sh-2022.pdf", "seiten": 100}, + "ssw-sh-2022": {"id": "ssw-sh-2022", "typ": "wahlprogramm", "partei": "SSW", "bundesland": "SH", "wp": 20, "gueltig_ab": "2022-05-08", "gueltig_bis": None, "name": "SSW Wahlprogramm 2022", "titel": "SSW Schleswig-Holstein Wahlprogramm 2022", "pdf": "ssw-sh-2022.pdf", "seiten": 80}, + + # ─── wahlprogramm · SL ─── + "cdu-sl-2012": {"id": "cdu-sl-2012", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "SL", "wp": 15, "gueltig_ab": "2012-03-25", "gueltig_bis": "2017-05-09", "name": "CDU SL Wahlprogramm 2012", "titel": None, "pdf": "cdu-sl-2012.pdf", "seiten": 36}, + "linke-sl-2012": {"id": "linke-sl-2012", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "SL", "wp": 15, "gueltig_ab": "2012-03-25", "gueltig_bis": "2017-05-09", "name": "DIE LINKE SL Wahlprogramm 2012", "titel": None, "pdf": "linke-sl-2012.pdf", "seiten": 2}, + "piraten-sl-2012": {"id": "piraten-sl-2012", "typ": "wahlprogramm", "partei": "PIRATEN", "bundesland": "SL", "wp": 15, "gueltig_ab": "2012-03-25", "gueltig_bis": "2017-05-09", "name": "PIRATEN SL Wahlprogramm 2012", "titel": None, "pdf": "piraten-sl-2012.pdf", "seiten": 21}, + "spd-sl-2012": {"id": "spd-sl-2012", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SL", "wp": 15, "gueltig_ab": "2012-03-25", "gueltig_bis": "2017-05-09", "name": "SPD SL Wahlprogramm 2012", "titel": None, "pdf": "spd-sl-2012.pdf", "seiten": 40}, + "afd-sl-2017": {"id": "afd-sl-2017", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "SL", "wp": 16, "gueltig_ab": "2017-03-26", "gueltig_bis": "2022-03-27", "name": "AfD SL Wahlprogramm 2017", "titel": None, "pdf": "afd-sl-2017.pdf", "seiten": 43}, + "cdu-sl-2017": {"id": "cdu-sl-2017", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "SL", "wp": 16, "gueltig_ab": "2017-03-26", "gueltig_bis": "2022-03-27", "name": "CDU SL Wahlprogramm 2017", "titel": None, "pdf": "cdu-sl-2017.pdf", "seiten": 72}, + "gruene-sl-2017": {"id": "gruene-sl-2017", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SL", "wp": 16, "gueltig_ab": "2017-03-26", "gueltig_bis": "2022-03-27", "name": "Grüne SL Wahlprogramm 2017", "titel": None, "pdf": "gruene-sl-2017.pdf", "seiten": 75}, + "linke-sl-2017": {"id": "linke-sl-2017", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "SL", "wp": 16, "gueltig_ab": "2017-03-26", "gueltig_bis": "2022-03-27", "name": "DIE LINKE SL Wahlprogramm 2017", "titel": None, "pdf": "linke-sl-2017.pdf", "seiten": 34}, + "spd-sl-2017": {"id": "spd-sl-2017", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SL", "wp": 16, "gueltig_ab": "2017-03-26", "gueltig_bis": "2022-03-27", "name": "SPD SL Wahlprogramm 2017", "titel": None, "pdf": "spd-sl-2017.pdf", "seiten": 41}, + "afd-sl-2022": {"id": "afd-sl-2022", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "SL", "wp": 17, "gueltig_ab": "2022-03-27", "gueltig_bis": None, "name": "AfD Saarland Wahlprogramm 2022", "titel": None, "pdf": "afd-sl-2022.pdf", "seiten": 100}, + "cdu-sl-2022": {"id": "cdu-sl-2022", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "SL", "wp": 17, "gueltig_ab": "2022-03-27", "gueltig_bis": None, "name": "CDU Saarland Wahlprogramm 2022", "titel": None, "pdf": "cdu-sl-2022.pdf", "seiten": 100}, + "spd-sl-2022": {"id": "spd-sl-2022", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SL", "wp": 17, "gueltig_ab": "2022-03-27", "gueltig_bis": None, "name": "SPD Saarland Wahlprogramm 2022", "titel": "SPD Saarland Regierungsprogramm 2022", "pdf": "spd-sl-2022.pdf", "seiten": 100}, + + # ─── wahlprogramm · SN ─── + "afd-sn-2014": {"id": "afd-sn-2014", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "SN", "wp": 6, "gueltig_ab": "2014-08-31", "gueltig_bis": "2019-09-01", "name": "AfD SN Wahlprogramm 2014", "titel": None, "pdf": "afd-sn-2014.pdf", "seiten": 26}, + "cdu-sn-2014": {"id": "cdu-sn-2014", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "SN", "wp": 6, "gueltig_ab": "2014-08-31", "gueltig_bis": "2019-09-01", "name": "CDU SN Wahlprogramm 2014", "titel": None, "pdf": "cdu-sn-2014.pdf", "seiten": 86}, + "gruene-sn-2014": {"id": "gruene-sn-2014", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SN", "wp": 6, "gueltig_ab": "2014-08-31", "gueltig_bis": "2019-09-01", "name": "Grüne SN Wahlprogramm 2014", "titel": None, "pdf": "gruene-sn-2014.pdf", "seiten": 149}, + "linke-sn-2014": {"id": "linke-sn-2014", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "SN", "wp": 6, "gueltig_ab": "2014-08-31", "gueltig_bis": "2019-09-01", "name": "DIE LINKE SN Wahlprogramm 2014", "titel": None, "pdf": "linke-sn-2014.pdf", "seiten": 56}, + "spd-sn-2014": {"id": "spd-sn-2014", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SN", "wp": 6, "gueltig_ab": "2014-08-31", "gueltig_bis": "2019-09-01", "name": "SPD SN Wahlprogramm 2014", "titel": None, "pdf": "spd-sn-2014.pdf", "seiten": 56}, + "afd-sn-2019": {"id": "afd-sn-2019", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "SN", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-01", "name": "AfD SN Wahlprogramm 2019", "titel": None, "pdf": "afd-sn-2019.pdf", "seiten": 76}, + "cdu-sn-2019": {"id": "cdu-sn-2019", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "SN", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-01", "name": "CDU SN Wahlprogramm 2019", "titel": None, "pdf": "cdu-sn-2019.pdf", "seiten": 71}, + "gruene-sn-2019": {"id": "gruene-sn-2019", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SN", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-01", "name": "Grüne SN Wahlprogramm 2019", "titel": None, "pdf": "gruene-sn-2019.pdf", "seiten": 86}, + "linke-sn-2019": {"id": "linke-sn-2019", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "SN", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-01", "name": "DIE LINKE SN Wahlprogramm 2019", "titel": None, "pdf": "linke-sn-2019.pdf", "seiten": 72}, + "spd-sn-2019": {"id": "spd-sn-2019", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SN", "wp": 7, "gueltig_ab": "2019-09-01", "gueltig_bis": "2024-09-01", "name": "SPD SN Wahlprogramm 2019", "titel": None, "pdf": "spd-sn-2019.pdf", "seiten": 105}, + "afd-sn-2024": {"id": "afd-sn-2024", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "SN", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "AfD Sachsen Wahlprogramm 2024", "titel": None, "pdf": "afd-sn-2024.pdf", "seiten": 100}, + "bsw-sn-2024": {"id": "bsw-sn-2024", "typ": "wahlprogramm", "partei": "BSW", "bundesland": "SN", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "BSW Sachsen Wahlprogramm 2024", "titel": None, "pdf": "bsw-sn-2024.pdf", "seiten": 50}, + "cdu-sn-2024": {"id": "cdu-sn-2024", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "SN", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "CDU Sachsen Wahlprogramm 2024", "titel": None, "pdf": "cdu-sn-2024.pdf", "seiten": 100}, + "gruene-sn-2024": {"id": "gruene-sn-2024", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SN", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "BÜNDNIS 90/DIE GRÜNEN Sachsen Wahlprogramm 2024", "titel": None, "pdf": "gruene-sn-2024.pdf", "seiten": 100}, + "linke-sn-2024": {"id": "linke-sn-2024", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "SN", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "DIE LINKE Sachsen Wahlprogramm 2024", "titel": None, "pdf": "linke-sn-2024.pdf", "seiten": 100}, + "spd-sn-2024": {"id": "spd-sn-2024", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SN", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "SPD Sachsen Wahlprogramm 2024", "titel": None, "pdf": "spd-sn-2024.pdf", "seiten": 100}, + + # ─── wahlprogramm · TH ─── + "afd-th-2014": {"id": "afd-th-2014", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "TH", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-10-27", "name": "AfD TH Wahlprogramm 2014", "titel": None, "pdf": "afd-th-2014.pdf", "seiten": 32}, + "cdu-th-2014": {"id": "cdu-th-2014", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "TH", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-10-27", "name": "CDU TH Wahlprogramm 2014", "titel": None, "pdf": "cdu-th-2014.pdf", "seiten": 82}, + "fdp-th-2014": {"id": "fdp-th-2014", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "TH", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-10-27", "name": "FDP TH Wahlprogramm 2014", "titel": None, "pdf": "fdp-th-2014.pdf", "seiten": 77}, + "gruene-th-2014": {"id": "gruene-th-2014", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "TH", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-10-27", "name": "Grüne TH Wahlprogramm 2014", "titel": None, "pdf": "gruene-th-2014.pdf", "seiten": 84}, + "linke-th-2014": {"id": "linke-th-2014", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "TH", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-10-27", "name": "DIE LINKE TH Wahlprogramm 2014", "titel": None, "pdf": "linke-th-2014.pdf", "seiten": 60}, + "spd-th-2014": {"id": "spd-th-2014", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "TH", "wp": 6, "gueltig_ab": "2014-09-14", "gueltig_bis": "2019-10-27", "name": "SPD TH Wahlprogramm 2014", "titel": None, "pdf": "spd-th-2014.pdf", "seiten": 52}, + "afd-th-2019": {"id": "afd-th-2019", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "TH", "wp": 7, "gueltig_ab": "2019-10-27", "gueltig_bis": "2024-09-01", "name": "AfD TH Wahlprogramm 2019", "titel": None, "pdf": "afd-th-2019.pdf", "seiten": 96}, + "cdu-th-2019": {"id": "cdu-th-2019", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "TH", "wp": 7, "gueltig_ab": "2019-10-27", "gueltig_bis": "2024-09-01", "name": "CDU TH Wahlprogramm 2019", "titel": None, "pdf": "cdu-th-2019.pdf", "seiten": 48}, + "fdp-th-2019": {"id": "fdp-th-2019", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "TH", "wp": 7, "gueltig_ab": "2019-10-27", "gueltig_bis": "2024-09-01", "name": "FDP TH Wahlprogramm 2019", "titel": None, "pdf": "fdp-th-2019.pdf", "seiten": 86}, + "gruene-th-2019": {"id": "gruene-th-2019", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "TH", "wp": 7, "gueltig_ab": "2019-10-27", "gueltig_bis": "2024-09-01", "name": "Grüne TH Wahlprogramm 2019", "titel": None, "pdf": "gruene-th-2019.pdf", "seiten": 83}, + "linke-th-2019": {"id": "linke-th-2019", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "TH", "wp": 7, "gueltig_ab": "2019-10-27", "gueltig_bis": "2024-09-01", "name": "DIE LINKE TH Wahlprogramm 2019", "titel": None, "pdf": "linke-th-2019.pdf", "seiten": 105}, + "spd-th-2019": {"id": "spd-th-2019", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "TH", "wp": 7, "gueltig_ab": "2019-10-27", "gueltig_bis": "2024-09-01", "name": "SPD TH Wahlprogramm 2019", "titel": None, "pdf": "spd-th-2019.pdf", "seiten": 32}, + "afd-th-2024": {"id": "afd-th-2024", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "TH", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "AfD Thüringen Wahlprogramm 2024", "titel": "AfD Thüringen Landtagswahlprogramm 2024", "pdf": "afd-th-2024.pdf", "seiten": 100}, + "bsw-th-2024": {"id": "bsw-th-2024", "typ": "wahlprogramm", "partei": "BSW", "bundesland": "TH", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "BSW Thüringen Wahlprogramm 2024", "titel": None, "pdf": "bsw-th-2024.pdf", "seiten": 50}, + "cdu-th-2024": {"id": "cdu-th-2024", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "TH", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "CDU Thüringen Wahlprogramm 2024", "titel": "Wahlprogramm der CDU Thüringen 2024", "pdf": "cdu-th-2024.pdf", "seiten": 83}, + "linke-th-2024": {"id": "linke-th-2024", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "TH", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "DIE LINKE Thüringen Wahlprogramm 2024", "titel": None, "pdf": "linke-th-2024.pdf", "seiten": 100}, + "spd-th-2024": {"id": "spd-th-2024", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "TH", "wp": 8, "gueltig_ab": "2024-09-01", "gueltig_bis": None, "name": "SPD Thüringen Wahlprogramm 2024", "titel": None, "pdf": "spd-th-2024.pdf", "seiten": 100}, +} # ───────────────────────────────────────────────────────────────────────────── @@ -86,6 +424,14 @@ def get_programm(programm_id: str) -> Optional[Programm]: return PROGRAMME.get(programm_id) +def get_programm_by_pdf(pdf: str) -> Optional[Programm]: + """Reverse-Lookup über pdf-Dateinamen.""" + for prog in PROGRAMME.values(): + if prog.get("pdf") == pdf: + return prog + return None + + def aktuelles_wahlprogramm(bundesland: str, partei: str) -> Optional[Programm]: """Aktuell gültiges Wahlprogramm einer Partei in einem Bundesland. @@ -109,9 +455,6 @@ def wahlprogramm_zum_zeitpunkt( ``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 ( @@ -134,11 +477,8 @@ def grundsatzprogramm_zum_zeitpunkt( 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" @@ -147,7 +487,6 @@ def grundsatzprogramm_zum_zeitpunkt( 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" @@ -189,366 +528,7 @@ def alle_versionen(bundesland: str, partei: str) -> list[Programm]: 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() + """Alle eingetragenen Programme.""" return list(PROGRAMME.values()) + diff --git a/app/redline_utils.py b/app/redline_utils.py index 118335e..64bdfb4 100644 --- a/app/redline_utils.py +++ b/app/redline_utils.py @@ -54,7 +54,7 @@ def build_pdf_href(zitat: dict, bundesland: str = "") -> str: Bevorzugt das bereits gepflegte url-Feld. Falls leer, rekonstruiert die URL aus dem quelle-Feld (Format: 'Titel · S. N' oder 'Titel, S. N') - über die WAHLPROGRAMME-Registry. + über die ``programme``-Registry. """ url = zitat.get("url", "") if url: @@ -66,18 +66,14 @@ def build_pdf_href(zitat: dict, bundesland: str = "") -> str: return "" seite = seite_m.group(1) - # pid aus WAHLPROGRAMME-Registry ermitteln: Dateiname ohne .pdf - from .wahlprogramme import WAHLPROGRAMME + # pid aus programme-Registry ermitteln: über titel oder name match. + from .programme import all_programme pid = "" - for bl_data in WAHLPROGRAMME.values(): - for partei_data in bl_data.values(): - titel = partei_data.get("titel", "") - partei_name = partei_data.get("partei", "") - file_name = partei_data.get("file", "") - if titel and (titel in quelle or partei_name in quelle): - pid = file_name.replace(".pdf", "") - break - if pid: + for prog in all_programme(): + titel = prog.get("titel") or "" + name = prog.get("name") or "" + if (titel and titel in quelle) or (name and name in quelle): + pid = prog.get("id", "") break if not pid: diff --git a/app/wahlprogramm_check.py b/app/wahlprogramm_check.py index 2383b5d..2a720a3 100644 --- a/app/wahlprogramm_check.py +++ b/app/wahlprogramm_check.py @@ -1,13 +1,13 @@ """Erkennung fehlender Wahlprogramme (#128). Prüft für ein gegebenes Bundesland, welche der im Landtag vertretenen -Fraktionen in der WAHLPROGRAMME-Registry nicht hinterlegt sind. -Wird nach dem LLM-Call in analyze_antrag() aufgerufen, damit das +Fraktionen in der ``programme``-Registry nicht hinterlegt sind. Wird +nach dem LLM-Call in ``analyze_antrag()`` aufgerufen, damit das Assessment-Ergebnis die Lücken explizit ausweist. """ from .bundeslaender import BUNDESLAENDER -from .wahlprogramme import WAHLPROGRAMME +from .programme import parteien_mit_wahlprogramm def check_missing_programmes(bundesland: str, fraktionen: list[str]) -> list[str]: @@ -33,5 +33,5 @@ def check_missing_programmes(bundesland: str, fraktionen: list[str]) -> list[str if not fraktionen: return [] - indexed = WAHLPROGRAMME.get(bundesland, {}) + indexed = set(parteien_mit_wahlprogramm(bundesland)) return [f for f in fraktionen if f not in indexed] diff --git a/app/wahlprogramm_fetch.py b/app/wahlprogramm_fetch.py index dcf15fa..2e03782 100644 --- a/app/wahlprogramm_fetch.py +++ b/app/wahlprogramm_fetch.py @@ -254,7 +254,7 @@ def get_missing_programmes(bundesland: Optional[str] = None) -> list[dict]: Returns: Liste von Dicts mit ``bl``, ``partei``, ``dateiname``, ``kandidaten``. """ - from .wahlprogramme import WAHLPROGRAMME + from .programme import aktuelles_wahlprogramm missing: list[dict] = [] data = _load_links() @@ -266,14 +266,15 @@ def get_missing_programmes(bundesland: Optional[str] = None) -> list[dict]: if isinstance(kandidaten, dict): kandidaten = [kandidaten] - wp_info = WAHLPROGRAMME.get(bl, {}).get(partei) + wp_info = aktuelles_wahlprogramm(bl, partei) if wp_info: - dateiname = wp_info["file"] - dest = _REFERENZEN_DIR / dateiname - if dest.exists(): - continue # Datei liegt bereits vor + dateiname = wp_info.get("pdf") + if dateiname: + dest = _REFERENZEN_DIR / dateiname + if dest.exists(): + continue # Datei liegt bereits vor else: - dateiname = None # noch nicht in WAHLPROGRAMME registriert + dateiname = None # noch nicht in programme.PROGRAMME registriert missing.append({ "bl": bl, @@ -311,24 +312,29 @@ def _cli() -> None: args = parser.parse_args() if args.pin_existing: - from .wahlprogramme import WAHLPROGRAMME + from .programme import all_programme lock = _load_lock() added = 0 - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - dateiname = info.get("file") if isinstance(info, dict) else None - if not dateiname: - continue - pdf_path = _REFERENZEN_DIR / dateiname - if not pdf_path.exists(): - continue - key = _lock_key(dateiname) - if key in lock: - continue - lock[key] = sha256_of_file(pdf_path) - added += 1 - print(f" pinned {bl}/{partei}: {dateiname} → {lock[key][:12]}…") + for prog in all_programme(): + if prog.get("typ") != "wahlprogramm": + continue + if prog.get("gueltig_bis") is not None: + continue # nur aktuelle Wahlprogramme pinnen + bl = prog.get("bundesland") + partei = prog.get("partei") + dateiname = prog.get("pdf") + if not dateiname: + continue + pdf_path = _REFERENZEN_DIR / dateiname + if not pdf_path.exists(): + continue + key = _lock_key(dateiname) + if key in lock: + continue + lock[key] = sha256_of_file(pdf_path) + added += 1 + print(f" pinned {bl}/{partei}: {dateiname} → {lock[key][:12]}…") if added: _save_lock(lock) print(f"\n{added} neue Eintraege in {_LOCK_FILE.name}.") @@ -362,16 +368,16 @@ def _cli() -> None: print(f"Keine URL-Kandidaten für {bl}/{partei} in wahlprogramm-links.yaml.") sys.exit(1) - from .wahlprogramme import WAHLPROGRAMME - wp_info = WAHLPROGRAMME.get(bl, {}).get(partei) + from .programme import aktuelles_wahlprogramm + wp_info = aktuelles_wahlprogramm(bl, partei) if not wp_info: print( - f"WARNUNG: {bl}/{partei} ist noch nicht in wahlprogramme.py eingetragen.\n" + f"WARNUNG: {bl}/{partei} ist noch nicht in programme.py eingetragen.\n" "Die Datei wird heruntergeladen, muss aber manuell registriert werden." ) dateiname = f"{partei.lower()}-{bl.lower()}-neu.pdf" else: - dateiname = wp_info["file"] + dateiname = wp_info.get("pdf", f"{partei.lower()}-{bl.lower()}-neu.pdf") dest = _REFERENZEN_DIR / dateiname diff --git a/app/wahlprogramme.py b/app/wahlprogramme.py index b442edd..e1a6f03 100644 --- a/app/wahlprogramme.py +++ b/app/wahlprogramme.py @@ -1,12 +1,20 @@ -"""Wahlprogramm-Referenzsystem mit Zitaten und Seitenreferenzen. +"""Wahlprogramm-Suche (Keyword-Fallback, wenn Embeddings nicht verfügbar). -Bundesland-bewusst seit Issue #5: ``WAHLPROGRAMME[bundesland][partei]`` statt -flach. Konsumiert ``BUNDESLAENDER`` aus ``bundeslaender.py`` für die -Regierungsfraktionen-Lookup und für Plausibilitätsprüfungen. +Bis #222: hier lebte zusätzlich die ``WAHLPROGRAMME``-Datentabelle als +zweite Source-of-Truth. Sie wurde nach ``programme.PROGRAMME`` migriert +(siehe ADR 0013). Dieses Modul wurde dadurch reduziert auf: -Verantwortlich für die schlüsselwortbasierte Fallback-Suche in den -paged-Textversionen der Wahlprogramme. Die semantische Suche lebt in -``embeddings.py``. +- ``get_wahlprogramm(bl, partei)`` — Compat-Adapter auf + ``programme.aktuelles_wahlprogramm`` (für Aufrufer, die das Programm + via (Bundesland, Partei) suchen — Hauptverwender ist die Keyword-Suche + hier in diesem Modul plus admin-Tools). +- ``load_wahlprogramm_text`` / ``search_wahlprogramm`` / + ``find_relevant_quotes`` / ``format_quote_for_prompt`` — Keyword- + Fallback für die ``analyzer.py``-Pipeline, wenn die Embeddings-DB + nicht initialisiert ist. + +Die semantische Suche lebt in ``embeddings.py``, die Programm-Stamm- +daten in ``programme.py``. """ import re @@ -14,168 +22,12 @@ from pathlib import Path from typing import Optional from .bundeslaender import BUNDESLAENDER +from .programme import ( + aktuelles_wahlprogramm, + parteien_mit_wahlprogramm, +) -# WAHLPROGRAMME[bundesland][partei] -> Metadaten -# -# Pflichtfelder: file, titel, partei, jahr, seiten -# Geltungsdaten: regierungsbildung (ISO YYYY-MM-DD) — Tag der Vereidigung der -# Regierung, die nach der Wahl aus diesem Programm hervorging. -# NICHT das Wahldatum: das Programm wird mit der Regierungs- -# bildung wirksam (Koalitionsvertrag, Kabinett vereidigt). -# regierungsende (ISO YYYY-MM-DD oder None) — Ende der -# Regierungs-Geltung. ``None`` = aktuell laufende Regierung. -# -# Beim Hinzufügen eines neuen Bundeslands: Eintrag hier UND parallel -# in WAHLPROGRAMM_KONTEXT_FILES. -WAHLPROGRAMME: dict[str, dict[str, dict]] = { - # NRW — LTW 15.05.2022. Kabinett Wüst II (CDU+GRÜNE) vereidigt 29.06.2022. - "NRW": { - "CDU": {"file": "cdu-nrw-2022.pdf", "titel": "Machen, worauf es ankommt", "partei": "CDU NRW", "jahr": 2022, "seiten": 109, "regierungsbildung": "2022-06-29", "regierungsende": None}, - "SPD": {"file": "spd-nrw-2022.pdf", "titel": "Unser Land von morgen", "partei": "SPD NRW", "jahr": 2022, "seiten": 116, "regierungsbildung": "2022-06-29", "regierungsende": None}, - "GRÜNE": {"file": "gruene-nrw-2022.pdf","titel": "Von hier an Zukunft", "partei": "BÜNDNIS 90/DIE GRÜNEN NRW", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-06-29", "regierungsende": None}, - "FDP": {"file": "fdp-nrw-2022.pdf", "titel": "Nie gab es mehr zu tun", "partei": "FDP NRW", "jahr": 2022, "seiten": 96, "regierungsbildung": "2022-06-29", "regierungsende": None}, - "AfD": {"file": "afd-nrw-2022.pdf", "titel": "Wer sonst.", "partei": "AfD NRW", "jahr": 2022, "seiten": 68, "regierungsbildung": "2022-06-29", "regierungsende": None}, - }, - # Sachsen-Anhalt — LTW 06.06.2021. Kabinett Haseloff III (CDU+SPD+FDP) vereidigt 16.09.2021. - "LSA": { - "CDU": {"file": "cdu-lsa-2021.pdf", "titel": "Unsere Heimat. Unsere Verantwortung.", "partei": "CDU Sachsen-Anhalt", "jahr": 2021, "seiten": 82, "regierungsbildung": "2021-09-16", "regierungsende": None}, - "SPD": {"file": "spd-lsa-2021.pdf", "titel": "Zusammenhalt und neue Chancen. Politik fürs ganze Land", "partei": "SPD Sachsen-Anhalt", "jahr": 2021, "seiten": 77, "regierungsbildung": "2021-09-16", "regierungsende": None}, - "GRÜNE": {"file": "gruene-lsa-2021.pdf","titel": "Verlässlich für Sachsen-Anhalt", "partei": "BÜNDNIS 90/DIE GRÜNEN Sachsen-Anhalt","jahr": 2021, "seiten": 164,"regierungsbildung": "2021-09-16", "regierungsende": None}, - "FDP": {"file": "fdp-lsa-2021.pdf", "titel": "Wahlprogramm der FDP Sachsen-Anhalt zur Landtagswahl 2021", "partei": "FDP Sachsen-Anhalt", "jahr": 2021, "seiten": 76, "regierungsbildung": "2021-09-16", "regierungsende": None}, - "AfD": {"file": "afd-lsa-2021.pdf", "titel": "Alles für unsere Heimat! Programm der AfD Sachsen-Anhalt zur Landtagswahl 2021","partei": "AfD Sachsen-Anhalt", "jahr": 2021, "seiten": 64, "regierungsbildung": "2021-09-16", "regierungsende": None}, - "LINKE": {"file": "linke-lsa-2021.pdf", "titel": "Wahlprogramm zur Landtagswahl 2021", "partei": "DIE LINKE Sachsen-Anhalt", "jahr": 2021, "seiten": 88, "regierungsbildung": "2021-09-16", "regierungsende": None}, - }, - # Mecklenburg-Vorpommern — LTW 26.09.2021. Kabinett Schwesig II (SPD+LINKE) vereidigt 15.11.2021. - "MV": { - "CDU": {"file": "cdu-mv-2021.pdf", "titel": "Zusammen. Den Blick nach vorn. Gemeinsam die Zukunft meistern", "partei": "CDU Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 56, "regierungsbildung": "2021-11-15", "regierungsende": None}, - "SPD": {"file": "spd-mv-2021.pdf", "titel": "Verantwortung für heute und morgen — Regierungsprogramm 2021–2026", "partei": "SPD Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 95, "regierungsbildung": "2021-11-15", "regierungsende": None}, - "GRÜNE": {"file": "gruene-mv-2021.pdf","titel": "Für Klima, Land und ein besseres Miteinander", "partei": "BÜNDNIS 90/DIE GRÜNEN Mecklenburg-Vorpommern","jahr": 2021, "seiten": 88, "regierungsbildung": "2021-11-15", "regierungsende": None}, - "FDP": {"file": "fdp-mv-2021.pdf", "titel": "Wahlprogramm der Freien Demokraten Mecklenburg-Vorpommern zur Landtagswahl 2021","partei": "FDP Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 120,"regierungsbildung": "2021-11-15", "regierungsende": None}, - "AfD": {"file": "afd-mv-2021.pdf", "titel": "Landeswahlprogramm der AfD Mecklenburg-Vorpommern 2021", "partei": "AfD Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 84, "regierungsbildung": "2021-11-15", "regierungsende": None}, - "LINKE": {"file": "linke-mv-2021.pdf", "titel": "Das ist links! — Zukunftsprogramm für Mecklenburg-Vorpommern", "partei": "DIE LINKE Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 82, "regierungsbildung": "2021-11-15", "regierungsende": None}, - }, - # Berlin — AGH-Wahl 26.09.2021, Wiederholungswahl 12.02.2023. Senat Wegner I (CDU+SPD) vereidigt 27.04.2023. - "BE": { - "CDU": {"file": "cdu-be-2023.pdf", "titel": "Unser Berlin. Mehr geht nur gemeinsam. — Berlin-Plan der CDU Berlin 2021–2026", "partei": "CDU Berlin", "jahr": 2021, "seiten": 135, "regierungsbildung": "2023-04-27", "regierungsende": None}, - "SPD": {"file": "spd-be-2023.pdf", "titel": "Ganz sicher Berlin — Wahlprogramm der SPD Berlin zur Abgeordnetenhauswahl 2021", "partei": "SPD Berlin", "jahr": 2021, "seiten": 86, "regierungsbildung": "2023-04-27", "regierungsende": None}, - "GRÜNE": {"file": "gruene-be-2023.pdf","titel": "Unser Plan für Berlin — Landeswahlprogramm BÜNDNIS 90/DIE GRÜNEN Berlin 2021", "partei": "BÜNDNIS 90/DIE GRÜNEN Berlin", "jahr": 2021, "seiten": 280, "regierungsbildung": "2023-04-27", "regierungsende": None}, - "LINKE": {"file": "linke-be-2023.pdf", "titel": "rot. radikal. realistisch. — Unser Programm für die soziale Stadt", "partei": "DIE LINKE Berlin", "jahr": 2021, "seiten": 130, "regierungsbildung": "2023-04-27", "regierungsende": None}, - "AfD": {"file": "afd-be-2023.pdf", "titel": "Wahlprogramm der AfD Berlin für die Wahl des Abgeordnetenhauses am 26. September 2021","partei": "AfD Berlin", "jahr": 2021, "seiten": 166, "regierungsbildung": "2023-04-27", "regierungsende": None}, - }, - # Thüringen — LTW 01.09.2024. Kabinett Voigt I (CDU+BSW+SPD, Brombeer) vereidigt 12.12.2024. - "TH": { - "CDU": {"file": "cdu-th-2024.pdf", "titel": "Wahlprogramm der CDU Thüringen 2024", "partei": "CDU Thüringen", "jahr": 2024, "seiten": 83, "regierungsbildung": "2024-12-12", "regierungsende": None}, - "AfD": {"file": "afd-th-2024.pdf", "titel": "AfD Thüringen Landtagswahlprogramm 2024","partei": "AfD Thüringen", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-12", "regierungsende": None}, - "LINKE": {"file": "linke-th-2024.pdf", "titel": "DIE LINKE Thüringen Wahlprogramm 2024", "partei": "DIE LINKE Thüringen", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-12", "regierungsende": None}, - "BSW": {"file": "bsw-th-2024.pdf", "titel": "BSW Thüringen Wahlprogramm 2024", "partei": "BSW Thüringen", "jahr": 2024, "seiten": 50, "regierungsbildung": "2024-12-12", "regierungsende": None}, - "SPD": {"file": "spd-th-2024.pdf", "titel": "SPD Thüringen Wahlprogramm 2024", "partei": "SPD Thüringen", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-12", "regierungsende": None}, - }, - # Brandenburg — LTW 22.09.2024. Kabinett Woidke III (SPD+BSW) vereidigt 11.12.2024. - "BB": { - "SPD": {"file": "spd-bb-2024.pdf", "titel": "SPD Brandenburg Wahlprogramm 2024", "partei": "SPD Brandenburg", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-11", "regierungsende": None}, - "AfD": {"file": "afd-bb-2024.pdf", "titel": "AfD Brandenburg Wahlprogramm 2024", "partei": "AfD Brandenburg", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-11", "regierungsende": None}, - "CDU": {"file": "cdu-bb-2024.pdf", "titel": "CDU Brandenburg Wahlprogramm 2024", "partei": "CDU Brandenburg", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-11", "regierungsende": None}, - "BSW": {"file": "bsw-bb-2024.pdf", "titel": "BSW Brandenburg Wahlprogramm 2024", "partei": "BSW Brandenburg", "jahr": 2024, "seiten": 50, "regierungsbildung": "2024-12-11", "regierungsende": None}, - }, - # Hamburg — Bürgerschaftswahl 02.03.2025. Senat Tschentscher III (SPD+GRÜNE) vereidigt 07.05.2025. - "HH": { - "SPD": {"file": "spd-hh-2025.pdf", "titel": "SPD Hamburg Wahlprogramm Bürgerschaftswahl 2025", "partei": "SPD Hamburg", "jahr": 2025, "seiten": 100, "regierungsbildung": "2025-05-07", "regierungsende": None}, - "CDU": {"file": "cdu-hh-2025.pdf", "titel": "CDU Hamburg Wahlprogramm Bürgerschaftswahl 2025", "partei": "CDU Hamburg", "jahr": 2025, "seiten": 100, "regierungsbildung": "2025-05-07", "regierungsende": None}, - "GRÜNE": {"file": "gruene-hh-2025.pdf","titel": "Gute Gründe für Grün — Regierungsprogramm BÜNDNIS 90/DIE GRÜNEN Hamburg 2025", "partei": "BÜNDNIS 90/DIE GRÜNEN Hamburg", "jahr": 2025, "seiten": 100, "regierungsbildung": "2025-05-07", "regierungsende": None}, - "LINKE": {"file": "linke-hh-2025.pdf", "titel": "DIE LINKE Hamburg Wahlprogramm Bürgerschaftswahl 2025", "partei": "DIE LINKE Hamburg", "jahr": 2025, "seiten": 100, "regierungsbildung": "2025-05-07", "regierungsende": None}, - "AfD": {"file": "afd-hh-2025.pdf", "titel": "AfD Hamburg Wahlprogramm Bürgerschaftswahl 2025", "partei": "AfD Hamburg", "jahr": 2025, "seiten": 100, "regierungsbildung": "2025-05-07", "regierungsende": None}, - }, - # Schleswig-Holstein — LTW 08.05.2022. Kabinett Günther II (CDU+GRÜNE) vereidigt 29.06.2022. - "SH": { - "CDU": {"file": "cdu-sh-2022.pdf", "titel": "CDU Schleswig-Holstein Wahlprogramm 2022", "partei": "CDU Schleswig-Holstein", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-06-29", "regierungsende": None}, - "SPD": {"file": "spd-sh-2022.pdf", "titel": "SPD Schleswig-Holstein Wahlprogramm 2022", "partei": "SPD Schleswig-Holstein", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-06-29", "regierungsende": None}, - "GRÜNE": {"file": "gruene-sh-2022.pdf","titel": "BÜNDNIS 90/DIE GRÜNEN Schleswig-Holstein Wahlprogramm 2022", "partei": "BÜNDNIS 90/DIE GRÜNEN Schleswig-Holstein", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-06-29", "regierungsende": None}, - "FDP": {"file": "fdp-sh-2022.pdf", "titel": "FDP Schleswig-Holstein Wahlprogramm 2022", "partei": "FDP Schleswig-Holstein", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-06-29", "regierungsende": None}, - "SSW": {"file": "ssw-sh-2022.pdf", "titel": "SSW Schleswig-Holstein Wahlprogramm 2022", "partei": "SSW", "jahr": 2022, "seiten": 80, "regierungsbildung": "2022-06-29", "regierungsende": None}, - }, - # Baden-Württemberg — LTW 14.03.2021. Kabinett Kretschmann III (GRÜNE+CDU) vereidigt 12.05.2021. - "BW": { - "GRÜNE": {"file": "gruene-bw-2021.pdf","titel": "Wachsen wir über uns hinaus — Landtagswahlprogramm BÜNDNIS 90/DIE GRÜNEN Baden-Württemberg 2021", "partei": "BÜNDNIS 90/DIE GRÜNEN Baden-Württemberg", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-12", "regierungsende": None}, - "CDU": {"file": "cdu-bw-2021.pdf", "titel": "Neue Ideen für eine neue Zeit — Regierungsprogramm der CDU Baden-Württemberg zur Landtagswahl 2021", "partei": "CDU Baden-Württemberg", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-12", "regierungsende": None}, - "AfD": {"file": "afd-bw-2021.pdf", "titel": "AfD Baden-Württemberg Landtagswahlprogramm 2021", "partei": "AfD Baden-Württemberg", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-12", "regierungsende": None}, - "SPD": {"file": "spd-bw-2021.pdf", "titel": "SPD Baden-Württemberg Wahlprogramm zur Landtagswahl 2021", "partei": "SPD Baden-Württemberg", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-12", "regierungsende": None}, - "FDP": {"file": "fdp-bw-2021.pdf", "titel": "FDP Baden-Württemberg Landtagswahlprogramm 2021", "partei": "FDP Baden-Württemberg", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-12", "regierungsende": None}, - }, - # Rheinland-Pfalz — LTW 14.03.2021. Kabinett Dreyer III (SPD+GRÜNE+FDP) vereidigt 18.05.2021; - # seit 10.07.2024 fortgeführt als Schweitzer I in derselben WP18. - "RP": { - "SPD": {"file": "spd-rp-2021.pdf","titel": "Wir mit Ihr — Regierungsprogramm der SPD Rheinland-Pfalz 2021–2026", "partei": "SPD Rheinland-Pfalz", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-18", "regierungsende": None}, - "CDU": {"file": "cdu-rp-2021.pdf","titel": "Regierungsprogramm der CDU RLP 2021–26", "partei": "CDU Rheinland-Pfalz", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-18", "regierungsende": None}, - "AfD": {"file": "afd-rp-2021.pdf","titel": "AfD Rheinland-Pfalz Wahlprogramm 2021", "partei": "AfD Rheinland-Pfalz", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-18", "regierungsende": None}, - "GRÜNE": {"file": "gruene-rp-2021.pdf","titel": "BÜNDNIS 90/DIE GRÜNEN Rheinland-Pfalz Landtagswahlprogramm 2021", "partei": "BÜNDNIS 90/DIE GRÜNEN Rheinland-Pfalz", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-18", "regierungsende": None}, - "FREIE WÄHLER": {"file": "fw-rp-2021.pdf", "titel": "FREIE WÄHLER Rheinland-Pfalz Wahlprogramm 2021", "partei": "FREIE WÄHLER Rheinland-Pfalz", "jahr": 2021, "seiten": 80, "regierungsbildung": "2021-05-18", "regierungsende": None}, - "FDP": {"file": "fdp-rp-2021.pdf","titel": "FDP Rheinland-Pfalz Landtagswahlprogramm 2021", "partei": "FDP Rheinland-Pfalz", "jahr": 2021, "seiten": 100, "regierungsbildung": "2021-05-18", "regierungsende": None}, - }, - # Bayern — LTW 08.10.2023. Kabinett Söder III (CSU+FREIE WÄHLER) vereidigt 07.11.2023. - "BY": { - "CSU": {"file": "csu-by-2023.pdf", "titel": "CSU Bayern Bayernplan 2023", "partei": "CSU Bayern", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-11-07", "regierungsende": None}, - "FREIE WÄHLER": {"file": "fw-by-2023.pdf", "titel": "FREIE WÄHLER Bayern Wahlprogramm 2023", "partei": "FREIE WÄHLER Bayern", "jahr": 2023, "seiten": 80, "regierungsbildung": "2023-11-07", "regierungsende": None}, - "GRÜNE": {"file": "gruene-by-2023.pdf","titel": "BÜNDNIS 90/DIE GRÜNEN Bayern Regierungsprogramm 2023", "partei": "BÜNDNIS 90/DIE GRÜNEN Bayern", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-11-07", "regierungsende": None}, - "SPD": {"file": "spd-by-2023.pdf", "titel": "SPD Bayern Zukunftsprogramm 2023", "partei": "SPD Bayern", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-11-07", "regierungsende": None}, - "AfD": {"file": "afd-by-2023.pdf", "titel": "AfD Bayern Wahlprogramm 2023", "partei": "AfD Bayern", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-11-07", "regierungsende": None}, - }, - # Bremen — Bürgerschaftswahl 14.05.2023. Senat Bovenschulte II (SPD+GRÜNE+LINKE) vereidigt 05.07.2023. - # AfD war wegen Listenstreit nicht zur Wahl zugelassen — stattdessen ist - # BIW (Bürger in Wut) als 6. Fraktion in der 21. Bürgerschaft. - "HB": { - "SPD": {"file": "spd-hb-2023.pdf", "titel": "SPD Bremen Wahlprogramm Bürgerschaftswahl 2023", "partei": "SPD Bremen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-07-05", "regierungsende": None}, - "CDU": {"file": "cdu-hb-2023.pdf", "titel": "CDU Bremen Wahlprogramm Bürgerschaftswahl 2023", "partei": "CDU Bremen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-07-05", "regierungsende": None}, - "GRÜNE": {"file": "gruene-hb-2023.pdf","titel": "BÜNDNIS 90/DIE GRÜNEN Bremen Wahlprogramm 2023", "partei": "BÜNDNIS 90/DIE GRÜNEN Bremen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-07-05", "regierungsende": None}, - "LINKE": {"file": "linke-hb-2023.pdf", "titel": "DIE LINKE Bremen Wahlprogramm Bürgerschaftswahl 2023", "partei": "DIE LINKE Bremen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2023-07-05", "regierungsende": None}, - "BiW": {"file": "biw-hb-2023.pdf", "titel": "BÜRGER IN WUT — Programm für die Bürgerschaftswahl 2023", "partei": "BiW Bremen", "jahr": 2023, "seiten": 26, "regierungsbildung": "2023-07-05", "regierungsende": None}, - }, - # Hessen — LTW 08.10.2023. Kabinett Rhein II (CDU+SPD) vereidigt 18.01.2024. - "HE": { - "CDU": {"file": "cdu-he-2023.pdf", "titel": "CDU Hessen Regierungsprogramm 2023", "partei": "CDU Hessen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2024-01-18", "regierungsende": None}, - "SPD": {"file": "spd-he-2023.pdf", "titel": "SPD Hessen Wahlprogramm 2023", "partei": "SPD Hessen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2024-01-18", "regierungsende": None}, - "GRÜNE": {"file": "gruene-he-2023.pdf","titel": "BÜNDNIS 90/DIE GRÜNEN Hessen Wahlprogramm 2023", "partei": "BÜNDNIS 90/DIE GRÜNEN Hessen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2024-01-18", "regierungsende": None}, - "FDP": {"file": "fdp-he-2023.pdf", "titel": "FDP Hessen Wahlprogramm 2023", "partei": "FDP Hessen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2024-01-18", "regierungsende": None}, - "AfD": {"file": "afd-he-2023.pdf", "titel": "AfD Hessen Wahlprogramm 2023", "partei": "AfD Hessen", "jahr": 2023, "seiten": 100, "regierungsbildung": "2024-01-18", "regierungsende": None}, - }, - # Niedersachsen — LTW 09.10.2022. Kabinett Weil III (SPD+GRÜNE) vereidigt 08.11.2022. - "NI": { - "SPD": {"file": "spd-ni-2022.pdf", "titel": "SPD Niedersachsen Regierungsprogramm 2022", "partei": "SPD Niedersachsen", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-11-08", "regierungsende": None}, - "CDU": {"file": "cdu-ni-2022.pdf", "titel": "CDU Niedersachsen Regierungsprogramm 2022", "partei": "CDU Niedersachsen", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-11-08", "regierungsende": None}, - "GRÜNE": {"file": "gruene-ni-2022.pdf","titel": "BÜNDNIS 90/DIE GRÜNEN Niedersachsen Wahlprogramm 2022", "partei": "BÜNDNIS 90/DIE GRÜNEN Niedersachsen", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-11-08", "regierungsende": None}, - "AfD": {"file": "afd-ni-2022.pdf", "titel": "AfD Niedersachsen Wahlprogramm 2022", "partei": "AfD Niedersachsen", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-11-08", "regierungsende": None}, - }, - # Saarland — LTW 27.03.2022. Kabinett Rehlinger I (SPD-Alleinregierung) vereidigt 25.04.2022. - "SL": { - "SPD": {"file": "spd-sl-2022.pdf", "titel": "SPD Saarland Regierungsprogramm 2022", "partei": "SPD Saarland", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-04-25", "regierungsende": None}, - "CDU": {"file": "cdu-sl-2022.pdf", "titel": "CDU Saarland Wahlprogramm 2022", "partei": "CDU Saarland", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-04-25", "regierungsende": None}, - "AfD": {"file": "afd-sl-2022.pdf", "titel": "AfD Saarland Wahlprogramm 2022", "partei": "AfD Saarland", "jahr": 2022, "seiten": 100, "regierungsbildung": "2022-04-25", "regierungsende": None}, - }, - # Sachsen — LTW 01.09.2024. Kabinett Kretschmer III (CDU+SPD, Minderheit) vereidigt 18.12.2024. - "SN": { - "CDU": {"file": "cdu-sn-2024.pdf", "titel": "CDU Sachsen Wahlprogramm 2024", "partei": "CDU Sachsen", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-18", "regierungsende": None}, - "SPD": {"file": "spd-sn-2024.pdf", "titel": "SPD Sachsen Wahlprogramm 2024", "partei": "SPD Sachsen", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-18", "regierungsende": None}, - "AfD": {"file": "afd-sn-2024.pdf", "titel": "AfD Sachsen Wahlprogramm 2024", "partei": "AfD Sachsen", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-18", "regierungsende": None}, - "BSW": {"file": "bsw-sn-2024.pdf", "titel": "BSW Sachsen Wahlprogramm 2024", "partei": "BSW Sachsen", "jahr": 2024, "seiten": 50, "regierungsbildung": "2024-12-18", "regierungsende": None}, - "LINKE": {"file": "linke-sn-2024.pdf", "titel": "DIE LINKE Sachsen Wahlprogramm 2024", "partei": "DIE LINKE Sachsen", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-18", "regierungsende": None}, - "GRÜNE": {"file": "gruene-sn-2024.pdf","titel": "BÜNDNIS 90/DIE GRÜNEN Sachsen Wahlprogramm 2024","partei": "BÜNDNIS 90/DIE GRÜNEN Sachsen", "jahr": 2024, "seiten": 100, "regierungsbildung": "2024-12-18", "regierungsende": None}, - }, - # Bundestag — BTW 23.02.2025. Kabinett Merz I (CDU+CSU+SPD) vereidigt 06.05.2025. - # Aktiv die BTW-2025-Wahlprogramme aller acht im 21. Bundestag relevanten - # Parteien. Grundsatzprogramme bleiben in embeddings.PROGRAMME als - # zweite Referenz erhalten. - "BUND": { - "CDU": {"file": "cdu-bund-2025.pdf", "titel": "Politikwechsel für Deutschland — Wahlprogramm CDU/CSU BTW 2025", "partei": "CDU", "jahr": 2025, "seiten": 82, "regierungsbildung": "2025-05-06", "regierungsende": None}, - "CSU": {"file": "csu-bund-2025.pdf", "titel": "Politikwechsel für Deutschland — Wahlprogramm CDU/CSU BTW 2025 (CSU)", "partei": "CSU", "jahr": 2025, "seiten": 81, "regierungsbildung": "2025-05-06", "regierungsende": None}, - "SPD": {"file": "spd-bund-2025.pdf", "titel": "Mehr für Dich. Besser für Deutschland. — SPD Regierungsprogramm BTW 2025", "partei": "SPD", "jahr": 2025, "seiten": 68, "regierungsbildung": "2025-05-06", "regierungsende": None}, - "GRÜNE": {"file": "gruene-bund-2025.pdf","titel": "Zusammen wachsen — Regierungsprogramm BÜNDNIS 90/DIE GRÜNEN BTW 2025", "partei": "BÜNDNIS 90/DIE GRÜNEN", "jahr": 2025, "seiten": 160, "regierungsbildung": "2025-05-06", "regierungsende": None}, - "FDP": {"file": "fdp-bund-2025.pdf", "titel": "Alles lässt sich ändern — FDP Wahlprogramm BTW 2025", "partei": "FDP", "jahr": 2025, "seiten": 52, "regierungsbildung": "2025-05-06", "regierungsende": None}, - "AfD": {"file": "afd-bund-2025.pdf", "titel": "Zeit für Deutschland — AfD Bundestagswahlprogramm 2025", "partei": "AfD", "jahr": 2025, "seiten": 177, "regierungsbildung": "2025-05-06", "regierungsende": None}, - "LINKE": {"file": "linke-bund-2025.pdf", "titel": "Alle wollen regieren. Wir wollen verändern. — DIE LINKE Wahlprogramm BTW 2025", "partei": "DIE LINKE", "jahr": 2025, "seiten": 60, "regierungsbildung": "2025-05-06", "regierungsende": None}, - "BSW": {"file": "bsw-bund-2025.pdf", "titel": "Unser Land verdient mehr — BSW Wahlprogramm BTW 2025", "partei": "BSW", "jahr": 2025, "seiten": 45, "regierungsbildung": "2025-05-06", "regierungsende": None}, - }, -} - # Pro Bundesland: Markdown-Übersichtsdatei mit Wahlprogramm-Zusammenfassungen, # wird als Kontext in den LLM-Prompt geladen (nicht für die Suche). WAHLPROGRAMM_KONTEXT_FILES: dict[str, str] = { @@ -187,52 +39,15 @@ KONTEXT_PATH = Path(__file__).parent / "kontext" def get_wahlprogramm(bundesland: str, partei: str) -> Optional[dict]: - """Liefert die Wahlprogramm-Metadaten oder None, wenn keins vorliegt.""" - return WAHLPROGRAMME.get(bundesland, {}).get(partei) + """Liefert das aktuell gültige Wahlprogramm dieser Partei in dem + Bundesland — als ``programme.Programm``-Dict (oder ``None``). - -def parteien_mit_wahlprogramm(bundesland: str) -> list[str]: - """Liste der Parteien, für die im gegebenen Bundesland ein Wahlprogramm vorliegt.""" - return list(WAHLPROGRAMME.get(bundesland, {}).keys()) - - -def regierungsbildung_for(bundesland: str) -> Optional[str]: - """Datum der Regierungsbildung (Vereidigung) der Regierung, die nach der - aktuellen Wahl aus diesem Programm hervorging. - - Achtung: NICHT die Vereidigung der gerade amtierenden Regierung — bei - Sukzessionen innerhalb derselben WP (z.B. RP Dreyer III → Schweitzer I - in WP18) bleibt das Datum bei der ersten Regierung der WP. Die - Wahlprogramme bleiben zur Wahl wirksam, auch wenn die MP-Person später - wechselt. Für die "aktuell amtierende" Regierung siehe - ``legislaturen.aktuelle_regierung``. + Compat-Adapter auf ``programme.aktuelles_wahlprogramm``. Aufrufer, + die früher ``info["file"]`` gelesen haben, müssen ``info["pdf"]`` + lesen; ``info["partei"]`` (Langform) gibt es nicht mehr — Kurzform + + ``info["bundesland"]`` reichen für die Anzeige. """ - parteien = WAHLPROGRAMME.get(bundesland, {}) - for info in parteien.values(): - rb = info.get("regierungsbildung") - if rb is not None: - return rb - return None - - -def regierungsende_for(bundesland: str) -> Optional[str]: - """Datum, ab dem die aus dieser Wahl hervorgegangene Regierungsperiode - endet. ``None`` solange laufend (auch nach Sukzession in derselben WP). - """ - parteien = WAHLPROGRAMME.get(bundesland, {}) - for info in parteien.values(): - return info.get("regierungsende") - return None - - -def regierung_aktuell(bundesland: str) -> bool: - """True, wenn die aus dieser Wahl hervorgegangene Regierungsperiode noch - läuft (bildung gesetzt, ende=None).""" - parteien = WAHLPROGRAMME.get(bundesland, {}) - if not parteien: - return False - info = next(iter(parteien.values())) - return info.get("regierungsbildung") is not None and info.get("regierungsende") is None + return aktuelles_wahlprogramm(bundesland, partei) def load_wahlprogramm_text(bundesland: str, partei: str) -> dict[int, str]: @@ -246,11 +61,14 @@ def load_wahlprogramm_text(bundesland: str, partei: str) -> dict[int, str]: if not info: return {} - # Versuche paged-Textdatei zu laden - paged_file = KONTEXT_PATH / info['file'].replace('.pdf', '-paged.txt') + pdf_name = info.get("pdf", "") + if not pdf_name: + return {} + + paged_file = KONTEXT_PATH / pdf_name.replace('.pdf', '-paged.txt') if not paged_file.exists(): # Fallback: Normale Textdatei - txt_file = KONTEXT_PATH / info['file'].replace('.pdf', '.txt') + txt_file = KONTEXT_PATH / pdf_name.replace('.pdf', '.txt') if txt_file.exists(): return {1: txt_file.read_text()} return {} @@ -283,16 +101,11 @@ def search_wahlprogramm( keywords: list[str], max_results: int = 3, ) -> list[dict]: - """Sucht relevante Passagen in einem Wahlprogramm. - - Args: - bundesland: Bundesland-Code (NRW, LSA, …) - partei: Partei-Kürzel (CDU, SPD, GRÜNE, FDP, AfD, …) - keywords: Suchbegriffe - max_results: Maximale Anzahl Ergebnisse + """Sucht relevante Passagen in einem Wahlprogramm (Keyword-basiert, + Fallback wenn die Embeddings-DB nicht da ist). Returns: - Liste von {bundesland, partei, seite, text, score, url, quelle} + Liste von {bundesland, partei, seite, text, score, url, quelle}. """ info = get_wahlprogramm(bundesland, partei) if not info: @@ -302,6 +115,10 @@ def search_wahlprogramm( if not pages: return [] + pdf_name = info.get("pdf", "") + name = info.get("name") or partei + jahr = info.get("gueltig_ab", "")[:4] or "?" + results = [] keywords_lower = [k.lower() for k in keywords] @@ -335,8 +152,8 @@ def search_wahlprogramm( "seite": page_num, "text": best_para, "score": score, - "url": f"/static/referenzen/{info['file']}#page={page_num}", - "quelle": f"{info['partei']} Wahlprogramm {info['jahr']}, S. {page_num}", + "url": f"/static/referenzen/{pdf_name}#page={page_num}", + "quelle": f"{name}, S. {page_num}", }) results.sort(key=lambda x: x['score'], reverse=True) @@ -402,12 +219,9 @@ def format_quote_for_prompt(quotes: dict[str, list[dict]]) -> str: return "" lines = ["\n## Relevante Passagen aus Wahlprogrammen\n"] - lines.append("Nutze diese Originalzitate als Belege in deiner Bewertung:\n") - - for partei, zitate in quotes.items(): - for z in zitate: - lines.append(f"### {z['quelle']}") - lines.append(f'> "{z["text"]}"') - lines.append("") - + for partei, partei_quotes in quotes.items(): + if partei_quotes: + lines.append(f"\n### {partei}") + for q in partei_quotes: + lines.append(f"- S. {q['seite']}: \"{q['text']}\"") return "\n".join(lines) diff --git a/tests/test_redline_parser.py b/tests/test_redline_parser.py index 03bfd06..4230e63 100644 --- a/tests/test_redline_parser.py +++ b/tests/test_redline_parser.py @@ -157,16 +157,16 @@ class TestBuildPdfHref: assert build_pdf_href(zitat) == "/api/wahlprogramm-cite?pid=cdu-nrw-2022&seite=15" def test_empty_url_falls_back_to_quelle_lookup(self): - """Ohne url muss die quelle reconstruiert werden via WAHLPROGRAMME.""" + """Ohne url muss die quelle reconstruiert werden via programme.PROGRAMME.""" from app.redline_utils import build_pdf_href - # Ein in WAHLPROGRAMME hinterlegter Titel - from app.wahlprogramme import WAHLPROGRAMME - # Pick the first programme from the registry - bl, parteien = next(iter(WAHLPROGRAMME.items())) - partei, info = next(iter(parteien.items())) - titel = info.get("titel", "") - if not titel: - pytest.skip("Kein WAHLPROGRAMME-Eintrag mit titel verfuegbar") + from app.programme import all_programme + prog = next( + (p for p in all_programme() if p.get("titel")), + None, + ) + if prog is None: + pytest.skip("Kein Programm-Eintrag mit titel verfügbar") + titel = prog["titel"] zitat = { "quelle": f"{titel} · S. 42", "text": "Wir wollen die Energiewende", @@ -193,12 +193,11 @@ class TestBuildPdfHref: def test_query_uses_first_5_words_of_text(self): from app.redline_utils import build_pdf_href - from app.wahlprogramme import WAHLPROGRAMME - bl, parteien = next(iter(WAHLPROGRAMME.items())) - partei, info = next(iter(parteien.items())) - titel = info.get("titel", "") - if not titel: - pytest.skip("Kein WAHLPROGRAMME-Eintrag mit titel verfuegbar") + from app.programme import all_programme + prog = next((p for p in all_programme() if p.get("titel")), None) + if prog is None: + pytest.skip("Kein Programm-Eintrag mit titel verfügbar") + titel = prog["titel"] zitat = { "quelle": f"{titel} · S. 5", "text": "Eins zwei drei vier fünf sechs sieben", @@ -214,12 +213,11 @@ class TestBuildPdfHref: def test_handles_seite_with_comma_separator(self): """Quelle 'Titel, S. 42' (Komma) muss genauso parsen wie '· S. 42'.""" from app.redline_utils import build_pdf_href - from app.wahlprogramme import WAHLPROGRAMME - bl, parteien = next(iter(WAHLPROGRAMME.items())) - partei, info = next(iter(parteien.items())) - titel = info.get("titel", "") - if not titel: - pytest.skip("Kein WAHLPROGRAMME-Eintrag mit titel verfuegbar") + from app.programme import all_programme + prog = next((p for p in all_programme() if p.get("titel")), None) + if prog is None: + pytest.skip("Kein Programm-Eintrag mit titel verfügbar") + titel = prog["titel"] zitat = {"quelle": f"{titel}, S. 17", "text": "x", "url": ""} href = build_pdf_href(zitat) assert "seite=17" in href diff --git a/tests/test_wahlprogramm_check.py b/tests/test_wahlprogramm_check.py index bb1f4db..533a0da 100644 --- a/tests/test_wahlprogramm_check.py +++ b/tests/test_wahlprogramm_check.py @@ -2,7 +2,7 @@ import pytest from app.wahlprogramm_check import check_missing_programmes -from app.wahlprogramme import WAHLPROGRAMME +from app.programme import parteien_mit_wahlprogramm as _parteien_mit_wp from app.bundeslaender import BUNDESLAENDER @@ -12,8 +12,7 @@ class TestCheckMissingProgrammes: def test_all_covered_returns_empty(self): """Alle Fraktionen haben ein hinterlegtes Programm → leere Liste.""" bl = "NRW" - # Nur Fraktionen übergeben, die in WAHLPROGRAMME["NRW"] liegen - indexed = list(WAHLPROGRAMME[bl].keys()) + indexed = _parteien_mit_wp(bl) result = check_missing_programmes(bl, indexed) assert result == [], ( f"Erwartet [], bekommen {result!r} — alle Fraktionen sollten abgedeckt sein" @@ -23,7 +22,7 @@ class TestCheckMissingProgrammes: """Eine Fraktion ohne Programm → wird in der Rückgabe gemeldet.""" bl = "NRW" # AfD ist in NRW hinterlegt, BSW nicht - fraktionen = list(WAHLPROGRAMME[bl].keys()) + ["BSW"] + fraktionen = _parteien_mit_wp(bl) + ["BSW"] result = check_missing_programmes(bl, fraktionen) assert "BSW" in result @@ -49,14 +48,14 @@ class TestCheckMissingProgrammes: check_missing_programmes("XX", ["CDU"]) def test_bundesland_without_wahlprogramme_entry(self): - """Aktives Bundesland ohne WAHLPROGRAMME-Eintrag → alle Fraktionen fehlend.""" - # Finde ein aktives BL, das keinen Eintrag in WAHLPROGRAMME hat + """Aktives Bundesland ohne Programm-Einträge → alle Fraktionen fehlend.""" + # Finde ein aktives BL, das keine Wahlprogramme registriert hat bl_without = next( - (code for code in BUNDESLAENDER if code not in WAHLPROGRAMME), + (code for code in BUNDESLAENDER if not _parteien_mit_wp(code)), None, ) if bl_without is None: - pytest.skip("Alle bekannten Bundesländer haben WAHLPROGRAMME-Einträge") + pytest.skip("Alle bekannten Bundesländer haben Programm-Einträge") fraktionen = BUNDESLAENDER[bl_without].landtagsfraktionen[:2] result = check_missing_programmes(bl_without, fraktionen) assert result == fraktionen diff --git a/tests/test_wahlprogramme.py b/tests/test_wahlprogramme.py index fbc28c5..574faa4 100644 --- a/tests/test_wahlprogramme.py +++ b/tests/test_wahlprogramme.py @@ -1,342 +1,102 @@ -"""Tests for wahlprogramme.py — registry consistency + file existence.""" -import re +"""Tests for wahlprogramme.py. +Nach dem #222-Refactor ist dieses Modul nur noch ein dünner Wrapper: +keyword-basierte Suche + PDF-Text-Loader + ein Compat-Adapter +``get_wahlprogramm`` der zu ``programme.aktuelles_wahlprogramm`` +delegiert. Die Stamm-Daten (``WAHLPROGRAMME``-Literal) sind nach +``programme.PROGRAMME`` gewandert. Strukturelle Daten-Tests leben +deshalb in ``test_programme.py``. +""" import pytest from app.wahlprogramme import ( - WAHLPROGRAMME, REFERENZEN_PATH, get_wahlprogramm, - parteien_mit_wahlprogramm, - regierung_aktuell, - regierungsbildung_for, - regierungsende_for, + load_wahlprogramm_text, + search_wahlprogramm, + find_relevant_quotes, + format_quote_for_prompt, ) +from app.programme import parteien_mit_wahlprogramm # ───────────────────────────────────────────────────────────────────────────── -# Registry consistency +# Stichproben aktiver Bundesländer + zugeordnete Parteien # ───────────────────────────────────────────────────────────────────────────── -class TestRegistryStructure: - def test_active_bundeslaender_present(self): - for code in ["NRW", "LSA", "MV", "BE"]: - assert code in WAHLPROGRAMME, f"missing wahlprogramme entry for {code}" - - def test_each_entry_has_required_keys(self): - required = {"file", "titel", "partei", "jahr", "seiten"} - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - missing = required - set(info.keys()) - assert not missing, f"{bl}/{partei} missing keys: {missing}" - - def test_jahr_is_integer(self): - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - assert isinstance(info["jahr"], int), f"{bl}/{partei} jahr not int" - - def test_seiten_is_positive_integer(self): - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - assert isinstance(info["seiten"], int) - assert info["seiten"] > 0 - - def test_file_extension_is_pdf(self): - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - assert info["file"].endswith(".pdf") - - -# ───────────────────────────────────────────────────────────────────────────── -# Regierungsbildungs-Felder — Konsistenz pro Bundesland -# ───────────────────────────────────────────────────────────────────────────── - -class TestRegierungsbildung: - """Pro Bundesland gehören alle Wahlprogramm-Einträge zur gleichen - Legislatur — d.h. dasselbe regierungsbildung-Datum. Ausnahme: BUND, wo - Grundsatzprogramme stehen, die regierungsbildung=None tragen.""" - - _ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$") - - def test_every_entry_has_regierungs_fields(self): - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - assert "regierungsbildung" in info, f"{bl}/{partei}: regierungsbildung fehlt" - assert "regierungsende" in info, f"{bl}/{partei}: regierungsende fehlt" - - def test_regierungsbildung_iso_or_none(self): - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - rb = info.get("regierungsbildung") - if rb is not None: - assert isinstance(rb, str) and self._ISO_DATE.match(rb), \ - f"{bl}/{partei}: regierungsbildung kein ISO-Datum: {rb!r}" - - def test_regierungsende_iso_or_none(self): - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - re_ = info.get("regierungsende") - if re_ is not None: - assert isinstance(re_, str) and self._ISO_DATE.match(re_), \ - f"{bl}/{partei}: regierungsende kein ISO-Datum: {re_!r}" - - def test_alle_parteien_eines_bl_haben_gleiches_datum(self): - """Alle Wahlprogramm-Einträge eines Bundeslands gehören zur selben - Regierung und müssen daher dasselbe Bildungs-/Endedatum tragen.""" - for bl, parteien in WAHLPROGRAMME.items(): - bildung = {info.get("regierungsbildung") for info in parteien.values()} - ende = {info.get("regierungsende") for info in parteien.values()} - assert len(bildung) == 1, \ - f"{bl}: regierungsbildung divergent: {bildung}" - assert len(ende) == 1, \ - f"{bl}: regierungsende divergent: {ende}" - - def test_grundsatzprogramme_haben_keine_regierung(self): - """Grundsatzprogramme (ist_grundsatz=True) tragen keine Regierungs- - bildung — sie sind zeitlos.""" - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - if info.get("ist_grundsatz"): - assert info.get("regierungsbildung") is None, \ - f"{bl}/{partei} ist Grundsatzprogramm, sollte regierungsbildung=None haben" - - -class TestRegierungsHelper: - def test_regierungsbildung_for_known_bl(self): - assert regierungsbildung_for("NRW") == "2022-06-29" - assert regierungsbildung_for("HH") == "2025-05-07" - - def test_regierungsbildung_for_bund_btw2025(self): - # BUND tragt nun die BTW-2025-Wahlprogramme; Kabinett Merz I - # vereidigt 06.05.2025. Grundsatzprogramme bleiben nur in - # embeddings.PROGRAMME als zweite Referenz. - assert regierungsbildung_for("BUND") == "2025-05-06" - - def test_regierungsbildung_for_unknown_bl(self): - assert regierungsbildung_for("XX") is None - - def test_regierung_aktuell_true_for_active_bl(self): - assert regierung_aktuell("NRW") is True - assert regierung_aktuell("BB") is True - assert regierung_aktuell("BUND") is True - - def test_regierungsende_for_active_is_none(self): - assert regierungsende_for("NRW") is None - - -# ───────────────────────────────────────────────────────────────────────────── -# File existence — every registered file must exist on disk -# ───────────────────────────────────────────────────────────────────────────── - -class TestFileExistence: - """Catches typos in the file field that would silently break embedding - indexing or PDF download links.""" - - def test_every_registered_pdf_exists(self): - missing = [] - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - path = REFERENZEN_PATH / info["file"] - if not path.exists(): - missing.append(f"{bl}/{partei}: {info['file']}") - assert not missing, "missing PDFs:\n " + "\n ".join(missing) - - -# ───────────────────────────────────────────────────────────────────────────── -# Lookup helpers -# ───────────────────────────────────────────────────────────────────────────── - -class TestGetWahlprogramm: - def test_returns_dict_for_known_combination(self): - info = get_wahlprogramm("MV", "CDU") - assert info is not None - assert info["partei"] == "CDU Mecklenburg-Vorpommern" - - def test_returns_none_for_unknown_bundesland(self): - assert get_wahlprogramm("XX", "CDU") is None - - def test_returns_none_for_unknown_partei(self): - assert get_wahlprogramm("NRW", "BSW") is None - class TestParteienMitWahlprogramm: + """Smoke-Test: in den jeweiligen BL liegen die erwarteten Fraktionen. + Strikte Schema-Tests gegen ``programme.PROGRAMME`` in test_programme.py.""" + def test_nrw_has_five_parteien(self): parteien = parteien_mit_wahlprogramm("NRW") - assert len(parteien) == 5 assert set(parteien) == {"CDU", "SPD", "GRÜNE", "FDP", "AfD"} - def test_mv_has_six_parteien(self): - parteien = parteien_mit_wahlprogramm("MV") - assert set(parteien) == {"CDU", "SPD", "GRÜNE", "FDP", "AfD", "LINKE"} - - def test_be_has_five_parteien(self): - parteien = parteien_mit_wahlprogramm("BE") - assert set(parteien) == {"CDU", "SPD", "GRÜNE", "LINKE", "AfD"} - def test_bund_has_eight_parteien(self): - # BTW 2025: CDU, CSU, SPD, GRÜNE, FDP, AfD, LINKE, BSW. parteien = parteien_mit_wahlprogramm("BUND") assert set(parteien) == {"CDU", "CSU", "SPD", "GRÜNE", "FDP", "AfD", "LINKE", "BSW"} - def test_by_has_five_parteien(self): - parteien = parteien_mit_wahlprogramm("BY") - assert set(parteien) == {"CSU", "FREIE WÄHLER", "GRÜNE", "SPD", "AfD"} - def test_hb_has_five_parteien(self): # AfD war wegen Listenstreit nicht zur Bürgerschaftswahl 2023 zugelassen. # Stattdessen ist BiW (Bürger in Wut) als 6. Fraktion in der 21. WP. parteien = parteien_mit_wahlprogramm("HB") assert set(parteien) == {"SPD", "CDU", "GRÜNE", "LINKE", "BiW"} - def test_he_has_five_parteien(self): - parteien = parteien_mit_wahlprogramm("HE") - assert set(parteien) == {"CDU", "SPD", "GRÜNE", "FDP", "AfD"} - - def test_ni_has_four_parteien(self): - parteien = parteien_mit_wahlprogramm("NI") - assert set(parteien) == {"SPD", "CDU", "GRÜNE", "AfD"} - - def test_sl_has_three_parteien(self): - parteien = parteien_mit_wahlprogramm("SL") - assert set(parteien) == {"SPD", "CDU", "AfD"} - - def test_sn_has_six_parteien(self): - parteien = parteien_mit_wahlprogramm("SN") - assert set(parteien) == {"CDU", "SPD", "AfD", "BSW", "LINKE", "GRÜNE"} - def test_unknown_bundesland_empty_list(self): assert parteien_mit_wahlprogramm("XX") == [] # ───────────────────────────────────────────────────────────────────────────── -# embeddings.PROGRAMME consistency cross-check +# get_wahlprogramm — Compat-Adapter # ───────────────────────────────────────────────────────────────────────────── -class TestEmbeddingsRegistryConsistency: - """Every entry in WAHLPROGRAMME must also exist in embeddings.PROGRAMME - so the indexer can find it. Mismatch is the kind of bug a manual smoke - misses but would show up during indexing.""" - def test_every_wahlprogramm_has_embeddings_entry(self): - from app.embeddings import PROGRAMME +class TestGetWahlprogramm: + def test_returns_programm_for_known_combination(self): + prog = get_wahlprogramm("NRW", "CDU") + assert prog is not None + assert prog["pdf"] == "cdu-nrw-2022.pdf" + assert prog["partei"] == "CDU" + assert prog["bundesland"] == "NRW" - # Match WAHLPROGRAMME-Eintrag → PROGRAMME-Eintrag entweder ueber den - # file-stem (Standard: "cdu-mv-2021" matcht "cdu-mv-2021") ODER ueber - # den `pdf`-Wert in PROGRAMME (BUND-Grundsatzprogramme nutzen kuerzere - # PROGRAMME-Keys wie "cdu-grundsatz" obwohl die Datei - # "cdu-grundsatzprogramm.pdf" heisst). - pdf_to_pid = {p.get("pdf"): pid for pid, p in PROGRAMME.items() if p.get("pdf")} - missing = [] - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - file_stem = info["file"].rsplit(".", 1)[0] - if file_stem in PROGRAMME: - continue - if info["file"] in pdf_to_pid: - continue - missing.append(f"{bl}/{partei} → {info['file']}") - assert not missing, ( - "WAHLPROGRAMME entries missing in embeddings.PROGRAMME:\n " - + "\n ".join(missing) - ) + def test_returns_none_for_unknown_bundesland(self): + assert get_wahlprogramm("XX", "CDU") is None + + def test_returns_none_for_unknown_partei(self): + assert get_wahlprogramm("NRW", "BSW") is None # BSW nicht im NRW-Landtag # ───────────────────────────────────────────────────────────────────────────── -# Strikte Cross-Konsistenz mit programme.PROGRAMME (Drift-Schutz, ADR 0013) +# File existence — every registered pdf must exist on disk # ───────────────────────────────────────────────────────────────────────────── -class TestWahlprogrammeProgrammeConsistency: - """WAHLPROGRAMME (legacy, mit titel/seiten/regierung) und - programme.PROGRAMME (zentrale Registry mit Geltungsdaten) speichern - überlappende Felder. Diese Tests fangen stille Drift, bis #222 die - Quelle vereinheitlicht (Compat-Shim). - Invarianten: - - Für jedes (bl, partei) in WAHLPROGRAMME liefert - ``aktuelles_wahlprogramm(bl, partei)`` einen Eintrag (nicht None). - - Der ``pdf``-Wert in PROGRAMME stimmt mit ``file`` in WAHLPROGRAMME - überein. - - Die ``partei``-Kurzform stimmt überein (PROGRAMME["partei"] ist die - Kurzform; WAHLPROGRAMME-Key ist auch Kurzform). - """ +class TestFileExistence: + """Catches typos im pdf-Feld der Programm-Registry, die das + Indexing oder PDF-Download silently brechen würden.""" - def test_every_wahlprogramm_has_aktuelles_programm_match(self): - from app.programme import aktuelles_wahlprogramm - - mismatches = [] - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - prog = aktuelles_wahlprogramm(bl, partei) - if prog is None: - mismatches.append( - f"{bl}/{partei}: aktuelles_wahlprogramm liefert None, " - f"obwohl WAHLPROGRAMME-Eintrag {info['file']} existiert" - ) - assert not mismatches, "\n ".join(mismatches) - - def test_pdf_filenames_match_between_registries(self): - from app.programme import aktuelles_wahlprogramm - - drift = [] - for bl, parteien in WAHLPROGRAMME.items(): - for partei, info in parteien.items(): - prog = aktuelles_wahlprogramm(bl, partei) - if prog is None: - continue # vom Vortest abgedeckt - wp_pdf = info["file"] - pr_pdf = prog.get("pdf") - if wp_pdf != pr_pdf: - drift.append(f"{bl}/{partei}: WAHLPROGRAMME.file={wp_pdf!r} ≠ PROGRAMME.pdf={pr_pdf!r}") - assert not drift, "Drift zwischen WAHLPROGRAMME und PROGRAMME:\n " + "\n ".join(drift) - - def test_partei_kurzform_consistency(self): - """PROGRAMME["partei"] ist die Kurzform (z.B. 'CDU'), nicht - die Langform ('CDU NRW'). Test-Sicherheitsnetz, falls jemand - versehentlich die Langform reinträgt.""" - from app.programme import aktuelles_wahlprogramm - - wrong = [] - for bl, parteien in WAHLPROGRAMME.items(): - for partei in parteien.keys(): - prog = aktuelles_wahlprogramm(bl, partei) - if prog is None: - continue - if prog.get("partei") != partei: - wrong.append( - f"{bl}/{partei}: PROGRAMME.partei={prog.get('partei')!r} " - f"≠ WAHLPROGRAMME-Key {partei!r}" - ) - assert not wrong, "\n ".join(wrong) - - def test_no_orphan_aktuelle_programme_in_registry(self): - """Die andere Richtung: jedes aktuelle Wahlprogramm in PROGRAMME - (gueltig_bis IS NULL, typ='wahlprogramm') muss in WAHLPROGRAMME - vorhanden sein. Sonst ist die Bewertungs-Pipeline blind dafür.""" + def test_every_registered_pdf_exists(self): from app.programme import all_programme - orphans = [] + missing = [] for prog in all_programme(): - if prog.get("typ") != "wahlprogramm": + pdf = prog.get("pdf") + if not pdf: continue - if prog.get("gueltig_bis") is not None: - continue # historisches Programm - bl = prog.get("bundesland") - partei = prog.get("partei") - if bl not in WAHLPROGRAMME: - orphans.append(f"{prog['id']}: BL {bl} nicht in WAHLPROGRAMME") - continue - if partei not in WAHLPROGRAMME[bl]: - orphans.append(f"{prog['id']}: {bl}/{partei} fehlt in WAHLPROGRAMME") - assert not orphans, "Aktuelle Wahlprogramme in PROGRAMME ohne WAHLPROGRAMME-Eintrag:\n " + "\n ".join(orphans) + path = REFERENZEN_PATH / pdf + if not path.exists(): + missing.append(f"{prog['id']}: {pdf}") + assert not missing, "missing PDFs:\n " + "\n ".join(missing) # ───────────────────────────────────────────────────────────────────────────── # load_wahlprogramm_text — Fallback-Pfade (#134 Coverage-Backfill) # ───────────────────────────────────────────────────────────────────────────── + class TestLoadWahlprogrammText: def test_returns_empty_for_unknown_combination(self): - from app.wahlprogramme import load_wahlprogramm_text assert load_wahlprogramm_text("XX", "XYZ") == {} def test_paged_textfile_used_when_present(self, tmp_path, monkeypatch): @@ -345,7 +105,7 @@ class TestLoadWahlprogrammText: from app import wahlprogramme as wp_mod # Mock get_wahlprogramm -> bekannte Datei monkeypatch.setattr(wp_mod, "get_wahlprogramm", - lambda bl, p: {"file": "test.pdf"}) + lambda bl, p: {"pdf": "test.pdf"}) paged = tmp_path / "test-paged.txt" paged.write_text("--- PAGE 1 ---\nseite eins\n--- PAGE 2 ---\nseite zwei") monkeypatch.setattr(wp_mod, "KONTEXT_PATH", tmp_path) @@ -355,11 +115,11 @@ class TestLoadWahlprogrammText: assert "seite zwei" in result[2] def test_falls_back_to_normal_textfile(self, tmp_path, monkeypatch): - """Ohne paged-Datei wird auf normale .txt-Datei zurueckgefallen, + """Ohne paged-Datei wird auf normale .txt-Datei zurückgefallen, komplett unter Seite 1.""" from app import wahlprogramme as wp_mod monkeypatch.setattr(wp_mod, "get_wahlprogramm", - lambda bl, p: {"file": "test.pdf"}) + lambda bl, p: {"pdf": "test.pdf"}) normal = tmp_path / "test.txt" normal.write_text("flacher text ohne seitenmarker") monkeypatch.setattr(wp_mod, "KONTEXT_PATH", tmp_path) @@ -368,39 +128,50 @@ class TestLoadWahlprogrammText: assert result == {1: "flacher text ohne seitenmarker"} def test_returns_empty_when_no_textfile(self, tmp_path, monkeypatch): - """Weder paged- noch normale Textdatei → leeres Dict.""" + """Weder paged- noch flat-Textdatei vorhanden → leeres Dict.""" from app import wahlprogramme as wp_mod monkeypatch.setattr(wp_mod, "get_wahlprogramm", - lambda bl, p: {"file": "test.pdf"}) - # tmp_path ist leer + lambda bl, p: {"pdf": "test.pdf"}) monkeypatch.setattr(wp_mod, "KONTEXT_PATH", tmp_path) - assert wp_mod.load_wahlprogramm_text("X", "Y") == {} + result = wp_mod.load_wahlprogramm_text("X", "Y") + assert result == {} + + +# ───────────────────────────────────────────────────────────────────────────── +# search_wahlprogramm — Edge cases +# ───────────────────────────────────────────────────────────────────────────── class TestSearchWahlprogramm: def test_returns_empty_for_unknown_combination(self): - from app.wahlprogramme import search_wahlprogramm - assert search_wahlprogramm("XX", "XYZ", ["test"]) == [] + result = search_wahlprogramm("XX", "XYZ", ["foo"]) + assert result == [] def test_returns_empty_when_text_missing(self, monkeypatch): - """Bekannte Partei + Bundesland aber keine Textdatei → leer.""" + """get_wahlprogramm liefert ein Programm, aber kein paged-Text: + search_wahlprogramm muss [] liefern, nicht crashen.""" from app import wahlprogramme as wp_mod monkeypatch.setattr(wp_mod, "get_wahlprogramm", - lambda bl, p: {"file": "missing.pdf"}) + lambda bl, p: {"pdf": "missing.pdf", + "name": "X Wahlprogramm 2024", + "gueltig_ab": "2024-01-01"}) monkeypatch.setattr(wp_mod, "load_wahlprogramm_text", lambda bl, p: {}) - assert wp_mod.search_wahlprogramm("X", "Y", ["test"]) == [] + assert search_wahlprogramm("X", "Y", ["foo"]) == [] + + +# ───────────────────────────────────────────────────────────────────────────── +# find_relevant_quotes — Bundesland-Validierung +# ───────────────────────────────────────────────────────────────────────────── class TestFindRelevantQuotes: def test_unknown_bundesland_raises(self): - from app.wahlprogramme import find_relevant_quotes with pytest.raises(ValueError, match="Unbekanntes Bundesland"): - find_relevant_quotes("Antrag-Text", ["CDU"], bundesland="ZZ") + find_relevant_quotes("text", ["CDU"], "XX") class TestFormatQuoteForPrompt: def test_empty_quotes_returns_empty_string(self): - from app.wahlprogramme import format_quote_for_prompt assert format_quote_for_prompt({}) == "" diff --git a/tools/build_programme_literal.py b/tools/build_programme_literal.py new file mode 100644 index 0000000..2c2e21c --- /dev/null +++ b/tools/build_programme_literal.py @@ -0,0 +1,361 @@ +"""Generate the new programme.PROGRAMME literal from the lazy-migrated state. + +Reads the in-memory PROGRAMME dict (after _migrate_from_legacy ran), enriches +with titel from WAHLPROGRAMME (where available) and seiten from PyMuPDF, then +emits a Python literal sorted by (typ, bundesland, gueltig_ab). +""" +import sys +sys.path.insert(0, "/app") +from pathlib import Path + +# Trigger migration +from app import programme as programme_mod +programme_mod._ensure_initialized() +PROGRAMME_NEU = programme_mod.PROGRAMME + +# Quelle für titel +from app.wahlprogramme import WAHLPROGRAMME + +# Override: titel-Mapping aus _ARCHIVED_SKELETONS (programme.py). +# Diese sind über partei+bundesland+typ identifiziert, weil die Skeleton-IDs +# Jahres-Suffixe haben, die embeddings.PROGRAMME-IDs nicht. +TITEL_OVERRIDE_BY_PARTEI_BL_TYP = { + ("CSU", "BY", "grundsatzprogramm-land"): "Für ein neues Miteinander — Grundsatzprogramm der CSU", + ("CDU", "NRW", "grundsatzprogramm-land"): "Aufstieg, Sicherheit, Perspektive — Das Nordrhein-Westfalen-Programm", + ("CDU", "SN", "grundsatzprogramm-land"): "Zukunftsplan für Sachsen", + ("CDU", "LSA", "grundsatzprogramm-land"): "Sachsen-Anhalt. Unsere Verantwortung. Unsere Zukunft.", + ("SSW", "SH", "grundsatzprogramm-land"): "SSW Rahmenprogramm", +} + +import fitz + +REFERENZEN = Path("/app/app/static/referenzen") + +def get_titel(prog: dict) -> str | None: + """titel aus WAHLPROGRAMME[bl][partei] holen, falls vorhanden.""" + bl = prog.get("bundesland") + partei = prog.get("partei") + if not bl or not partei: + return None + wp_entry = WAHLPROGRAMME.get(bl, {}).get(partei) + if not wp_entry: + return None + # Nur wenn das pdf zueinander passt + if wp_entry.get("file") != prog.get("pdf"): + return None + return wp_entry.get("titel") or None + +def get_seiten(pdf: str) -> int | None: + """PDF-Seitenzahl via PyMuPDF.""" + p = REFERENZEN / pdf + if not p.exists(): + return None + try: + doc = fitz.open(p) + n = doc.page_count + doc.close() + return n + except Exception as e: + print(f"# fitz-Fehler bei {pdf}: {e}", file=sys.stderr) + return None + +# Schlüssel für Python-Literal — diktiert die Reihenfolge im Output-Dict +KEYS_ORDER = [ + "id", "typ", "partei", "bundesland", "wp", + "gueltig_ab", "gueltig_bis", + "name", "titel", "pdf", "seiten", + # Verbleibende, die Tests heute lesen — im Schema-Refactor entfernt: + # "beschluss", "wahl", "hinweis", "ist_grundsatz" — übergehen. +] + +def repr_value(v): + if v is None: + return "None" + if isinstance(v, bool): + return "True" if v else "False" + if isinstance(v, (int, float)): + return repr(v) + if isinstance(v, str): + # double-quote-strings, mit Escape von eingebetteten "-Zeichen + escaped = v.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + return repr(v) + +def fmt_entry(prog: dict) -> str: + out = [] + for k in KEYS_ORDER: + v = prog.get(k) + if v is None and k in ("titel", "wp", "seiten", "bundesland", "gueltig_bis"): + # Optional-Felder: explizit None auch raus, wenn Feld nicht gesetzt + out.append(f'"{k}": None') + elif v is None: + continue # Pflichtfeld nicht gesetzt → überspringen, fällt sonst auf None + else: + out.append(f'"{k}": {repr_value(v)}') + return "{" + ", ".join(out) + "}" + +def gruppe(prog: dict): + """Sortier-Schlüssel.""" + typ = prog.get("typ", "zzz") + bl = prog.get("bundesland") or "" + ga = prog.get("gueltig_ab") or "0000" + partei = prog.get("partei", "") + return (typ, bl, ga, partei) + +# Anreichern + ausgeben +results = [] +for pid, prog in PROGRAMME_NEU.items(): + enriched = dict(prog) + enriched.setdefault("id", pid) + # Migration setzt titel = name als Default — das ist kein echter Slogan, + # nur Doppelung. titel ECHT nur dann tragen, wenn entweder explizit + # in WAHLPROGRAMME ("Machen, worauf es ankommt") oder vom Skeleton + # in programme.py (Land-Grundsatzprogramme). + if enriched.get("titel") == enriched.get("name"): + enriched.pop("titel", None) + if not enriched.get("titel"): + t = get_titel(prog) + if t and t != enriched.get("name"): + enriched["titel"] = t + # Override für Land-Grundsatzprogramme (Slogans aus alten Skeletons) + if not enriched.get("titel"): + key = ( + prog.get("partei"), + prog.get("bundesland"), + prog.get("typ"), + ) + override = TITEL_OVERRIDE_BY_PARTEI_BL_TYP.get(key) + if override: + enriched["titel"] = override + if not enriched.get("seiten"): + s = get_seiten(prog.get("pdf", "")) + if s: + enriched["seiten"] = s + results.append(enriched) + +results.sort(key=gruppe) + +# Statistik +n_titel = sum(1 for p in results if p.get("titel")) +n_seiten = sum(1 for p in results if p.get("seiten")) +print(f"# {len(results)} Einträge insgesamt", file=sys.stderr) +print(f"# {n_titel} mit titel ({len(results) - n_titel} ohne)", file=sys.stderr) +print(f"# {n_seiten} mit seiten ({len(results) - n_seiten} ohne)", file=sys.stderr) + +# Output: vollständige neue programme.py +header = '''"""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) +- ``analyzer.py`` (sucht das zum Antrag passende Wahlprogramm) +- UI (zeigt Geltungszeitraum + zugeordnete Regierung pro Programm) + +Siehe ``app/legislaturen.py`` für Wahlperioden + Regierungen — Programm- +Daten beschreiben das Dokument selbst, nicht die Regierung, die aus ihm +hervorging. Die Verbindung läuft über +``legislaturen.regierung_zum_zeitpunkt(bundesland, datum)``. +""" +from __future__ import annotations + +from pathlib import Path +from typing import Literal, Optional, TypedDict + + +# ───────────────────────────────────────────────────────────────────────────── +# Schema +# ───────────────────────────────────────────────────────────────────────────── + + +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, typ, partei, gueltig_ab, name, pdf. + Optional: bundesland, wp, gueltig_bis, titel, seiten. + + Was hier bewusst NICHT drin ist: + - ``regierungsbildung`` / ``regierungsende`` — gehört zu + ``legislaturen.REGIERUNGEN``. Verbindung Programm→Regierung läuft + über ``legislaturen.regierung_zum_zeitpunkt(bl, antrag_datum)``. + - ``partei`` Langform ("CDU NRW") — ableitbar via partei + bundesland. + - ``jahr`` — ``int(gueltig_ab[:4])`` reicht. + """ + id: str # eindeutiger Schlüssel, z.B. "cdu-nrw-2022" + typ: ProgrammTyp + partei: str # kanonisch (CDU, BiW, BÜNDNIS 90/DIE GRÜNEN, …) + bundesland: Optional[str] # BL-Code; None nur bei Bundesgrundsatzprogrammen + wp: Optional[int] # Legislatur-Nummer; nur typ=wahlprogramm + gueltig_ab: str # ISO YYYY-MM-DD; bei wahl: Wahltag + gueltig_bis: Optional[str] # ISO; None = aktuell gültig + name: str # voll-qualifiziert für Citation, z.B. "CDU NRW Wahlprogramm 2022" + titel: Optional[str] # Slogan ("Machen, worauf es ankommt"); None wenn nicht erfasst + pdf: str # Dateiname in static/referenzen/ + seiten: Optional[int] # PDF-Seitenzahl + + +REFERENZEN_PATH = Path(__file__).parent / "static" / "referenzen" +KONTEXT_PATH = Path(__file__).parent / "kontext" + + +# ───────────────────────────────────────────────────────────────────────────── +# Daten — alle 287 Programme (historisch + aktuell), sortiert nach +# (typ, bundesland, gueltig_ab, partei). Auto-generiert aus der bisherigen +# embeddings.PROGRAMME + WAHLPROGRAMME-Lazy-Migration via fitz für seiten. +# Generator: tools/build_programme_literal.py (siehe Commit-Historie). +# ───────────────────────────────────────────────────────────────────────────── + +''' +print(header) + +# Literal +print("PROGRAMME: dict[str, Programm] = {") +last_gruppe = None +for prog in results: + typ = prog.get("typ", "?") + bl = prog.get("bundesland") or "BUND" + cur = (typ, bl) + if cur != last_gruppe: + print(f"\n # ─── {typ} · {bl} ───") + last_gruppe = cur + print(f' "{prog["id"]}": {fmt_entry(prog)},') +print("}") + +# Helper-API +helpers = ''' + +# ───────────────────────────────────────────────────────────────────────────── +# 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 get_programm_by_pdf(pdf: str) -> Optional[Programm]: + """Reverse-Lookup über pdf-Dateinamen.""" + for prog in PROGRAMME.values(): + if prog.get("pdf") == pdf: + return prog + return None + + +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. + """ + 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. + """ + if bundesland is not None: + 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 + 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 + + +def all_programme() -> list[Programm]: + """Alle eingetragenen Programme.""" + return list(PROGRAMME.values()) +''' +print(helpers)