v0.5.84: matches origin filter, mobile gate, refresh reliability.

Ship Web refresh cache/lock, mobile demand gate, matches 职业/国服 filter, and related site updates through 0.5.84.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-29 18:31:55 +08:00
co-authored by Cursor
parent 7681fdb069
commit b01552ee6e
50 changed files with 3406 additions and 487 deletions
+110 -24
View File
@@ -29,13 +29,13 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import argparse
import json
import os
import tempfile
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
from shared.grid import hero_table
from shared.http_utils import write_json_atomic
from shared.paths import DATA, ROOT
API = "https://api.stratz.com/graphql"
@@ -165,28 +165,6 @@ def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def write_json_atomic(path: Path, payload: dict) -> None:
"""Write JSON via temp file then replace, so partial writes never corrupt cache."""
path.parent.mkdir(parents=True, exist_ok=True)
text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
fd, tmp_name = tempfile.mkstemp(
prefix=f".{path.name}.",
suffix=".tmp",
dir=str(path.parent),
)
tmp_path = Path(tmp_name)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(text)
os.replace(tmp_path, path)
except Exception:
try:
tmp_path.unlink(missing_ok=True)
except OSError:
pass
raise
def fetch_weeks_for_bracket(
token: str, hero_ids: list[int], bracket: str, take: int
) -> list[dict]:
@@ -511,6 +489,47 @@ def build_meta(
}
def preserve_failed_meta_brackets(
fresh: dict,
previous: dict,
*,
failed_weeks: set[str],
failed_positions: set[str],
) -> dict:
"""Keep previous bracket cells when STRATZ returns no usable rows."""
if not failed_weeks and not failed_positions:
fresh["stale"] = False
fresh["stale_brackets"] = []
return fresh
previous_by_hero = previous.get("by_hero") or {}
fresh_by_hero = fresh.get("by_hero") or {}
for key, cell in fresh_by_hero.items():
old = previous_by_hero.get(key) or {}
for bracket in failed_weeks:
cell["weeks"][bracket] = list((old.get("weeks") or {}).get(bracket) or [])
old_latest = (old.get("latest") or {}).get(bracket)
if old_latest:
cell["latest"][bracket] = dict(old_latest)
for bracket in failed_positions:
cell["positions"][bracket] = dict(
(old.get("positions") or {}).get(bracket) or {}
)
for bracket in failed_weeks:
old_total = (previous.get("totals") or {}).get(bracket)
old_board = (previous.get("meta_board") or {}).get(bracket)
if old_total:
fresh["totals"][bracket] = dict(old_total)
if old_board:
fresh["meta_board"][bracket] = list(old_board)
fresh["stale"] = True
fresh["stale_brackets"] = sorted(failed_weeks | failed_positions)
fresh["last_attempt_at"] = _now_iso()
return fresh
def refresh_matchup_tops(
token: str,
id_to_key: dict[int, str],
@@ -526,6 +545,14 @@ def refresh_matchup_tops(
On per-hero failure, keep the previous cell and mark ``stale=True``.
"""
started_at = _now_iso()
previous_file: dict | None = None
if out_path.is_file():
try:
loaded = json.loads(out_path.read_text(encoding="utf-8"))
if isinstance(loaded, dict):
previous_file = loaded
except (OSError, json.JSONDecodeError):
previous_file = None
prev = load_previous_matchups(out_path)
hero_ids = sorted(id_to_key.keys())
@@ -539,7 +566,8 @@ def refresh_matchup_tops(
)
else:
pending = list(hero_ids)
by_hero_mu = {}
# Keep the complete previous set until each cell is successfully replaced.
by_hero_mu = dict(prev)
print(
f"full matchup refresh: {len(pending)} heroes "
f"(prev cached={len(prev)} as failure fallback)",
@@ -547,6 +575,7 @@ def refresh_matchup_tops(
)
ok = failed = stale_kept = 0
missing_fallback: list[str] = []
for n, hid in enumerate(pending, start=1):
key = id_to_key[hid]
try:
@@ -588,6 +617,7 @@ def refresh_matchup_tops(
)
else:
print(f" [{n}/{len(pending)}] {key} failed: {e}", flush=True)
missing_fallback.append(key)
time.sleep(delay * 2)
continue
@@ -641,6 +671,15 @@ def refresh_matchup_tops(
f"stale_kept={stale_kept}{out_path}",
flush=True,
)
if missing_fallback:
if previous_file is not None:
write_json_atomic(out_path, previous_file)
else:
out_path.unlink(missing_ok=True)
raise RuntimeError(
"STRATZ matchup refresh had failures without cached fallback: "
+ ", ".join(missing_fallback[:10])
)
return payload
@@ -712,9 +751,56 @@ def main() -> None:
)
time.sleep(args.delay)
failed_weeks = {b for b, rows in weeks_by_bracket.items() if not rows}
failed_positions = {
b for b, rows in position_rows_by_bracket.items() if not rows
}
previous_meta: dict = {}
if args.out_meta.is_file():
try:
loaded = json.loads(args.out_meta.read_text(encoding="utf-8"))
if isinstance(loaded, dict):
previous_meta = loaded
except (OSError, json.JSONDecodeError):
previous_meta = {}
previous_by_hero = previous_meta.get("by_hero") or {}
missing_week_fallback = [
bracket
for bracket in failed_weeks
if not any(
(cell.get("weeks") or {}).get(bracket)
for cell in previous_by_hero.values()
if isinstance(cell, dict)
)
]
missing_position_fallback = [
bracket
for bracket in failed_positions
if not any(
(cell.get("positions") or {}).get(bracket)
for cell in previous_by_hero.values()
if isinstance(cell, dict)
)
]
if missing_week_fallback or missing_position_fallback:
raise RuntimeError(
"STRATZ meta failed without cached fallback: weeks="
f"{missing_week_fallback}, positions={missing_position_fallback}"
)
meta = build_meta(
weeks_by_bracket, position_rows_by_bracket, id_to_key, args.weeks
)
meta = preserve_failed_meta_brackets(
meta,
previous_meta,
failed_weeks=failed_weeks,
failed_positions=failed_positions,
)
if len(failed_weeks) == len(BRACKET_ORDER):
# Preserve the timestamp of the last successful data snapshot.
meta["fetched_at"] = previous_meta.get("fetched_at")
write_json_atomic(args.out_meta, meta)
print(f"wrote {args.out_meta}", flush=True)