"""Backfill D1 from local pc/player_pages profile (+ live OpenDota if needed). Used when Worker edge cannot reach OpenDota (429). No secret echo. """ from __future__ import annotations import json import os import sys import urllib.error import urllib.request from datetime import datetime, timezone from pathlib import Path ROOT = Path(__file__).resolve().parents[2] ACCOUNT = "510534f7f6284344aadaf2f5a0794d48" DB = "9eeb24ba-acc5-4520-b4e7-754ea776394e" AID = int(os.environ.get("CLIMPEROR_SYNC_ACCOUNT_ID", "143712136")) PROFILE = ROOT / "pc" / "player_pages" / str(AID) / "profile.json" def _creds() -> tuple[str, str]: email = os.environ.get("CLOUDFLARE_EMAIL") or os.environ.get( "KEYZOO_ASSET_META_USERNAME" ) key = os.environ.get("CLOUDFLARE_API_KEY") or os.environ.get( "KEYZOO_ASSET_SECRET_GLOBAL_API_KEY" ) if not email or not key: raise SystemExit("missing Cloudflare credentials") return email, key def utc_now() -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def d1_batch(statements: list[dict]) -> None: email, key = _creds() for st in statements: payload = {"sql": st["sql"]} if "params" in st: payload["params"] = st["params"] data = json.dumps(payload).encode() req = urllib.request.Request( f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT}/d1/database/{DB}/query", data=data, method="POST", headers={ "Content-Type": "application/json", "X-Auth-Email": email, "X-Auth-Key": key, }, ) try: with urllib.request.urlopen(req, timeout=60) as r: out = json.loads(r.read().decode()) except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", errors="replace") raise SystemExit(f"D1 {e.code}: {raw[:500]}\nSQL: {st['sql'][:200]}") from e if not out.get("success"): raise SystemExit(json.dumps(out, ensure_ascii=False)[:800]) def esc(v) -> str: if v is None: return "NULL" if isinstance(v, bool): return "1" if v else "0" if isinstance(v, (int, float)) and not isinstance(v, bool): if isinstance(v, float) and (v != v): # NaN return "NULL" return str(v) s = str(v).replace("'", "''") return f"'{s}'" def main() -> int: if not PROFILE.is_file(): raise SystemExit(f"missing {PROFILE}") p = json.loads(PROFILE.read_text(encoding="utf-8")) now = utc_now() steamid = str(AID + 76561197960265728) personaname = p.get("personaname") or "refining" avatar = p.get("avatar") public_share = 1 if p.get("public_share") else 0 avail = p.get("availability") or {} career = p.get("career") or {} recent20 = p.get("recent_20") or {} activity = p.get("activity_180") or {} top_heroes = p.get("top_heroes") or [] peers = p.get("peers") or [] recent = (p.get("recent") or [])[:20] stmts: list[dict] = [] stmts.append( { "sql": ( f"INSERT INTO users (account_id, steamid, personaname, avatar, public_share, created_at, last_login_at) " f"VALUES ({AID}, {esc(steamid)}, {esc(personaname)}, {esc(avatar)}, {public_share}, {esc(now)}, {esc(now)}) " f"ON CONFLICT(account_id) DO UPDATE SET " f"personaname=excluded.personaname, avatar=COALESCE(excluded.avatar, users.avatar), " f"last_login_at=excluded.last_login_at" ) } ) stmts.append( { "sql": ( f"INSERT INTO player_profiles (" f"account_id, rank_tier, leaderboard_rank, availability_status, availability_note, " f"availability_complete, source, fetched_at, enriched_at, updated_at) VALUES (" f"{AID}, {esc(p.get('rank_tier'))}, {esc(p.get('leaderboard_rank'))}, " f"{esc(avail.get('status') or 'public')}, {esc(avail.get('note'))}, " f"{1 if avail.get('complete', True) else 0}, " f"{esc(avail.get('source') or 'opendota+steam')}, " f"{esc(avail.get('fetched_at') or now)}, {esc(p.get('enriched_at') or now)}, {esc(now)}) " f"ON CONFLICT(account_id) DO UPDATE SET " f"rank_tier=excluded.rank_tier, leaderboard_rank=excluded.leaderboard_rank, " f"availability_status=excluded.availability_status, availability_note=excluded.availability_note, " f"availability_complete=excluded.availability_complete, source=excluded.source, " f"fetched_at=excluded.fetched_at, enriched_at=excluded.enriched_at, updated_at=excluded.updated_at" ) } ) def stats_sql(scope: str, stats: dict) -> dict: payload = json.dumps(stats, ensure_ascii=False).replace("'", "''") sample = stats.get("sample") if stats.get("sample") is not None else stats.get("games") or 0 return { "sql": ( f"INSERT INTO player_stats (" f"account_id, scope, sample, wins, losses, winrate, kills, deaths, assists, kda, " f"avg_kills, avg_deaths, avg_assists, avg_gpm, avg_xpm, avg_hero_damage, payload_json, updated_at) " f"VALUES ({AID}, {esc(scope)}, {int(sample)}, {int(stats.get('wins') or 0)}, " f"{int(stats.get('losses') or 0)}, {esc(stats.get('winrate'))}, " f"{esc(stats.get('kills'))}, {esc(stats.get('deaths'))}, {esc(stats.get('assists'))}, " f"{esc(stats.get('kda'))}, {esc(stats.get('avg_kills'))}, {esc(stats.get('avg_deaths'))}, " f"{esc(stats.get('avg_assists'))}, {esc(stats.get('avg_gpm'))}, {esc(stats.get('avg_xpm'))}, " f"{esc(stats.get('avg_hero_damage'))}, '{payload}', {esc(now)}) " f"ON CONFLICT(account_id, scope) DO UPDATE SET " f"sample=excluded.sample, wins=excluded.wins, losses=excluded.losses, winrate=excluded.winrate, " f"kills=excluded.kills, deaths=excluded.deaths, assists=excluded.assists, kda=excluded.kda, " f"avg_kills=excluded.avg_kills, avg_deaths=excluded.avg_deaths, avg_assists=excluded.avg_assists, " f"avg_gpm=excluded.avg_gpm, avg_xpm=excluded.avg_xpm, avg_hero_damage=excluded.avg_hero_damage, " f"payload_json=excluded.payload_json, updated_at=excluded.updated_at" ) } if career: stmts.append(stats_sql("career", career)) if recent20: stmts.append(stats_sql("recent20", recent20)) if activity: stmts.append(stats_sql("recent180", activity)) stmts.append({"sql": f"DELETE FROM player_heroes WHERE account_id={AID}"}) for h in top_heroes[:8]: stmts.append( { "sql": ( f"INSERT INTO player_heroes (" f"account_id, hero_id, hero_key, hero_name_loc, games, wins, winrate, last_played, updated_at) " f"VALUES ({AID}, {int(h.get('hero_id') or 0)}, {esc(h.get('hero_key'))}, " f"{esc(h.get('hero_name_loc'))}, {int(h.get('games') or 0)}, {int(h.get('wins') or 0)}, " f"{esc(h.get('winrate'))}, {esc(h.get('last_played'))}, {esc(now)})" ) } ) stmts.append({"sql": f"DELETE FROM player_peers WHERE account_id={AID}"}) for peer in peers[:8]: stmts.append( { "sql": ( f"INSERT INTO player_peers (" f"account_id, peer_account_id, personaname, avatar, games, wins, winrate, updated_at) " f"VALUES ({AID}, {int(peer.get('account_id') or 0)}, {esc(peer.get('personaname'))}, " f"{esc(peer.get('avatar'))}, {int(peer.get('games') or 0)}, {int(peer.get('wins') or 0)}, " f"{esc(peer.get('winrate'))}, {esc(now)})" ) } ) for r in recent: mid = int(r.get("match_id") or 0) if mid <= 0: continue stmts.append( { "sql": ( f"INSERT INTO player_matches (" f"account_id, match_id, start_time, duration, won, hero_id, hero_key, hero_name_loc, " f"kills, deaths, assists, kda, gpm, xpm, hero_damage, game_mode, lobby_type, r2_key, updated_at) " f"VALUES ({AID}, {mid}, {esc(r.get('start_time'))}, {esc(r.get('duration'))}, " f"{1 if r.get('won') else 0}, {esc(r.get('hero_id'))}, {esc(r.get('hero_key'))}, " f"{esc(r.get('hero_name_loc'))}, {esc(r.get('kills'))}, {esc(r.get('deaths'))}, " f"{esc(r.get('assists'))}, {esc(r.get('kda'))}, {esc(r.get('gpm'))}, {esc(r.get('xpm'))}, " f"{esc(r.get('hero_damage'))}, {esc(r.get('game_mode'))}, {esc(r.get('lobby_type'))}, " f"NULL, {esc(now)}) " f"ON CONFLICT(account_id, match_id) DO UPDATE SET " f"start_time=excluded.start_time, duration=excluded.duration, won=excluded.won, " f"hero_id=excluded.hero_id, hero_key=excluded.hero_key, hero_name_loc=excluded.hero_name_loc, " f"kills=excluded.kills, deaths=excluded.deaths, assists=excluded.assists, kda=excluded.kda, " f"gpm=excluded.gpm, xpm=excluded.xpm, hero_damage=excluded.hero_damage, " f"game_mode=excluded.game_mode, lobby_type=excluded.lobby_type, updated_at=excluded.updated_at" ) } ) print(f"backfill {AID} from {PROFILE.name}: {len(stmts)} statements …", flush=True) d1_batch(stmts) print("done", flush=True) return 0 if __name__ == "__main__": sys.exit(main())