feat: Feature 1.2 (NBA stats FastAPI service) + Feature 1.4 (database schema)

Feature 1.2: Python FastAPI microservice wrapping nba_api
- GET /stats/season-avg, /stats/last-n, /stats/splits, /players/search
- Redis caching (24hr/1hr/6hr/7day), 0.6s rate limiting, PRA derived stat
- 27 Python tests passing

Feature 1.4: Complete Supabase database schema
- 6 tables: users, picks, scan_sessions, bets, outcomes, performance
- RLS enabled on all tables with auth.uid() policies
- 3 triggers: auto-create user, updated_at, scan count reset
- 37 schema validation tests passing
- Migration SQL ready, pending manual apply (WSL2 DNS blocker)

Total: 92 tests (65 Node.js + 27 Python), all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-03-21 10:58:58 -04:00
parent 00409fd6cd
commit 3da1b4242c
27 changed files with 2360 additions and 16 deletions
+93
View File
@@ -0,0 +1,93 @@
from fastapi import FastAPI, HTTPException, Query
from app.services.stats import get_season_avg, get_last_n, get_splits
from app.utils.player_map import search_players
from app.utils.cache import cache_health
app = FastAPI(title="BetonBLK NBA Stats Service", version="1.0.0")
VALID_STAT_TYPES = {
"points", "rebounds", "assists", "threes", "blocks",
"steals", "pra", "turnovers", "minutes", "games_played",
}
VALID_SPLIT_TYPES = {"home_away", "rest_days", "vs_team"}
@app.get("/health")
async def health():
return {"status": "ok", "cache": "connected" if cache_health() else "disconnected"}
@app.get("/players/search")
async def player_search(name: str = Query(..., min_length=2)):
results = search_players(name)
if not results:
raise HTTPException(status_code=404, detail=f"Player not found: {name}")
return {"results": results}
@app.get("/stats/season-avg")
async def season_avg(
player: str = Query(..., min_length=2),
stat_type: str = Query(None),
season: str = Query(None),
):
if stat_type and stat_type not in VALID_STAT_TYPES:
raise HTTPException(status_code=400, detail=f"Invalid stat_type: {stat_type}")
try:
result = get_season_avg(player, stat_type=stat_type, season=season)
except Exception as e:
raise HTTPException(status_code=503, detail="NBA stats service unavailable")
if result is None:
raise HTTPException(status_code=404, detail=f"Player not found: {player}")
return result
@app.get("/stats/last-n")
async def last_n(
player: str = Query(..., min_length=2),
n: int = Query(10, ge=1, le=30),
stat_type: str = Query(None),
):
if stat_type and stat_type not in VALID_STAT_TYPES:
raise HTTPException(status_code=400, detail=f"Invalid stat_type: {stat_type}")
try:
result = get_last_n(player, n=n, stat_type=stat_type)
except Exception as e:
raise HTTPException(status_code=503, detail="NBA stats service unavailable")
if result is None:
raise HTTPException(status_code=404, detail=f"Player not found: {player}")
return result
@app.get("/stats/splits")
async def splits(
player: str = Query(..., min_length=2),
stat_type: str = Query(...),
split_type: str = Query(...),
opponent: str = Query(None),
):
if stat_type not in VALID_STAT_TYPES:
raise HTTPException(status_code=400, detail=f"Invalid stat_type: {stat_type}")
if split_type not in VALID_SPLIT_TYPES:
raise HTTPException(status_code=400, detail=f"Invalid split_type: {split_type}")
if split_type == "vs_team" and not opponent:
raise HTTPException(status_code=400, detail="opponent is required when split_type=vs_team")
try:
result = get_splits(player, stat_type, split_type, opponent=opponent)
except Exception as e:
raise HTTPException(status_code=503, detail="NBA stats service unavailable")
if result is None:
raise HTTPException(status_code=404, detail=f"Player not found: {player}")
return result