#!/usr/bin/env python3 """Verify the GitHub top-100 Python repositories artifact against a fresh API fetch. Re-queries the same search endpoint and diffs the produced table against the committed artifact (github-python-repos-.csv / .json). Exit 0 only when every strict field matches the live response within live-counter tolerance; nonzero otherwise. Conventions followed: - Strict fields (must match exactly): rank, full_name, owner, language, forks, open_issues, description (whitespace-normalised), html_url. - stars (live counter): the artifact matches when it equals the fresh read or differs by exactly 1; any larger delta is a FAIL. Each off-by-one is printed as a NOTE with the delta. """ import csv import json import os import re import sys import urllib.request from datetime import datetime, timezone API_BASE = "https://api.github.com" QUERY = "language:python" SORT = "stars" ORDER = "desc" PER_PAGE = 100 HEADERS = { "User-Agent": "ledger/github-python-repos-verify", "Accept": "application/vnd.github+json", } OUT_DIR = os.path.dirname(os.path.abspath(__file__)) STAMPS = sorted( f for f in os.listdir(OUT_DIR) if re.match(r"github-python-repos-\d{4}-\d{2}-\d{2}\.csv$", f) ) ARTIFACT = os.path.join(OUT_DIR, STAMPS[-1]) if STAMPS else None CSV_PATH = ARTIFACT JSON_PATH = os.path.splitext(ARTIFACT)[0] + ".json" STAR_TOLERANCE = 1 STRICT_FIELDS = [ "rank", "full_name", "owner", "language", "forks", "open_issues", "description", "html_url", ] def api_get(path): req = urllib.request.Request(API_BASE + path, headers=HEADERS, method="GET") with urllib.request.urlopen(req, timeout=60) as r: return json.loads(r.read().decode("utf-8")) def norm(s): return re.sub(r"\s+", " ", (s or "").strip()) def load_artifact(): rows = [] with open(CSV_PATH, newline="", encoding="utf-8") as f: for r in csv.DictReader(f): rows.append(r) return rows def fresh_rows(): data = api_get( "/search/repositories?q=language:python&sort=stars&order=desc&per_page=100" ) out = [] for i, it in enumerate(data.get("items", []), 1): out.append({ "rank": i, "full_name": it.get("full_name", ""), "owner": (it.get("owner") or {}).get("login", ""), "description": norm(it.get("description")), "language": it.get("language") or "", "stars": int(it.get("stargazers_count", 0)), "forks": int(it.get("forks_count", 0)), "open_issues": int(it.get("open_issues_count", 0)), "created_at": it.get("created_at", ""), "updated_at": it.get("updated_at", ""), "homepage": it.get("homepage") or "", "html_url": it.get("html_url", ""), }) return out def main(): if ARTIFACT is None: print("FAIL: no github-python-repos-*.csv found in {}".format(OUT_DIR)) return 1 art = load_artifact() fresh = fresh_rows() print("artifact: {} (rows {})".format(CSV_PATH, len(art))) fresh_payload = api_get( "/search/repositories?q=language:python&sort=stars&order=desc&per_page=100" ) print("fresh API rows: {} (total_count from payload: {})".format(len(fresh), fresh_payload.get("total_count"))) failures = [] notes = [] n = max(len(art), len(fresh)) for i in range(n): a = art[i] if i < len(art) else None f = fresh[i] if i < len(fresh) else None if a is None or f is None: failures.append((i + 1, "count mismatch", a, f)) continue for field in STRICT_FIELDS: av = a.get(field, "") fv = f.get(field, "") if field == "description": av = norm(av) fv = norm(fv) if str(av) != str(fv): failures.append((i + 1, field, av, fv)) break da = int(a.get("stars", 0)) df = int(f.get("stars", 0)) if da != df: delta = df - da if abs(delta) > STAR_TOLERANCE: failures.append((i + 1, "stars", da, df)) else: notes.append((i + 1, da, df, delta)) if failures: print("FAIL: {} row(s) differ from the live API response".format(len(failures))) for rank, field, av, fv in failures[:10]: print(" rank={} field={!r} artifact={!r} fresh={!r}".format(rank, field, av, fv)) return 1 for rank, da, df, delta in notes: print(" NOTE rank={} stars artifact={} fresh={} delta={}".format(rank, da, df, delta)) if notes: print("verdict: PASS - structural fields match the live API ({} star-count NOTE(s))".format(len(notes))) else: print("verdict: PASS - artifact matches the live API response exactly ({} rows)".format(len(fresh))) return 0 if __name__ == "__main__": sys.exit(main())