feat: implement MCP server and dashboard for football data platform
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
This commit is contained in:
383
scripts/discover_sportsdb.py
Normal file
383
scripts/discover_sportsdb.py
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TheSportsDB API Discovery — walk the API top-down, save raw responses.
|
||||
|
||||
Uses V1 free key ('3') for basic structure validation.
|
||||
With a premium key, also tests V2 endpoints.
|
||||
|
||||
Workflow:
|
||||
1. Search for leagues (MLS, Premier League) → IDs
|
||||
2. Search for teams (Toronto FC, Arsenal) → IDs
|
||||
3. Get team schedules (next + previous matches) → event IDs
|
||||
4. Get match detail, stats, lineup, timeline for a finished match
|
||||
5. Get squad roster + sample player detail
|
||||
6. Get standings (V1)
|
||||
7. Get events by date (V1)
|
||||
8. Get livescores
|
||||
|
||||
Saves every response to docs/api_samples/sportsdb/{step}_{endpoint}.json
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from nike import config
|
||||
from nike import sportsdb as api
|
||||
|
||||
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "docs" / "api_samples" / "sportsdb"
|
||||
SAMPLES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Track API calls
|
||||
_call_count = 0
|
||||
|
||||
|
||||
def save(name: str, data) -> None:
|
||||
"""Save raw API response to a JSON file."""
|
||||
global _call_count
|
||||
_call_count += 1
|
||||
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 show_keys(data, label="response"):
|
||||
"""Print top-level structure of a response."""
|
||||
if isinstance(data, dict):
|
||||
for k, v in data.items():
|
||||
if isinstance(v, list):
|
||||
print(f" {label}.{k}: list ({len(v)} items)")
|
||||
if v and isinstance(v[0], dict):
|
||||
print(f" [0] keys: {list(v[0].keys())}")
|
||||
elif isinstance(v, dict):
|
||||
print(f" {label}.{k}: dict ({len(v)} keys)")
|
||||
elif v is None:
|
||||
print(f" {label}.{k}: null")
|
||||
else:
|
||||
print(f" {label}.{k}: {str(v)[:80]}")
|
||||
elif isinstance(data, list):
|
||||
print(f" {label}: list ({len(data)} items)")
|
||||
if data and isinstance(data[0], dict):
|
||||
print(f" [0] keys: {list(data[0].keys())}")
|
||||
elif data is None:
|
||||
print(f" {label}: null")
|
||||
|
||||
|
||||
def extract_items(data: dict, key: str) -> list:
|
||||
"""Safely extract a list from a response dict."""
|
||||
items = data.get(key)
|
||||
return items if isinstance(items, list) else []
|
||||
|
||||
|
||||
def main():
|
||||
is_premium = config.SPORTSDB_KEY not in ('3', '')
|
||||
print("=" * 60)
|
||||
print(" TheSportsDB API Discovery")
|
||||
print(f" Key: {'Premium' if is_premium else 'Free (V1 only)'}")
|
||||
print("=" * 60)
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# STEP 1: Search for leagues (V1 free works for search)
|
||||
# ══════════════════════════════════════════════════════
|
||||
print("\n[1/8] Searching for leagues...")
|
||||
|
||||
mls_id = None
|
||||
epl_id = None
|
||||
|
||||
if is_premium:
|
||||
# V2 search
|
||||
mls = api.search_leagues("MLS")
|
||||
save("01_v2_search_leagues_mls", mls)
|
||||
show_keys(mls)
|
||||
items = extract_items(mls, "search")
|
||||
if items:
|
||||
mls_id = items[0].get("idLeague")
|
||||
print(f" → MLS: {items[0].get('strLeague')} (ID: {mls_id})")
|
||||
|
||||
epl = api.search_leagues("Premier League")
|
||||
save("01_v2_search_leagues_epl", epl)
|
||||
items = extract_items(epl, "search")
|
||||
if items:
|
||||
epl_id = items[0].get("idLeague")
|
||||
print(f" → EPL: {items[0].get('strLeague')} (ID: {epl_id})")
|
||||
else:
|
||||
print(" (V2 search requires premium — using V1 search)")
|
||||
|
||||
# Also try V1 search (works on free key)
|
||||
mls_v1 = api.v1_search_teams("Toronto FC")
|
||||
save("01_v1_search_teams_tfc", mls_v1)
|
||||
show_keys(mls_v1)
|
||||
teams = extract_items(mls_v1, "teams")
|
||||
tfc_id = None
|
||||
if teams:
|
||||
tfc_id = teams[0].get("idTeam")
|
||||
mls_id = mls_id or teams[0].get("idLeague")
|
||||
print(f" → Toronto FC: ID={tfc_id}, league={teams[0].get('strLeague')} "
|
||||
f"(leagueID={teams[0].get('idLeague')})")
|
||||
|
||||
ars_v1 = api.v1_search_teams("Arsenal")
|
||||
save("01_v1_search_teams_arsenal", ars_v1)
|
||||
teams = extract_items(ars_v1, "teams")
|
||||
ars_id = None
|
||||
if teams:
|
||||
ars_id = teams[0].get("idTeam")
|
||||
epl_id = epl_id or teams[0].get("idLeague")
|
||||
print(f" → Arsenal: ID={ars_id}, league={teams[0].get('strLeague')} "
|
||||
f"(leagueID={teams[0].get('idLeague')})")
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# STEP 2: Team detail
|
||||
# ══════════════════════════════════════════════════════
|
||||
print("\n[2/8] Fetching team details...")
|
||||
|
||||
if tfc_id:
|
||||
tfc_detail = api.v1_lookup_team(int(tfc_id))
|
||||
save("02_v1_team_detail_tfc", tfc_detail)
|
||||
teams = extract_items(tfc_detail, "teams")
|
||||
if teams:
|
||||
t = teams[0]
|
||||
print(f" → {t.get('strTeam')}: {t.get('strStadium')}, "
|
||||
f"{t.get('strStadiumLocation')}")
|
||||
print(f" Keys: {list(t.keys())[:15]}...")
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# STEP 3: Schedule — next + previous matches
|
||||
# ══════════════════════════════════════════════════════
|
||||
print("\n[3/8] Fetching team schedules...")
|
||||
|
||||
event_id = None
|
||||
|
||||
if tfc_id:
|
||||
# V1 previous (last 5)
|
||||
prev = api.v1_previous_team(int(tfc_id))
|
||||
save("03_v1_previous_tfc", prev)
|
||||
results = extract_items(prev, "results")
|
||||
print(f" Previous matches: {len(results)}")
|
||||
for m in results[:3]:
|
||||
eid = m.get("idEvent")
|
||||
home = m.get("strHomeTeam", "?")
|
||||
away = m.get("strAwayTeam", "?")
|
||||
hscore = m.get("intHomeScore", "?")
|
||||
ascore = m.get("intAwayScore", "?")
|
||||
date = m.get("dateEvent", "?")
|
||||
print(f" {date} {home} {hscore}-{ascore} {away} (eventID: {eid})")
|
||||
if not event_id and hscore is not None:
|
||||
event_id = int(eid)
|
||||
|
||||
# V1 next (next 5)
|
||||
nxt = api.v1_next_team(int(tfc_id))
|
||||
save("03_v1_next_tfc", nxt)
|
||||
events = extract_items(nxt, "events")
|
||||
print(f" Upcoming matches: {len(events)}")
|
||||
for m in events[:3]:
|
||||
home = m.get("strHomeTeam", "?")
|
||||
away = m.get("strAwayTeam", "?")
|
||||
date = m.get("dateEvent", "?")
|
||||
print(f" {date} {home} vs {away}")
|
||||
|
||||
if is_premium and tfc_id:
|
||||
# V2 schedule endpoints
|
||||
prev_v2 = api.schedule_previous_team(int(tfc_id))
|
||||
save("03_v2_previous_tfc", prev_v2)
|
||||
show_keys(prev_v2)
|
||||
|
||||
next_v2 = api.schedule_next_team(int(tfc_id))
|
||||
save("03_v2_next_tfc", next_v2)
|
||||
show_keys(next_v2)
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# STEP 4: Match detail (V2 lookup for a finished match)
|
||||
# ══════════════════════════════════════════════════════
|
||||
print("\n[4/8] Fetching match detail...")
|
||||
|
||||
if event_id:
|
||||
print(f" Using event ID: {event_id}")
|
||||
|
||||
# V1 event lookup (works on free)
|
||||
ev1 = api.v1_event_results(event_id)
|
||||
save("04_v1_event_detail", ev1)
|
||||
events = extract_items(ev1, "events")
|
||||
if events:
|
||||
e = events[0]
|
||||
print(f" V1 event keys: {list(e.keys())[:15]}...")
|
||||
print(f" → {e.get('strHomeTeam')} {e.get('intHomeScore')}-"
|
||||
f"{e.get('intAwayScore')} {e.get('strAwayTeam')}")
|
||||
print(f" Venue: {e.get('strVenue')}, Referee: {e.get('strOfficial')}")
|
||||
|
||||
if is_premium:
|
||||
# V2 event detail
|
||||
ev2 = api.lookup_event(event_id)
|
||||
save("04_v2_event_detail", ev2)
|
||||
show_keys(ev2)
|
||||
|
||||
# V2 stats
|
||||
stats = api.lookup_event_stats(event_id)
|
||||
save("04_v2_event_stats", stats)
|
||||
show_keys(stats)
|
||||
stat_items = extract_items(stats, "lookup")
|
||||
if stat_items:
|
||||
print(f" Stats sample:")
|
||||
for s in stat_items[:5]:
|
||||
print(f" {s.get('strStat')}: "
|
||||
f"Home={s.get('intHome')} Away={s.get('intAway')}")
|
||||
|
||||
# V2 timeline
|
||||
timeline = api.lookup_event_timeline(event_id)
|
||||
save("04_v2_event_timeline", timeline)
|
||||
show_keys(timeline)
|
||||
tl_items = extract_items(timeline, "lookup")
|
||||
if tl_items:
|
||||
print(f" Timeline events: {len(tl_items)}")
|
||||
for t in tl_items[:5]:
|
||||
print(f" {t.get('intTime')}' {t.get('strTimeline')}: "
|
||||
f"{t.get('strPlayer')} ({t.get('strTeam')})")
|
||||
|
||||
# V2 lineup
|
||||
lineup = api.lookup_event_lineup(event_id)
|
||||
save("04_v2_event_lineup", lineup)
|
||||
show_keys(lineup)
|
||||
lineup_items = extract_items(lineup, "lookup")
|
||||
if lineup_items:
|
||||
print(f" Lineup entries: {len(lineup_items)}")
|
||||
for p in lineup_items[:3]:
|
||||
print(f" #{p.get('intSquadNumber')} {p.get('strPlayer')} "
|
||||
f"({p.get('strPosition')}) "
|
||||
f"home={p.get('strHome')} sub={p.get('strSubstitute')}")
|
||||
else:
|
||||
print(" ⚠ No event ID found — skipping")
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# STEP 5: Squad roster + player detail
|
||||
# ══════════════════════════════════════════════════════
|
||||
print("\n[5/8] Fetching squad & player detail...")
|
||||
|
||||
player_id = None
|
||||
if is_premium and tfc_id:
|
||||
squad = api.list_players(int(tfc_id))
|
||||
save("05_v2_squad_tfc", squad)
|
||||
show_keys(squad)
|
||||
players = extract_items(squad, "list")
|
||||
if players:
|
||||
p = players[0]
|
||||
player_id = p.get("idPlayer")
|
||||
print(f" Squad size: {len(players)}")
|
||||
print(f" Sample player: {p.get('strPlayer')} "
|
||||
f"(#{p.get('strNumber')}, {p.get('strPosition')})")
|
||||
print(f" Player keys: {list(p.keys())[:15]}...")
|
||||
|
||||
if player_id:
|
||||
pdetail = api.lookup_player(int(player_id))
|
||||
save("05_v2_player_detail", pdetail)
|
||||
show_keys(pdetail)
|
||||
else:
|
||||
# V1 player search
|
||||
players_v1 = api.v1_search_players("Bernardeschi")
|
||||
save("05_v1_search_player", players_v1)
|
||||
show_keys(players_v1)
|
||||
items = extract_items(players_v1, "player")
|
||||
if items:
|
||||
player_id = items[0].get("idPlayer")
|
||||
print(f" → {items[0].get('strPlayer')} (ID: {player_id})")
|
||||
print(f" Keys: {list(items[0].keys())[:15]}...")
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# STEP 6: Standings (V1)
|
||||
# ══════════════════════════════════════════════════════
|
||||
print("\n[6/8] Fetching standings...")
|
||||
|
||||
if epl_id:
|
||||
# EPL should have current season data
|
||||
standings = api.v1_standings(int(epl_id), "2025-2026")
|
||||
save("06_v1_standings_epl", standings)
|
||||
table = extract_items(standings, "table")
|
||||
print(f" EPL standings: {len(table)} teams")
|
||||
if table:
|
||||
print(f" [0] keys: {list(table[0].keys())}")
|
||||
for row in table[:3]:
|
||||
print(f" #{row.get('intRank')} {row.get('strTeam')} "
|
||||
f"P:{row.get('intPlayed')} W:{row.get('intWin')} "
|
||||
f"D:{row.get('intDraw')} L:{row.get('intLoss')} "
|
||||
f"Pts:{row.get('intPoints')}")
|
||||
|
||||
if mls_id:
|
||||
standings_mls = api.v1_standings(int(mls_id), "2026")
|
||||
save("06_v1_standings_mls", standings_mls)
|
||||
table = extract_items(standings_mls, "table")
|
||||
print(f" MLS standings: {len(table)} teams")
|
||||
if table:
|
||||
for row in table[:3]:
|
||||
print(f" #{row.get('intRank')} {row.get('strTeam')} "
|
||||
f"Pts:{row.get('intPoints')}")
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# STEP 7: Events by date (V1)
|
||||
# ══════════════════════════════════════════════════════
|
||||
print("\n[7/8] Fetching events by date...")
|
||||
|
||||
events_today = api.v1_events_by_date("2026-03-09")
|
||||
save("07_v1_events_today", events_today)
|
||||
ev_list = extract_items(events_today, "events")
|
||||
print(f" Soccer events today: {len(ev_list)}")
|
||||
if ev_list:
|
||||
print(f" [0] keys: {list(ev_list[0].keys())[:15]}...")
|
||||
for e in ev_list[:3]:
|
||||
print(f" {e.get('strLeague')}: {e.get('strHomeTeam')} vs "
|
||||
f"{e.get('strAwayTeam')} ({e.get('strStatus')})")
|
||||
|
||||
if mls_id:
|
||||
mls_events = api.v1_events_by_date_league("2026-03-09", int(mls_id))
|
||||
save("07_v1_events_today_mls", mls_events)
|
||||
ev_list = extract_items(mls_events, "events")
|
||||
print(f" MLS events today: {len(ev_list)}")
|
||||
for e in ev_list[:5]:
|
||||
print(f" {e.get('strHomeTeam')} vs {e.get('strAwayTeam')} "
|
||||
f"({e.get('strStatus')})")
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# STEP 8: Livescores (V2 premium only)
|
||||
# ══════════════════════════════════════════════════════
|
||||
print("\n[8/8] Fetching livescores...")
|
||||
|
||||
if is_premium:
|
||||
live = api.livescores_soccer()
|
||||
save("08_v2_livescores", live)
|
||||
show_keys(live)
|
||||
games = extract_items(live, "livescore")
|
||||
print(f" Live soccer matches: {len(games)}")
|
||||
for g in games[:3]:
|
||||
print(f" {g.get('strHomeTeam')} {g.get('intHomeScore')}-"
|
||||
f"{g.get('intAwayScore')} {g.get('strAwayTeam')} "
|
||||
f"({g.get('strStatus')} {g.get('strProgress')}')")
|
||||
else:
|
||||
print(" (Livescores require premium key)")
|
||||
|
||||
# ══════════════════════════════════════════════════════
|
||||
# Summary
|
||||
# ══════════════════════════════════════════════════════
|
||||
print("\n" + "=" * 60)
|
||||
files = sorted(SAMPLES_DIR.glob("*.json"))
|
||||
print(f" Saved {len(files)} response samples to docs/api_samples/sportsdb/")
|
||||
for f in files:
|
||||
size_kb = f.stat().st_size / 1024
|
||||
print(f" {f.name:.<55} {size_kb:.1f} KB")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\n API calls made: {_call_count}")
|
||||
print(f" Key type: {'Premium (V1+V2)' if is_premium else 'Free (V1 only)'}")
|
||||
print(f"\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()
|
||||
Reference in New Issue
Block a user