"""BL-uebergreifende Ingest-CLI fuer Plenarprotokolle (#106 / #126). Pipeline: 1. PDF laden (Pfad oder URL) 2. ``protokoll_parsers.parse_protocol(bundesland, pdf_path)`` waehlt den BL-spezifischen Parser aus der Registry 3. ``upsert_plenum_vote()`` schreibt jede Abstimmung in die DB CLI: python -m app.ingest_votes --pdf MMP18-119.pdf python -m app.ingest_votes --url https://landtag.nrw.de/.../MMP18-119.pdf python -m app.ingest_votes --pdf x.pdf --bundesland NRW --protokoll-id MMP18-119 python -m app.ingest_votes --supported # Liste der BL mit Parser Aktuell registriert: NRW. Folge-BL via app/protokoll_parsers/.py + Eintrag in PROTOKOLL_PARSERS — siehe ADR 0009. """ from __future__ import annotations import argparse import asyncio import logging import sys import tempfile import urllib.request from pathlib import Path from typing import Optional from .protokoll_parsers import parse_protocol, supported_bundeslaender from .database import upsert_plenum_vote logger = logging.getLogger(__name__) def _derive_protokoll_id(pdf_path: Path) -> str: """Ermittle Protokoll-ID aus dem Datei-Stem (z.B. 'MMP18-119.pdf' → 'MMP18-119').""" return pdf_path.stem def _download_pdf(url: str, dest: Path) -> Path: """Lade ein PDF von einer URL in einen Pfad. Wirft bei HTTP-Fehlern.""" req = urllib.request.Request( url, headers={"User-Agent": "GWOeAntragspruefer/1.0 (+https://gwoe.toppyr.de)"}, ) with urllib.request.urlopen(req, timeout=60) as resp: dest.write_bytes(resp.read()) return dest async def ingest_pdf( pdf_path: Path, *, bundesland: str = "NRW", protokoll_id: Optional[str] = None, quelle_url: Optional[str] = None, ) -> dict: """Parse das PDF mit dem BL-Parser und schreibe alle Abstimmungen in die DB. Returns: Statistik-Dict ``{parsed, written, skipped_no_drucksache, errors, protokoll_id, bundesland}``. Raises: NotImplementedError: wenn fuer ``bundesland`` kein Parser registriert ist. """ pid = protokoll_id or _derive_protokoll_id(pdf_path) parsed = parse_protocol(bundesland, str(pdf_path)) written = 0 skipped_no_ds = 0 errors: list[str] = [] for entry in parsed: ds = entry.get("drucksache") if not ds: skipped_no_ds += 1 continue try: await upsert_plenum_vote( bundesland=bundesland, drucksache=ds, ergebnis=entry["ergebnis"], einstimmig=bool(entry.get("einstimmig", False)), fraktionen_ja=entry.get("votes", {}).get("ja", []), fraktionen_nein=entry.get("votes", {}).get("nein", []), fraktionen_enthaltung=entry.get("votes", {}).get("enthaltung", []), quelle_protokoll=pid, quelle_url=quelle_url, ) written += 1 except Exception as exc: logger.exception("Upsert fehlgeschlagen fuer %s", ds) errors.append(f"{ds}: {exc}") return { "parsed": len(parsed), "written": written, "skipped_no_drucksache": skipped_no_ds, "errors": errors, "protokoll_id": pid, "bundesland": bundesland, } def _cli() -> None: logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") parser = argparse.ArgumentParser( description="Plenarprotokoll → plenum_vote_results (#106 / #126)", ) src = parser.add_mutually_exclusive_group(required=False) src.add_argument("--pdf", help="Pfad zu lokalem PDF") src.add_argument("--url", help="HTTP(S)-URL zum PDF") parser.add_argument("--bundesland", default="NRW", help="Bundesland-Code (default: NRW)") parser.add_argument("--protokoll-id", help="Protokoll-ID (default: aus Datei-Stem)") parser.add_argument("--supported", action="store_true", help="Liste alle BL-Codes mit registriertem Parser") args = parser.parse_args() if args.supported: for bl in supported_bundeslaender(): print(bl) sys.exit(0) if not args.pdf and not args.url: parser.error("--pdf oder --url ist erforderlich") if args.url: # Download in tmp und nach dem Run wieder loeschen with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp: tmp_path = Path(tmp.name) try: print(f"Lade {args.url} → {tmp_path} …") _download_pdf(args.url, tmp_path) pid = args.protokoll_id or args.url.rsplit("/", 1)[-1].rsplit(".", 1)[0] stats = asyncio.run(ingest_pdf( tmp_path, bundesland=args.bundesland, protokoll_id=pid, quelle_url=args.url, )) finally: tmp_path.unlink(missing_ok=True) else: pdf_path = Path(args.pdf) if not pdf_path.exists(): print(f"FEHLER: PDF nicht gefunden: {pdf_path}", file=sys.stderr) sys.exit(1) stats = asyncio.run(ingest_pdf( pdf_path, bundesland=args.bundesland, protokoll_id=args.protokoll_id, )) print() print(f"Protokoll {stats['protokoll_id']} ({stats['bundesland']})") print(f" parsed: {stats['parsed']}") print(f" written: {stats['written']}") if stats["skipped_no_drucksache"]: print(f" ohne DS: {stats['skipped_no_drucksache']}") if stats["errors"]: print(f" errors: {len(stats['errors'])}") for e in stats["errors"][:5]: print(f" {e}") if stats["written"] == 0 and not stats["errors"]: sys.exit(2) if __name__ == "__main__": _cli()