179 lines
6.4 KiB
Python
179 lines
6.4 KiB
Python
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)
|