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:
49
scripts/apply_schema.py
Normal file
49
scripts/apply_schema.py
Normal 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
327
scripts/discover_api.py
Normal 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()
|
||||
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()
|
||||
103
scripts/pull_tfc.py
Normal file
103
scripts/pull_tfc.py
Normal 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
75
scripts/test_api.py
Normal 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
36
scripts/test_db.py
Normal 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
107
scripts/test_rapidapi.py
Normal 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
71
scripts/verify_db.py
Normal 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()
|
||||
Reference in New Issue
Block a user