Add complete Nike football data platform with: - FastMCP server exposing football data tools over HTTP - RapidAPI client for free-api-live-football-data integration - Bootstrap web dashboard with live match/standings views - REST API endpoints for dashboard consumption - Docker support with multi-stage build - Comprehensive README with architecture docs - Minimal .gitignore replacing verbose Python template
328 lines
13 KiB
Python
328 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
API Discovery Script — walk the RapidAPI top-down, save raw responses.
|
|
|
|
Workflow:
|
|
1. Search for leagues (MLS, Premier League) → get league IDs
|
|
2. Search for teams (Toronto FC, Arsenal) → get team IDs
|
|
3. Get league matches → find recent past + upcoming event IDs
|
|
4. Get match detail, score, stats, location for a finished match
|
|
5. Get match highlights (goals, cards, events)
|
|
6. Get lineups for a finished match
|
|
7. Get squad roster + sample player detail
|
|
8. Get standings
|
|
|
|
Saves every response to docs/api_samples/{step}_{endpoint}.json
|
|
Cost: ~18 API calls
|
|
"""
|
|
import sys
|
|
import json
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from nike import rapidapi as rapi
|
|
|
|
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "docs" / "api_samples"
|
|
SAMPLES_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def save(name: str, data) -> None:
|
|
"""Save raw API response to a JSON file."""
|
|
path = SAMPLES_DIR / f"{name}.json"
|
|
with open(path, "w") as f:
|
|
json.dump(data, f, indent=2, default=str)
|
|
size_kb = path.stat().st_size / 1024
|
|
print(f" ✓ Saved {path.name} ({size_kb:.1f} KB)")
|
|
|
|
|
|
def extract_id(data, label="item") -> int | None:
|
|
"""Try to pull an ID from the first search result."""
|
|
resp = data.get("response") if isinstance(data, dict) else None
|
|
|
|
# Handle nested response structures
|
|
items = []
|
|
if isinstance(resp, dict):
|
|
for key in ("suggestions", "teams", "leagues", "players", "matches"):
|
|
if key in resp and isinstance(resp[key], list) and resp[key]:
|
|
items = resp[key]
|
|
break
|
|
if not items:
|
|
items = [resp]
|
|
elif isinstance(resp, list):
|
|
items = resp
|
|
else:
|
|
# Try alternate envelope keys
|
|
for key in ("data", "result"):
|
|
val = data.get(key)
|
|
if isinstance(val, list) and val:
|
|
items = val
|
|
break
|
|
|
|
for item in items:
|
|
if isinstance(item, dict):
|
|
eid = item.get("id") or item.get("primaryId")
|
|
if eid:
|
|
name = item.get("name") or item.get("title") or "?"
|
|
print(f" → {label}: {name} (ID: {eid})")
|
|
return int(eid)
|
|
|
|
print(f" ⚠ Could not extract ID from {label} response")
|
|
# Dump top-level keys for debugging
|
|
if isinstance(data, dict):
|
|
print(f" Top-level keys: {list(data.keys())}")
|
|
if isinstance(resp, dict):
|
|
print(f" response keys: {list(resp.keys())}")
|
|
return None
|
|
|
|
|
|
def find_event_ids(data, limit=2):
|
|
"""Extract event IDs from a matches/fixtures response."""
|
|
resp = data.get("response") if isinstance(data, dict) else None
|
|
events = {"past": [], "upcoming": []}
|
|
|
|
items = []
|
|
if isinstance(resp, dict):
|
|
for key in ("matches", "events", "fixtures", "allMatches"):
|
|
if key in resp and isinstance(resp[key], list):
|
|
items = resp[key]
|
|
break
|
|
if not items:
|
|
# Flatten if resp itself contains sub-lists
|
|
for key, val in resp.items():
|
|
if isinstance(val, list) and val and isinstance(val[0], dict):
|
|
items = val
|
|
break
|
|
elif isinstance(resp, list):
|
|
items = resp
|
|
else:
|
|
for key in ("data", "result"):
|
|
val = data.get(key)
|
|
if isinstance(val, list):
|
|
items = val
|
|
break
|
|
|
|
for item in items:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
eid = item.get("id") or item.get("eventId") or item.get("primaryId")
|
|
if not eid:
|
|
continue
|
|
|
|
# Detect finished vs upcoming
|
|
status = item.get("status", {})
|
|
if isinstance(status, dict):
|
|
finished = (status.get("finished", False) or
|
|
status.get("short") == "FT" or
|
|
status.get("type") == "finished")
|
|
elif isinstance(status, str):
|
|
finished = status.lower() in ("ft", "aet", "pen", "finished")
|
|
else:
|
|
finished = False
|
|
|
|
bucket = "past" if finished else "upcoming"
|
|
events[bucket].append(int(eid))
|
|
|
|
return {k: v[:limit] for k, v in events.items()}
|
|
|
|
|
|
def main():
|
|
print("=" * 60)
|
|
print(" Nike API Discovery — Mapping Response Structures")
|
|
print("=" * 60)
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
# STEP 1: League discovery
|
|
# ══════════════════════════════════════════════════════
|
|
print("\n[1/8] Searching for leagues...")
|
|
|
|
mls = rapi.search_leagues("MLS")
|
|
save("01_search_leagues_mls", mls)
|
|
mls_id = extract_id(mls, "MLS")
|
|
|
|
epl = rapi.search_leagues("Premier League")
|
|
save("01_search_leagues_epl", epl)
|
|
epl_id = extract_id(epl, "Premier League")
|
|
|
|
popular = rapi.get_popular_leagues()
|
|
save("01_popular_leagues", popular)
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
# STEP 2: Team discovery
|
|
# ══════════════════════════════════════════════════════
|
|
print("\n[2/8] Searching for teams...")
|
|
|
|
tfc = rapi.search_teams("Toronto FC")
|
|
save("02_search_teams_tfc", tfc)
|
|
tfc_id = extract_id(tfc, "Toronto FC")
|
|
|
|
ars = rapi.search_teams("Arsenal")
|
|
save("02_search_teams_arsenal", ars)
|
|
ars_id = extract_id(ars, "Arsenal")
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
# STEP 3: League matches (fixtures)
|
|
# ══════════════════════════════════════════════════════
|
|
print("\n[3/8] Fetching league matches...")
|
|
|
|
mls_events = {"past": [], "upcoming": []}
|
|
if mls_id:
|
|
mls_matches = rapi.get_league_matches(mls_id)
|
|
save("03_league_matches_mls", mls_matches)
|
|
mls_events = find_event_ids(mls_matches)
|
|
print(f" → Events found: {len(mls_events['past'])} past, "
|
|
f"{len(mls_events['upcoming'])} upcoming")
|
|
if not mls_events["past"] and not mls_events["upcoming"]:
|
|
# Dump structure hints to help debug
|
|
resp = mls_matches.get("response")
|
|
if isinstance(resp, dict):
|
|
print(f" response keys: {list(resp.keys())}")
|
|
for k, v in resp.items():
|
|
if isinstance(v, list) and v:
|
|
print(f" response.{k}[0] keys: "
|
|
f"{list(v[0].keys()) if isinstance(v[0], dict) else type(v[0])}")
|
|
elif isinstance(resp, list) and resp:
|
|
print(f" response[0] keys: "
|
|
f"{list(resp[0].keys()) if isinstance(resp[0], dict) else type(resp[0])}")
|
|
else:
|
|
print(" ⚠ No MLS ID, skipping")
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
# STEP 4: Match detail (finished match)
|
|
# ══════════════════════════════════════════════════════
|
|
print("\n[4/8] Fetching match detail...")
|
|
|
|
event_id = None
|
|
if mls_events["past"]:
|
|
event_id = mls_events["past"][0]
|
|
elif mls_events["upcoming"]:
|
|
event_id = mls_events["upcoming"][0]
|
|
|
|
if event_id:
|
|
print(f" Using event ID: {event_id}")
|
|
|
|
detail = rapi.get_match_detail(event_id)
|
|
save("04_match_detail", detail)
|
|
|
|
score = rapi.get_match_score(event_id)
|
|
save("04_match_score", score)
|
|
|
|
status = rapi.get_match_status(event_id)
|
|
save("04_match_status", status)
|
|
|
|
location = rapi.get_match_location(event_id)
|
|
save("04_match_location", location)
|
|
else:
|
|
print(" ⚠ No event ID found — skipping match detail")
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
# STEP 5: Match stats + highlights (goals, cards, events)
|
|
# ══════════════════════════════════════════════════════
|
|
print("\n[5/8] Fetching match stats & highlights...")
|
|
|
|
if event_id:
|
|
stats = rapi.get_match_stats(event_id)
|
|
save("05_match_stats", stats)
|
|
|
|
highlights = rapi.get_match_highlights(event_id)
|
|
save("05_match_highlights", highlights)
|
|
|
|
referee = rapi.get_match_referee(event_id)
|
|
save("05_match_referee", referee)
|
|
else:
|
|
print(" ⚠ Skipping (no event ID)")
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
# STEP 6: Lineups
|
|
# ══════════════════════════════════════════════════════
|
|
print("\n[6/8] Fetching lineups...")
|
|
|
|
if event_id:
|
|
home_lineup = rapi.get_home_lineup(event_id)
|
|
save("06_lineup_home", home_lineup)
|
|
|
|
away_lineup = rapi.get_away_lineup(event_id)
|
|
save("06_lineup_away", away_lineup)
|
|
else:
|
|
print(" ⚠ Skipping (no event ID)")
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
# STEP 7: Squad & player detail
|
|
# ══════════════════════════════════════════════════════
|
|
print("\n[7/8] Fetching squad & player detail...")
|
|
|
|
player_id = None
|
|
if tfc_id:
|
|
squad = rapi.get_squad(tfc_id)
|
|
save("07_squad_tfc", squad)
|
|
|
|
# Extract a player ID from the squad response
|
|
resp = squad.get("response") if isinstance(squad, dict) else None
|
|
|
|
if isinstance(resp, list) and resp:
|
|
for p in resp:
|
|
if isinstance(p, dict):
|
|
pid = p.get("id") or p.get("primaryId")
|
|
if pid:
|
|
player_id = int(pid)
|
|
print(f" → Sample player: {p.get('name', '?')} (ID: {player_id})")
|
|
break
|
|
elif isinstance(resp, dict):
|
|
# May be nested under squad/players/roster
|
|
for key in ("squad", "players", "roster", "members"):
|
|
if key in resp and isinstance(resp[key], list):
|
|
for p in resp[key]:
|
|
if isinstance(p, dict):
|
|
pid = p.get("id") or p.get("primaryId")
|
|
if pid:
|
|
player_id = int(pid)
|
|
print(f" → Sample player: {p.get('name', '?')} "
|
|
f"(ID: {player_id})")
|
|
break
|
|
break
|
|
if not player_id:
|
|
print(f" response keys: {list(resp.keys())}")
|
|
|
|
if player_id:
|
|
player = rapi.get_player_detail(player_id)
|
|
save("07_player_detail", player)
|
|
else:
|
|
print(" ⚠ Could not find a player ID in squad response")
|
|
else:
|
|
print(" ⚠ No TFC ID, skipping")
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
# STEP 8: Standings
|
|
# ══════════════════════════════════════════════════════
|
|
print("\n[8/8] Fetching standings...")
|
|
|
|
if mls_id:
|
|
standings = rapi.get_standings(mls_id)
|
|
save("08_standings_mls", standings)
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
# Summary
|
|
# ══════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
files = sorted(SAMPLES_DIR.glob("*.json"))
|
|
print(f" Saved {len(files)} response samples to docs/api_samples/")
|
|
for f in files:
|
|
size_kb = f.stat().st_size / 1024
|
|
print(f" {f.name:.<50} {size_kb:.1f} KB")
|
|
print("=" * 60)
|
|
|
|
print("\n Discovered IDs:")
|
|
print(f" MLS league ID : {mls_id}")
|
|
print(f" EPL league ID : {epl_id}")
|
|
print(f" Toronto FC team ID : {tfc_id}")
|
|
print(f" Arsenal team ID : {ars_id}")
|
|
if event_id:
|
|
print(f" Sample event ID : {event_id}")
|
|
if player_id:
|
|
print(f" Sample player ID : {player_id}")
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|