158 lines
4.7 KiB
Python
158 lines
4.7 KiB
Python
"""
|
||
WNBA stats — uses nba_api with league_id='10'.
|
||
|
||
Kept self-contained (not a wrapper over NBA's stats.py) so the existing
|
||
NBA code path stays untouched. Shape of the returned dicts mirrors
|
||
stats.py so callers can dispatch on `sport` without branching downstream.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
from datetime import datetime, timezone
|
||
from typing import Optional
|
||
|
||
from nba_api.stats.endpoints import playercareerstats, playergamelog
|
||
from nba_api.stats.static import players as wnba_players
|
||
|
||
from app.utils.cache import cache_get, cache_set
|
||
from app.config import (
|
||
NBA_API_DELAY, NBA_API_TIMEOUT,
|
||
SEASON_AVG_TTL, LAST_N_TTL,
|
||
)
|
||
|
||
WNBA_LEAGUE_ID = "10"
|
||
_STAT_MAP = {
|
||
"PTS": "points",
|
||
"REB": "rebounds",
|
||
"AST": "assists",
|
||
"FG3M": "threes",
|
||
"BLK": "blocks",
|
||
"STL": "steals",
|
||
"TOV": "turnovers",
|
||
"MIN": "minutes",
|
||
"GP": "games_played",
|
||
}
|
||
|
||
|
||
def _wnba_current_season() -> str:
|
||
now = datetime.now(timezone.utc)
|
||
# WNBA season is roughly May–September; use the calendar year.
|
||
return str(now.year)
|
||
|
||
|
||
def _safe(func, **kwargs):
|
||
"""Tiny rate-limited wrapper around nba_api endpoints."""
|
||
time.sleep(NBA_API_DELAY)
|
||
return func(timeout=NBA_API_TIMEOUT, **kwargs)
|
||
|
||
|
||
def _resolve_wnba_player(name: str) -> tuple[Optional[int], str]:
|
||
name = (name or "").strip()
|
||
if len(name) < 2:
|
||
return None, ""
|
||
# nba_api.static.players only ships NBA player lists; for WNBA we resolve
|
||
# via the search endpoint (commonteamroster also works). For now we fall
|
||
# back to a name match across the (NBA + WNBA) static set, then verify
|
||
# with the live endpoint if needed.
|
||
matches = wnba_players.find_players_by_full_name(name)
|
||
if matches:
|
||
return matches[0]["id"], matches[0]["full_name"]
|
||
return None, ""
|
||
|
||
|
||
def _map_stats(row: dict) -> dict:
|
||
return {our: row[their] for their, our in _STAT_MAP.items() if their in row}
|
||
|
||
|
||
def wnba_season_avg(player_name: str, stat_type: Optional[str] = None, season: Optional[str] = None) -> Optional[dict]:
|
||
player_id, full_name = _resolve_wnba_player(player_name)
|
||
if player_id is None:
|
||
return None
|
||
|
||
season = season or _wnba_current_season()
|
||
cache_key = f"wnba:season:{player_id}:{season}"
|
||
cached = cache_get(cache_key)
|
||
if cached is not None:
|
||
cached["source"] = "cache"
|
||
if stat_type and stat_type in cached.get("stats", {}):
|
||
cached["stats"] = {stat_type: cached["stats"][stat_type]}
|
||
return cached
|
||
|
||
career = _safe(
|
||
playercareerstats.PlayerCareerStats,
|
||
player_id=player_id,
|
||
league_id_nullable=WNBA_LEAGUE_ID,
|
||
)
|
||
df = career.get_data_frames()[0]
|
||
season_row = df[df["SEASON_ID"] == season]
|
||
|
||
stats = _map_stats(season_row.iloc[0].to_dict()) if not season_row.empty else {}
|
||
|
||
result = {
|
||
"player": full_name,
|
||
"player_id": player_id,
|
||
"team": season_row.iloc[0]["TEAM_ABBREVIATION"] if not season_row.empty else "UNK",
|
||
"season": season,
|
||
"league": "wnba",
|
||
"source": "live",
|
||
"stats": stats,
|
||
}
|
||
cache_set(cache_key, result, SEASON_AVG_TTL)
|
||
|
||
if stat_type and stat_type in stats:
|
||
result["stats"] = {stat_type: stats[stat_type]}
|
||
return result
|
||
|
||
|
||
def wnba_last_n(player_name: str, n: int = 10, stat_type: Optional[str] = None) -> Optional[dict]:
|
||
player_id, full_name = _resolve_wnba_player(player_name)
|
||
if player_id is None:
|
||
return None
|
||
|
||
n = min(max(int(n), 1), 30)
|
||
cache_key = f"wnba:last:{player_id}:{n}"
|
||
cached = cache_get(cache_key)
|
||
if cached is not None:
|
||
cached["source"] = "cache"
|
||
if stat_type and stat_type in cached.get("stats", {}):
|
||
cached["stats"] = {stat_type: cached["stats"][stat_type]}
|
||
return cached
|
||
|
||
season = _wnba_current_season()
|
||
gamelog = _safe(
|
||
playergamelog.PlayerGameLog,
|
||
player_id=player_id,
|
||
season=season,
|
||
league_id_nullable=WNBA_LEAGUE_ID,
|
||
)
|
||
df = gamelog.get_data_frames()[0]
|
||
|
||
if df.empty:
|
||
return {
|
||
"player": full_name,
|
||
"player_id": player_id,
|
||
"team": "UNK",
|
||
"last_n": n,
|
||
"league": "wnba",
|
||
"source": "live",
|
||
"stats": {},
|
||
}
|
||
|
||
recent = df.head(n)
|
||
averages = {our: float(recent[their].mean()) for their, our in _STAT_MAP.items() if their in recent.columns}
|
||
|
||
result = {
|
||
"player": full_name,
|
||
"player_id": player_id,
|
||
"team": str(recent.iloc[0].get("MATCHUP", "")).split(" ")[0] or "UNK",
|
||
"last_n": n,
|
||
"league": "wnba",
|
||
"source": "live",
|
||
"stats": averages,
|
||
}
|
||
cache_set(cache_key, result, LAST_N_TTL)
|
||
|
||
if stat_type and stat_type in averages:
|
||
result["stats"] = {stat_type: averages[stat_type]}
|
||
return result
|