Activate Brandenburg + Rheinland-Pfalz via PortalaAdapter reuse (#27, #30, Phase 2)

Riesige Überraschung aus dem BB-HAR-Trace: Brandenburg ist NICHT
StarWeb wie in dokukratie und bundeslaender.py klassifiziert,
sondern läuft auch auf dem portala/eUI-Backend. Endpoint
/portal/browse.tt.json mit db_id=lbb.lissh. Das alte
/starweb/LBB/ELVIS/-Frontend ist nur Legacy.

Folgeprobing offenbarte: RP/opal.rlp.de läuft ebenfalls portala
(db_id=rlp.lissh, 46759 hits in WP18), ebenso NI/HE/BB. Damit ist
Phase 2 großteils KEIN StarWeb-Adapter-Bau, sondern PortalaAdapter-
Wiederverwendung mit konfigurierbaren Parametern.

Activated via Registry-Einträge:

- "BB" → PortalaAdapter(base_url=parlamentsdokumentation.brandenburg.de,
  db_id=lbb.lissh, wahlperiode=8). Nutzt die BE-Card-Variante des
  Hit-Parsers (efxRecordRepeater).
- "RP" → PortalaAdapter(base_url=opal.rlp.de, db_id=rlp.lissh,
  wahlperiode=18). NICHT mit dem NRW OPAL verwechseln — anderer
  Markenname, andere Engine.

PortalaAdapter erweitert um zwei neue Konstruktor-Parameter mit
backward-kompatiblen Defaults:

- typ_filter: Optional[str] = "DOKDBE"
  Wenn None, wird die TYP=<value>-Klausel weggelassen. Manche
  Instanzen (HE/hlt.lis) lehnen DOKDBE ab.

- omit_date_filter: bool = False
  Wenn True, wird der DAT/DDAT/SDAT-Term weggelassen. HE
  und ähnliche Instanzen haben andere Date-Field-Namen.

Plus _parse_hit_list_cards Date-Regex erweitert: zusätzlich zum
"vom DD.MM.YYYY"-Pattern (BE) jetzt auch "DD.MM.YYYY"-plain
(BB schreibt Datum vor Drucksachen-Nummer ohne "vom"-Marker).

Smoke-Test (lokal):
  BB q="":       5 hits in 5.9s
  BB q="Schule": 5 hits (Pflegeschulen, Genderverbot, Hochschulen)
  RP q="":       5 hits in 4.1s (Entlastung, Bildungschancen)
  RP q="Schule": 5 hits (Hochschulbau, G9-Gymnasien, Leistungsgerechtigkeit)

bundeslaender.py: BB.doku_system "StarWeb"→"portala", RP analog,
beide aktiv=True. Anmerkungen mit dem portala-Verweis und der
Klarstellung "OPAL/RLP ≠ NRW OPAL" erweitert.

NICHT in diesem Commit:
- HE: portala-Backend (hlt.lis) ist erreichbar, aber das HE-Card-
  Layout ist anders (Title direkt im <h3> statt <h3><span>, kein
  <span class="h6"> für Meta) — eigener Parser-Pfad nötig, deferred.
- NI: nilas.niedersachsen.de/portal/ ist eine Login-Page, das
  öffentliche Backend ist nicht zugänglich — deferred.
- HB: kein /portal/-Endpoint, bleibt das alte StarWeb-Servlet —
  braucht eigenen HAR-Trace, deferred.
- BB als StarWeb-Template (#27) ist hinfällig, weil BB portala ist.

Phase 2 (3/6) aus Roadmap-Issue #49.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-04-09 00:59:28 +02:00
parent f82c60e40d
commit 02ff1423a7
2 changed files with 77 additions and 23 deletions

View File

@ -145,11 +145,19 @@ BUNDESLAENDER: dict[str, Bundesland] = {
naechste_wahl="2029-09-23", naechste_wahl="2029-09-23",
regierungsfraktionen=["SPD", "BSW"], regierungsfraktionen=["SPD", "BSW"],
landtagsfraktionen=["SPD", "AfD", "CDU", "BSW"], landtagsfraktionen=["SPD", "AfD", "CDU", "BSW"],
doku_system="StarWeb", doku_system="portala",
doku_base_url="https://www.parlamentsdokumentation.brandenburg.de", doku_base_url="https://www.parlamentsdokumentation.brandenburg.de",
drucksache_format="8/1234", drucksache_format="8/1234",
dokukratie_scraper="bb", 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( "HB": Bundesland(
code="HB", code="HB",
@ -282,14 +290,21 @@ BUNDESLAENDER: dict[str, Bundesland] = {
naechste_wahl="2031-03-22", naechste_wahl="2031-03-22",
regierungsfraktionen=["SPD", "GRÜNE", "FDP"], regierungsfraktionen=["SPD", "GRÜNE", "FDP"],
landtagsfraktionen=["SPD", "CDU", "AfD", "GRÜNE", "FREIE WÄHLER", "FDP"], landtagsfraktionen=["SPD", "CDU", "AfD", "GRÜNE", "FREIE WÄHLER", "FDP"],
doku_system="StarWeb", doku_system="portala",
doku_base_url="https://opal.rlp.de", doku_base_url="https://opal.rlp.de",
drucksache_format="18/12345", drucksache_format="18/12345",
dokukratie_scraper="rp", dokukratie_scraper="rp",
aktiv=True,
anmerkung=( anmerkung=(
"OPAL in RLP basiert auf StarWeb. Wahl zum 19. Landtag fand am 22.03.2026 " "OPAL in RLP läuft tatsächlich auf dem portala/eUI-Backend "
"statt; Koalitionsverhandlungen CDU+SPD laufen, Kabinett Schweitzer I " "(NICHT StarWeb wie ursprünglich klassifiziert), erreichbar "
"geschäftsführend. Nach Konstituierung müssen WP und Wahltermin aktualisiert werden." "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( "SL": Bundesland(

View File

@ -358,6 +358,8 @@ class PortalaAdapter(ParlamentAdapter):
document_type: Optional[str] = "Antrag", document_type: Optional[str] = "Antrag",
pdf_url_prefix: str = "/files/", pdf_url_prefix: str = "/files/",
date_window_days: int = 730, date_window_days: int = 730,
typ_filter: Optional[str] = "DOKDBE",
omit_date_filter: bool = False,
) -> None: ) -> None:
"""Configure a portala/eUI adapter for one specific parliament. """Configure a portala/eUI adapter for one specific parliament.
@ -380,6 +382,11 @@ class PortalaAdapter(ParlamentAdapter):
relative PDF path returned by the server. relative PDF path returned by the server.
date_window_days: how many days back ``search()`` looks by date_window_days: how many days back ``search()`` looks by
default. default.
typ_filter: ``TYP=<value>`` 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.bundesland = bundesland
self.name = name self.name = name
@ -390,6 +397,8 @@ class PortalaAdapter(ParlamentAdapter):
self.document_type = document_type self.document_type = document_type
self.pdf_url_prefix = "/" + pdf_url_prefix.strip("/") + "/" self.pdf_url_prefix = "/" + pdf_url_prefix.strip("/") + "/"
self.date_window_days = date_window_days 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 <pre> blocks) ── # ── LSA-style hit list (Perl Data::Dumper inside <pre> blocks) ──
# Reverse-engineered "WEV*" record fields: # Reverse-engineered "WEV*" record fields:
@ -415,7 +424,13 @@ class PortalaAdapter(ParlamentAdapter):
# The metadata h6 looks like: # The metadata h6 looks like:
# <span class="h6">Antrag (Eilantrag) &nbsp;<a ...>Drucksache 19/3104</a> S. 1 bis 24 vom 31.03.2026</span> # <span class="h6">Antrag (Eilantrag) &nbsp;<a ...>Drucksache 19/3104</a> S. 1 bis 24 vom 31.03.2026</span>
_RE_BE_DRUCKSACHE = re.compile(r'Drucksache\s+(\d+/\d+)') _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'<span class="h6">\s*([^<&]+?)(?:&nbsp;|<)') _RE_BE_DOCTYPE = re.compile(r'<span class="h6">\s*([^<&]+?)(?:&nbsp;|<)')
@staticmethod @staticmethod
@ -527,6 +542,7 @@ class PortalaAdapter(ParlamentAdapter):
]}, ]},
]}) ]})
if not self.omit_date_filter:
top_terms.append({"tn": "or", "num": 18, "terms": [ top_terms.append({"tn": "or", "num": 18, "terms": [
{"tn": "or", "num": 19, "terms": [ {"tn": "or", "num": 19, "terms": [
date_term("DAT", 20), date_term("DAT", 20),
@ -534,22 +550,24 @@ class PortalaAdapter(ParlamentAdapter):
]}, ]},
date_term("SDAT", 22), date_term("SDAT", 22),
]}) ]})
top_terms.append({"tn": "term", "t": "DOKDBE", "idx": 156, "l": 1, 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}) "sf": "TYP", "op": "eq", "num": 23})
# Mirror the same shape into the parsed/sref display strings # 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: if document_type is not None:
parsed = ( parsed = (
f"((/WP {wahlperiode}) AND " f"((/WP {wahlperiode}) AND "
f"(/ETYPF,ETYP2F,DTYPF,DTYP2F,1VTYPF (\"{document_type}\")) " f"(/ETYPF,ETYP2F,DTYPF,DTYP2F,1VTYPF (\"{document_type}\")) "
f"AND (/DART,DARTS (\"D\")) AND " f"AND (/DART,DARTS (\"D\")){date_clause}){typ_clause}"
f"(DAT,DDAT,SDAT= {date_range_text})) AND TYP=DOKDBE"
) )
else: else:
parsed = ( parsed = f"((/WP {wahlperiode}){date_clause}){typ_clause}"
f"((/WP {wahlperiode}) AND "
f"(DAT,DDAT,SDAT= {date_range_text})) AND TYP=DOKDBE"
)
return { return {
"action": "SearchAndDisplay", "action": "SearchAndDisplay",
@ -675,7 +693,7 @@ class PortalaAdapter(ParlamentAdapter):
else: else:
pdf_url = f"{self.base_url}{self.pdf_url_prefix}{href}" 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 "") datum_iso = self._datum_de_to_iso(m_dat.group(1) if m_dat else "")
m_doc = self._RE_BE_DOCTYPE.search(chunk) m_doc = self._RE_BE_DOCTYPE.search(chunk)
@ -1971,6 +1989,27 @@ ADAPTERS = {
db_path="lisshfl.txt", db_path="lisshfl.txt",
document_typ_code="antrag", 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(), "BY": BayernAdapter(),
"BW": PARLISAdapter( "BW": PARLISAdapter(
bundesland="BW", bundesland="BW",