strat-gameplay-webapp/backend/app/services/sba_api_client.py
Cal Corum a87d149788 CLAUDE: Implement game creation and lineup submission workflow
Complete implementation of pre-game setup flow allowing players to create games
and submit lineups before gameplay starts.

Backend Changes:
- Extended games.py with create game, lineup submission, and game start endpoints
- Added teams.py roster endpoint with season filtering
- Enhanced SBA API client with player data fetching and caching
- Comprehensive validation for lineup submission (position conflicts, DH rules)

Frontend Changes:
- Redesigned create.vue with improved team selection and game options
- Enhanced index.vue with active/pending game filtering and navigation
- Added lineup/[id].vue for interactive lineup builder with drag-and-drop
- Implemented auth.client.ts plugin for client-side auth initialization
- Added comprehensive TypeScript types for API contracts
- Updated middleware for better auth handling

Key Features:
- Game creation with home/away team selection
- Full lineup builder with position assignment and batting order
- DH rule validation (pitcher can be excluded from batting order)
- Season-based roster filtering (Season 3)
- Auto-start game when both lineups submitted
- Real-time game list updates

Workflow:
1. Create game → select teams → set options
2. Submit home lineup → validate positions/order
3. Submit away lineup → validate positions/order
4. Game auto-starts → navigates to game page
5. WebSocket connection → loads game state

Ready for Phase F4 - connecting gameplay UI to complete the at-bat loop.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 23:57:03 -06:00

218 lines
7.0 KiB
Python

"""
SBA API client for fetching player and team data.
Integrates with SBA REST API to retrieve player and team information
for use in game lineup display and game creation.
Author: Claude
Date: 2025-01-10
"""
import logging
from typing import Any
import httpx
from app.config import get_settings
from app.models.player_models import SbaPlayer
logger = logging.getLogger(f"{__name__}.SbaApiClient")
settings = get_settings()
class SbaApiClient:
"""Client for SBA API player and team data lookups."""
def __init__(
self,
base_url: str = "https://api.sba.manticorum.com",
api_key: str | None = None,
):
"""
Initialize SBA API client.
Args:
base_url: Base URL for SBA API (default: production)
api_key: Bearer token for API authentication
"""
self.base_url = base_url
self.api_key = api_key or settings.sba_api_key
self.timeout = httpx.Timeout(10.0, connect=5.0)
def _get_headers(self) -> dict[str, str]:
"""Get headers with auth token."""
headers = {}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
return headers
async def get_teams(self, season: int, active_only: bool = True) -> list[dict[str, Any]]:
"""
Fetch teams from SBA API for a specific season.
Args:
season: Season number (e.g., 3 for Season 3)
active_only: If True, filter out IL teams and teams without divisions
Returns:
List of team dictionaries with id, name, abbreviation, etc.
Example:
teams = await client.get_teams(season=3)
for team in teams:
print(f"{team['abbrev']}: {team['lname']}")
"""
url = f"{self.base_url}/teams"
params = {"season": season}
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(
url, headers=self._get_headers(), params=params
)
response.raise_for_status()
data = response.json()
teams = data.get("teams", [])
if active_only:
# Filter out injured list teams (IL suffix) and teams without divisions
teams = [
t for t in teams
if not t["abbrev"].endswith("IL") and t.get("division")
]
logger.info(f"Loaded {len(teams)} teams for season {season}")
return teams
except httpx.HTTPError as e:
logger.error(f"Failed to fetch teams: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error fetching teams: {e}")
raise
async def get_player(self, player_id: int) -> SbaPlayer:
"""
Fetch player data from SBA API.
Args:
player_id: SBA player ID
Returns:
SbaPlayer instance with all player data
Raises:
httpx.HTTPError: If API request fails
Example:
player = await client.get_player(12288)
print(f"Name: {player.name}") # "Ronald Acuna Jr"
print(f"Positions: {player.get_positions()}") # ['RF']
"""
url = f"{self.base_url}/players/{player_id}"
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(url, headers=self._get_headers())
response.raise_for_status()
data = response.json()
player = SbaPlayer.from_api_response(data)
logger.info(f"Loaded player {player_id}: {player.name}")
return player
except httpx.HTTPError as e:
logger.error(f"Failed to fetch player {player_id}: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error fetching player {player_id}: {e}")
raise
async def get_roster(self, team_id: int, season: int) -> list[dict[str, Any]]:
"""
Fetch roster for a specific team from SBA API.
Args:
team_id: Team ID
season: Season number (e.g., 3 for Season 3)
Returns:
List of player dictionaries from the SBA API
Example:
roster = await client.get_roster(team_id=35, season=3)
for player in roster:
print(f"{player['name']}: {player['position']}")
"""
url = f"{self.base_url}/players"
params = {"season": season, "team_id": team_id}
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(
url, headers=self._get_headers(), params=params
)
response.raise_for_status()
data = response.json()
players = data.get("players", [])
count = data.get("count", len(players))
logger.info(f"Loaded {count} players for team {team_id}, season {season}")
return players
except httpx.HTTPError as e:
logger.error(f"Failed to fetch roster for team {team_id}: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error fetching roster: {e}")
raise
async def get_players_batch(self, player_ids: list[int]) -> dict[int, SbaPlayer]:
"""
Fetch multiple players in parallel.
Args:
player_ids: List of SBA player IDs to fetch
Returns:
Dictionary mapping player_id to SbaPlayer instance
Players that fail to load will be omitted from the result
Example:
players = await client.get_players_batch([12288, 12289, 12290])
for player_id, player in players.items():
print(f"{player.name}: {player.get_positions()}")
"""
if not player_ids:
return {}
results: dict[int, SbaPlayer] = {}
async with httpx.AsyncClient(timeout=self.timeout) as client:
for player_id in player_ids:
try:
url = f"{self.base_url}/players/{player_id}"
response = await client.get(url)
response.raise_for_status()
data = response.json()
player = SbaPlayer.from_api_response(data)
results[player_id] = player
except httpx.HTTPError as e:
logger.warning(f"Failed to fetch player {player_id}: {e}")
# Continue with other players
except Exception as e:
logger.warning(f"Unexpected error fetching player {player_id}: {e}")
# Continue with other players
logger.info(f"Loaded {len(results)}/{len(player_ids)} players")
return results
# Singleton instance
sba_api_client = SbaApiClient()