strat-gameplay-webapp/backend/app/services/box_score_service.py
Cal Corum a4b99ee53e CLAUDE: Replace black and flake8 with ruff for formatting and linting
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>
2025-11-20 15:33:21 -06:00

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()