""" BoxScoreService - Query materialized views for formatted box scores. Provides game statistics, batting stats, and pitching stats by querying PostgreSQL materialized views created in migration 004. Author: Claude Date: 2025-11-07 """ import logging from typing import Optional, Dict, List from uuid import UUID from sqlalchemy import text from app.database.session import AsyncSessionLocal logger = logging.getLogger(f'{__name__}.BoxScoreService') class BoxScoreService: """ Service for retrieving box score data from materialized views. Queries three materialized views: - game_stats: Team totals and linescore - batting_game_stats: Player batting statistics - pitching_game_stats: Player pitching statistics """ async def get_box_score(self, game_id: UUID) -> Optional[Dict]: """ Get complete box score for a game. Args: game_id: Game identifier Returns: Dictionary with game_stats, batting_stats, and pitching_stats Returns None if game not found Raises: Exception: If database query fails """ async with AsyncSessionLocal() as session: try: # Query all three views game_stats = await self._get_game_stats(session, game_id) if not game_stats: logger.warning(f"No game stats found for game {game_id}") return None batting_stats = await self._get_batting_stats(session, game_id) pitching_stats = await self._get_pitching_stats(session, game_id) logger.info(f"Retrieved box score for game {game_id}: " f"{len(batting_stats)} batters, {len(pitching_stats)} pitchers") return { 'game_stats': game_stats, 'batting_stats': batting_stats, 'pitching_stats': pitching_stats } except Exception as e: logger.error(f"Failed to get box score for game {game_id}: {e}", exc_info=True) raise async def _get_game_stats(self, session, game_id: UUID) -> Optional[Dict]: """ Query game_stats materialized view for team totals and linescore. Args: session: Active database session game_id: Game identifier Returns: Dictionary with game statistics or None if not found """ query = text(""" SELECT game_id, home_runs, away_runs, home_hits, away_hits, home_errors, away_errors, home_linescore, away_linescore FROM game_stats WHERE game_id = :game_id """) result = await session.execute(query, {'game_id': str(game_id)}) row = result.fetchone() if not row: return None return { 'game_id': row[0], 'home_runs': row[1] or 0, 'away_runs': row[2] or 0, 'home_hits': row[3] or 0, 'away_hits': row[4] or 0, 'home_errors': row[5] or 0, 'away_errors': row[6] or 0, 'home_linescore': row[7] or [], 'away_linescore': row[8] or [] } async def _get_batting_stats(self, session, game_id: UUID) -> List[Dict]: """ Query batting_game_stats materialized view for player batting lines. Args: session: Active database session game_id: Game identifier Returns: List of batting statistics dictionaries """ query = text(""" SELECT lineup_id, game_id, player_card_id, pa, ab, run, hit, rbi, double, triple, hr, bb, so, hbp, sac, sb, cs, gidp FROM batting_game_stats WHERE game_id = :game_id ORDER BY lineup_id """) result = await session.execute(query, {'game_id': str(game_id)}) rows = result.fetchall() stats = [] for row in rows: stats.append({ 'lineup_id': row[0], 'game_id': row[1], 'player_card_id': row[2], 'pa': row[3] or 0, 'ab': row[4] or 0, 'run': row[5] or 0, 'hit': row[6] or 0, 'rbi': row[7] or 0, 'double': row[8] or 0, 'triple': row[9] or 0, 'hr': row[10] or 0, 'bb': row[11] or 0, 'so': row[12] or 0, 'hbp': row[13] or 0, 'sac': row[14] or 0, 'sb': row[15] or 0, 'cs': row[16] or 0, 'gidp': row[17] or 0 }) return stats async def _get_pitching_stats(self, session, game_id: UUID) -> List[Dict]: """ Query pitching_game_stats materialized view for player pitching lines. Args: session: Active database session game_id: Game identifier Returns: List of pitching statistics dictionaries """ query = text(""" SELECT lineup_id, game_id, player_card_id, batters_faced, hit_allowed, run_allowed, erun, bb, so, hbp, hr_allowed, wp, ip FROM pitching_game_stats WHERE game_id = :game_id ORDER BY lineup_id """) result = await session.execute(query, {'game_id': str(game_id)}) rows = result.fetchall() stats = [] for row in rows: stats.append({ 'lineup_id': row[0], 'game_id': row[1], 'player_card_id': row[2], 'batters_faced': row[3] or 0, 'hit_allowed': row[4] or 0, 'run_allowed': row[5] or 0, 'erun': row[6] or 0, 'bb': row[7] or 0, 'so': row[8] or 0, 'hbp': row[9] or 0, 'hr_allowed': row[10] or 0, 'wp': row[11] or 0, 'ip': float(row[12]) if row[12] is not None else 0.0 }) return stats # Singleton instance box_score_service = BoxScoreService()