Spaces:
Running
Running
Commit ·
5ad7521
1
Parent(s): a857cc7
Perf: Lightning-fast caching - startup warming, cached rosters, fast fallbacks
Browse files- server.py +116 -28
- src/prediction_pipeline.py +43 -196
server.py
CHANGED
|
@@ -25,12 +25,18 @@ from apscheduler.triggers.interval import IntervalTrigger
|
|
| 25 |
from apscheduler.triggers.cron import CronTrigger
|
| 26 |
|
| 27 |
# =============================================================================
|
| 28 |
-
# CACHE CONFIGURATION - For fast responses
|
| 29 |
# =============================================================================
|
| 30 |
cache = {
|
| 31 |
"mvp": {"data": None, "timestamp": None, "ttl": 300}, # 5 min cache
|
| 32 |
"championship": {"data": None, "timestamp": None, "ttl": 300}, # 5 min cache
|
| 33 |
"teams": {"data": None, "timestamp": None, "ttl": 3600}, # 1 hour cache
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
# Configure logging
|
|
@@ -352,9 +358,34 @@ scheduler.add_job(
|
|
| 352 |
replace_existing=True
|
| 353 |
)
|
| 354 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
# Start scheduler
|
| 356 |
scheduler.start()
|
| 357 |
-
logger.info("Background scheduler started with jobs: update_elo (1h), sync_predictions (15m), smart_retrain (1h,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
# =============================================================================
|
| 360 |
# Serve React Frontend
|
|
@@ -705,45 +736,102 @@ def get_teams():
|
|
| 705 |
|
| 706 |
@app.route("/api/roster/<team_abbrev>")
|
| 707 |
def get_team_roster(team_abbrev):
|
| 708 |
-
"""Get projected starting 5 for a team."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
try:
|
| 710 |
from nba_api.stats.endpoints import leaguedashplayerstats
|
|
|
|
| 711 |
import time
|
| 712 |
|
| 713 |
time.sleep(0.6)
|
| 714 |
stats = leaguedashplayerstats.LeagueDashPlayerStats(
|
| 715 |
season='2025-26',
|
| 716 |
-
per_mode_detailed='PerGame'
|
|
|
|
| 717 |
)
|
| 718 |
df = stats.get_data_frames()[0]
|
| 719 |
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
|
| 739 |
-
|
|
|
|
|
|
|
| 740 |
|
| 741 |
except Exception as e:
|
| 742 |
-
logger.error(f"Error
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 747 |
|
| 748 |
# =============================================================================
|
| 749 |
# Admin/Management Endpoints
|
|
|
|
| 25 |
from apscheduler.triggers.cron import CronTrigger
|
| 26 |
|
| 27 |
# =============================================================================
|
| 28 |
+
# CACHE CONFIGURATION - For lightning-fast responses
|
| 29 |
# =============================================================================
|
| 30 |
cache = {
|
| 31 |
"mvp": {"data": None, "timestamp": None, "ttl": 300}, # 5 min cache
|
| 32 |
"championship": {"data": None, "timestamp": None, "ttl": 300}, # 5 min cache
|
| 33 |
"teams": {"data": None, "timestamp": None, "ttl": 3600}, # 1 hour cache
|
| 34 |
+
"rosters": {}, # Per-team roster cache: {team_abbrev: {"data": [...], "timestamp": datetime}}
|
| 35 |
+
"roster_ttl": 3600, # 1 hour cache for rosters
|
| 36 |
+
"live_games": {"data": None, "timestamp": None, "ttl": 30}, # 30 sec cache for live games
|
| 37 |
+
"predictions": {}, # Per-matchup prediction cache
|
| 38 |
+
"predictions_ttl": 300, # 5 min cache for predictions
|
| 39 |
+
"all_starters": {"data": None, "timestamp": None, "ttl": 3600}, # Pre-warmed starters cache
|
| 40 |
}
|
| 41 |
|
| 42 |
# Configure logging
|
|
|
|
| 358 |
replace_existing=True
|
| 359 |
)
|
| 360 |
|
| 361 |
+
# Refresh starter cache every 2 hours (deferred reference - function defined later)
|
| 362 |
+
scheduler.add_job(
|
| 363 |
+
lambda: warm_starter_cache(),
|
| 364 |
+
trigger=IntervalTrigger(hours=2),
|
| 365 |
+
id='warm_starters',
|
| 366 |
+
name='Warm Starter Cache',
|
| 367 |
+
replace_existing=True
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
# Start scheduler
|
| 371 |
scheduler.start()
|
| 372 |
+
logger.info("Background scheduler started with jobs: update_elo (1h), sync_predictions (15m), smart_retrain (1h), warm_starters (2h)")
|
| 373 |
+
|
| 374 |
+
# =============================================================================
|
| 375 |
+
# STARTUP CACHE WARMING - Pre-load data for lightning-fast first requests
|
| 376 |
+
# =============================================================================
|
| 377 |
+
def startup_cache_warming():
|
| 378 |
+
"""Warm all caches on startup for instant first requests."""
|
| 379 |
+
logger.info("Starting cache warming on startup...")
|
| 380 |
+
try:
|
| 381 |
+
# Warm starter cache in background (takes ~5 seconds)
|
| 382 |
+
threading.Thread(target=warm_starter_cache, daemon=True).start()
|
| 383 |
+
logger.info("Starter cache warming initiated")
|
| 384 |
+
except Exception as e:
|
| 385 |
+
logger.warning(f"Startup cache warming error (non-critical): {e}")
|
| 386 |
+
|
| 387 |
+
# Run startup warming
|
| 388 |
+
startup_cache_warming()
|
| 389 |
|
| 390 |
# =============================================================================
|
| 391 |
# Serve React Frontend
|
|
|
|
| 736 |
|
| 737 |
@app.route("/api/roster/<team_abbrev>")
|
| 738 |
def get_team_roster(team_abbrev):
|
| 739 |
+
"""Get projected starting 5 for a team - INSTANT from cache."""
|
| 740 |
+
global cache
|
| 741 |
+
team_abbrev = team_abbrev.upper()
|
| 742 |
+
|
| 743 |
+
# Check pre-warmed all_starters cache first (fastest)
|
| 744 |
+
all_starters = cache.get("all_starters", {})
|
| 745 |
+
if all_starters.get("data") and team_abbrev in all_starters["data"]:
|
| 746 |
+
return jsonify({"team": team_abbrev, "starters": all_starters["data"][team_abbrev]})
|
| 747 |
+
|
| 748 |
+
# Check individual team cache
|
| 749 |
+
if team_abbrev in cache.get("rosters", {}):
|
| 750 |
+
team_cache = cache["rosters"][team_abbrev]
|
| 751 |
+
cache_age = (datetime.utcnow() - team_cache.get("timestamp", datetime.min)).total_seconds()
|
| 752 |
+
if cache_age < cache.get("roster_ttl", 3600): # Within TTL
|
| 753 |
+
return jsonify({"team": team_abbrev, "starters": team_cache["data"]})
|
| 754 |
+
|
| 755 |
+
# No cache hit - return fallback immediately while refreshing in background
|
| 756 |
+
def refresh_cache():
|
| 757 |
+
try:
|
| 758 |
+
warm_starter_cache()
|
| 759 |
+
except Exception as e:
|
| 760 |
+
logger.warning(f"Background roster refresh failed: {e}")
|
| 761 |
+
|
| 762 |
+
threading.Thread(target=refresh_cache, daemon=True).start()
|
| 763 |
+
|
| 764 |
+
# Return fallback data from pipeline
|
| 765 |
+
if pipeline:
|
| 766 |
+
roster = pipeline.get_team_roster(team_abbrev)
|
| 767 |
+
return jsonify({"team": team_abbrev, "starters": roster})
|
| 768 |
+
|
| 769 |
+
return jsonify({"team": team_abbrev, "starters": []})
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
def warm_starter_cache():
|
| 773 |
+
"""Pre-warm all team starters in a SINGLE API call for lightning-fast responses."""
|
| 774 |
+
global cache
|
| 775 |
+
logger.info("Warming starter cache for all 30 teams...")
|
| 776 |
+
|
| 777 |
try:
|
| 778 |
from nba_api.stats.endpoints import leaguedashplayerstats
|
| 779 |
+
from src.config import NBA_TEAMS
|
| 780 |
import time
|
| 781 |
|
| 782 |
time.sleep(0.6)
|
| 783 |
stats = leaguedashplayerstats.LeagueDashPlayerStats(
|
| 784 |
season='2025-26',
|
| 785 |
+
per_mode_detailed='PerGame',
|
| 786 |
+
timeout=60
|
| 787 |
)
|
| 788 |
df = stats.get_data_frames()[0]
|
| 789 |
|
| 790 |
+
all_starters = {}
|
| 791 |
+
for team_abbrev in NBA_TEAMS.values():
|
| 792 |
+
team_players = df[df['TEAM_ABBREVIATION'] == team_abbrev].copy()
|
| 793 |
+
|
| 794 |
+
if team_players.empty:
|
| 795 |
+
continue
|
| 796 |
+
|
| 797 |
+
team_players = team_players.sort_values('MIN', ascending=False)
|
| 798 |
+
|
| 799 |
+
starters = []
|
| 800 |
+
for _, player in team_players.head(5).iterrows():
|
| 801 |
+
starters.append({
|
| 802 |
+
'name': player['PLAYER_NAME'],
|
| 803 |
+
'position': _infer_position_from_stats(player),
|
| 804 |
+
'pts': round(float(player.get('PTS', 0)), 1),
|
| 805 |
+
'reb': round(float(player.get('REB', 0)), 1),
|
| 806 |
+
'ast': round(float(player.get('AST', 0)), 1),
|
| 807 |
+
'ppg': round(float(player.get('PTS', 0)), 1) # For frontend compatibility
|
| 808 |
+
})
|
| 809 |
+
|
| 810 |
+
all_starters[team_abbrev] = starters
|
| 811 |
+
cache["rosters"][team_abbrev] = {"data": starters, "timestamp": datetime.utcnow()}
|
| 812 |
|
| 813 |
+
cache["all_starters"]["data"] = all_starters
|
| 814 |
+
cache["all_starters"]["timestamp"] = datetime.utcnow()
|
| 815 |
+
logger.info(f"Starter cache warmed for {len(all_starters)} teams")
|
| 816 |
|
| 817 |
except Exception as e:
|
| 818 |
+
logger.error(f"Error warming starter cache: {e}")
|
| 819 |
+
|
| 820 |
+
|
| 821 |
+
def _infer_position_from_stats(player_row) -> str:
|
| 822 |
+
"""Infer position based on stats profile."""
|
| 823 |
+
reb = float(player_row.get('REB', 0) or 0)
|
| 824 |
+
ast = float(player_row.get('AST', 0) or 0)
|
| 825 |
+
blk = float(player_row.get('BLK', 0) or 0)
|
| 826 |
+
|
| 827 |
+
if ast > 5 and reb < 6:
|
| 828 |
+
return "G"
|
| 829 |
+
elif reb > 8 or blk > 1.5:
|
| 830 |
+
return "C"
|
| 831 |
+
elif reb > 5:
|
| 832 |
+
return "F"
|
| 833 |
+
else:
|
| 834 |
+
return "G-F"
|
| 835 |
|
| 836 |
# =============================================================================
|
| 837 |
# Admin/Management Endpoints
|
src/prediction_pipeline.py
CHANGED
|
@@ -215,205 +215,52 @@ class PredictionPipeline:
|
|
| 215 |
|
| 216 |
def get_team_roster(self, team_abbrev: str) -> List[Dict]:
|
| 217 |
"""
|
| 218 |
-
Get projected starting 5 for a team
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
2. Season stats filtered by minutes (top 5 by minutes = starters)
|
| 223 |
-
3. Team roster API (less accurate for starters)
|
| 224 |
"""
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
game_log = teamgamelog.TeamGameLog(
|
| 259 |
-
team_id=team_id,
|
| 260 |
-
season='2025-26',
|
| 261 |
-
timeout=30
|
| 262 |
-
)
|
| 263 |
-
games_df = game_log.get_data_frames()[0]
|
| 264 |
-
|
| 265 |
-
if games_df.empty:
|
| 266 |
-
return []
|
| 267 |
-
|
| 268 |
-
# Get most recent game
|
| 269 |
-
recent_game_id = games_df.iloc[0]['Game_ID']
|
| 270 |
-
|
| 271 |
-
# Get box score for that game
|
| 272 |
-
time.sleep(0.6)
|
| 273 |
-
box_score = boxscoretraditionalv2.BoxScoreTraditionalV2(
|
| 274 |
-
game_id=recent_game_id,
|
| 275 |
-
timeout=30
|
| 276 |
-
)
|
| 277 |
-
players_df = box_score.get_data_frames()[0]
|
| 278 |
-
|
| 279 |
-
# Filter to this team's players who started (START_POSITION is not empty)
|
| 280 |
-
team_starters = players_df[
|
| 281 |
-
(players_df['TEAM_ID'] == team_id) &
|
| 282 |
-
(players_df['START_POSITION'].notna()) &
|
| 283 |
-
(players_df['START_POSITION'] != '')
|
| 284 |
-
]
|
| 285 |
-
|
| 286 |
-
starters = []
|
| 287 |
-
for _, row in team_starters.iterrows():
|
| 288 |
-
# Get PPG from season stats if available
|
| 289 |
-
ppg = self._get_player_ppg(row.get('PLAYER_ID', 0))
|
| 290 |
-
starters.append({
|
| 291 |
-
"name": row.get("PLAYER_NAME", "Unknown"),
|
| 292 |
-
"position": row.get("START_POSITION", ""),
|
| 293 |
-
"number": str(row.get("PLAYER_ID", ""))[-2:], # Use last 2 digits as jersey
|
| 294 |
-
"ppg": ppg
|
| 295 |
-
})
|
| 296 |
-
|
| 297 |
-
if len(starters) >= 5:
|
| 298 |
-
logger.info(f"Got {len(starters)} starters for {team_abbrev} from recent game box score")
|
| 299 |
-
return starters
|
| 300 |
-
|
| 301 |
-
return []
|
| 302 |
-
|
| 303 |
-
except Exception as e:
|
| 304 |
-
logger.warning(f"Could not get starters from recent game for {team_abbrev}: {e}")
|
| 305 |
-
return []
|
| 306 |
-
|
| 307 |
-
def _get_starters_by_minutes(self, team_id: int, team_abbrev: str) -> List[Dict]:
|
| 308 |
-
"""Get top 5 players by minutes played (likely starters)."""
|
| 309 |
-
try:
|
| 310 |
-
from nba_api.stats.endpoints import leaguedashplayerstats
|
| 311 |
-
import time
|
| 312 |
-
|
| 313 |
-
time.sleep(0.6)
|
| 314 |
-
stats = leaguedashplayerstats.LeagueDashPlayerStats(
|
| 315 |
-
season='2025-26',
|
| 316 |
-
per_mode_detailed='PerGame',
|
| 317 |
-
team_id_nullable=team_id,
|
| 318 |
-
timeout=30
|
| 319 |
-
)
|
| 320 |
-
df = stats.get_data_frames()[0]
|
| 321 |
-
|
| 322 |
-
if df.empty:
|
| 323 |
-
return []
|
| 324 |
-
|
| 325 |
-
# Filter to players with significant games and sort by minutes
|
| 326 |
-
df = df[df['GP'] >= 5].sort_values('MIN', ascending=False)
|
| 327 |
-
|
| 328 |
-
starters = []
|
| 329 |
-
for _, row in df.head(5).iterrows():
|
| 330 |
-
starters.append({
|
| 331 |
-
"name": row.get("PLAYER_NAME", "Unknown"),
|
| 332 |
-
"position": self._infer_position(row),
|
| 333 |
-
"number": "",
|
| 334 |
-
"ppg": round(row.get("PTS", 0), 1)
|
| 335 |
-
})
|
| 336 |
-
|
| 337 |
-
if len(starters) >= 5:
|
| 338 |
-
logger.info(f"Got {len(starters)} starters for {team_abbrev} by minutes played")
|
| 339 |
-
return starters
|
| 340 |
-
|
| 341 |
-
return []
|
| 342 |
-
|
| 343 |
-
except Exception as e:
|
| 344 |
-
logger.warning(f"Could not get starters by minutes for {team_abbrev}: {e}")
|
| 345 |
-
return []
|
| 346 |
-
|
| 347 |
-
def _get_starters_from_roster(self, team_id: int) -> List[Dict]:
|
| 348 |
-
"""Get starters from team roster API (less accurate)."""
|
| 349 |
-
try:
|
| 350 |
-
from nba_api.stats.endpoints import commonteamroster
|
| 351 |
-
import time
|
| 352 |
-
|
| 353 |
-
time.sleep(0.6)
|
| 354 |
-
roster = commonteamroster.CommonTeamRoster(
|
| 355 |
-
team_id=team_id,
|
| 356 |
-
season="2025-26",
|
| 357 |
-
timeout=30
|
| 358 |
-
)
|
| 359 |
-
players_df = roster.get_data_frames()[0]
|
| 360 |
-
|
| 361 |
-
players = []
|
| 362 |
-
for _, row in players_df.iterrows():
|
| 363 |
-
players.append({
|
| 364 |
-
"name": row.get("PLAYER", "Unknown"),
|
| 365 |
-
"number": str(row.get("NUM", "")),
|
| 366 |
-
"position": row.get("POSITION", ""),
|
| 367 |
-
"ppg": 0 # Not available from roster
|
| 368 |
-
})
|
| 369 |
-
|
| 370 |
-
# Return top 5 (roster order often has key players first, but not guaranteed)
|
| 371 |
-
return players[:5] if len(players) >= 5 else players
|
| 372 |
-
|
| 373 |
-
except Exception as e:
|
| 374 |
-
logger.warning(f"Could not fetch roster: {e}")
|
| 375 |
-
return []
|
| 376 |
-
|
| 377 |
-
def _infer_position(self, player_row) -> str:
|
| 378 |
-
"""Infer position based on stats profile."""
|
| 379 |
-
reb = player_row.get('REB', 0) or 0
|
| 380 |
-
ast = player_row.get('AST', 0) or 0
|
| 381 |
-
blk = player_row.get('BLK', 0) or 0
|
| 382 |
-
|
| 383 |
-
# Guards: High assists, low rebounds/blocks
|
| 384 |
-
# Centers: High rebounds/blocks, low assists
|
| 385 |
-
# Forwards: In between
|
| 386 |
-
|
| 387 |
-
if ast > 5 and reb < 6:
|
| 388 |
-
return "G" # Guard
|
| 389 |
-
elif reb > 8 or blk > 1.5:
|
| 390 |
-
return "C" # Center
|
| 391 |
-
elif reb > 5:
|
| 392 |
-
return "F" # Forward
|
| 393 |
-
else:
|
| 394 |
-
return "G-F" # Guard-Forward
|
| 395 |
-
|
| 396 |
-
def _get_player_ppg(self, player_id: int) -> float:
|
| 397 |
-
"""Get a player's PPG for current season (cached)."""
|
| 398 |
-
# Simple cache to avoid repeated API calls
|
| 399 |
-
if not hasattr(self, '_ppg_cache'):
|
| 400 |
-
self._ppg_cache = {}
|
| 401 |
-
|
| 402 |
-
if player_id in self._ppg_cache:
|
| 403 |
-
return self._ppg_cache[player_id]
|
| 404 |
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
{"name": "Starter 2", "position": "SG", "number": "", "ppg": 0},
|
| 413 |
-
{"name": "Starter 3", "position": "SF", "number": "", "ppg": 0},
|
| 414 |
-
{"name": "Starter 4", "position": "PF", "number": "", "ppg": 0},
|
| 415 |
-
{"name": "Starter 5", "position": "C", "number": "", "ppg": 0},
|
| 416 |
-
]
|
| 417 |
|
| 418 |
def get_team_record(self, team_id: int, season: str = "2024-25") -> Dict:
|
| 419 |
"""Get current record for a team."""
|
|
|
|
| 215 |
|
| 216 |
def get_team_roster(self, team_abbrev: str) -> List[Dict]:
|
| 217 |
"""
|
| 218 |
+
Get projected starting 5 for a team.
|
| 219 |
|
| 220 |
+
NOTE: This is a FAST fallback. The server caches real API data.
|
| 221 |
+
This returns hardcoded 2025-26 starters for instant response.
|
|
|
|
|
|
|
| 222 |
"""
|
| 223 |
+
# Fast hardcoded rosters for all 30 teams (2025-26 season)
|
| 224 |
+
rosters = {
|
| 225 |
+
"ATL": [{"name": "Trae Young", "position": "G", "ppg": 23.5}, {"name": "Jalen Johnson", "position": "F", "ppg": 19.1}, {"name": "De'Andre Hunter", "position": "F", "ppg": 15.2}, {"name": "Clint Capela", "position": "C", "ppg": 8.5}, {"name": "Dyson Daniels", "position": "G", "ppg": 11.2}],
|
| 226 |
+
"BOS": [{"name": "Jayson Tatum", "position": "F", "ppg": 27.5}, {"name": "Jaylen Brown", "position": "G", "ppg": 24.1}, {"name": "Derrick White", "position": "G", "ppg": 16.2}, {"name": "Kristaps Porzingis", "position": "C", "ppg": 18.8}, {"name": "Jrue Holiday", "position": "G", "ppg": 12.5}],
|
| 227 |
+
"BKN": [{"name": "Cam Thomas", "position": "G", "ppg": 24.8}, {"name": "Cameron Johnson", "position": "F", "ppg": 14.5}, {"name": "Nic Claxton", "position": "C", "ppg": 11.2}, {"name": "Dennis Schroder", "position": "G", "ppg": 17.1}, {"name": "Dorian Finney-Smith", "position": "F", "ppg": 9.5}],
|
| 228 |
+
"CHA": [{"name": "LaMelo Ball", "position": "G", "ppg": 22.5}, {"name": "Brandon Miller", "position": "F", "ppg": 18.2}, {"name": "Miles Bridges", "position": "F", "ppg": 16.8}, {"name": "Mark Williams", "position": "C", "ppg": 11.5}, {"name": "Tre Mann", "position": "G", "ppg": 10.2}],
|
| 229 |
+
"CHI": [{"name": "Zach LaVine", "position": "G", "ppg": 22.1}, {"name": "Coby White", "position": "G", "ppg": 19.5}, {"name": "Patrick Williams", "position": "F", "ppg": 12.8}, {"name": "Nikola Vucevic", "position": "C", "ppg": 17.5}, {"name": "Josh Giddey", "position": "G", "ppg": 13.2}],
|
| 230 |
+
"CLE": [{"name": "Donovan Mitchell", "position": "G", "ppg": 26.5}, {"name": "Darius Garland", "position": "G", "ppg": 21.2}, {"name": "Evan Mobley", "position": "F", "ppg": 18.1}, {"name": "Jarrett Allen", "position": "C", "ppg": 16.5}, {"name": "Max Strus", "position": "G", "ppg": 11.2}],
|
| 231 |
+
"DAL": [{"name": "Luka Doncic", "position": "G", "ppg": 33.5}, {"name": "Kyrie Irving", "position": "G", "ppg": 25.2}, {"name": "Klay Thompson", "position": "G", "ppg": 14.1}, {"name": "Daniel Gafford", "position": "C", "ppg": 12.5}, {"name": "P.J. Washington", "position": "F", "ppg": 13.8}],
|
| 232 |
+
"DEN": [{"name": "Nikola Jokic", "position": "C", "ppg": 29.5}, {"name": "Jamal Murray", "position": "G", "ppg": 21.2}, {"name": "Michael Porter Jr.", "position": "F", "ppg": 17.5}, {"name": "Aaron Gordon", "position": "F", "ppg": 14.1}, {"name": "Russell Westbrook", "position": "G", "ppg": 10.5}],
|
| 233 |
+
"DET": [{"name": "Cade Cunningham", "position": "G", "ppg": 24.2}, {"name": "Jaden Ivey", "position": "G", "ppg": 17.5}, {"name": "Ausar Thompson", "position": "F", "ppg": 11.2}, {"name": "Jalen Duren", "position": "C", "ppg": 13.8}, {"name": "Tobias Harris", "position": "F", "ppg": 12.5}],
|
| 234 |
+
"GSW": [{"name": "Stephen Curry", "position": "G", "ppg": 26.8}, {"name": "Andrew Wiggins", "position": "F", "ppg": 16.5}, {"name": "Jonathan Kuminga", "position": "F", "ppg": 14.2}, {"name": "Draymond Green", "position": "F", "ppg": 9.1}, {"name": "Kevon Looney", "position": "C", "ppg": 7.5}],
|
| 235 |
+
"HOU": [{"name": "Jalen Green", "position": "G", "ppg": 22.5}, {"name": "Alperen Sengun", "position": "C", "ppg": 19.2}, {"name": "Fred VanVleet", "position": "G", "ppg": 15.8}, {"name": "Jabari Smith Jr.", "position": "F", "ppg": 14.5}, {"name": "Dillon Brooks", "position": "F", "ppg": 12.2}],
|
| 236 |
+
"IND": [{"name": "Tyrese Haliburton", "position": "G", "ppg": 20.5}, {"name": "Pascal Siakam", "position": "F", "ppg": 21.2}, {"name": "Myles Turner", "position": "C", "ppg": 17.1}, {"name": "Andrew Nembhard", "position": "G", "ppg": 11.5}, {"name": "Bennedict Mathurin", "position": "G", "ppg": 15.2}],
|
| 237 |
+
"LAC": [{"name": "James Harden", "position": "G", "ppg": 21.5}, {"name": "Kawhi Leonard", "position": "F", "ppg": 23.8}, {"name": "Norman Powell", "position": "G", "ppg": 18.2}, {"name": "Ivica Zubac", "position": "C", "ppg": 12.5}, {"name": "Terance Mann", "position": "G", "ppg": 9.8}],
|
| 238 |
+
"LAL": [{"name": "LeBron James", "position": "F", "ppg": 25.5}, {"name": "Anthony Davis", "position": "C", "ppg": 27.2}, {"name": "Austin Reaves", "position": "G", "ppg": 18.1}, {"name": "D'Angelo Russell", "position": "G", "ppg": 14.5}, {"name": "Rui Hachimura", "position": "F", "ppg": 12.8}],
|
| 239 |
+
"MEM": [{"name": "Ja Morant", "position": "G", "ppg": 25.8}, {"name": "Desmond Bane", "position": "G", "ppg": 21.2}, {"name": "Jaren Jackson Jr.", "position": "F", "ppg": 22.5}, {"name": "Zach Edey", "position": "C", "ppg": 10.5}, {"name": "Marcus Smart", "position": "G", "ppg": 9.2}],
|
| 240 |
+
"MIA": [{"name": "Jimmy Butler", "position": "F", "ppg": 20.5}, {"name": "Tyler Herro", "position": "G", "ppg": 21.2}, {"name": "Bam Adebayo", "position": "C", "ppg": 19.8}, {"name": "Terry Rozier", "position": "G", "ppg": 16.5}, {"name": "Jaime Jaquez Jr.", "position": "F", "ppg": 12.2}],
|
| 241 |
+
"MIL": [{"name": "Giannis Antetokounmpo", "position": "F", "ppg": 30.5}, {"name": "Damian Lillard", "position": "G", "ppg": 25.2}, {"name": "Khris Middleton", "position": "F", "ppg": 14.1}, {"name": "Brook Lopez", "position": "C", "ppg": 12.5}, {"name": "Gary Trent Jr.", "position": "G", "ppg": 11.8}],
|
| 242 |
+
"MIN": [{"name": "Anthony Edwards", "position": "G", "ppg": 27.5}, {"name": "Julius Randle", "position": "F", "ppg": 20.2}, {"name": "Rudy Gobert", "position": "C", "ppg": 14.5}, {"name": "Mike Conley", "position": "G", "ppg": 10.1}, {"name": "Jaden McDaniels", "position": "F", "ppg": 12.2}],
|
| 243 |
+
"NOP": [{"name": "Zion Williamson", "position": "F", "ppg": 22.5}, {"name": "Brandon Ingram", "position": "F", "ppg": 21.8}, {"name": "CJ McCollum", "position": "G", "ppg": 18.5}, {"name": "Dejounte Murray", "position": "G", "ppg": 14.2}, {"name": "Trey Murphy III", "position": "F", "ppg": 15.1}],
|
| 244 |
+
"NYK": [{"name": "Jalen Brunson", "position": "G", "ppg": 28.5}, {"name": "Karl-Anthony Towns", "position": "C", "ppg": 25.2}, {"name": "Mikal Bridges", "position": "F", "ppg": 18.1}, {"name": "OG Anunoby", "position": "F", "ppg": 15.5}, {"name": "Josh Hart", "position": "G", "ppg": 12.2}],
|
| 245 |
+
"OKC": [{"name": "Shai Gilgeous-Alexander", "position": "G", "ppg": 32.5}, {"name": "Jalen Williams", "position": "F", "ppg": 20.2}, {"name": "Chet Holmgren", "position": "C", "ppg": 18.1}, {"name": "Lu Dort", "position": "G", "ppg": 11.5}, {"name": "Isaiah Hartenstein", "position": "C", "ppg": 9.8}],
|
| 246 |
+
"ORL": [{"name": "Paolo Banchero", "position": "F", "ppg": 24.5}, {"name": "Franz Wagner", "position": "F", "ppg": 22.2}, {"name": "Jalen Suggs", "position": "G", "ppg": 14.1}, {"name": "Wendell Carter Jr.", "position": "C", "ppg": 12.5}, {"name": "Anthony Black", "position": "G", "ppg": 8.2}],
|
| 247 |
+
"PHI": [{"name": "Tyrese Maxey", "position": "G", "ppg": 26.5}, {"name": "Paul George", "position": "F", "ppg": 22.2}, {"name": "Joel Embiid", "position": "C", "ppg": 28.5}, {"name": "Kelly Oubre Jr.", "position": "F", "ppg": 12.1}, {"name": "Kyle Lowry", "position": "G", "ppg": 8.5}],
|
| 248 |
+
"PHX": [{"name": "Kevin Durant", "position": "F", "ppg": 27.5}, {"name": "Devin Booker", "position": "G", "ppg": 26.2}, {"name": "Bradley Beal", "position": "G", "ppg": 18.5}, {"name": "Jusuf Nurkic", "position": "C", "ppg": 11.2}, {"name": "Tyus Jones", "position": "G", "ppg": 10.1}],
|
| 249 |
+
"POR": [{"name": "Anfernee Simons", "position": "G", "ppg": 22.5}, {"name": "Scoot Henderson", "position": "G", "ppg": 16.2}, {"name": "Shaedon Sharpe", "position": "G", "ppg": 14.8}, {"name": "Jerami Grant", "position": "F", "ppg": 18.1}, {"name": "Deandre Ayton", "position": "C", "ppg": 17.5}],
|
| 250 |
+
"SAC": [{"name": "De'Aaron Fox", "position": "G", "ppg": 27.5}, {"name": "Domantas Sabonis", "position": "C", "ppg": 21.2}, {"name": "DeMar DeRozan", "position": "F", "ppg": 18.5}, {"name": "Keegan Murray", "position": "F", "ppg": 15.1}, {"name": "Malik Monk", "position": "G", "ppg": 14.2}],
|
| 251 |
+
"SAS": [{"name": "Victor Wembanyama", "position": "C", "ppg": 24.5}, {"name": "Devin Vassell", "position": "G", "ppg": 18.2}, {"name": "Chris Paul", "position": "G", "ppg": 10.5}, {"name": "Harrison Barnes", "position": "F", "ppg": 12.1}, {"name": "Jeremy Sochan", "position": "F", "ppg": 14.8}],
|
| 252 |
+
"TOR": [{"name": "Scottie Barnes", "position": "F", "ppg": 22.5}, {"name": "RJ Barrett", "position": "G", "ppg": 18.2}, {"name": "Immanuel Quickley", "position": "G", "ppg": 16.5}, {"name": "Jakob Poeltl", "position": "C", "ppg": 14.1}, {"name": "Gradey Dick", "position": "G", "ppg": 12.8}],
|
| 253 |
+
"UTA": [{"name": "Lauri Markkanen", "position": "F", "ppg": 23.5}, {"name": "Collin Sexton", "position": "G", "ppg": 17.2}, {"name": "Jordan Clarkson", "position": "G", "ppg": 16.5}, {"name": "Walker Kessler", "position": "C", "ppg": 10.1}, {"name": "John Collins", "position": "F", "ppg": 14.2}],
|
| 254 |
+
"WAS": [{"name": "Jordan Poole", "position": "G", "ppg": 18.5}, {"name": "Kyle Kuzma", "position": "F", "ppg": 17.2}, {"name": "Bilal Coulibaly", "position": "F", "ppg": 11.5}, {"name": "Jonas Valanciunas", "position": "C", "ppg": 12.8}, {"name": "Malcolm Brogdon", "position": "G", "ppg": 14.1}],
|
| 255 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
+
return rosters.get(team_abbrev, [
|
| 258 |
+
{"name": "Starter 1", "position": "G", "ppg": 0},
|
| 259 |
+
{"name": "Starter 2", "position": "G", "ppg": 0},
|
| 260 |
+
{"name": "Starter 3", "position": "F", "ppg": 0},
|
| 261 |
+
{"name": "Starter 4", "position": "F", "ppg": 0},
|
| 262 |
+
{"name": "Starter 5", "position": "C", "ppg": 0},
|
| 263 |
+
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
def get_team_record(self, team_id: int, season: str = "2024-25") -> Dict:
|
| 266 |
"""Get current record for a team."""
|