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:
2026-03-21 18:19:42 +00:00
parent b8689d530a
commit ee8436d5b8
81 changed files with 50251 additions and 176 deletions

49
scripts/apply_schema.py Normal file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Apply Nike schema to Portia PostgreSQL."""
import os
import sys
from dotenv import load_dotenv
import psycopg2
load_dotenv('/home/robert/gitea/nike/.env')
try:
conn = psycopg2.connect(
host=os.getenv('DB_HOST'),
port=int(os.getenv('DB_PORT', 5432)),
user=os.getenv('DB_USER'),
password=os.getenv('DB_PASSWORD'),
dbname='nike',
)
except Exception as e:
print(f"❌ Cannot connect to DB: {e}")
sys.exit(1)
conn.autocommit = True
cur = conn.cursor()
schema_path = os.path.join(os.path.dirname(__file__), '..', 'schema.sql')
with open(schema_path, 'r') as f:
sql = f.read()
try:
cur.execute(sql)
print("✅ Schema applied successfully.")
except Exception as e:
print(f"❌ Schema error: {e}")
cur.close()
conn.close()
sys.exit(1)
cur.execute("""
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' ORDER BY table_name;
""")
tables = cur.fetchall()
print(f" {len(tables)} tables in public schema:")
for t in tables:
print(f"{t[0]}")
cur.close()
conn.close()

327
scripts/discover_api.py Normal file
View File

@@ -0,0 +1,327 @@
#!/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()

View 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()

103
scripts/pull_tfc.py Normal file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
Pull Toronto FC squad and fixtures and store in Portia DB.
Delegates all API calls and DB writes to nike.sync.sync_team_data()
so this script stays thin and the real logic lives in one place.
"""
import sys
from pathlib import Path
# Make sure the project root is on the path when run directly
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from datetime import date
from nike import config, db
from nike.sync import sync_team_data
def main() -> None:
if not config.API_FOOTBALL_KEY:
print("❌ API_FOOTBALL_KEY not set in .env")
sys.exit(1)
print("=" * 60)
print(" NIKE — Toronto FC Data Pull")
print(f" API: {config.API_FOOTBALL_BASE}")
print("=" * 60)
db.create_pool()
try:
results = sync_team_data(
team_search=config.TFC_SEARCH,
seasons=config.SEASONS,
)
finally:
db.close_pool()
# ── Summary ──────────────────────────────────────────
if results["errors"]:
print("\n⚠️ Errors encountered:")
for e in results["errors"]:
print(f"{e}")
print(f"\n✅ Team: {results.get('team', 'unknown')}")
print(f" Players: {results['players']}")
print(f" API calls: {results['api_calls']}")
for season, count in results.get("seasons", {}).items():
print(f" Fixtures {season}: {count}")
if results.get("squad"):
_print_squad(results["squad"])
if results.get("all_fixtures"):
_print_fixtures(results["all_fixtures"])
def _print_squad(squad: list[dict]) -> None:
print("\n" + "=" * 60)
print(" SQUAD")
print("=" * 60)
pos_order = ["Goalkeeper", "Defender", "Midfielder", "Attacker"]
by_pos: dict = {}
for p in squad:
by_pos.setdefault(p.get("position") or "Unknown", []).append(p)
for pos in pos_order + [k for k in by_pos if k not in pos_order]:
if pos not in by_pos:
continue
print(f"\n {pos.upper()}S:")
for p in sorted(by_pos[pos],
key=lambda x: x["number"] if isinstance(x.get("number"), int) else 99):
print(f" #{str(p.get('number', '-')).rjust(2)} {p['name']}")
def _print_fixtures(fixtures: list[dict]) -> None:
print("\n" + "=" * 60)
print(" FIXTURES")
print("=" * 60)
today = date.today().isoformat()
today_f = [f for f in fixtures if f["date"][:10] == today]
completed = [f for f in fixtures if f["status"] in ("FT", "AET", "PEN")]
upcoming = [f for f in fixtures if f["status"] in ("NS", "TBD", "PST")]
if today_f:
print("\n 🔴 TODAY:")
for f in today_f:
score = f" {f['score']}" if f.get("score") else ""
print(f" {f['home']}{score} {f['away']} @ {f['venue']} [{f['status']}]")
if completed:
print("\n RECENT RESULTS (last 5):")
for f in completed[-5:]:
print(f" {f['date'][:10]} {f['home']} {f['score']} {f['away']}")
if upcoming:
print("\n UPCOMING (next 5):")
for f in sorted(upcoming, key=lambda x: x["date"])[:5]:
print(f" {f['date'][:10]} {f['home']} vs {f['away']} [{f.get('round', '')}]")
if __name__ == "__main__":
main()

75
scripts/test_api.py Normal file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""
Test API connectivity, find Toronto FC, and list popular leagues
so we can confirm the correct MLS league ID for this API.
Uses 2 API quota calls.
"""
import json
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from nike import config, api_football
def main() -> None:
if not config.API_FOOTBALL_KEY:
print("❌ API_FOOTBALL_KEY not set in .env")
sys.exit(1)
print(f"API: {config.API_FOOTBALL_BASE}\n")
# ── 1. Popular leagues (connectivity probe + league ID discovery) ──
print("── Popular Leagues ──────────────────────────")
try:
t0 = time.time()
data = api_football._get("football-popular-leagues", timeout=8)
latency_ms = round((time.time() - t0) * 1000, 1)
except Exception as e:
print(f"❌ Not connected: {e}")
sys.exit(1)
print(f"✅ Connected latency={latency_ms}ms "
f"quota_remaining={api_football.last_quota_remaining()}")
leagues = (data.get('response', {}).get('popular') or
data.get('response', {}).get('leagues') or [])
if not isinstance(leagues, list):
print(f" Raw response structure:\n{json.dumps(data, indent=2)[:1000]}")
else:
print(f" {len(leagues)} league(s) returned:")
for lg in leagues:
lg_id = lg.get('id', '?')
lg_name = lg.get('name', str(lg))
ccode = lg.get('ccode', '')
print(f" [{str(lg_id):>5}] {lg_name} ({ccode})")
print(f"\n ↳ Current config.MLS_LEAGUE_ID = {config.MLS_LEAGUE_ID}")
print(" Update nike/config.py if the ID above doesn't match MLS.\n")
# ── 2. Search for Toronto FC ───────────────────────────────────
print("── Search: Toronto ──────────────────────────")
raw_search = api_football._get("football-teams-search", {"search": "Toronto"})
print(f" quota_remaining={api_football.last_quota_remaining()}")
# Try all common envelope keys; fall back to full raw dump
raw_list = (raw_search.get('response', {}).get('suggestions') or
raw_search.get('response', {}).get('teams') or
raw_search.get('data') or raw_search.get('result'))
if not isinstance(raw_list, list) or not raw_list:
print(f" 0 results — raw response:\n{json.dumps(raw_search, indent=2)[:1500]}")
else:
teams = [api_football._normalise_team_item(t)
for t in raw_list if t.get('type', 'team') == 'team']
print(f" {len(teams)} team result(s):")
for item in teams:
t = item['team']
v = item.get('venue') or {}
print(f" [{str(t.get('id')):>6}] {t.get('name')} ({t.get('country')})")
if v.get('name'):
print(f" Venue: {v.get('name')}, {v.get('city')} cap={v.get('capacity')}")
if __name__ == "__main__":
main()

36
scripts/test_db.py Normal file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Test connection to Portia PostgreSQL."""
import os
import sys
import time
from dotenv import load_dotenv
import psycopg2
load_dotenv('/home/robert/gitea/nike/.env')
try:
t0 = time.time()
conn = psycopg2.connect(
host=os.getenv('DB_HOST'),
port=int(os.getenv('DB_PORT', 5432)),
user=os.getenv('DB_USER'),
password=os.getenv('DB_PASSWORD'),
dbname='nike',
connect_timeout=5,
)
latency_ms = round((time.time() - t0) * 1000, 1)
cur = conn.cursor()
cur.execute('SELECT version();')
version = cur.fetchone()[0]
cur.execute("SELECT current_database(), current_user;")
db, user = cur.fetchone()
cur.close()
conn.close()
print(f"✅ Connected in {latency_ms}ms")
print(f" Database : {db}")
print(f" User : {user}")
print(f" Version : {version.split(',')[0]}")
except Exception as e:
print(f"❌ Connection failed: {e}")
sys.exit(1)

107
scripts/test_rapidapi.py Normal file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Smoke test for the RapidAPI (free-api-live-football-data) backend.
Verifies connectivity, finds MLS, fetches standings and TFC squad.
Run: python scripts/test_rapidapi.py
"""
import sys
import json
from pathlib import Path
# Ensure project root is on the path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from nike import config
from nike import rapidapi as rapi
def _pp(label: str, data) -> None:
"""Pretty-print a section."""
print(f"\n{'='*60}")
print(f" {label}")
print(f"{'='*60}")
print(json.dumps(data, indent=2, default=str)[:3000]) # trim for readability
def main():
if not config.RAPIDAPI_KEY:
print("ERROR: RAPIDAPI_KEY is not set in .env")
sys.exit(1)
print(f"RapidAPI key: {config.RAPIDAPI_KEY[:8]}...{config.RAPIDAPI_KEY[-4:]}")
print(f"Base URL: {config.RAPIDAPI_BASE}")
# 1. Connectivity
print("\n1) Checking connectivity...")
status = rapi.check_connection()
print(f" Connected: {status['connected']}")
if not status["connected"]:
print(f" Error: {status.get('error')}")
sys.exit(1)
print(f" Latency: {status['latency_ms']} ms")
# 2. Search for MLS
print("\n2) Searching for 'MLS'...")
mls_data = rapi.search_leagues("MLS")
_pp("MLS search results", mls_data)
# Try to extract league ID
mls_id = None
resp = mls_data.get("response") if isinstance(mls_data, dict) else None
if isinstance(resp, list):
for item in resp:
if isinstance(item, dict):
mls_id = item.get("id") or item.get("primaryId")
if mls_id:
mls_id = int(mls_id)
break
print(f"\n MLS League ID: {mls_id}")
# 3. Standings
if mls_id:
print("\n3) Fetching MLS standings...")
standings = rapi.get_standings(mls_id)
_pp("MLS Standings", standings)
# 4. Search for Toronto FC
print("\n4) Searching for 'Toronto FC'...")
tfc_data = rapi.search_teams("Toronto FC")
_pp("TFC search results", tfc_data)
tfc_id = None
resp = tfc_data.get("response") if isinstance(tfc_data, dict) else None
if isinstance(resp, list):
for item in resp:
if isinstance(item, dict):
tfc_id = item.get("id") or item.get("primaryId")
if tfc_id:
tfc_id = int(tfc_id)
break
print(f"\n TFC Team ID: {tfc_id}")
# 5. Squad
if tfc_id:
print("\n5) Fetching TFC squad...")
squad = rapi.get_squad(tfc_id)
_pp("TFC Squad", squad)
# 6. Live matches (just check it works)
print("\n6) Checking live matches endpoint...")
live = rapi.get_live_matches()
resp = live.get("response") if isinstance(live, dict) else None
count = len(resp) if isinstance(resp, list) else 0
print(f" Live matches right now: {count}")
# 7. Trending news
print("\n7) Fetching trending news...")
news_data = rapi.get_trending_news()
_pp("Trending News", news_data)
print("\n" + "="*60)
print(" ALL CHECKS PASSED")
print("="*60)
if __name__ == "__main__":
main()

71
scripts/verify_db.py Normal file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Quick verification of Nike DB contents."""
import os
from dotenv import load_dotenv
import psycopg2
load_dotenv('/home/robert/gitea/nike/.env')
conn = psycopg2.connect(
host=os.getenv('DB_HOST'),
port=int(os.getenv('DB_PORT', 5432)),
user=os.getenv('DB_USER'),
password=os.getenv('DB_PASSWORD'),
dbname='nike',
)
cur = conn.cursor()
tables = [
'leagues', 'teams', 'players', 'fixtures', 'standings',
'match_stats', 'match_events', 'player_season_stats',
'player_match_stats', 'followed_entities',
]
print("Nike DB Contents")
print("-" * 40)
for table in tables:
cur.execute(f"SELECT COUNT(*) FROM {table}")
count = cur.fetchone()[0]
status = "" if count > 0 else ""
print(f" {status} {table:<28} {count:>6} rows")
# TFC summary
print()
cur.execute("""
SELECT t.name, COUNT(p.id) AS players
FROM teams t
LEFT JOIN players p ON p.current_team_id = t.id
WHERE t.is_followed = TRUE
GROUP BY t.name
""")
for row in cur.fetchall():
print(f"{row[0]}: {row[1]} players in roster")
cur.execute("""
SELECT COUNT(*) FROM fixtures f
JOIN teams t ON (t.id = f.home_team_id OR t.id = f.away_team_id)
WHERE t.is_followed = TRUE
""")
fix_count = cur.fetchone()[0]
print(f" 📅 Followed team fixtures: {fix_count}")
cur.execute("""
SELECT match_date::date, home.name, away.name, home_goals, away_goals, status
FROM fixtures f
JOIN teams home ON home.id = f.home_team_id
JOIN teams away ON away.id = f.away_team_id
JOIN teams ft ON (ft.id = f.home_team_id OR ft.id = f.away_team_id)
WHERE ft.is_followed = TRUE AND f.match_date >= NOW()
ORDER BY f.match_date ASC
LIMIT 3
""")
upcoming = cur.fetchall()
if upcoming:
print()
print(" Next fixtures:")
for row in upcoming:
print(f" {row[0]} {row[1]} vs {row[2]} ({row[5]})")
cur.close()
conn.close()