- scripts/sync-elo.py: downloads Excel sheet, auto-detects columns, writes public/elo-data.json keyed by lowercase player name - .gitlab-ci.yml: sync-elo stage (schedules only) commits the JSON then a normal push pipeline handles build + deploy; build/deploy skip on scheduled runs to avoid double deployment - index.html + main.js: ELO Rank column with sort support; loads elo-data.json asynchronously and re-renders when ready; fails silently if data is missing Requires one-time setup: 1. GitLab project token (write_repository) stored as GITLAB_PUSH_TOKEN 2. Pipeline schedule: cron 0 2 * * 3 (Wednesday 02:00 UTC) on main Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
3.5 KiB
Python
115 lines
3.5 KiB
Python
"""
|
|
sync-elo.py — download the stat-check.com ELO Excel sheet and emit public/elo-data.json.
|
|
|
|
Column auto-detection: scans the header row for keywords.
|
|
If headers change, check the CI log — it always prints what it found.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from io import BytesIO
|
|
|
|
try:
|
|
import requests
|
|
import openpyxl
|
|
except ImportError:
|
|
print("Installing dependencies…", flush=True)
|
|
import subprocess
|
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "requests", "openpyxl", "-q"])
|
|
import requests
|
|
import openpyxl
|
|
|
|
DOWNLOAD_URL = (
|
|
"https://excel.officeapps.live.com/x/_layouts/XlFileHandler.aspx"
|
|
"?WacUserType=WOPI"
|
|
"&usid=4bcd36aa-91bc-4c95-a692-c2ef5de1d13a"
|
|
"&NoAuth=1"
|
|
"&waccluster=PCA1"
|
|
)
|
|
OUTPUT_PATH = "public/elo-data.json"
|
|
|
|
|
|
def find_col(headers, *keywords):
|
|
"""Return index of first header containing any keyword (case-insensitive)."""
|
|
for i, h in enumerate(headers):
|
|
hl = str(h).lower()
|
|
if any(kw in hl for kw in keywords):
|
|
return i
|
|
return None
|
|
|
|
|
|
def main():
|
|
print(f"Downloading ELO sheet…", flush=True)
|
|
resp = requests.get(DOWNLOAD_URL, timeout=120, headers={"User-Agent": "Mozilla/5.0"})
|
|
resp.raise_for_status()
|
|
print(f"Downloaded {len(resp.content):,} bytes", flush=True)
|
|
|
|
wb = openpyxl.load_workbook(BytesIO(resp.content), read_only=True, data_only=True)
|
|
ws = wb.active
|
|
|
|
rows = ws.iter_rows(values_only=True)
|
|
raw_headers = next(rows)
|
|
headers = [str(c).strip() if c is not None else "" for c in raw_headers]
|
|
print(f"Headers ({len(headers)}): {headers}", flush=True)
|
|
|
|
rank_col = find_col(headers, "rank")
|
|
name_col = find_col(headers, "name", "player")
|
|
rating_col = find_col(headers, "rating", "elo", "score", "points")
|
|
|
|
if name_col is None:
|
|
print("ERROR: Could not find a name/player column. Check the headers above.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(
|
|
f"Mapped rank→col {rank_col} ('{headers[rank_col] if rank_col is not None else '—'}') "
|
|
f"name→col {name_col} ('{headers[name_col]}') "
|
|
f"rating→col {rating_col} ('{headers[rating_col] if rating_col is not None else '—'}')",
|
|
flush=True,
|
|
)
|
|
|
|
by_name = {}
|
|
skipped = 0
|
|
for seq, row in enumerate(rows, start=2):
|
|
raw_name = row[name_col] if len(row) > name_col else None
|
|
if not raw_name:
|
|
skipped += 1
|
|
continue
|
|
|
|
name = str(raw_name).strip()
|
|
key = name.lower()
|
|
|
|
raw_rank = row[rank_col] if rank_col is not None and len(row) > rank_col else None
|
|
raw_rating = row[rating_col] if rating_col is not None and len(row) > rating_col else None
|
|
|
|
try:
|
|
rank = int(raw_rank) if raw_rank is not None else seq
|
|
except (ValueError, TypeError):
|
|
rank = seq
|
|
|
|
try:
|
|
rating = round(float(raw_rating), 1) if raw_rating is not None else None
|
|
except (ValueError, TypeError):
|
|
rating = None
|
|
|
|
by_name[key] = {"rank": rank, "rating": rating, "name": name}
|
|
|
|
wb.close()
|
|
|
|
print(f"Parsed {len(by_name):,} players ({skipped} blank rows skipped)", flush=True)
|
|
|
|
output = {
|
|
"updated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"count": len(by_name),
|
|
"byName": by_name,
|
|
}
|
|
|
|
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
|
|
json.dump(output, f, separators=(",", ":"), ensure_ascii=False)
|
|
|
|
print(f"Wrote {OUTPUT_PATH}", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|