Migrated to ruff for faster, modern code formatting and linting: Configuration changes: - pyproject.toml: Added ruff 0.8.6, removed black/flake8 - Configured ruff with black-compatible formatting (88 chars) - Enabled comprehensive linting rules (pycodestyle, pyflakes, isort, pyupgrade, bugbear, comprehensions, simplify, return) - Updated CLAUDE.md: Changed code quality commands to use ruff Code improvements (490 auto-fixes): - Modernized type hints: List[T] → list[T], Dict[K,V] → dict[K,V], Optional[T] → T | None - Sorted all imports (isort integration) - Removed unused imports - Fixed whitespace issues - Reformatted 38 files for consistency Bug fixes: - app/core/play_resolver.py: Fixed type hint bug (any → Any) - tests/unit/core/test_runner_advancement.py: Removed obsolete random mock Testing: - All 739 unit tests passing (100%) - No regressions introduced 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
243 lines
6.9 KiB
Python
243 lines
6.9 KiB
Python
"""
|
|
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()
|