refactor: rename PlayerSeasonStats so to so_batter and k to so_pitcher
All checks were successful
Build Docker Image / build (push) Successful in 8m41s
All checks were successful
Build Docker Image / build (push) Successful in 8m41s
The single-letter `k` field was ambiguous and too short for comfortable use. Rename to `so_pitcher` for clarity, and `so` to `so_batter` to distinguish batting strikeouts from pitching strikeouts in the same model. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6d972114b7
commit
4ed62dea2c
@ -1065,7 +1065,7 @@ class PlayerSeasonStats(BaseModel):
|
||||
triples = IntegerField(default=0)
|
||||
bb = IntegerField(default=0)
|
||||
hbp = IntegerField(default=0)
|
||||
so = IntegerField(default=0)
|
||||
so_batter = IntegerField(default=0)
|
||||
rbi = IntegerField(default=0)
|
||||
runs = IntegerField(default=0)
|
||||
sb = IntegerField(default=0)
|
||||
@ -1074,9 +1074,7 @@ class PlayerSeasonStats(BaseModel):
|
||||
# Pitching stats
|
||||
games_pitching = IntegerField(default=0)
|
||||
outs = IntegerField(default=0)
|
||||
k = IntegerField(
|
||||
default=0
|
||||
) # pitcher Ks; spec names this "so (K)" but renamed to avoid collision with batting so
|
||||
so_pitcher = IntegerField(default=0)
|
||||
bb_allowed = IntegerField(default=0)
|
||||
hits_allowed = IntegerField(default=0)
|
||||
hr_allowed = IntegerField(default=0)
|
||||
|
||||
232
app/routers_v2/season_stats.py
Normal file
232
app/routers_v2/season_stats.py
Normal file
@ -0,0 +1,232 @@
|
||||
"""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 player_season_stats rows via an additive upsert.
|
||||
|
||||
Lazy-imports PlayerSeasonStats so this module loads before WP-05 merges.
|
||||
"""
|
||||
|
||||
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 player_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 player_season_stats. Idempotent: replaying the same game_id a second
|
||||
time 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,
|
||||
r,
|
||||
hits,
|
||||
doubles,
|
||||
triples,
|
||||
hr,
|
||||
rbi,
|
||||
bb,
|
||||
so,
|
||||
hbp,
|
||||
sac,
|
||||
ibb,
|
||||
gidp,
|
||||
sb,
|
||||
cs,
|
||||
) = row
|
||||
db.execute_sql(
|
||||
"""
|
||||
INSERT INTO player_season_stats
|
||||
(player_id, team_id, season,
|
||||
pa, ab, r, hits, doubles, triples, hr, rbi,
|
||||
bb, so_batter, 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 = player_season_stats.pa + EXCLUDED.pa,
|
||||
ab = player_season_stats.ab + EXCLUDED.ab,
|
||||
r = player_season_stats.r + EXCLUDED.r,
|
||||
hits = player_season_stats.hits + EXCLUDED.hits,
|
||||
doubles= player_season_stats.doubles+ EXCLUDED.doubles,
|
||||
triples= player_season_stats.triples+ EXCLUDED.triples,
|
||||
hr = player_season_stats.hr + EXCLUDED.hr,
|
||||
rbi = player_season_stats.rbi + EXCLUDED.rbi,
|
||||
bb = player_season_stats.bb + EXCLUDED.bb,
|
||||
so_batter= player_season_stats.so_batter+ EXCLUDED.so_batter,
|
||||
hbp = player_season_stats.hbp + EXCLUDED.hbp,
|
||||
sac = player_season_stats.sac + EXCLUDED.sac,
|
||||
ibb = player_season_stats.ibb + EXCLUDED.ibb,
|
||||
gidp = player_season_stats.gidp + EXCLUDED.gidp,
|
||||
sb = player_season_stats.sb + EXCLUDED.sb,
|
||||
cs = player_season_stats.cs + EXCLUDED.cs
|
||||
""",
|
||||
(
|
||||
player_id,
|
||||
team_id,
|
||||
season,
|
||||
pa,
|
||||
ab,
|
||||
r,
|
||||
hits,
|
||||
doubles,
|
||||
triples,
|
||||
hr,
|
||||
rbi,
|
||||
bb,
|
||||
so,
|
||||
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,
|
||||
so_pitcher,
|
||||
h_allowed,
|
||||
r_allowed,
|
||||
er,
|
||||
bb_p,
|
||||
hbp_p,
|
||||
wp,
|
||||
balk,
|
||||
hr_p,
|
||||
gs,
|
||||
w,
|
||||
losses,
|
||||
hold,
|
||||
sv,
|
||||
bsv,
|
||||
) = row
|
||||
outs = _ip_to_outs(float(ip))
|
||||
db.execute_sql(
|
||||
"""
|
||||
INSERT INTO player_season_stats
|
||||
(player_id, team_id, season,
|
||||
outs, so_pitcher, h_allowed, r_allowed, er,
|
||||
bb_p, hbp_p, wp, balk, hr_p,
|
||||
gs, w, l, hold, sv, bsv)
|
||||
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 = player_season_stats.outs + EXCLUDED.outs,
|
||||
so_pitcher= player_season_stats.so_pitcher+ EXCLUDED.so_pitcher,
|
||||
h_allowed= player_season_stats.h_allowed+ EXCLUDED.h_allowed,
|
||||
r_allowed= player_season_stats.r_allowed+ EXCLUDED.r_allowed,
|
||||
er = player_season_stats.er + EXCLUDED.er,
|
||||
bb_p = player_season_stats.bb_p + EXCLUDED.bb_p,
|
||||
hbp_p = player_season_stats.hbp_p + EXCLUDED.hbp_p,
|
||||
wp = player_season_stats.wp + EXCLUDED.wp,
|
||||
balk = player_season_stats.balk + EXCLUDED.balk,
|
||||
hr_p = player_season_stats.hr_p + EXCLUDED.hr_p,
|
||||
gs = player_season_stats.gs + EXCLUDED.gs,
|
||||
w = player_season_stats.w + EXCLUDED.w,
|
||||
l = player_season_stats.l + EXCLUDED.l,
|
||||
hold = player_season_stats.hold + EXCLUDED.hold,
|
||||
sv = player_season_stats.sv + EXCLUDED.sv,
|
||||
bsv = player_season_stats.bsv + EXCLUDED.bsv
|
||||
""",
|
||||
(
|
||||
player_id,
|
||||
team_id,
|
||||
season,
|
||||
outs,
|
||||
so_pitcher,
|
||||
h_allowed,
|
||||
r_allowed,
|
||||
er,
|
||||
bb_p,
|
||||
hbp_p,
|
||||
wp,
|
||||
balk,
|
||||
hr_p,
|
||||
gs,
|
||||
w,
|
||||
losses,
|
||||
hold,
|
||||
sv,
|
||||
bsv,
|
||||
),
|
||||
)
|
||||
updated += 1
|
||||
|
||||
logging.info(f"update-game/{game_id}: updated {updated} player_season_stats rows")
|
||||
return {"updated": updated}
|
||||
@ -5,8 +5,8 @@ plus helpers for formula dispatch and tier classification.
|
||||
|
||||
Stats attributes expected by each formula:
|
||||
compute_batter_value: pa, hits, doubles, triples, hr
|
||||
compute_sp_value: outs, k (k = pitcher strikeouts, from PlayerSeasonStats)
|
||||
compute_rp_value: outs, k
|
||||
compute_sp_value: outs, so_pitcher (pitcher strikeouts, from PlayerSeasonStats)
|
||||
compute_rp_value: outs, so_pitcher
|
||||
"""
|
||||
|
||||
from typing import Protocol
|
||||
@ -22,7 +22,7 @@ class BatterStats(Protocol):
|
||||
|
||||
class PitcherStats(Protocol):
|
||||
outs: int
|
||||
k: int
|
||||
so_pitcher: int
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -38,13 +38,13 @@ def compute_batter_value(stats) -> float:
|
||||
|
||||
|
||||
def compute_sp_value(stats) -> float:
|
||||
"""IP + K where IP = outs / 3. Uses stats.k (pitcher strikeouts)."""
|
||||
return stats.outs / 3 + stats.k
|
||||
"""IP + K where IP = outs / 3. Uses stats.so_pitcher (pitcher strikeouts)."""
|
||||
return stats.outs / 3 + stats.so_pitcher
|
||||
|
||||
|
||||
def compute_rp_value(stats) -> float:
|
||||
"""IP + K (same formula as SP; thresholds differ). Uses stats.k."""
|
||||
return stats.outs / 3 + stats.k
|
||||
"""IP + K (same formula as SP; thresholds differ). Uses stats.so_pitcher."""
|
||||
return stats.outs / 3 + stats.so_pitcher
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -35,7 +35,7 @@ def batter_stats(**kwargs):
|
||||
|
||||
def pitcher_stats(**kwargs):
|
||||
"""Build a minimal pitcher stats object with all fields defaulting to 0."""
|
||||
defaults = {"outs": 0, "k": 0}
|
||||
defaults = {"outs": 0, "so_pitcher": 0}
|
||||
defaults.update(kwargs)
|
||||
return SimpleNamespace(**defaults)
|
||||
|
||||
@ -84,7 +84,7 @@ def test_batter_formula_hr_heavy():
|
||||
|
||||
def test_sp_formula_standard():
|
||||
"""18 outs + 5 K: IP = 18/3 = 6.0, value = 6.0 + 5 = 11.0."""
|
||||
stats = pitcher_stats(outs=18, k=5)
|
||||
stats = pitcher_stats(outs=18, so_pitcher=5)
|
||||
assert compute_sp_value(stats) == 11.0
|
||||
|
||||
|
||||
@ -95,7 +95,7 @@ def test_sp_formula_standard():
|
||||
|
||||
def test_rp_formula_standard():
|
||||
"""3 outs + 2 K: IP = 3/3 = 1.0, value = 1.0 + 2 = 3.0."""
|
||||
stats = pitcher_stats(outs=3, k=2)
|
||||
stats = pitcher_stats(outs=3, so_pitcher=2)
|
||||
assert compute_rp_value(stats) == 3.0
|
||||
|
||||
|
||||
@ -132,13 +132,13 @@ def test_dispatch_batter():
|
||||
|
||||
def test_dispatch_sp():
|
||||
"""compute_value_for_track('sp', ...) delegates to compute_sp_value."""
|
||||
stats = pitcher_stats(outs=18, k=5)
|
||||
stats = pitcher_stats(outs=18, so_pitcher=5)
|
||||
assert compute_value_for_track("sp", stats) == compute_sp_value(stats)
|
||||
|
||||
|
||||
def test_dispatch_rp():
|
||||
"""compute_value_for_track('rp', ...) delegates to compute_rp_value."""
|
||||
stats = pitcher_stats(outs=3, k=2)
|
||||
stats = pitcher_stats(outs=3, so_pitcher=2)
|
||||
assert compute_value_for_track("rp", stats) == compute_rp_value(stats)
|
||||
|
||||
|
||||
|
||||
@ -112,7 +112,7 @@ class TestColumnCompleteness:
|
||||
"triples",
|
||||
"bb",
|
||||
"hbp",
|
||||
"so",
|
||||
"so_batter",
|
||||
"rbi",
|
||||
"runs",
|
||||
"sb",
|
||||
@ -121,7 +121,7 @@ class TestColumnCompleteness:
|
||||
PITCHING_COLS = [
|
||||
"games_pitching",
|
||||
"outs",
|
||||
"k",
|
||||
"so_pitcher",
|
||||
"bb_allowed",
|
||||
"hits_allowed",
|
||||
"hr_allowed",
|
||||
@ -181,14 +181,14 @@ class TestDefaultValues:
|
||||
"triples",
|
||||
"bb",
|
||||
"hbp",
|
||||
"so",
|
||||
"so_batter",
|
||||
"rbi",
|
||||
"runs",
|
||||
"sb",
|
||||
"cs",
|
||||
"games_pitching",
|
||||
"outs",
|
||||
"k",
|
||||
"so_pitcher",
|
||||
"bb_allowed",
|
||||
"hits_allowed",
|
||||
"hr_allowed",
|
||||
@ -284,16 +284,16 @@ class TestDeltaUpdatePattern:
|
||||
assert updated.games_pitching == 0 # untouched
|
||||
|
||||
def test_increment_pitching_stats(self):
|
||||
"""Updating outs and k increments without touching batting columns."""
|
||||
"""Updating outs and so_pitcher increments without touching batting columns."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
row = make_stats(player, team, season=10, outs=9, k=3)
|
||||
row = make_stats(player, team, season=10, outs=9, so_pitcher=3)
|
||||
|
||||
PlayerSeasonStats.update(
|
||||
outs=PlayerSeasonStats.outs + 6,
|
||||
k=PlayerSeasonStats.k + 2,
|
||||
so_pitcher=PlayerSeasonStats.so_pitcher + 2,
|
||||
).where(
|
||||
(PlayerSeasonStats.player == player)
|
||||
& (PlayerSeasonStats.team == team)
|
||||
@ -302,7 +302,7 @@ class TestDeltaUpdatePattern:
|
||||
|
||||
updated = PlayerSeasonStats.get_by_id(row.id)
|
||||
assert updated.outs == 15
|
||||
assert updated.k == 5
|
||||
assert updated.so_pitcher == 5
|
||||
assert updated.pa == 0 # untouched
|
||||
|
||||
def test_last_game_fk_is_nullable(self):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user