"""Fetch Valve Immortal regional leaderboards into data/leaderboards.json. Pulls the official division boards (americas / europe / se_asia / china) and keeps Top 100 per region for the Climperor web「排行」page. Valve does not expose MMR or account_id on this endpoint — only rank, name, and optional team/country. Divisions use separate MMR scales (no global board). Preview only — do not merge into relations/heroes or recommend. Usage: python fetch_leaderboards.py python fetch_leaderboards.py --out data/leaderboards.json """ from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import argparse from datetime import datetime, timezone from shared.http_utils import http_json, write_json_atomic from shared.paths import DATA OUT = DATA / "leaderboards.json" API = ( "https://www.dota2.com/webapi/ILeaderboard/GetDivisionLeaderboard/v0001" "?division={division}&leaderboard=0" ) # Display order for the web rankings tab (China first for CN audience). REGION_ORDER = ("china", "europe", "americas", "se_asia") REGION_LABELS = { "china": "中国", "europe": "欧洲", "americas": "美洲", "se_asia": "东南亚", } TOP_N = 100 def slim_entry(row: dict) -> dict: out: dict = { "rank": int(row.get("rank") or 0), "name": str(row.get("name") or ""), } team = row.get("team_tag") if isinstance(team, str) and team.strip(): out["team_tag"] = team.strip() country = row.get("country") if isinstance(country, str) and country.strip(): out["country"] = country.strip().lower() return out def fetch_division(division: str) -> dict: url = API.format(division=division) print(f"fetching {division} ...", flush=True) raw = http_json(url) if not isinstance(raw, dict): raise SystemExit(f"{division}: unexpected payload type {type(raw).__name__}") lb = raw.get("leaderboard") if not isinstance(lb, list): raise SystemExit(f"{division}: missing leaderboard array") if len(lb) < TOP_N: raise SystemExit( f"{division}: leaderboard only returned {len(lb)} rows; " "keeping previous cache" ) top = [slim_entry(e) for e in lb[:TOP_N] if isinstance(e, dict)] return { "division": division, "label_zh": REGION_LABELS.get(division, division), "time_posted": raw.get("time_posted"), "count": len(lb), "top100": top, } def build_payload(regions: dict[str, dict]) -> dict: return { "fetched_at": datetime.now(timezone.utc).isoformat(), "source": "valve", "attribution": "https://www.dota2.com/leaderboards", "note": ( "Official Immortal division boards; no MMR/account_id in payload; " "region MMR scales are not comparable across divisions" ), "default_region": "china", "region_order": list(REGION_ORDER), "regions": regions, } def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--out", type=Path, default=OUT) args = ap.parse_args() regions: dict[str, dict] = {} for div in REGION_ORDER: regions[div] = fetch_division(div) payload = build_payload(regions) write_json_atomic(args.out, payload) parts = [ f"{div}={len((regions[div].get('top100') or []))}" for div in REGION_ORDER ] print(f"done: {', '.join(parts)} -> {args.out}", flush=True) if __name__ == "__main__": main()