diff --git a/app/legislaturen.py b/app/legislaturen.py new file mode 100644 index 0000000..a54c3aa --- /dev/null +++ b/app/legislaturen.py @@ -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 diff --git a/app/programme.py b/app/programme.py new file mode 100644 index 0000000..b430751 --- /dev/null +++ b/app/programme.py @@ -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()) diff --git a/app/static/referenzen/afd-bund-2025.pdf b/app/static/referenzen/afd-bund-2025.pdf new file mode 100644 index 0000000..fb4a008 Binary files /dev/null and b/app/static/referenzen/afd-bund-2025.pdf differ diff --git a/app/static/referenzen/bsw-bund-2025.pdf b/app/static/referenzen/bsw-bund-2025.pdf new file mode 100644 index 0000000..3581129 Binary files /dev/null and b/app/static/referenzen/bsw-bund-2025.pdf differ diff --git a/app/static/referenzen/cdu-bund-2025.pdf b/app/static/referenzen/cdu-bund-2025.pdf new file mode 100644 index 0000000..6a529e5 Binary files /dev/null and b/app/static/referenzen/cdu-bund-2025.pdf differ diff --git a/app/static/referenzen/csu-bund-2025.pdf b/app/static/referenzen/csu-bund-2025.pdf new file mode 100644 index 0000000..e594c1a Binary files /dev/null and b/app/static/referenzen/csu-bund-2025.pdf differ diff --git a/app/static/referenzen/fdp-bund-2025.pdf b/app/static/referenzen/fdp-bund-2025.pdf new file mode 100644 index 0000000..9bad831 Binary files /dev/null and b/app/static/referenzen/fdp-bund-2025.pdf differ diff --git a/app/static/referenzen/gruene-bund-2025.pdf b/app/static/referenzen/gruene-bund-2025.pdf new file mode 100644 index 0000000..50fd184 Binary files /dev/null and b/app/static/referenzen/gruene-bund-2025.pdf differ diff --git a/app/static/referenzen/linke-bund-2025.pdf b/app/static/referenzen/linke-bund-2025.pdf new file mode 100644 index 0000000..65a638d Binary files /dev/null and b/app/static/referenzen/linke-bund-2025.pdf differ diff --git a/app/static/referenzen/spd-bund-2025.pdf b/app/static/referenzen/spd-bund-2025.pdf new file mode 100644 index 0000000..a502d2a Binary files /dev/null and b/app/static/referenzen/spd-bund-2025.pdf differ diff --git a/tests/test_legislaturen.py b/tests/test_legislaturen.py new file mode 100644 index 0000000..a66928e --- /dev/null +++ b/tests/test_legislaturen.py @@ -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