Files
climperor/fetch_leaderboards.py
T
vosonandCursor 3ec8007077 v0.5.59: item counter evidence for fears, catch up Web features to site version.
Ship OpenDota counter-stats reordering for feared items, finalize SITE_VERSION/docs for rankings/streamers/trends/matches/mechanics and draft archetypes, and ignore regenerable Web data caches.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 02:11:49 +08:00

114 lines
3.4 KiB
Python

"""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 argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from common import DATA
from http_utils import http_json
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")
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)
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
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()