gwoe-antragspruefer/app/sync_abgeordnetenwatch.py

158 lines
4.8 KiB
Python
Raw Permalink Normal View History

"""CLI-Sync-Skript für abgeordnetenwatch.de (#106 Phase 1).
Holt Polls + namentliche Stimmen für alle oder einen bestimmten BL-Code
und speichert sie via UPSERT in der lokalen SQLite-DB.
Aufruf:
python -m app.sync_abgeordnetenwatch [--bundesland NRW] [--limit 50]
Ohne --bundesland werden alle in PARLIAMENT_ID eingetragenen BL-Codes
abgearbeitet (BUND-Alias wird übersprungen, BT genügt).
Ausgabe:
NRW: 12 polls neu, 340 votes neu
BT: 0 polls neu, 0 votes neu
"""
from __future__ import annotations
import argparse
import asyncio
import logging
from datetime import datetime, timezone
logger = logging.getLogger(__name__)
async def sync_bundesland(bundesland_code: str, limit: int) -> tuple[int, int]:
"""Synct einen BL-Code. Gibt (neue_polls, neue_votes) zurück."""
from .abgeordnetenwatch import (
PARLIAMENT_ID, fetch_polls, fetch_votes_for_poll,
fallback_drucksache_by_date_title,
)
from .database import init_db, upsert_aw_poll, upsert_aw_vote
await init_db()
parliament_id = PARLIAMENT_ID[bundesland_code.upper()]
synced_at = datetime.now(timezone.utc).isoformat()
polls = await fetch_polls(bundesland_code, limit=limit)
new_polls = 0
new_votes = 0
for poll in polls:
poll_id = poll.get("id")
if poll_id is None:
continue
legislature = poll.get("field_legislature") or {}
legislature_label = (
legislature.get("label") or legislature.get("name") or ""
if isinstance(legislature, dict) else str(legislature)
)
topics_raw = poll.get("field_topics") or []
topics = [
(t.get("label") or t.get("name") or str(t))
if isinstance(t, dict) else str(t)
for t in topics_raw
]
# Primär: Drucksache aus intro-HTML geparst; Fallback über Datum+Titel
# für BL ohne PDF-URL im intro (MV/BY/BB/TH/HH/SL — Fix #142 Phase 3).
drucksache = poll.get("drucksache")
if drucksache is None:
drucksache = await fallback_drucksache_by_date_title(
datum=poll.get("field_poll_date"),
titel=poll.get("label"),
bundesland=bundesland_code,
)
is_new_poll = await upsert_aw_poll(
poll_id=poll_id,
parliament_id=parliament_id,
bundesland=bundesland_code.upper(),
drucksache=drucksache,
titel=poll.get("label"),
datum=poll.get("field_poll_date"),
accepted=poll.get("field_accepted"),
topics=topics,
legislature_label=legislature_label,
synced_at=synced_at,
)
if is_new_poll:
new_polls += 1
# Votes laden und speichern
try:
votes = await fetch_votes_for_poll(poll_id)
except Exception:
logger.exception("Fehler beim Laden von Votes für poll_id=%d", poll_id)
continue
for vote in votes:
politician_id = vote.get("politician_id")
if politician_id is None:
continue
is_new_vote = await upsert_aw_vote(
poll_id=poll_id,
politician_id=politician_id,
politician_name=vote.get("politician_name"),
partei=vote.get("partei"),
vote=vote.get("vote", "no_show"),
)
if is_new_vote:
new_votes += 1
return new_polls, new_votes
async def main(bundesland: str | None, limit: int) -> None:
from .abgeordnetenwatch import PARLIAMENT_ID
# Alle Codes ohne BUND-Alias (BT und BUND zeigen auf die selbe ID)
if bundesland:
codes = [bundesland.upper()]
else:
seen_ids: set[int] = set()
codes = []
for code, pid in PARLIAMENT_ID.items():
if pid not in seen_ids:
seen_ids.add(pid)
codes.append(code)
for code in codes:
try:
new_polls, new_votes = await sync_bundesland(code, limit)
print(f"{code:4s}: {new_polls} polls neu, {new_votes} votes neu")
except Exception:
logger.exception("Fehler beim Sync für %s", code)
print(f"{code:4s}: FEHLER (siehe Log)")
def _cli() -> None:
parser = argparse.ArgumentParser(
description="Sync abgeordnetenwatch-Abstimmungsdaten in die lokale DB."
)
parser.add_argument(
"--bundesland", "-b",
default=None,
help="BL-Code (z.B. NRW, BT). Ohne Angabe: alle Codes.",
)
parser.add_argument(
"--limit", "-n",
type=int,
default=100,
help="Maximale Anzahl Polls pro BL (default: 100).",
)
args = parser.parse_args()
asyncio.run(main(args.bundesland, args.limit))
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
_cli()