Some checks failed
Build Docker Image / build (push) Has been cancelled
Separate batting and pitching into distinct tables with descriptive column names. Eliminates naming collisions (so/k ambiguity) and column mismatches between the ORM model and raw SQL. Each table now covers all aggregatable fields from its source (BattingStat/PitchingStat) including sac, ibb, gidp, earned_runs, runs_allowed, wild_pitches, balks, and games_started. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
233 lines
8.3 KiB
Python
233 lines
8.3 KiB
Python
"""Season stats API endpoints.
|
|
|
|
Covers WP-13 (Post-Game Callback Integration):
|
|
POST /api/v2/season-stats/update-game/{game_id}
|
|
|
|
Aggregates BattingStat and PitchingStat rows for a completed game and
|
|
increments the corresponding batting_season_stats / pitching_season_stats
|
|
rows via an additive upsert.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
from ..db_engine import db
|
|
from ..dependencies import oauth2_scheme, valid_token
|
|
|
|
router = APIRouter(prefix="/api/v2/season-stats", tags=["season-stats"])
|
|
|
|
|
|
def _ip_to_outs(ip: float) -> int:
|
|
"""Convert innings-pitched float (e.g. 6.1) to integer outs (e.g. 19).
|
|
|
|
Baseball stores IP as whole.partial where the fractional digit is outs
|
|
(0, 1, or 2), not tenths. 6.1 = 6 innings + 1 out = 19 outs.
|
|
"""
|
|
whole = int(ip)
|
|
partial = round((ip - whole) * 10)
|
|
return whole * 3 + partial
|
|
|
|
|
|
@router.post("/update-game/{game_id}")
|
|
async def update_game_season_stats(game_id: int, token: str = Depends(oauth2_scheme)):
|
|
"""Increment season stats with batting and pitching deltas from a game.
|
|
|
|
Queries BattingStat and PitchingStat rows for game_id, aggregates by
|
|
(player_id, team_id, season), then performs an additive ON CONFLICT upsert
|
|
into batting_season_stats and pitching_season_stats respectively.
|
|
|
|
Replaying the same game_id will double-count stats, so callers must ensure
|
|
this is only called once per game.
|
|
|
|
Response: {"updated": N} where N is the number of player rows touched.
|
|
"""
|
|
if not valid_token(token):
|
|
logging.warning("Bad Token: [REDACTED]")
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
|
|
updated = 0
|
|
|
|
# --- Batting ---
|
|
bat_rows = list(
|
|
db.execute_sql(
|
|
"""
|
|
SELECT c.player_id, bs.team_id, bs.season,
|
|
SUM(bs.pa), SUM(bs.ab), SUM(bs.run), SUM(bs.hit),
|
|
SUM(bs.double), SUM(bs.triple), SUM(bs.hr), SUM(bs.rbi),
|
|
SUM(bs.bb), SUM(bs.so), SUM(bs.hbp), SUM(bs.sac),
|
|
SUM(bs.ibb), SUM(bs.gidp), SUM(bs.sb), SUM(bs.cs)
|
|
FROM battingstat bs
|
|
JOIN card c ON bs.card_id = c.id
|
|
WHERE bs.game_id = %s
|
|
GROUP BY c.player_id, bs.team_id, bs.season
|
|
""",
|
|
(game_id,),
|
|
)
|
|
)
|
|
|
|
for row in bat_rows:
|
|
(
|
|
player_id,
|
|
team_id,
|
|
season,
|
|
pa,
|
|
ab,
|
|
runs,
|
|
hits,
|
|
doubles,
|
|
triples,
|
|
hr,
|
|
rbi,
|
|
bb,
|
|
strikeouts,
|
|
hbp,
|
|
sac,
|
|
ibb,
|
|
gidp,
|
|
sb,
|
|
cs,
|
|
) = row
|
|
db.execute_sql(
|
|
"""
|
|
INSERT INTO batting_season_stats
|
|
(player_id, team_id, season,
|
|
pa, ab, runs, hits, doubles, triples, hr, rbi,
|
|
bb, strikeouts, hbp, sac, ibb, gidp, sb, cs)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT (player_id, team_id, season) DO UPDATE SET
|
|
pa = batting_season_stats.pa + EXCLUDED.pa,
|
|
ab = batting_season_stats.ab + EXCLUDED.ab,
|
|
runs = batting_season_stats.runs + EXCLUDED.runs,
|
|
hits = batting_season_stats.hits + EXCLUDED.hits,
|
|
doubles = batting_season_stats.doubles + EXCLUDED.doubles,
|
|
triples = batting_season_stats.triples + EXCLUDED.triples,
|
|
hr = batting_season_stats.hr + EXCLUDED.hr,
|
|
rbi = batting_season_stats.rbi + EXCLUDED.rbi,
|
|
bb = batting_season_stats.bb + EXCLUDED.bb,
|
|
strikeouts= batting_season_stats.strikeouts+ EXCLUDED.strikeouts,
|
|
hbp = batting_season_stats.hbp + EXCLUDED.hbp,
|
|
sac = batting_season_stats.sac + EXCLUDED.sac,
|
|
ibb = batting_season_stats.ibb + EXCLUDED.ibb,
|
|
gidp = batting_season_stats.gidp + EXCLUDED.gidp,
|
|
sb = batting_season_stats.sb + EXCLUDED.sb,
|
|
cs = batting_season_stats.cs + EXCLUDED.cs
|
|
""",
|
|
(
|
|
player_id,
|
|
team_id,
|
|
season,
|
|
pa,
|
|
ab,
|
|
runs,
|
|
hits,
|
|
doubles,
|
|
triples,
|
|
hr,
|
|
rbi,
|
|
bb,
|
|
strikeouts,
|
|
hbp,
|
|
sac,
|
|
ibb,
|
|
gidp,
|
|
sb,
|
|
cs,
|
|
),
|
|
)
|
|
updated += 1
|
|
|
|
# --- Pitching ---
|
|
pit_rows = list(
|
|
db.execute_sql(
|
|
"""
|
|
SELECT c.player_id, ps.team_id, ps.season,
|
|
SUM(ps.ip), SUM(ps.so), SUM(ps.hit), SUM(ps.run), SUM(ps.erun),
|
|
SUM(ps.bb), SUM(ps.hbp), SUM(ps.wp), SUM(ps.balk), SUM(ps.hr),
|
|
SUM(ps.gs), SUM(ps.win), SUM(ps.loss), SUM(ps.hold),
|
|
SUM(ps.sv), SUM(ps.bsv)
|
|
FROM pitchingstat ps
|
|
JOIN card c ON ps.card_id = c.id
|
|
WHERE ps.game_id = %s
|
|
GROUP BY c.player_id, ps.team_id, ps.season
|
|
""",
|
|
(game_id,),
|
|
)
|
|
)
|
|
|
|
for row in pit_rows:
|
|
(
|
|
player_id,
|
|
team_id,
|
|
season,
|
|
ip,
|
|
strikeouts,
|
|
hits_allowed,
|
|
runs_allowed,
|
|
earned_runs,
|
|
bb,
|
|
hbp,
|
|
wild_pitches,
|
|
balks,
|
|
hr_allowed,
|
|
games_started,
|
|
wins,
|
|
losses,
|
|
holds,
|
|
saves,
|
|
blown_saves,
|
|
) = row
|
|
outs = _ip_to_outs(float(ip))
|
|
db.execute_sql(
|
|
"""
|
|
INSERT INTO pitching_season_stats
|
|
(player_id, team_id, season,
|
|
outs, strikeouts, hits_allowed, runs_allowed, earned_runs,
|
|
bb, hbp, wild_pitches, balks, hr_allowed,
|
|
games_started, wins, losses, holds, saves, blown_saves)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT (player_id, team_id, season) DO UPDATE SET
|
|
outs = pitching_season_stats.outs + EXCLUDED.outs,
|
|
strikeouts = pitching_season_stats.strikeouts + EXCLUDED.strikeouts,
|
|
hits_allowed= pitching_season_stats.hits_allowed+ EXCLUDED.hits_allowed,
|
|
runs_allowed= pitching_season_stats.runs_allowed+ EXCLUDED.runs_allowed,
|
|
earned_runs = pitching_season_stats.earned_runs + EXCLUDED.earned_runs,
|
|
bb = pitching_season_stats.bb + EXCLUDED.bb,
|
|
hbp = pitching_season_stats.hbp + EXCLUDED.hbp,
|
|
wild_pitches= pitching_season_stats.wild_pitches+ EXCLUDED.wild_pitches,
|
|
balks = pitching_season_stats.balks + EXCLUDED.balks,
|
|
hr_allowed = pitching_season_stats.hr_allowed + EXCLUDED.hr_allowed,
|
|
games_started= pitching_season_stats.games_started+ EXCLUDED.games_started,
|
|
wins = pitching_season_stats.wins + EXCLUDED.wins,
|
|
losses = pitching_season_stats.losses + EXCLUDED.losses,
|
|
holds = pitching_season_stats.holds + EXCLUDED.holds,
|
|
saves = pitching_season_stats.saves + EXCLUDED.saves,
|
|
blown_saves = pitching_season_stats.blown_saves + EXCLUDED.blown_saves
|
|
""",
|
|
(
|
|
player_id,
|
|
team_id,
|
|
season,
|
|
outs,
|
|
strikeouts,
|
|
hits_allowed,
|
|
runs_allowed,
|
|
earned_runs,
|
|
bb,
|
|
hbp,
|
|
wild_pitches,
|
|
balks,
|
|
hr_allowed,
|
|
games_started,
|
|
wins,
|
|
losses,
|
|
holds,
|
|
saves,
|
|
blown_saves,
|
|
),
|
|
)
|
|
updated += 1
|
|
|
|
logging.info(f"update-game/{game_id}: updated {updated} season stats rows")
|
|
return {"updated": updated}
|