from fastapi import FastAPI, HTTPException, Query from app.services.stats import get_season_avg, get_last_n, get_splits from app.services.wnba import wnba_season_avg, wnba_last_n from app.services.refs import get_tonight_officials, get_referee_tendencies from app.services.mlb_statcast import get_pitcher_profile, get_batter_vs_pitcher from app.services.mlb_umpire import get_umpire_profile from app.utils.player_map import search_players from app.utils.cache import cache_health app = FastAPI(title="VYNDR Stats Service", version="1.1.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 # ── WNBA ───────────────────────────────────────────────────────────────────── @app.get("/wnba/stats/season-avg") async def wnba_season( 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 = wnba_season_avg(player, stat_type=stat_type, season=season) except Exception: raise HTTPException(status_code=503, detail="WNBA stats service unavailable") if result is None: raise HTTPException(status_code=404, detail=f"Player not found: {player}") return result @app.get("/wnba/stats/last-n") async def wnba_last( 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 = wnba_last_n(player, n=n, stat_type=stat_type) except Exception: raise HTTPException(status_code=503, detail="WNBA stats service unavailable") if result is None: raise HTTPException(status_code=404, detail=f"Player not found: {player}") return result # ── NBA Referees ───────────────────────────────────────────────────────────── @app.get("/refs/game/{game_id}") async def refs_game(game_id: str): if not game_id.isalnum() or len(game_id) > 16: raise HTTPException(status_code=400, detail="invalid game_id") return get_tonight_officials(game_id) @app.get("/refs/tendencies") async def refs_tendencies( season: str = Query("2025-26"), league: str = Query("nba"), ): if league not in {"nba", "wnba"}: raise HTTPException(status_code=400, detail="league must be nba or wnba") return get_referee_tendencies(season=season, league=league) # ── MLB Statcast ───────────────────────────────────────────────────────────── @app.get("/mlb/pitcher/{pitcher_id}") async def mlb_pitcher(pitcher_id: int, days_back: int = Query(30, ge=7, le=90)): if pitcher_id <= 0: raise HTTPException(status_code=400, detail="invalid pitcher_id") return get_pitcher_profile(pitcher_id=pitcher_id, days_back=days_back) @app.get("/mlb/bvp") async def mlb_bvp( batter_id: int = Query(..., gt=0), pitcher_id: int = Query(..., gt=0), years_back: int = Query(3, ge=1, le=5), ): return get_batter_vs_pitcher(batter_id=batter_id, pitcher_id=pitcher_id, years_back=years_back) @app.get("/mlb/umpires") async def mlb_umpires( umpire: str = Query(None, max_length=64), days_back: int = Query(30, ge=7, le=45), ): return get_umpire_profile(umpire_name=umpire, days_back=days_back)