feat: Programme + Legislaturen-Registry mit historisch korrekter Geltung

Neue Module:
- app/programme.py: zentrale Programm-Registry (alle Wahl- und Grundsatz-
  programme in einem Index), mit Geltungsdaten gueltig_ab/gueltig_bis und
  Helpern wahlprogramm_zum_zeitpunkt(), grundsatzprogramm_zum_zeitpunkt(),
  alle_versionen(). Skelett fuer 6 zusaetzliche Eintraege (CSU 2023,
  CDU NRW 2015, CDU SN 2023, CDU LSA 2023, SSW SH 2016, FREIE WAEHLER)
  vorbereitet — PDFs folgen.
- app/legislaturen.py: 56 Legislaturen + 70 Regierungen fuer 16 BL + Bund.
  Helper legislatur_zum_zeitpunkt(), regierung_zum_zeitpunkt(),
  regierungen_einer_wp() fuer historisch korrekte Antrags-Bewertung
  (z.B. Kemmerich-28-Tage-Kabinett, RP-Uebergang Dreyer III -> Schweitzer I,
  BUND Scholz-Ampel -> geschaeftsfuehrend -> Merz I).
- tests/test_legislaturen.py: 20 Tests zu Konsistenz + Historie.

Datenbasis: 8 BTW-2025-Wahlprogramme (CDU, CSU, SPD, GRUENE, FDP, AfD,
LINKE, BSW) als PDFs hinzugefuegt. SHA-256-Pinning in
app/wahlprogramm-shas.lock.json (separat).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-07 23:26:23 +02:00 committed by Dotty Dotter
parent 7793705486
commit 991d1eb903
11 changed files with 960 additions and 0 deletions

313
app/legislaturen.py Normal file
View File

@ -0,0 +1,313 @@
"""Wahlperioden + Regierungen pro Bundesland — historisch + aktuell.
Diese Datei beantwortet zwei Fragen pro Bundesland:
1. **Welche Legislatur lief zum Zeitpunkt X?** (``legislatur_zum_zeitpunkt``)
2. **Welche Regierung war zum Zeitpunkt X im Amt?** (``regierung_zum_zeitpunkt``)
Die Strukturen sind ergänzend zu ``app/programme.py``: Programme verweisen
über ``wp`` (Legislatur-Nummer) bzw. ``gueltig_ab`` (= Vereidigung der
Regierung) auf die hier gelisteten Einträge.
Sonderfälle, die das Schema unterstützt:
- Mehrere Regierungen in einer Legislatur (z.B. RP: Dreyer III Schweitzer I)
- Vorgezogene Wahlen / verkürzte Legislaturen (z.B. NRW WP15 2010-2012)
- BUND als 17. Eintrag (technisch kein BL, aber Schema-konform)
"""
from __future__ import annotations
from typing import Optional, TypedDict
# ─────────────────────────────────────────────────────────────────────────────
# Typ
# ─────────────────────────────────────────────────────────────────────────────
class Legislatur(TypedDict, total=False):
"""Eine Wahlperiode eines deutschen Parlaments."""
bundesland: str # BL-Code (NRW, BUND, …)
wp: int # Wahlperioden-Nummer
wahltermin: str # ISO YYYY-MM-DD
konstituierung: str # ISO YYYY-MM-DD (= Beginn der WP)
ende: Optional[str] # ISO oder None bei aktueller WP
class Regierung(TypedDict, total=False):
"""Eine Landes- oder Bundesregierung (Kabinett).
Eine Legislatur kann mehrere Regierungen enthalten (z.B. wenn mitten in
der WP der/die Ministerpräsident:in zurücktritt). Bei diesen wird ``bis``
der Vorgängerin = ``von`` der Nachfolgerin gesetzt.
"""
bundesland: str
name: str # "Wüst II", "Schweitzer I", "Merz I", …
wp: int # Legislatur-Nummer
parteien: list[str] # Koalitionspartner (sortiert nach Größe)
ministerpraesident: str # MP / Bundeskanzler:in
von: str # ISO — Vereidigung
bis: Optional[str] # ISO — Vereidigung der Nachfolge oder Ende der WP
# ─────────────────────────────────────────────────────────────────────────────
# Daten-Container
# Werden vom Daten-Modul ``legislaturen_data.py`` befüllt sobald die
# Recherche fertig ist. Hier bleiben sie initial leer und werden später
# importiert / erweitert.
# ─────────────────────────────────────────────────────────────────────────────
LEGISLATUREN: list[Legislatur] = [
# ─── Baden-Württemberg ───────────────────────────────────────────────
{"bundesland": "BW", "wp": 15, "wahltermin": "2011-03-27", "konstituierung": "2011-05-11", "ende": "2016-05-11"},
{"bundesland": "BW", "wp": 16, "wahltermin": "2016-03-13", "konstituierung": "2016-05-11", "ende": "2021-05-11"},
{"bundesland": "BW", "wp": 17, "wahltermin": "2021-03-14", "konstituierung": "2021-05-11", "ende": None},
# ─── Bayern ──────────────────────────────────────────────────────────
{"bundesland": "BY", "wp": 17, "wahltermin": "2013-09-15", "konstituierung": "2013-10-07", "ende": "2018-11-05"},
{"bundesland": "BY", "wp": 18, "wahltermin": "2018-10-14", "konstituierung": "2018-11-05", "ende": "2023-10-30"},
{"bundesland": "BY", "wp": 19, "wahltermin": "2023-10-08", "konstituierung": "2023-10-30", "ende": None},
# ─── Berlin ──────────────────────────────────────────────────────────
{"bundesland": "BE", "wp": 17, "wahltermin": "2011-09-18", "konstituierung": "2011-10-27", "ende": "2016-10-27"},
{"bundesland": "BE", "wp": 18, "wahltermin": "2016-09-18", "konstituierung": "2016-10-27", "ende": "2021-11-04"},
# WP19: Originalwahl 2021-09-26; Wiederholung 2023-02-12 (BVerfG). Formal weiter WP19.
{"bundesland": "BE", "wp": 19, "wahltermin": "2021-09-26", "konstituierung": "2021-11-04", "ende": None},
# ─── Brandenburg ─────────────────────────────────────────────────────
{"bundesland": "BB", "wp": 6, "wahltermin": "2014-09-14", "konstituierung": "2014-10-21", "ende": "2019-09-25"},
{"bundesland": "BB", "wp": 7, "wahltermin": "2019-09-01", "konstituierung": "2019-09-25", "ende": "2024-10-23"},
{"bundesland": "BB", "wp": 8, "wahltermin": "2024-09-22", "konstituierung": "2024-10-23", "ende": None},
# ─── Bremen ──────────────────────────────────────────────────────────
{"bundesland": "HB", "wp": 18, "wahltermin": "2011-05-22", "konstituierung": "2011-06-07", "ende": "2015-06-15"},
{"bundesland": "HB", "wp": 19, "wahltermin": "2015-05-10", "konstituierung": "2015-06-15", "ende": "2019-07-04"},
{"bundesland": "HB", "wp": 20, "wahltermin": "2019-05-26", "konstituierung": "2019-07-04", "ende": "2023-07-04"},
{"bundesland": "HB", "wp": 21, "wahltermin": "2023-05-14", "konstituierung": "2023-07-04", "ende": None},
# ─── Hamburg ─────────────────────────────────────────────────────────
{"bundesland": "HH", "wp": 21, "wahltermin": "2015-02-15", "konstituierung": "2015-03-18", "ende": "2020-03-18"},
{"bundesland": "HH", "wp": 22, "wahltermin": "2020-02-23", "konstituierung": "2020-03-18", "ende": "2025-03-19"},
{"bundesland": "HH", "wp": 23, "wahltermin": "2025-03-02", "konstituierung": "2025-03-19", "ende": None},
# ─── Hessen ──────────────────────────────────────────────────────────
{"bundesland": "HE", "wp": 19, "wahltermin": "2013-09-22", "konstituierung": "2014-01-18", "ende": "2019-01-18"},
{"bundesland": "HE", "wp": 20, "wahltermin": "2018-10-28", "konstituierung": "2019-01-18", "ende": "2024-01-18"},
{"bundesland": "HE", "wp": 21, "wahltermin": "2023-10-08", "konstituierung": "2024-01-18", "ende": None},
# ─── Mecklenburg-Vorpommern ──────────────────────────────────────────
{"bundesland": "MV", "wp": 6, "wahltermin": "2011-09-04", "konstituierung": "2011-10-04", "ende": "2016-10-04"},
{"bundesland": "MV", "wp": 7, "wahltermin": "2016-09-04", "konstituierung": "2016-10-04", "ende": "2021-10-26"},
{"bundesland": "MV", "wp": 8, "wahltermin": "2021-09-26", "konstituierung": "2021-10-26", "ende": None},
# ─── Niedersachsen ───────────────────────────────────────────────────
{"bundesland": "NI", "wp": 17, "wahltermin": "2013-01-20", "konstituierung": "2013-02-19", "ende": "2017-11-14"},
{"bundesland": "NI", "wp": 18, "wahltermin": "2017-10-15", "konstituierung": "2017-11-14", "ende": "2022-11-08"},
{"bundesland": "NI", "wp": 19, "wahltermin": "2022-10-09", "konstituierung": "2022-11-08", "ende": None},
# ─── Nordrhein-Westfalen ─────────────────────────────────────────────
{"bundesland": "NRW", "wp": 15, "wahltermin": "2010-05-09", "konstituierung": "2010-06-09", "ende": "2012-06-12"},
{"bundesland": "NRW", "wp": 16, "wahltermin": "2012-05-13", "konstituierung": "2012-06-12", "ende": "2017-06-01"},
{"bundesland": "NRW", "wp": 17, "wahltermin": "2017-05-14", "konstituierung": "2017-06-01", "ende": "2022-06-01"},
{"bundesland": "NRW", "wp": 18, "wahltermin": "2022-05-15", "konstituierung": "2022-06-01", "ende": None},
# ─── Rheinland-Pfalz ─────────────────────────────────────────────────
{"bundesland": "RP", "wp": 16, "wahltermin": "2011-03-27", "konstituierung": "2011-05-18", "ende": "2016-05-18"},
{"bundesland": "RP", "wp": 17, "wahltermin": "2016-03-13", "konstituierung": "2016-05-18", "ende": "2021-05-18"},
{"bundesland": "RP", "wp": 18, "wahltermin": "2021-03-14", "konstituierung": "2021-05-18", "ende": None},
# ─── Saarland ────────────────────────────────────────────────────────
{"bundesland": "SL", "wp": 14, "wahltermin": "2009-08-30", "konstituierung": "2009-09-30", "ende": "2012-04-25"},
{"bundesland": "SL", "wp": 15, "wahltermin": "2012-03-25", "konstituierung": "2012-04-25", "ende": "2017-05-09"},
{"bundesland": "SL", "wp": 16, "wahltermin": "2017-03-26", "konstituierung": "2017-05-09", "ende": "2022-04-25"},
{"bundesland": "SL", "wp": 17, "wahltermin": "2022-03-27", "konstituierung": "2022-04-25", "ende": None},
# ─── Sachsen ─────────────────────────────────────────────────────────
{"bundesland": "SN", "wp": 5, "wahltermin": "2009-08-30", "konstituierung": "2009-09-29", "ende": "2014-09-29"},
{"bundesland": "SN", "wp": 6, "wahltermin": "2014-08-31", "konstituierung": "2014-09-29", "ende": "2019-10-01"},
{"bundesland": "SN", "wp": 7, "wahltermin": "2019-09-01", "konstituierung": "2019-10-01", "ende": "2024-10-01"},
{"bundesland": "SN", "wp": 8, "wahltermin": "2024-09-01", "konstituierung": "2024-10-01", "ende": None},
# ─── Sachsen-Anhalt ──────────────────────────────────────────────────
{"bundesland": "LSA", "wp": 6, "wahltermin": "2011-03-20", "konstituierung": "2011-04-19", "ende": "2016-04-13"},
{"bundesland": "LSA", "wp": 7, "wahltermin": "2016-03-13", "konstituierung": "2016-04-13", "ende": "2021-07-06"},
{"bundesland": "LSA", "wp": 8, "wahltermin": "2021-06-06", "konstituierung": "2021-07-06", "ende": None},
# ─── Schleswig-Holstein ──────────────────────────────────────────────
{"bundesland": "SH", "wp": 18, "wahltermin": "2012-05-06", "konstituierung": "2012-06-05", "ende": "2017-06-06"},
{"bundesland": "SH", "wp": 19, "wahltermin": "2017-05-07", "konstituierung": "2017-06-06", "ende": "2022-06-07"},
{"bundesland": "SH", "wp": 20, "wahltermin": "2022-05-08", "konstituierung": "2022-06-07", "ende": None},
# ─── Thüringen ───────────────────────────────────────────────────────
{"bundesland": "TH", "wp": 6, "wahltermin": "2014-09-14", "konstituierung": "2014-10-14", "ende": "2019-11-26"},
{"bundesland": "TH", "wp": 7, "wahltermin": "2019-10-27", "konstituierung": "2019-11-26", "ende": "2024-09-26"},
{"bundesland": "TH", "wp": 8, "wahltermin": "2024-09-01", "konstituierung": "2024-09-26", "ende": None},
# ─── Bund (Deutscher Bundestag) ──────────────────────────────────────
{"bundesland": "BUND", "wp": 18, "wahltermin": "2013-09-22", "konstituierung": "2013-10-22", "ende": "2017-10-24"},
{"bundesland": "BUND", "wp": 19, "wahltermin": "2017-09-24", "konstituierung": "2017-10-24", "ende": "2021-10-26"},
{"bundesland": "BUND", "wp": 20, "wahltermin": "2021-09-26", "konstituierung": "2021-10-26", "ende": "2025-03-25"},
{"bundesland": "BUND", "wp": 21, "wahltermin": "2025-02-23", "konstituierung": "2025-03-25", "ende": None},
]
REGIERUNGEN: list[Regierung] = [
# ─── Baden-Württemberg ───────────────────────────────────────────────
{"bundesland": "BW", "name": "Kretschmann I", "wp": 15, "parteien": ["GRÜNE", "SPD"], "ministerpraesident": "Winfried Kretschmann", "von": "2011-05-12", "bis": "2016-05-12"},
{"bundesland": "BW", "name": "Kretschmann II", "wp": 16, "parteien": ["GRÜNE", "CDU"], "ministerpraesident": "Winfried Kretschmann", "von": "2016-05-12", "bis": "2021-05-12"},
{"bundesland": "BW", "name": "Kretschmann III", "wp": 17, "parteien": ["GRÜNE", "CDU"], "ministerpraesident": "Winfried Kretschmann", "von": "2021-05-12", "bis": None},
# ─── Bayern ──────────────────────────────────────────────────────────
{"bundesland": "BY", "name": "Seehofer II", "wp": 17, "parteien": ["CSU"], "ministerpraesident": "Horst Seehofer", "von": "2013-10-10", "bis": "2018-03-21"},
{"bundesland": "BY", "name": "Söder I", "wp": 17, "parteien": ["CSU"], "ministerpraesident": "Markus Söder", "von": "2018-03-21", "bis": "2018-11-12"},
{"bundesland": "BY", "name": "Söder II", "wp": 18, "parteien": ["CSU", "FREIE WÄHLER"], "ministerpraesident": "Markus Söder", "von": "2018-11-12", "bis": "2023-11-07"},
{"bundesland": "BY", "name": "Söder III", "wp": 19, "parteien": ["CSU", "FREIE WÄHLER"], "ministerpraesident": "Markus Söder", "von": "2023-11-07", "bis": None},
# ─── Berlin ──────────────────────────────────────────────────────────
{"bundesland": "BE", "name": "Wowereit IV", "wp": 17, "parteien": ["SPD", "CDU"], "ministerpraesident": "Klaus Wowereit", "von": "2011-11-24", "bis": "2014-12-11"},
{"bundesland": "BE", "name": "Müller I", "wp": 17, "parteien": ["SPD", "CDU"], "ministerpraesident": "Michael Müller", "von": "2014-12-11", "bis": "2016-12-08"},
{"bundesland": "BE", "name": "Müller II", "wp": 18, "parteien": ["SPD", "LINKE", "GRÜNE"], "ministerpraesident": "Michael Müller", "von": "2016-12-08", "bis": "2021-12-21"},
{"bundesland": "BE", "name": "Giffey I", "wp": 19, "parteien": ["SPD", "GRÜNE", "LINKE"], "ministerpraesident": "Franziska Giffey", "von": "2021-12-21", "bis": "2023-04-27"},
{"bundesland": "BE", "name": "Wegner I", "wp": 19, "parteien": ["CDU", "SPD"], "ministerpraesident": "Kai Wegner", "von": "2023-04-27", "bis": None},
# ─── Brandenburg ─────────────────────────────────────────────────────
{"bundesland": "BB", "name": "Woidke I", "wp": 6, "parteien": ["SPD", "LINKE"], "ministerpraesident": "Dietmar Woidke", "von": "2014-11-05", "bis": "2019-11-20"},
{"bundesland": "BB", "name": "Woidke II", "wp": 7, "parteien": ["SPD", "CDU", "GRÜNE"], "ministerpraesident": "Dietmar Woidke", "von": "2019-11-20", "bis": "2024-12-11"},
{"bundesland": "BB", "name": "Woidke III", "wp": 8, "parteien": ["SPD", "BSW"], "ministerpraesident": "Dietmar Woidke", "von": "2024-12-11", "bis": None},
# ─── Bremen ──────────────────────────────────────────────────────────
{"bundesland": "HB", "name": "Böhrnsen III", "wp": 18, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Jens Böhrnsen", "von": "2011-06-29", "bis": "2015-07-15"},
{"bundesland": "HB", "name": "Sieling I", "wp": 19, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Carsten Sieling", "von": "2015-07-15", "bis": "2019-08-15"},
{"bundesland": "HB", "name": "Bovenschulte I", "wp": 20, "parteien": ["SPD", "GRÜNE", "LINKE"], "ministerpraesident": "Andreas Bovenschulte", "von": "2019-08-15", "bis": "2023-07-05"},
{"bundesland": "HB", "name": "Bovenschulte II", "wp": 21, "parteien": ["SPD", "GRÜNE", "LINKE"], "ministerpraesident": "Andreas Bovenschulte", "von": "2023-07-05", "bis": None},
# ─── Hamburg ─────────────────────────────────────────────────────────
{"bundesland": "HH", "name": "Scholz II", "wp": 21, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Olaf Scholz", "von": "2015-04-15", "bis": "2018-03-28"},
{"bundesland": "HH", "name": "Tschentscher I", "wp": 21, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Peter Tschentscher", "von": "2018-03-28", "bis": "2020-06-10"},
{"bundesland": "HH", "name": "Tschentscher II", "wp": 22, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Peter Tschentscher", "von": "2020-06-10", "bis": "2025-05-07"},
{"bundesland": "HH", "name": "Tschentscher III", "wp": 23, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Peter Tschentscher", "von": "2025-05-07", "bis": None},
# ─── Hessen ──────────────────────────────────────────────────────────
{"bundesland": "HE", "name": "Bouffier II", "wp": 19, "parteien": ["CDU", "GRÜNE"], "ministerpraesident": "Volker Bouffier", "von": "2014-01-18", "bis": "2019-01-18"},
{"bundesland": "HE", "name": "Bouffier III", "wp": 20, "parteien": ["CDU", "GRÜNE"], "ministerpraesident": "Volker Bouffier", "von": "2019-01-18", "bis": "2022-05-31"},
{"bundesland": "HE", "name": "Rhein I", "wp": 20, "parteien": ["CDU", "GRÜNE"], "ministerpraesident": "Boris Rhein", "von": "2022-05-31", "bis": "2024-01-18"},
{"bundesland": "HE", "name": "Rhein II", "wp": 21, "parteien": ["CDU", "SPD"], "ministerpraesident": "Boris Rhein", "von": "2024-01-18", "bis": None},
# ─── Mecklenburg-Vorpommern ──────────────────────────────────────────
{"bundesland": "MV", "name": "Sellering II", "wp": 6, "parteien": ["SPD", "CDU"], "ministerpraesident": "Erwin Sellering", "von": "2011-10-25", "bis": "2016-11-01"},
{"bundesland": "MV", "name": "Sellering III", "wp": 7, "parteien": ["SPD", "CDU"], "ministerpraesident": "Erwin Sellering", "von": "2016-11-01", "bis": "2017-07-04"},
{"bundesland": "MV", "name": "Schwesig I", "wp": 7, "parteien": ["SPD", "CDU"], "ministerpraesident": "Manuela Schwesig", "von": "2017-07-04", "bis": "2021-11-15"},
{"bundesland": "MV", "name": "Schwesig II", "wp": 8, "parteien": ["SPD", "LINKE"], "ministerpraesident": "Manuela Schwesig", "von": "2021-11-15", "bis": None},
# ─── Niedersachsen ───────────────────────────────────────────────────
{"bundesland": "NI", "name": "Weil I", "wp": 17, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Stephan Weil", "von": "2013-02-19", "bis": "2017-11-22"},
{"bundesland": "NI", "name": "Weil II", "wp": 18, "parteien": ["SPD", "CDU"], "ministerpraesident": "Stephan Weil", "von": "2017-11-22", "bis": "2022-11-08"},
{"bundesland": "NI", "name": "Weil III", "wp": 19, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Stephan Weil", "von": "2022-11-08", "bis": None},
# ─── Nordrhein-Westfalen ─────────────────────────────────────────────
{"bundesland": "NRW", "name": "Kraft I", "wp": 15, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Hannelore Kraft", "von": "2010-07-15", "bis": "2012-06-20"},
{"bundesland": "NRW", "name": "Kraft II", "wp": 16, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Hannelore Kraft", "von": "2012-06-20", "bis": "2017-06-30"},
{"bundesland": "NRW", "name": "Laschet I", "wp": 17, "parteien": ["CDU", "FDP"], "ministerpraesident": "Armin Laschet", "von": "2017-06-30", "bis": "2021-10-27"},
{"bundesland": "NRW", "name": "Wüst I", "wp": 17, "parteien": ["CDU", "FDP"], "ministerpraesident": "Hendrik Wüst", "von": "2021-10-27", "bis": "2022-06-29"},
{"bundesland": "NRW", "name": "Wüst II", "wp": 18, "parteien": ["CDU", "GRÜNE"], "ministerpraesident": "Hendrik Wüst", "von": "2022-06-29", "bis": None},
# ─── Rheinland-Pfalz ─────────────────────────────────────────────────
{"bundesland": "RP", "name": "Beck V", "wp": 16, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Kurt Beck", "von": "2011-05-18", "bis": "2013-01-16"},
{"bundesland": "RP", "name": "Dreyer I", "wp": 16, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Malu Dreyer", "von": "2013-01-16", "bis": "2016-05-18"},
{"bundesland": "RP", "name": "Dreyer II", "wp": 17, "parteien": ["SPD", "GRÜNE", "FDP"], "ministerpraesident": "Malu Dreyer", "von": "2016-05-18", "bis": "2021-05-18"},
{"bundesland": "RP", "name": "Dreyer III", "wp": 18, "parteien": ["SPD", "GRÜNE", "FDP"], "ministerpraesident": "Malu Dreyer", "von": "2021-05-18", "bis": "2024-07-10"},
{"bundesland": "RP", "name": "Schweitzer I", "wp": 18, "parteien": ["SPD", "GRÜNE", "FDP"], "ministerpraesident": "Alexander Schweitzer", "von": "2024-07-10", "bis": None},
# ─── Saarland ────────────────────────────────────────────────────────
{"bundesland": "SL", "name": "Müller III", "wp": 14, "parteien": ["CDU", "FDP", "GRÜNE"], "ministerpraesident": "Peter Müller", "von": "2009-11-10", "bis": "2011-08-10"},
{"bundesland": "SL", "name": "Kramp-Karrenbauer I", "wp": 14, "parteien": ["CDU", "FDP", "GRÜNE"], "ministerpraesident": "Annegret Kramp-Karrenbauer", "von": "2011-08-10", "bis": "2012-05-09"},
{"bundesland": "SL", "name": "Kramp-Karrenbauer II", "wp": 15, "parteien": ["CDU", "SPD"], "ministerpraesident": "Annegret Kramp-Karrenbauer", "von": "2012-05-09", "bis": "2017-05-17"},
{"bundesland": "SL", "name": "Kramp-Karrenbauer III", "wp": 16, "parteien": ["CDU", "SPD"], "ministerpraesident": "Annegret Kramp-Karrenbauer", "von": "2017-05-17", "bis": "2018-02-28"},
{"bundesland": "SL", "name": "Hans I", "wp": 16, "parteien": ["CDU", "SPD"], "ministerpraesident": "Tobias Hans", "von": "2018-02-28", "bis": "2022-04-25"},
{"bundesland": "SL", "name": "Rehlinger I", "wp": 17, "parteien": ["SPD"], "ministerpraesident": "Anke Rehlinger", "von": "2022-04-25", "bis": None},
# ─── Sachsen ─────────────────────────────────────────────────────────
{"bundesland": "SN", "name": "Tillich I", "wp": 5, "parteien": ["CDU", "FDP"], "ministerpraesident": "Stanislaw Tillich", "von": "2009-09-29", "bis": "2014-11-13"},
{"bundesland": "SN", "name": "Tillich II", "wp": 6, "parteien": ["CDU", "SPD"], "ministerpraesident": "Stanislaw Tillich", "von": "2014-11-13", "bis": "2017-12-13"},
{"bundesland": "SN", "name": "Kretschmer I", "wp": 6, "parteien": ["CDU", "SPD"], "ministerpraesident": "Michael Kretschmer", "von": "2017-12-13", "bis": "2019-12-20"},
{"bundesland": "SN", "name": "Kretschmer II", "wp": 7, "parteien": ["CDU", "GRÜNE", "SPD"], "ministerpraesident": "Michael Kretschmer", "von": "2019-12-20", "bis": "2024-12-18"},
{"bundesland": "SN", "name": "Kretschmer III", "wp": 8, "parteien": ["CDU", "SPD"], "ministerpraesident": "Michael Kretschmer", "von": "2024-12-18", "bis": None},
# ─── Sachsen-Anhalt ──────────────────────────────────────────────────
{"bundesland": "LSA", "name": "Haseloff I", "wp": 6, "parteien": ["CDU", "SPD"], "ministerpraesident": "Reiner Haseloff", "von": "2011-04-19", "bis": "2016-04-25"},
{"bundesland": "LSA", "name": "Haseloff II", "wp": 7, "parteien": ["CDU", "SPD", "GRÜNE"], "ministerpraesident": "Reiner Haseloff", "von": "2016-04-25", "bis": "2021-09-16"},
{"bundesland": "LSA", "name": "Haseloff III", "wp": 8, "parteien": ["CDU", "SPD", "FDP"], "ministerpraesident": "Reiner Haseloff", "von": "2021-09-16", "bis": None},
# ─── Schleswig-Holstein ──────────────────────────────────────────────
{"bundesland": "SH", "name": "Albig I", "wp": 18, "parteien": ["SPD", "GRÜNE", "SSW"], "ministerpraesident": "Torsten Albig", "von": "2012-06-12", "bis": "2017-06-28"},
{"bundesland": "SH", "name": "Günther I", "wp": 19, "parteien": ["CDU", "GRÜNE", "FDP"], "ministerpraesident": "Daniel Günther", "von": "2017-06-28", "bis": "2022-06-29"},
{"bundesland": "SH", "name": "Günther II", "wp": 20, "parteien": ["CDU", "GRÜNE"], "ministerpraesident": "Daniel Günther", "von": "2022-06-29", "bis": None},
# ─── Thüringen ───────────────────────────────────────────────────────
{"bundesland": "TH", "name": "Ramelow I", "wp": 6, "parteien": ["LINKE", "SPD", "GRÜNE"], "ministerpraesident": "Bodo Ramelow", "von": "2014-12-05", "bis": "2020-02-05"},
{"bundesland": "TH", "name": "Kemmerich I", "wp": 7, "parteien": ["FDP"], "ministerpraesident": "Thomas Kemmerich", "von": "2020-02-05", "bis": "2020-03-04"},
{"bundesland": "TH", "name": "Ramelow II", "wp": 7, "parteien": ["LINKE", "SPD", "GRÜNE"], "ministerpraesident": "Bodo Ramelow", "von": "2020-03-04", "bis": "2024-12-12"},
{"bundesland": "TH", "name": "Voigt I", "wp": 8, "parteien": ["CDU", "BSW", "SPD"], "ministerpraesident": "Mario Voigt", "von": "2024-12-12", "bis": None},
# ─── Bund (Deutscher Bundestag) ──────────────────────────────────────
{"bundesland": "BUND", "name": "Merkel III", "wp": 18, "parteien": ["CDU", "CSU", "SPD"], "ministerpraesident": "Angela Merkel", "von": "2013-12-17", "bis": "2018-03-14"},
{"bundesland": "BUND", "name": "Merkel IV", "wp": 19, "parteien": ["CDU", "CSU", "SPD"], "ministerpraesident": "Angela Merkel", "von": "2018-03-14", "bis": "2021-12-08"},
{"bundesland": "BUND", "name": "Scholz I", "wp": 20, "parteien": ["SPD", "GRÜNE", "FDP"], "ministerpraesident": "Olaf Scholz", "von": "2021-12-08", "bis": "2024-11-06"},
{"bundesland": "BUND", "name": "Scholz I (geschäftsführend)", "wp": 20, "parteien": ["SPD", "GRÜNE"], "ministerpraesident": "Olaf Scholz", "von": "2024-11-06", "bis": "2025-05-06"},
{"bundesland": "BUND", "name": "Merz I", "wp": 21, "parteien": ["CDU", "CSU", "SPD"], "ministerpraesident": "Friedrich Merz", "von": "2025-05-06", "bis": None},
]
# ─────────────────────────────────────────────────────────────────────────────
# Helper-API
# ─────────────────────────────────────────────────────────────────────────────
def _datum_in_range(datum: str, von: str, bis: Optional[str]) -> bool:
if datum < von:
return False
if bis is None:
return True
return datum < bis
def legislatur_zum_zeitpunkt(
bundesland: str, datum: str,
) -> Optional[Legislatur]:
"""Welche Wahlperiode lief im Bundesland zum gegebenen Datum?"""
for leg in LEGISLATUREN:
if leg["bundesland"] != bundesland:
continue
if _datum_in_range(datum, leg["konstituierung"], leg.get("ende")):
return leg
return None
def regierung_zum_zeitpunkt(
bundesland: str, datum: str,
) -> Optional[Regierung]:
"""Welche Regierung war im Bundesland zum gegebenen Datum im Amt?"""
for reg in REGIERUNGEN:
if reg["bundesland"] != bundesland:
continue
if _datum_in_range(datum, reg["von"], reg.get("bis")):
return reg
return None
def aktuelle_regierung(bundesland: str) -> Optional[Regierung]:
"""Aktuelle Regierung des Bundeslands (bis=None)."""
for reg in REGIERUNGEN:
if reg["bundesland"] == bundesland and reg.get("bis") is None:
return reg
return None
def aktuelle_legislatur(bundesland: str) -> Optional[Legislatur]:
"""Laufende Wahlperiode (ende=None)."""
for leg in LEGISLATUREN:
if leg["bundesland"] == bundesland and leg.get("ende") is None:
return leg
return None
def regierungen_einer_wp(bundesland: str, wp: int) -> list[Regierung]:
"""Alle Regierungen einer bestimmten Legislatur, chronologisch."""
regs = [
r for r in REGIERUNGEN
if r["bundesland"] == bundesland and r["wp"] == wp
]
regs.sort(key=lambda r: r["von"])
return regs
def parteien_in_regierung_zum_zeitpunkt(
bundesland: str, datum: str,
) -> list[str]:
"""Welche Parteien waren zum Zeitpunkt in der Regierung?"""
reg = regierung_zum_zeitpunkt(bundesland, datum)
if reg is None:
return []
return list(reg.get("parteien", []))
def alle_legislaturen(bundesland: str) -> list[Legislatur]:
"""Alle Wahlperioden eines Bundeslands, sortiert nach WP-Nummer."""
legs = [leg for leg in LEGISLATUREN if leg["bundesland"] == bundesland]
legs.sort(key=lambda l: l["wp"])
return legs
def alle_regierungen(bundesland: str) -> list[Regierung]:
"""Alle Regierungen eines Bundeslands, chronologisch nach von."""
regs = [r for r in REGIERUNGEN if r["bundesland"] == bundesland]
regs.sort(key=lambda r: r["von"])
return regs

487
app/programme.py Normal file
View File

@ -0,0 +1,487 @@
"""Zentrale Programm-Registry — alle politischen Programm-Dokumente
(Wahlprogramme, Bundes-Grundsatzprogramme, Landes-Grundsatzprogramme),
historisch und aktuell.
Single Source of Truth für:
- ``embeddings.py`` (Indexer liest die PDFs aus dieser Liste)
- ``wahlprogramme.py`` (Compat-Shim, leitet die alte BL/Partei-API hierher)
- ``analyzer.py`` (sucht das zum Antrag passende Wahlprogramm)
- UI (zeigt Geltungszeitraum + zugeordnete Regierung pro Programm)
Siehe ``app/legislaturen.py`` für Wahlperioden + Regierungen.
"""
from __future__ import annotations
from pathlib import Path
from typing import Literal, Optional, TypedDict
# ─────────────────────────────────────────────────────────────────────────────
# Typ
# ─────────────────────────────────────────────────────────────────────────────
ProgrammTyp = Literal[
"wahlprogramm", # zur Wahl beschlossen, gilt für 1 Legislatur
"grundsatzprogramm-bund", # bundesweites Grundsatzprogramm
"grundsatzprogramm-land", # landesspezifisches Grundsatzprogramm
]
class Programm(TypedDict, total=False):
"""Single source of truth für ein politisches Programm-Dokument.
Pflichtfelder: id, titel, name, typ, partei, gueltig_ab, pdf, seiten.
Optional: bundesland, beschluss, wahl, wp, gueltig_bis, hinweis.
"""
id: str # eindeutiger Schlüssel, z.B. "cdu-nrw-2022"
titel: str # offizieller Titel ("Machen, worauf es ankommt")
name: str # voll-qualifiziert für Citation (z.B. "CDU NRW Wahlprogramm 2022")
typ: ProgrammTyp
partei: str # normalisierter Schlüssel (CDU, GRÜNE, FREIE WÄHLER, BSW, …)
bundesland: Optional[str] # BL-Code; None nur bei reinen Bundesgrundsatzprogrammen
beschluss: Optional[str] # ISO YYYY-MM-DD; bei grundsatz: Parteitags-Beschluss
wahl: Optional[str] # ISO YYYY-MM-DD; nur typ=wahlprogramm: Wahltag
wp: Optional[int] # Legislatur-Nummer; nur typ=wahlprogramm
gueltig_ab: str # ISO; bei wahl: regierungsbildung; bei grundsatz: beschluss
gueltig_bis: Optional[str] # ISO; None = Programm ist aktuell gültig
pdf: str # Dateiname in static/referenzen/
seiten: int
hinweis: Optional[str] # freier Text, z.B. "BSW hat kein Grundsatzprogramm — Wahlprogramm dient als Hauptquelle"
REFERENZEN_PATH = Path(__file__).parent / "static" / "referenzen"
KONTEXT_PATH = Path(__file__).parent / "kontext"
# ─────────────────────────────────────────────────────────────────────────────
# Programm-Registry
# Die Daten werden aus der bisherigen ``embeddings.PROGRAMME`` und
# ``wahlprogramme.WAHLPROGRAMME`` migriert. Historische Wahlprogramme +
# Landesgrundsatzprogramme werden ergänzt sobald die Recherche fertig ist.
# ─────────────────────────────────────────────────────────────────────────────
# Aktuelle Programme — gefüllt durch ``_register_initial_data()`` weiter unten,
# damit der Migrations-Pfad an einer Stelle zu sehen ist.
PROGRAMME: dict[str, Programm] = {}
# ─────────────────────────────────────────────────────────────────────────────
# Helper-API
# ─────────────────────────────────────────────────────────────────────────────
def _date_in_range(datum: str, ab: str, bis: Optional[str]) -> bool:
"""Liefert True, wenn ``datum`` (ISO) in [ab, bis) liegt."""
if datum < ab:
return False
if bis is None:
return True
return datum < bis
def get_programm(programm_id: str) -> Optional[Programm]:
"""Lookup nach ID."""
return PROGRAMME.get(programm_id)
def aktuelles_wahlprogramm(bundesland: str, partei: str) -> Optional[Programm]:
"""Aktuell gültiges Wahlprogramm einer Partei in einem Bundesland.
Es kann nur eines aktuell sein (gueltig_bis=None und typ=wahlprogramm).
"""
for prog in PROGRAMME.values():
if (
prog.get("typ") == "wahlprogramm"
and prog.get("bundesland") == bundesland
and prog.get("partei") == partei
and prog.get("gueltig_bis") is None
):
return prog
return None
def wahlprogramm_zum_zeitpunkt(
bundesland: str, partei: str, datum: str,
) -> Optional[Programm]:
"""Welches Wahlprogramm dieser Partei galt im Bundesland am gegebenen Datum?
``datum`` ist ISO-Datum. Es wird das Programm zurückgegeben, dessen
Geltungszeitraum [gueltig_ab, gueltig_bis) das Datum enthält.
Rückgabe ``None``, wenn die Partei zu dem Zeitpunkt nicht im Schema
erfasst ist (oder das Bundesland nicht).
"""
for prog in PROGRAMME.values():
if (
prog.get("typ") == "wahlprogramm"
and prog.get("bundesland") == bundesland
and prog.get("partei") == partei
and _date_in_range(datum, prog["gueltig_ab"], prog.get("gueltig_bis"))
):
return prog
return None
def grundsatzprogramm_zum_zeitpunkt(
partei: str,
datum: str,
bundesland: Optional[str] = None,
) -> Optional[Programm]:
"""Welches Grundsatzprogramm der Partei galt am gegebenen Datum?
Wenn ``bundesland`` gesetzt ist, wird zuerst nach einem
Landes-Grundsatzprogramm gesucht; falls keines existiert, fällt die
Suche auf das Bundes-Grundsatzprogramm zurück.
Wenn ``bundesland`` ``None`` ist, wird nur nach Bundes-Grundsatz gesucht.
"""
if bundesland is not None:
# Erst Land suchen
for prog in PROGRAMME.values():
if (
prog.get("typ") == "grundsatzprogramm-land"
and prog.get("partei") == partei
and prog.get("bundesland") == bundesland
and _date_in_range(datum, prog["gueltig_ab"], prog.get("gueltig_bis"))
):
return prog
# Bund als Fallback / oder primär
for prog in PROGRAMME.values():
if (
prog.get("typ") == "grundsatzprogramm-bund"
and prog.get("partei") == partei
and _date_in_range(datum, prog["gueltig_ab"], prog.get("gueltig_bis"))
):
return prog
return None
def parteien_mit_wahlprogramm(bundesland: str) -> list[str]:
"""Parteien mit einem aktuell gültigen Wahlprogramm in dem Bundesland.
Reihenfolge: nach Eintrags-Reihenfolge in PROGRAMME (deterministic).
"""
seen: list[str] = []
for prog in PROGRAMME.values():
if (
prog.get("typ") == "wahlprogramm"
and prog.get("bundesland") == bundesland
and prog.get("gueltig_bis") is None
):
partei = prog["partei"]
if partei not in seen:
seen.append(partei)
return seen
def alle_versionen(bundesland: str, partei: str) -> list[Programm]:
"""Alle Wahlprogramm-Versionen dieser Partei im Bundesland, sortiert
nach ``gueltig_ab`` aufsteigend."""
versions = [
prog for prog in PROGRAMME.values()
if prog.get("typ") == "wahlprogramm"
and prog.get("bundesland") == bundesland
and prog.get("partei") == partei
]
versions.sort(key=lambda p: p["gueltig_ab"])
return versions
# ─────────────────────────────────────────────────────────────────────────────
# Initiale Daten — wird über separate Daten-Datei eingespielt.
# Während des Migrationsfensters wird ``embeddings.PROGRAMME`` und
# ``wahlprogramme.WAHLPROGRAMME`` automatisch in PROGRAMME überführt.
# ─────────────────────────────────────────────────────────────────────────────
def _register(prog: Programm) -> None:
"""Add a Programm to the registry, validating uniqueness of id."""
if prog["id"] in PROGRAMME:
raise ValueError(f"Programm-id collision: {prog['id']}")
PROGRAMME[prog["id"]] = prog
# ─────────────────────────────────────────────────────────────────────────────
# Zusätzliche Programme — historische Vorgänger und Landesgrundsatzprogramme,
# die nicht aus WAHLPROGRAMME/embeddings.PROGRAMME migriert werden können.
#
# PDFs werden separat beschafft und hier eingetragen sobald verfügbar.
# Einträge ohne existierende PDF-Datei können aktiv sein (z.B. für reine
# Metadaten-Anzeige), aber sie werden in der Embeddings-Indizierung
# übersprungen und ``test_every_registered_pdf_exists`` filtert sie via
# ``hinweis="pdf-pending"`` Marker aus.
# ─────────────────────────────────────────────────────────────────────────────
_ADDITIONAL_PROGRAMME: 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
# Schritt 1: Wahlprogramme aus WAHLPROGRAMME
for bundesland, parteien in WAHLPROGRAMME.items():
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
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": None,
"wp": None,
"gueltig_ab": info.get("regierungsbildung") or "1900-01-01",
"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
for pid, info in _EMB_PROGRAMME.items():
if info.get("typ") != "parteiprogramm":
continue # Wahlprogramme schon in Schritt 1 (oder werden dort gepflegt)
if pid in PROGRAMME:
continue
prog2: Programm = {
"id": pid,
"titel": info.get("name", ""),
"name": info.get("name", ""),
"typ": "grundsatzprogramm-bund",
"partei": info.get("partei", ""),
"bundesland": None,
"beschluss": info.get("gueltig_ab"),
"wahl": None,
"wp": None,
"gueltig_ab": info.get("gueltig_ab") or "1900-01-01",
"gueltig_bis": info.get("gueltig_bis"),
"pdf": info.get("pdf", ""),
"seiten": 0,
"hinweis": None,
}
PROGRAMME[pid] = prog2
# Schritt 3: _ADDITIONAL_PROGRAMME — mit Vorgänger-bis-Update
for prog3 in _ADDITIONAL_PROGRAMME:
# Setze gueltig_bis von Vorgänger-Programmen rückwirkend.
for existing in list(PROGRAMME.values()):
if (
existing.get("typ") in ("grundsatzprogramm-bund",
"grundsatzprogramm-land")
and existing.get("partei") == prog3.get("partei")
and existing.get("bundesland") == prog3.get("bundesland")
and existing.get("gueltig_bis") is None
and existing.get("gueltig_ab", "9999-99-99") < prog3["gueltig_ab"]
):
# Wir mutieren ein TypedDict in der Registry — das ist OK,
# weil _migrate_from_legacy() einmal beim Import läuft.
existing["gueltig_bis"] = prog3["gueltig_ab"] # type: ignore[typeddict-item]
if prog3["id"] not in PROGRAMME:
PROGRAMME[prog3["id"]] = prog3
# Lazy initialisierung — erst beim ersten echten Zugriff. Dadurch vermeidet
# das Modul Import-Reihenfolge-Probleme mit ``embeddings.py`` (das viel mehr
# Initialisierung braucht: openai, fitz, etc.).
_INITIALIZED = False
def _ensure_initialized() -> None:
global _INITIALIZED
if _INITIALIZED:
return
_migrate_from_legacy()
_INITIALIZED = True
# Patch helper-API to ensure init runs on first call.
_get_programm = get_programm
_aktuelles_wahlprogramm = aktuelles_wahlprogramm
_wahlprogramm_zum_zeitpunkt = wahlprogramm_zum_zeitpunkt
_grundsatzprogramm_zum_zeitpunkt = grundsatzprogramm_zum_zeitpunkt
_parteien_mit_wahlprogramm = parteien_mit_wahlprogramm
_alle_versionen = alle_versionen
def get_programm(programm_id: str) -> Optional[Programm]: # type: ignore[no-redef]
_ensure_initialized()
return _get_programm(programm_id)
def aktuelles_wahlprogramm(bundesland: str, partei: str) -> Optional[Programm]: # type: ignore[no-redef]
_ensure_initialized()
return _aktuelles_wahlprogramm(bundesland, partei)
def wahlprogramm_zum_zeitpunkt( # type: ignore[no-redef]
bundesland: str, partei: str, datum: str,
) -> Optional[Programm]:
_ensure_initialized()
return _wahlprogramm_zum_zeitpunkt(bundesland, partei, datum)
def grundsatzprogramm_zum_zeitpunkt( # type: ignore[no-redef]
partei: str, datum: str, bundesland: Optional[str] = None,
) -> Optional[Programm]:
_ensure_initialized()
return _grundsatzprogramm_zum_zeitpunkt(partei, datum, bundesland)
def parteien_mit_wahlprogramm(bundesland: str) -> list[str]: # type: ignore[no-redef]
_ensure_initialized()
return _parteien_mit_wahlprogramm(bundesland)
def alle_versionen(bundesland: str, partei: str) -> list[Programm]: # type: ignore[no-redef]
_ensure_initialized()
return _alle_versionen(bundesland, partei)
def all_programme() -> list[Programm]:
"""Alle eingetragenen Programme (initialisiert, falls nötig)."""
_ensure_initialized()
return list(PROGRAMME.values())

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

160
tests/test_legislaturen.py Normal file
View File

@ -0,0 +1,160 @@
"""Tests for app.legislaturen — Wahlperioden + Regierungen."""
import pytest
from app.legislaturen import (
LEGISLATUREN, REGIERUNGEN,
legislatur_zum_zeitpunkt, regierung_zum_zeitpunkt,
aktuelle_legislatur, aktuelle_regierung,
regierungen_einer_wp, parteien_in_regierung_zum_zeitpunkt,
alle_legislaturen, alle_regierungen,
)
# ─────────────────────────────────────────────────────────────────────────────
# Daten-Konsistenz
# ─────────────────────────────────────────────────────────────────────────────
class TestKonsistenz:
def test_legislaturen_alle_17_bundeslaender_present(self):
# 16 BL + BUND
bls = {leg["bundesland"] for leg in LEGISLATUREN}
assert bls == {
"BW", "BY", "BE", "BB", "HB", "HH", "HE", "MV", "NI",
"NRW", "RP", "SL", "SN", "LSA", "SH", "TH", "BUND",
}
def test_jede_legislatur_hat_pflichtfelder(self):
for leg in LEGISLATUREN:
assert "bundesland" in leg
assert "wp" in leg
assert "wahltermin" in leg
assert "konstituierung" in leg
assert "ende" in leg
def test_jede_regierung_hat_pflichtfelder(self):
for reg in REGIERUNGEN:
for f in ("bundesland", "name", "wp", "parteien", "ministerpraesident", "von", "bis"):
assert f in reg, f"{reg['name']}: feld {f} fehlt"
def test_genau_eine_aktuelle_legislatur_pro_bl(self):
from collections import Counter
counts = Counter(
leg["bundesland"] for leg in LEGISLATUREN if leg["ende"] is None
)
for bl, n in counts.items():
assert n == 1, f"{bl} hat {n} aktuelle Legislaturen"
# alle 17 BL/BUND müssen eine aktuelle haben
assert len(counts) == 17
def test_genau_eine_aktuelle_regierung_pro_bl(self):
from collections import Counter
counts = Counter(
reg["bundesland"] for reg in REGIERUNGEN if reg["bis"] is None
)
assert len(counts) == 17
for bl, n in counts.items():
assert n == 1, f"{bl}: {n} aktuelle Regierungen"
def test_regierungs_intervalle_pro_bl_zusammenhängend(self):
"""Pro BL: die Regierungen-Zeitleiste hat keine Lücken."""
from collections import defaultdict
by_bl = defaultdict(list)
for reg in REGIERUNGEN:
by_bl[reg["bundesland"]].append(reg)
for bl, regs in by_bl.items():
regs.sort(key=lambda r: r["von"])
for i in range(1, len(regs)):
vorher = regs[i - 1]
jetzt = regs[i]
# Wenn vorher.bis gesetzt, sollte = jetzt.von sein
if vorher.get("bis") is not None:
assert vorher["bis"] == jetzt["von"], \
f"{bl}: Lücke zwischen {vorher['name']} ({vorher['bis']}) und {jetzt['name']} ({jetzt['von']})"
# ─────────────────────────────────────────────────────────────────────────────
# Helper-Korrektheit
# ─────────────────────────────────────────────────────────────────────────────
class TestHelpers:
def test_aktuelle_regierung_bund(self):
r = aktuelle_regierung("BUND")
assert r["name"] == "Merz I"
assert r["parteien"] == ["CDU", "CSU", "SPD"]
def test_aktuelle_regierung_th_voigt_brombeer(self):
r = aktuelle_regierung("TH")
assert r["name"] == "Voigt I"
assert "BSW" in r["parteien"]
def test_aktuelle_legislatur_nrw(self):
leg = aktuelle_legislatur("NRW")
assert leg["wp"] == 18
def test_legislatur_zum_zeitpunkt_nrw_2018(self):
leg = legislatur_zum_zeitpunkt("NRW", "2018-09-01")
assert leg["wp"] == 17
def test_legislatur_zum_zeitpunkt_nrw_2011(self):
leg = legislatur_zum_zeitpunkt("NRW", "2011-09-01")
assert leg["wp"] == 15
def test_kemmerich_kabinett_TH_2020_02(self):
"""Das berühmte 28-Tage-Kabinett von Kemmerich in TH."""
r = regierung_zum_zeitpunkt("TH", "2020-02-15")
assert r is not None
assert r["name"] == "Kemmerich I"
assert r["parteien"] == ["FDP"]
def test_rp_uebergang_dreyer_zu_schweitzer(self):
# Vor 2024-07-10: Dreyer III
r1 = regierung_zum_zeitpunkt("RP", "2024-07-09")
assert r1["name"] == "Dreyer III"
# Ab 2024-07-10: Schweitzer I
r2 = regierung_zum_zeitpunkt("RP", "2024-07-10")
assert r2["name"] == "Schweitzer I"
# Beide in WP18
assert r1["wp"] == 18 and r2["wp"] == 18
def test_bund_scholz_to_merz(self):
# Scholz I Ampel
r1 = regierung_zum_zeitpunkt("BUND", "2023-06-01")
assert r1["name"] == "Scholz I"
assert "FDP" in r1["parteien"]
# nach FDP-Ausstieg (2024-11-06)
r2 = regierung_zum_zeitpunkt("BUND", "2024-12-01")
assert "Scholz" in r2["name"]
assert "FDP" not in r2["parteien"]
# Merz seit 2025-05-06
r3 = regierung_zum_zeitpunkt("BUND", "2025-06-01")
assert r3["name"] == "Merz I"
def test_parteien_in_regierung_zum_zeitpunkt(self):
parteien = parteien_in_regierung_zum_zeitpunkt("BUND", "2025-06-01")
assert set(parteien) == {"CDU", "CSU", "SPD"}
def test_regierungen_einer_wp_rp18(self):
# In RP WP18 gibt es 2 Regierungen (Dreyer III + Schweitzer I)
regs = regierungen_einer_wp("RP", 18)
assert len(regs) == 2
assert [r["name"] for r in regs] == ["Dreyer III", "Schweitzer I"]
def test_alle_legislaturen_nrw(self):
legs = alle_legislaturen("NRW")
assert [l["wp"] for l in legs] == [15, 16, 17, 18]
def test_alle_regierungen_bund(self):
regs = alle_regierungen("BUND")
# Mind. 5: Merkel III, IV, Scholz I, Scholz I (gf), Merz I
assert len(regs) >= 5
class TestEdgecases:
def test_unbekannte_bl_returns_none(self):
assert legislatur_zum_zeitpunkt("XX", "2025-01-01") is None
assert regierung_zum_zeitpunkt("XX", "2025-01-01") is None
assert aktuelle_regierung("XX") is None
def test_zeitpunkt_vor_aller_dokumentation(self):
# In NRW reicht das Schema bis WP15 (ab 2010-06-09) zurück
assert legislatur_zum_zeitpunkt("NRW", "2005-01-01") is None