diff --git a/app/bundeslaender.py b/app/bundeslaender.py index d1b3261..71b46c5 100644 --- a/app/bundeslaender.py +++ b/app/bundeslaender.py @@ -145,11 +145,19 @@ BUNDESLAENDER: dict[str, Bundesland] = { naechste_wahl="2029-09-23", regierungsfraktionen=["SPD", "BSW"], landtagsfraktionen=["SPD", "AfD", "CDU", "BSW"], - doku_system="StarWeb", + doku_system="portala", doku_base_url="https://www.parlamentsdokumentation.brandenburg.de", drucksache_format="8/1234", dokukratie_scraper="bb", - anmerkung="Kabinett Woidke IV (SPD-BSW) seit Dezember 2024. Knappe Mehrheit (zwei Sitze).", + aktiv=True, + anmerkung=( + "Kabinett Woidke IV (SPD-BSW) seit Dezember 2024. Knappe " + "Mehrheit (zwei Sitze). Doku-System ist NICHT StarWeb wie " + "ursprünglich klassifiziert (das alte /starweb/LBB/ELVIS/-" + "Frontend ist nur Legacy), sondern das moderne portala/eUI-" + "Backend auf /portal/browse.tt.json mit db_id=lbb.lissh. " + "Wiederverwendet PortalaAdapter aus #2/#3 (#27)." + ), ), "HB": Bundesland( code="HB", @@ -282,14 +290,21 @@ BUNDESLAENDER: dict[str, Bundesland] = { naechste_wahl="2031-03-22", regierungsfraktionen=["SPD", "GRÜNE", "FDP"], landtagsfraktionen=["SPD", "CDU", "AfD", "GRÜNE", "FREIE WÄHLER", "FDP"], - doku_system="StarWeb", + doku_system="portala", doku_base_url="https://opal.rlp.de", drucksache_format="18/12345", dokukratie_scraper="rp", + aktiv=True, anmerkung=( - "OPAL in RLP basiert auf StarWeb. Wahl zum 19. Landtag fand am 22.03.2026 " - "statt; Koalitionsverhandlungen CDU+SPD laufen, Kabinett Schweitzer I " - "geschäftsführend. Nach Konstituierung müssen WP und Wahltermin aktualisiert werden." + "OPAL in RLP läuft tatsächlich auf dem portala/eUI-Backend " + "(NICHT StarWeb wie ursprünglich klassifiziert), erreichbar " + "unter /portal/browse.tt.json mit db_id=rlp.lissh. " + "Wiederverwendet PortalaAdapter aus #2/#3 (#30). NICHT " + "verwechseln mit dem NRW OPAL — anderer Markenname, " + "andere Engine. Wahl zum 19. Landtag fand am 22.03.2026 " + "statt; Koalitionsverhandlungen CDU+SPD laufen, Kabinett " + "Schweitzer I geschäftsführend. Nach Konstituierung müssen " + "WP und Wahltermin aktualisiert werden." ), ), "SL": Bundesland( diff --git a/app/parlamente.py b/app/parlamente.py index b96a3bd..ab9651d 100644 --- a/app/parlamente.py +++ b/app/parlamente.py @@ -358,6 +358,8 @@ class PortalaAdapter(ParlamentAdapter): document_type: Optional[str] = "Antrag", pdf_url_prefix: str = "/files/", date_window_days: int = 730, + typ_filter: Optional[str] = "DOKDBE", + omit_date_filter: bool = False, ) -> None: """Configure a portala/eUI adapter for one specific parliament. @@ -380,6 +382,11 @@ class PortalaAdapter(ParlamentAdapter): relative PDF path returned by the server. date_window_days: how many days back ``search()`` looks by default. + typ_filter: ``TYP=`` term in the parsed string and + JSON tree. ``DOKDBE`` works for LSA/BE/BB/BW (the + lissh-style instances). For Hessen (``hlt.lis``) and + similar instances the value is different or absent — + pass ``None`` to drop the term entirely. """ self.bundesland = bundesland self.name = name @@ -390,6 +397,8 @@ class PortalaAdapter(ParlamentAdapter): self.document_type = document_type self.pdf_url_prefix = "/" + pdf_url_prefix.strip("/") + "/" self.date_window_days = date_window_days + self.typ_filter = typ_filter + self.omit_date_filter = omit_date_filter # ── LSA-style hit list (Perl Data::Dumper inside
 blocks) ──
     # Reverse-engineered "WEV*" record fields:
@@ -415,7 +424,13 @@ class PortalaAdapter(ParlamentAdapter):
     # The metadata h6 looks like:
     #   Antrag (Eilantrag)   Drucksache 19/3104  S. 1 bis 24 vom 31.03.2026
     _RE_BE_DRUCKSACHE = re.compile(r'Drucksache\s+(\d+/\d+)')
-    _RE_BE_DATUM = re.compile(r'vom\s+(\d{1,2}\.\d{1,2}\.\d{4})')
+    # BE has "Drucksache 19/3104 S. 1 bis 24 vom 31.03.2026" — date is
+    # marked by ``vom``. BB has the BE card format too but writes the
+    # date BEFORE the Drucksachen-Nummer with no marker:
+    # "Antrag Reinhard Simon (BSW) 17.10.2024 Drucksache 8/2 (1 S.)".
+    # Try ``vom``-prefix first; fall back to the first plain date.
+    _RE_BE_DATUM_VOM = re.compile(r'vom\s+(\d{1,2}\.\d{1,2}\.\d{4})')
+    _RE_BE_DATUM_PLAIN = re.compile(r'(\d{1,2}\.\d{1,2}\.\d{4})')
     _RE_BE_DOCTYPE = re.compile(r'\s*([^<&]+?)(?: |<)')
 
     @staticmethod
@@ -527,29 +542,32 @@ class PortalaAdapter(ParlamentAdapter):
                 ]},
             ]})
 
-        top_terms.append({"tn": "or", "num": 18, "terms": [
-            {"tn": "or", "num": 19, "terms": [
-                date_term("DAT", 20),
-                date_term("DDAT", 21),
-            ]},
-            date_term("SDAT", 22),
-        ]})
-        top_terms.append({"tn": "term", "t": "DOKDBE", "idx": 156, "l": 1,
-                          "sf": "TYP", "op": "eq", "num": 23})
+        if not self.omit_date_filter:
+            top_terms.append({"tn": "or", "num": 18, "terms": [
+                {"tn": "or", "num": 19, "terms": [
+                    date_term("DAT", 20),
+                    date_term("DDAT", 21),
+                ]},
+                date_term("SDAT", 22),
+            ]})
+        if self.typ_filter is not None:
+            top_terms.append({"tn": "term", "t": self.typ_filter, "idx": 156, "l": 1,
+                              "sf": "TYP", "op": "eq", "num": 23})
 
         # Mirror the same shape into the parsed/sref display strings
+        typ_clause = f" AND TYP={self.typ_filter}" if self.typ_filter is not None else ""
+        date_clause = (
+            f" AND (DAT,DDAT,SDAT= {date_range_text})"
+            if not self.omit_date_filter else ""
+        )
         if document_type is not None:
             parsed = (
                 f"((/WP {wahlperiode}) AND "
                 f"(/ETYPF,ETYP2F,DTYPF,DTYP2F,1VTYPF (\"{document_type}\")) "
-                f"AND (/DART,DARTS (\"D\")) AND "
-                f"(DAT,DDAT,SDAT= {date_range_text})) AND TYP=DOKDBE"
+                f"AND (/DART,DARTS (\"D\")){date_clause}){typ_clause}"
             )
         else:
-            parsed = (
-                f"((/WP {wahlperiode}) AND "
-                f"(DAT,DDAT,SDAT= {date_range_text})) AND TYP=DOKDBE"
-            )
+            parsed = f"((/WP {wahlperiode}){date_clause}){typ_clause}"
 
         return {
             "action": "SearchAndDisplay",
@@ -675,7 +693,7 @@ class PortalaAdapter(ParlamentAdapter):
                 else:
                     pdf_url = f"{self.base_url}{self.pdf_url_prefix}{href}"
 
-            m_dat = self._RE_BE_DATUM.search(chunk)
+            m_dat = self._RE_BE_DATUM_VOM.search(chunk) or self._RE_BE_DATUM_PLAIN.search(chunk)
             datum_iso = self._datum_de_to_iso(m_dat.group(1) if m_dat else "")
 
             m_doc = self._RE_BE_DOCTYPE.search(chunk)
@@ -1971,6 +1989,27 @@ ADAPTERS = {
         db_path="lisshfl.txt",
         document_typ_code="antrag",
     ),
+    "BB": PortalaAdapter(
+        bundesland="BB",
+        name="Landtag Brandenburg (parladoku)",
+        base_url="https://www.parlamentsdokumentation.brandenburg.de",
+        db_id="lbb.lissh",
+        wahlperiode=8,
+        portala_path="/portal",
+        document_type="Antrag",
+        # BB packs the date BEFORE the Drucksachen-Nummer in the h6
+        # line and uses the BE-style efxRecordRepeater HTML cards;
+        # the auto-detect picks the card path automatically.
+    ),
+    "RP": PortalaAdapter(
+        bundesland="RP",
+        name="Landtag Rheinland-Pfalz (OPAL)",
+        base_url="https://opal.rlp.de",
+        db_id="rlp.lissh",
+        wahlperiode=18,
+        portala_path="/portal",
+        document_type="Antrag",
+    ),
     "BY": BayernAdapter(),
     "BW": PARLISAdapter(
         bundesland="BW",