The previous approach accumulated per-game deltas into season stats rows, which was fragile — partial processing corrupted stats, upsert bugs compounded, and there was no self-healing mechanism. Now update_season_stats() recomputes full season totals from all StratPlay rows for each affected player whenever a game is processed. The result replaces whatever was stored, eliminating double-counting and enabling self-healing via force=True. Also fixes: - evolution_evaluator.py: broken PlayerSeasonStats import → queries BattingSeasonStats or PitchingSeasonStats based on card_type - evolution_evaluator.py: r.k → r.strikeouts - test_evolution_models.py, test_postgame_evolution.py: PlayerSeasonStats → BattingSeasonStats (model never existed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
68 lines
2.4 KiB
Python
68 lines
2.4 KiB
Python
"""Season stats API endpoints.
|
|
|
|
Covers WP-13 (Post-Game Callback Integration):
|
|
POST /api/v2/season-stats/update-game/{game_id}
|
|
|
|
Delegates to app.services.season_stats.update_season_stats() which
|
|
aggregates StratPlay and Decision rows for a completed game and
|
|
performs an additive upsert into player_season_stats.
|
|
|
|
Idempotency is enforced by the service layer: re-delivery of the same
|
|
game_id returns {"updated": 0, "skipped": true} without modifying stats.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
from ..dependencies import oauth2_scheme, valid_token
|
|
|
|
router = APIRouter(prefix="/api/v2/season-stats", tags=["season-stats"])
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@router.post("/update-game/{game_id}")
|
|
async def update_game_season_stats(
|
|
game_id: int, force: bool = False, token: str = Depends(oauth2_scheme)
|
|
):
|
|
"""Recalculate season stats from all StratPlay and Decision rows for a game.
|
|
|
|
Calls update_season_stats(game_id, force=force) from the service layer which:
|
|
- Aggregates all StratPlay rows by (player_id, team_id, season)
|
|
- Merges Decision rows into pitching groups
|
|
- Performs an additive ON CONFLICT upsert into player_season_stats
|
|
- Guards against double-counting via the last_game FK check
|
|
|
|
Query params:
|
|
- force: if true, bypasses the idempotency guard and reprocesses a
|
|
previously seen game_id (useful for correcting stats after data fixes)
|
|
|
|
Response: {"updated": N, "skipped": false}
|
|
- N: total player_season_stats rows upserted (batters + pitchers)
|
|
- skipped: true when this game_id was already processed and force=false
|
|
|
|
Errors from the service are logged but re-raised as 500 so the bot
|
|
knows to retry.
|
|
"""
|
|
if not valid_token(token):
|
|
logging.warning("Bad Token: [REDACTED]")
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
|
|
from ..services.season_stats import update_season_stats
|
|
|
|
try:
|
|
result = update_season_stats(game_id, force=force)
|
|
except Exception as exc:
|
|
logger.error("update-game/%d failed: %s", game_id, exc, exc_info=True)
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Season stats update failed for game {game_id}: {exc}",
|
|
)
|
|
|
|
updated = result.get("batters_updated", 0) + result.get("pitchers_updated", 0)
|
|
return {
|
|
"updated": updated,
|
|
"skipped": result.get("skipped", False),
|
|
}
|