strat-gameplay-webapp/backend/app/services/lineup_service.py
Cal Corum b15f80310b CLAUDE: Add LineupService and SBA API client for player data integration
Created centralized services for SBA player data fetching at lineup creation:

Backend - New Services:
- app/services/sba_api_client.py: REST client for SBA API (api.sba.manticorum.com)
  with batch player fetching and caching support
- app/services/lineup_service.py: High-level service combining DB operations
  with API calls for complete lineup entries with player data

Backend - Refactored Components:
- app/core/game_engine.py: Replaced raw API calls with LineupService,
  reduced _prepare_next_play() from ~50 lines to ~15 lines
- app/core/substitution_manager.py: Updated pinch_hit(), defensive_replace(),
  change_pitcher() to use lineup_service.get_sba_player_data()
- app/models/game_models.py: Added player_name/player_image to LineupPlayerState
- app/services/__init__.py: Exported new LineupService components
- app/websocket/handlers.py: Enhanced lineup state handling

Frontend - SBA League:
- components/Game/CurrentSituation.vue: Restored player images with fallback
  badges (P/B letters) for both mobile and desktop layouts
- components/Game/GameBoard.vue: Enhanced game board visualization
- composables/useGameActions.ts: Updated game action handling
- composables/useWebSocket.ts: Improved WebSocket state management
- pages/games/[id].vue: Enhanced game page with better state handling
- types/game.ts: Updated type definitions
- types/websocket.ts: Added WebSocket type support

Architecture Improvement:
All SBA player data fetching now goes through LineupService:
- Lineup creation: add_sba_player_to_lineup()
- Lineup loading: load_team_lineup_with_player_data()
- Substitutions: get_sba_player_data()

All 739 unit tests pass.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 11:55:18 -06:00

201 lines
6.6 KiB
Python

"""
Lineup Service - High-level lineup operations with player data.
Combines database operations with API calls to provide complete
lineup entries with player data.
Author: Claude
Date: 2025-01-10
"""
import logging
from dataclasses import dataclass
from typing import Optional, List
from uuid import UUID
from app.database.operations import DatabaseOperations
from app.services.sba_api_client import sba_api_client
from app.models.game_models import TeamLineupState, LineupPlayerState
logger = logging.getLogger(f'{__name__}.LineupService')
@dataclass
class LineupEntryWithPlayer:
"""Lineup entry with associated player data."""
lineup_id: int
player_id: int
position: str
batting_order: Optional[int]
is_starter: bool
is_active: bool
# Player data from API
player_name: str
player_image: str
class LineupService:
"""
Service for lineup operations that include player data.
Combines database operations with SBA API calls to provide
complete lineup entries with player names and images.
"""
def __init__(self, db_ops: Optional[DatabaseOperations] = None):
self.db_ops = db_ops or DatabaseOperations()
async def add_sba_player_to_lineup(
self,
game_id: UUID,
team_id: int,
player_id: int,
position: str,
batting_order: Optional[int] = None,
is_starter: bool = True
) -> LineupEntryWithPlayer:
"""
Add SBA player to lineup with player data.
1. Creates lineup entry in database
2. Fetches player data from SBA API
3. Returns combined result
Args:
game_id: Game identifier
team_id: Team identifier
player_id: SBA player ID
position: Defensive position
batting_order: Batting order (1-9) if applicable
is_starter: Whether player is in starting lineup
Returns:
LineupEntryWithPlayer with lineup data + player info
Raises:
HTTPError: If SBA API call fails
SQLAlchemyError: If database operation fails
"""
# Step 1: Create lineup entry in database
lineup = await self.db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=team_id,
player_id=player_id,
position=position,
batting_order=batting_order,
is_starter=is_starter
)
# Step 2: Fetch player data from SBA API
player_name = f"Player #{player_id}"
player_image = ""
try:
player = await sba_api_client.get_player(player_id)
player_name = player.name
player_image = player.get_image_url()
logger.info(f"Loaded player data for {player_id}: {player_name}")
except Exception as e:
logger.warning(f"Failed to fetch player data for {player_id}: {e}")
# Continue with defaults - lineup entry is still valid
# Step 3: Return combined result
return LineupEntryWithPlayer(
lineup_id=lineup.id, # type: ignore[arg-type]
player_id=player_id,
position=position,
batting_order=batting_order,
is_starter=is_starter,
is_active=True,
player_name=player_name,
player_image=player_image
)
async def load_team_lineup_with_player_data(
self,
game_id: UUID,
team_id: int,
league_id: str
) -> Optional[TeamLineupState]:
"""
Load existing team lineup from database with player data.
1. Fetches active lineup from database
2. Fetches player data from SBA API (for SBA league)
3. Returns TeamLineupState with player info populated
Args:
game_id: Game identifier
team_id: Team identifier
league_id: League identifier ('sba' or 'pd')
Returns:
TeamLineupState with player data, or None if no lineup found
"""
# Step 1: Get lineup from database
lineup_entries = await self.db_ops.get_active_lineup(game_id, team_id)
if not lineup_entries:
return None
# Step 2: Fetch player data for SBA league
player_data = {}
if league_id == 'sba':
player_ids = [p.player_id for p in lineup_entries if p.player_id] # type: ignore[misc]
if player_ids:
try:
player_data = await sba_api_client.get_players_batch(player_ids)
logger.info(f"Loaded {len(player_data)}/{len(player_ids)} players for team {team_id}")
except Exception as e:
logger.warning(f"Failed to fetch player data for team {team_id}: {e}")
# Step 3: Build TeamLineupState with player data
players = []
for p in lineup_entries:
player_name = None
player_image = None
if league_id == 'sba' and p.player_id and player_data.get(p.player_id): # type: ignore[arg-type]
player = player_data.get(p.player_id) # type: ignore[arg-type]
player_name = player.name
player_image = player.get_image_url()
players.append(LineupPlayerState(
lineup_id=p.id, # type: ignore[arg-type]
card_id=p.card_id if p.card_id else (p.player_id or 0), # type: ignore[arg-type]
position=p.position, # type: ignore[arg-type]
batting_order=p.batting_order, # type: ignore[arg-type]
is_active=p.is_active, # type: ignore[arg-type]
is_starter=p.is_starter, # type: ignore[arg-type]
player_name=player_name,
player_image=player_image
))
return TeamLineupState(team_id=team_id, players=players)
async def get_sba_player_data(self, player_id: int) -> tuple[str, str]:
"""
Fetch player data for a single SBA player.
Args:
player_id: SBA player ID
Returns:
Tuple of (player_name, player_image)
Returns defaults if API call fails
"""
player_name = f"Player #{player_id}"
player_image = ""
try:
player = await sba_api_client.get_player(player_id)
player_name = player.name
player_image = player.get_image_url()
logger.info(f"Loaded player data for {player_id}: {player_name}")
except Exception as e:
logger.warning(f"Failed to fetch player data for {player_id}: {e}")
return player_name, player_image
# Singleton instance
lineup_service = LineupService()