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:
+110
-24
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user