""" 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 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) -> dict | None: """ 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) -> dict | None: """ 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()