#!/usr/bin/env python3 """WP44 — live verification of the landmark records against the OpenAlex API (single-record lookups are free): for each landmark id, fetch the live record, compare the normalised title with the snapshot title, and record the live cited_by_count and publication_year. Records whose live title differs (ids merged or re-assigned since the snapshot, or snapshot data errors) are flagged live_match = False and excluded from Part IX landmark statistics (they stay in the file). Output: literature_synthesis/landmarks/landmarks_verified.csv + verification_report.json. Politeness: 8 requests/s max.""" import argparse, json, os, re, time, datetime as dt, unicodedata, requests, pandas as pd UA = "WP44-UQO-systematic-mapping/0.1 (mailto:spbou4@icloud.com)" norm = lambda s: re.sub(r"[^a-z0-9]+", "", unicodedata.normalize("NFKD", str(s or "")).encode("ascii", "ignore").decode().lower()) def main(): ap = argparse.ArgumentParser(); ap.add_argument("--root", required=True); a = ap.parse_args(); R = a.root d = pd.read_csv(f"{R}/literature_synthesis/landmarks/landmarks.csv"); s = requests.Session(); rows = [] for i, r in enumerate(d.itertuples()): for attempt in range(3): try: resp = s.get(f"https://api.openalex.org/works/W{int(r.work_id)}", params={"mailto": "spbou4@icloud.com"}, headers={"User-Agent": UA}, timeout=30); break except requests.RequestException: time.sleep(5) if resp.status_code == 200: j = resp.json(); lt = j.get("title") or j.get("display_name") or ""; lid = j.get("id", "").rsplit("W", 1)[-1] match = norm(lt) == norm(r.title) or (norm(lt) and norm(r.title) and (norm(lt) in norm(r.title) or norm(r.title) in norm(lt))) rows.append({"work_id": int(r.work_id), "live_status": 200, "live_id": lid, "live_title": lt, "live_year": j.get("publication_year"), "live_cited_by_count": j.get("cited_by_count"), "live_match": bool(match), "id_changed": lid != str(int(r.work_id))}) else: rows.append({"work_id": int(r.work_id), "live_status": resp.status_code, "live_id": None, "live_title": None, "live_year": None, "live_cited_by_count": None, "live_match": False, "id_changed": None}) time.sleep(0.13) if (i + 1) % 200 == 0: print(i + 1, "checked", flush=True) v = d.merge(pd.DataFrame(rows), on="work_id", how="left"); v.to_csv(f"{R}/literature_synthesis/landmarks/landmarks_verified.csv", index=False) rep = {"n": int(len(v)), "live_match": int(v.live_match.sum()), "mismatch": int((~v.live_match).sum()), "id_changed": int(v.id_changed.fillna(False).sum()), "http_not_200": int((v.live_status != 200).sum()), "when": dt.datetime.now(dt.UTC).isoformat(), "mismatch_examples": v[~v.live_match][["work_id", "title", "live_title", "c36"]].head(15).to_dict("records")} json.dump(rep, open(f"{R}/literature_synthesis/landmarks/verification_report.json", "w"), indent=1, default=str); print({k: rep[k] for k in ("n", "live_match", "mismatch", "id_changed", "http_not_200")}) if __name__ == "__main__": main()