feat(analysis): fetch real MP metadata, fix anchor axis for party-level actors
- fetch_mp_metadata: use real OData URL with pagination (1200 records, 5 pages) uses Fractie.Afkorting not NaamNL for abbreviation matching skips Verwijderd=true records - upsert_mp_metadata: keep most recent membership (prefer active over ended, then higher Van date) so current party affiliations are not overwritten by historical - compute_anchor_axis: anchor directly on party-level SVD entities (GroenLinks-PvdA etc) before falling back to mp_metadata individual MP lookup - test_fetch_mp_metadata: fix mock for timeout kwarg + pagination + Afkorting field - Generated anchor axis HTML for 2025-Q2 through 2026-Q1 in outputs/
This commit is contained in:
@@ -34,61 +34,85 @@ def normalize_mp_name(
|
||||
return name
|
||||
|
||||
|
||||
_ODATA_BASE = "https://gegevensmagazijn.tweedekamer.nl/OData/v4/2.0"
|
||||
_PAGE_SIZE = 250
|
||||
|
||||
|
||||
def fetch_mp_metadata(
|
||||
db_path: str, odata_url: str = "https://odata.example/FractieZetelPersoon"
|
||||
db_path: str,
|
||||
odata_url: str = f"{_ODATA_BASE}/FractieZetelPersoon",
|
||||
) -> int:
|
||||
"""Fetch MP party membership and tenure from OData and upsert into DB.
|
||||
|
||||
Paginates through all records using $skip. Uses Fractie.Afkorting as
|
||||
the party name so it matches the abbreviations used in mp_votes.
|
||||
|
||||
Returns the number of records processed (inserted or updated).
|
||||
"""
|
||||
expand = "$expand=FractieZetel($expand=Fractie),Persoon"
|
||||
session = requests.Session()
|
||||
try:
|
||||
resp = session.get(odata_url)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
logger.error("Failed to fetch MP metadata: %s", e)
|
||||
raise
|
||||
|
||||
values = data.get("value") if isinstance(data, dict) else None
|
||||
if values is None:
|
||||
logger.error("Unexpected OData payload; missing 'value' list")
|
||||
return 0
|
||||
|
||||
db = MotionDatabase(db_path)
|
||||
processed = 0
|
||||
skip = 0
|
||||
|
||||
for item in values:
|
||||
while True:
|
||||
url = f"{odata_url}?{expand}&$top={_PAGE_SIZE}&$skip={skip}"
|
||||
try:
|
||||
persoon = item.get("Persoon") or {}
|
||||
fractiezetel = item.get("FractieZetel") or {}
|
||||
fractie = fractiezetel.get("Fractie") or {}
|
||||
resp = session.get(url, timeout=30)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
logger.error("Failed to fetch MP metadata (skip=%d): %s", skip, e)
|
||||
raise
|
||||
|
||||
achternaam = persoon.get("Achternaam")
|
||||
initialen = persoon.get("Initialen")
|
||||
tussenvoegsel = persoon.get("Tussenvoegsel")
|
||||
persoon_id = persoon.get("Id")
|
||||
values = data.get("value") if isinstance(data, dict) else None
|
||||
if values is None:
|
||||
logger.error("Unexpected OData payload at skip=%d; missing 'value'", skip)
|
||||
break
|
||||
|
||||
party = fractie.get("NaamNL")
|
||||
van = item.get("Van")
|
||||
tot_en_met = item.get("TotEnMet")
|
||||
if not values:
|
||||
break # no more pages
|
||||
|
||||
if not achternaam:
|
||||
logger.debug("Skipping record without achternaam: %s", item)
|
||||
continue
|
||||
for item in values:
|
||||
try:
|
||||
if item.get("Verwijderd"):
|
||||
continue
|
||||
|
||||
mp_name = normalize_mp_name(achternaam, initialen, tussenvoegsel)
|
||||
persoon = item.get("Persoon") or {}
|
||||
fractiezetel = item.get("FractieZetel") or {}
|
||||
fractie = fractiezetel.get("Fractie") or {}
|
||||
|
||||
db.upsert_mp_metadata(
|
||||
mp_name=mp_name,
|
||||
party=party,
|
||||
van=van,
|
||||
tot_en_met=tot_en_met,
|
||||
persoon_id=persoon_id,
|
||||
)
|
||||
processed += 1
|
||||
except Exception:
|
||||
logger.exception("Error processing OData item: %s", item)
|
||||
achternaam = persoon.get("Achternaam")
|
||||
initialen = persoon.get("Initialen")
|
||||
tussenvoegsel = persoon.get("Tussenvoegsel")
|
||||
persoon_id = persoon.get("Id")
|
||||
|
||||
# Use Afkorting (e.g. "VVD", "GroenLinks-PvdA") to match mp_votes party column
|
||||
party = fractie.get("Afkorting") or fractie.get("NaamNL")
|
||||
van = item.get("Van")
|
||||
tot_en_met = item.get("TotEnMet")
|
||||
|
||||
if not achternaam:
|
||||
logger.debug("Skipping record without achternaam: %s", item)
|
||||
continue
|
||||
|
||||
mp_name = normalize_mp_name(achternaam, initialen, tussenvoegsel)
|
||||
|
||||
db.upsert_mp_metadata(
|
||||
mp_name=mp_name,
|
||||
party=party,
|
||||
van=van,
|
||||
tot_en_met=tot_en_met,
|
||||
persoon_id=persoon_id,
|
||||
)
|
||||
processed += 1
|
||||
except Exception:
|
||||
logger.exception("Error processing OData item: %s", item)
|
||||
|
||||
logger.debug("Fetched page skip=%d, got %d records", skip, len(values))
|
||||
if len(values) < _PAGE_SIZE:
|
||||
break # last page
|
||||
skip += _PAGE_SIZE
|
||||
|
||||
logger.info("Processed %d MP metadata records", processed)
|
||||
return processed
|
||||
|
||||
Reference in New Issue
Block a user