一键导入
sdv-capture-endpoint
Hardened, provider-agnostic single-body capture for ESPN/Fox/CBS — structured id discovery, error-envelope skip, atomic writes.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Hardened, provider-agnostic single-body capture for ESPN/Fox/CBS — structured id discovery, error-envelope skip, atomic writes.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when porting R logic into the Python SportsDataverse package (sdv-py) — e.g. translating an nflfastR / cfbfastR / cfbscrapR / baseballr / hoopR R function, reconciling a fix from the pandas `0.36-live` branch into polars `main`, or re-implementing a modeling recipe (EP/WP/QBR/CPOE) from R. Enforces a parity-test-first workflow (golden fixture from the R output → failing parity test → port → green) and the polars-1x + ID-dtype + no-lookaround conventions. Also covers R numeric fidelity when outputs must match R bit-for-bit (R>=4.0 fround, 80-bit long-double sum, non-na.rm null-poisoning, dplyr group order), stale-fork sibling packages (wbigballR-style), and oracle integrity (a browser/rvest-captured oracle can be corrupt — fix the bug, don't match it). Invoke for "port this R function to polars", "port from nflfastR/bigballR", "reconcile 0.36-live into main", "translate this dplyr/np.select to polars", "re-implement the R model recipe in Python", or when a parity test fails on a handful of rounding-boundar
Use when shipping a change in sportsdataverse-py (sdv-py) — opening, pushing, or merging a PR. Runs the steps in the correct order (regenerate codegen docs, update changelog/docs/tutorials, lint, full pytest, commit + verify it landed, push, triage bot reviews CodeRabbit/Sourcery/Copilot immediately — in parallel with CI, not after it — wait for CI green, confirm merge, write a session note) and never cleans up a branch before the merge is confirmed. Invoke for "ship this", "open/merge the PR", "land this change", or any end-of-change release flow.
Use when executing a model-spine implementation plan (the ClaudeCowork specs/plans backlog — prediction stacks, ratings engines, projection/impact models) or building any oracle-gated analytics module in an SDV repo. Covers the full loop — isolated worktree + baseline, Phase-0 oracle harness (metrics/constants/leakage split/fixtures), per-task TDD with verified commits, oracle gates with the never-lower rule, league-shim parity, mypy/codegen close-out, reviewer pass, and the session restart prompt. Invoke for "implement T<x>", "start the <sport> model spine", "continue the prediction stack", or any multi-phase model build.
Use when capturing an external oracle/validation corpus into committed test fixtures — Torvik/KenPom ratings, ESPN BPI/predictor/odds, market closing lines, MoneyPuck, published RAPM/EPM, or any third-party reference a model gate will assert against. Covers column contracts, Utf8-id discipline, the team/player name-crosswalk recipe (contracting normalizer + candidate keys + alias table + match-rate reporting), rate-limit-aware sampling, and the mandatory provenance README. Invoke for "capture the oracle corpus", "commit the <source> fixtures", or any Task-0-style fixture capture in a model plan.
Use when porting Python logic from the SportsDataverse Python package (sdv-py) into a SportsDataverse R package — cfbfastR, hoopR, wehoop, baseballr, fastRhockey, softballR, etc. Enforces a parity-test-first workflow (golden fixture from the Python output → failing testthat test → port → green) plus the tidyverse/data.table idiom map and R package conventions (roxygen2 docs, snake_case, tibble returns, pkgdown reference coverage). Invoke for "port this to cfbfastR", "translate this polars logic to dplyr", "re-implement the sdv-py parser in R", or "bring this Python fix back to the R package".
Add a CBS Sports (NAPI) league/resource — capture + catalog in sdv-internal-refs/cbs.
| name | sdv-capture-endpoint |
| description | Hardened, provider-agnostic single-body capture for ESPN/Fox/CBS — structured id discovery, error-envelope skip, atomic writes. |
| disable-model-invocation | true |
Fetch one representative response body per endpoint and write it to
<provider>/inputs/sample_bodies/<league>/<host>/<endpoint>.json.
Avoids the two known bugs in naive capture scripts: (1) greedy first-long-number regex matching an inner id instead of the top-level event id; (2) storing ESPN error envelopes as if they were valid payloads.
ESPN_SITE = "https://site.api.espn.com/apis/site/v2/sports"
ESPN_SITE_ALT = "https://site.web.api.espn.com/apis/site/v2/sports"
ESPN_WEB = "https://web.api.espn.com/apis/v2/sports"
ESPN_CORE = "https://sports.core.api.espn.com/v2/sports"
ESPN_CORE3 = "https://sports.core.api.espn.com/v3/sports"
ESPN_CDN = "https://cdn.espn.com/core"
FOX_BASE = "https://bifrost.foxsports.com/prod/v1"
FOX_HEADERS = {"apikey": "<key>", "api-version": "2"}
CBS_BASE = "https://www.cbssports.com/nfl/scorestrip/feed" # napi variant per sport
def is_error_envelope(body: dict) -> bool:
"""Return True if ESPN/Fox returned an error dict, not real data."""
if not isinstance(body, dict):
return False
keys = set(body.keys())
# ESPN: {"code": 400, "message": "..."} or {"code":..., "detail":...}
if keys <= {"code", "message", "detail", "name", "error"}:
return True
# Fox/CBS: {"error": "..."} top-level only
if "error" in keys and len(keys) <= 2:
return True
return False
Never parse the scoreboard URL or body with a greedy re.search(r'\d{6,}', ...). Walk the structure:
import requests, json
def get_event_ids(sport: str, league: str, limit: int = 3) -> list[int]:
"""Discover top-level event ids from the ESPN scoreboard endpoint."""
url = f"{ESPN_SITE}/{sport}/{league}/scoreboard"
r = requests.get(url, params={"limit": limit}, timeout=15)
r.raise_for_status()
body = r.json()
# ESPN Site v2 shape: body["events"][i]["id"]
events = body.get("events") or []
return [int(e["id"]) for e in events if "id" in e]
For non-Site v2 endpoints (Core, Web), adapt the key path accordingly — always use ["id"] on the top-level event object, not on nested team/athlete dicts.
import os, tempfile, pathlib, json
def atomic_write(path: str | pathlib.Path, data: dict) -> None:
path = pathlib.Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
try:
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
tmp.rename(path)
except Exception:
tmp.unlink(missing_ok=True)
raise
import requests, pathlib, json
def capture_endpoint(
sport: str,
league: str,
endpoint: str, # e.g. "summary", "scoreboard"
params: dict, # e.g. {"event": event_id}
out_dir: pathlib.Path,
host: str = "site", # "site" | "site_alt" | "web" | "core"
) -> pathlib.Path | None:
base = {
"site": ESPN_SITE, "site_alt": ESPN_SITE_ALT,
"web": ESPN_WEB, "core": ESPN_CORE,
}[host]
url = f"{base}/{sport}/{league}/{endpoint}"
r = requests.get(url, params=params, timeout=30)
if r.status_code != 200:
print(f"SKIP {url} → HTTP {r.status_code}")
return None
body = r.json()
if is_error_envelope(body):
print(f"SKIP {url} → error envelope: {list(body.keys())}")
return None
out_path = out_dir / league / host / f"{endpoint}.json"
atomic_write(out_path, body)
print(f"OK {out_path}")
return out_path
# From sdv-internal-refs/espn/tools/
python espn_capture_league.py soccer epl
# Or call the recipe directly in a one-off script:
event_ids = get_event_ids("soccer", "epl")
capture_endpoint("soccer", "epl", "summary",
{"event": event_ids[0]},
pathlib.Path("espn/inputs/sample_bodies"))