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)
|
triples = IntegerField(default=0)
|
||||||
bb = IntegerField(default=0)
|
bb = IntegerField(default=0)
|
||||||
hbp = IntegerField(default=0)
|
hbp = IntegerField(default=0)
|
||||||
so = IntegerField(default=0)
|
so_batter = IntegerField(default=0)
|
||||||
rbi = IntegerField(default=0)
|
rbi = IntegerField(default=0)
|
||||||
runs = IntegerField(default=0)
|
runs = IntegerField(default=0)
|
||||||
sb = IntegerField(default=0)
|
sb = IntegerField(default=0)
|
||||||
@ -1074,9 +1074,7 @@ class PlayerSeasonStats(BaseModel):
|
|||||||
# Pitching stats
|
# Pitching stats
|
||||||
games_pitching = IntegerField(default=0)
|
games_pitching = IntegerField(default=0)
|
||||||
outs = IntegerField(default=0)
|
outs = IntegerField(default=0)
|
||||||
k = IntegerField(
|
so_pitcher = IntegerField(default=0)
|
||||||
default=0
|
|
||||||
) # pitcher Ks; spec names this "so (K)" but renamed to avoid collision with batting so
|
|
||||||
bb_allowed = IntegerField(default=0)
|
bb_allowed = IntegerField(default=0)
|
||||||
hits_allowed = IntegerField(default=0)
|
hits_allowed = IntegerField(default=0)
|
||||||
hr_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:
|
Stats attributes expected by each formula:
|
||||||
compute_batter_value: pa, hits, doubles, triples, hr
|
compute_batter_value: pa, hits, doubles, triples, hr
|
||||||
compute_sp_value: outs, k (k = pitcher strikeouts, from PlayerSeasonStats)
|
compute_sp_value: outs, so_pitcher (pitcher strikeouts, from PlayerSeasonStats)
|
||||||
compute_rp_value: outs, k
|
compute_rp_value: outs, so_pitcher
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
@ -22,7 +22,7 @@ class BatterStats(Protocol):
|
|||||||
|
|
||||||
class PitcherStats(Protocol):
|
class PitcherStats(Protocol):
|
||||||
outs: int
|
outs: int
|
||||||
k: int
|
so_pitcher: int
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -38,13 +38,13 @@ def compute_batter_value(stats) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def compute_sp_value(stats) -> float:
|
def compute_sp_value(stats) -> float:
|
||||||
"""IP + K where IP = outs / 3. Uses stats.k (pitcher strikeouts)."""
|
"""IP + K where IP = outs / 3. Uses stats.so_pitcher (pitcher strikeouts)."""
|
||||||
return stats.outs / 3 + stats.k
|
return stats.outs / 3 + stats.so_pitcher
|
||||||
|
|
||||||
|
|
||||||
def compute_rp_value(stats) -> float:
|
def compute_rp_value(stats) -> float:
|
||||||
"""IP + K (same formula as SP; thresholds differ). Uses stats.k."""
|
"""IP + K (same formula as SP; thresholds differ). Uses stats.so_pitcher."""
|
||||||
return stats.outs / 3 + stats.k
|
return stats.outs / 3 + stats.so_pitcher
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -35,7 +35,7 @@ def batter_stats(**kwargs):
|
|||||||
|
|
||||||
def pitcher_stats(**kwargs):
|
def pitcher_stats(**kwargs):
|
||||||
"""Build a minimal pitcher stats object with all fields defaulting to 0."""
|
"""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)
|
defaults.update(kwargs)
|
||||||
return SimpleNamespace(**defaults)
|
return SimpleNamespace(**defaults)
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ def test_batter_formula_hr_heavy():
|
|||||||
|
|
||||||
def test_sp_formula_standard():
|
def test_sp_formula_standard():
|
||||||
"""18 outs + 5 K: IP = 18/3 = 6.0, value = 6.0 + 5 = 11.0."""
|
"""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
|
assert compute_sp_value(stats) == 11.0
|
||||||
|
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ def test_sp_formula_standard():
|
|||||||
|
|
||||||
def test_rp_formula_standard():
|
def test_rp_formula_standard():
|
||||||
"""3 outs + 2 K: IP = 3/3 = 1.0, value = 1.0 + 2 = 3.0."""
|
"""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
|
assert compute_rp_value(stats) == 3.0
|
||||||
|
|
||||||
|
|
||||||
@ -132,13 +132,13 @@ def test_dispatch_batter():
|
|||||||
|
|
||||||
def test_dispatch_sp():
|
def test_dispatch_sp():
|
||||||
"""compute_value_for_track('sp', ...) delegates to compute_sp_value."""
|
"""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)
|
assert compute_value_for_track("sp", stats) == compute_sp_value(stats)
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_rp():
|
def test_dispatch_rp():
|
||||||
"""compute_value_for_track('rp', ...) delegates to compute_rp_value."""
|
"""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)
|
assert compute_value_for_track("rp", stats) == compute_rp_value(stats)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -112,7 +112,7 @@ class TestColumnCompleteness:
|
|||||||
"triples",
|
"triples",
|
||||||
"bb",
|
"bb",
|
||||||
"hbp",
|
"hbp",
|
||||||
"so",
|
"so_batter",
|
||||||
"rbi",
|
"rbi",
|
||||||
"runs",
|
"runs",
|
||||||
"sb",
|
"sb",
|
||||||
@ -121,7 +121,7 @@ class TestColumnCompleteness:
|
|||||||
PITCHING_COLS = [
|
PITCHING_COLS = [
|
||||||
"games_pitching",
|
"games_pitching",
|
||||||
"outs",
|
"outs",
|
||||||
"k",
|
"so_pitcher",
|
||||||
"bb_allowed",
|
"bb_allowed",
|
||||||
"hits_allowed",
|
"hits_allowed",
|
||||||
"hr_allowed",
|
"hr_allowed",
|
||||||
@ -181,14 +181,14 @@ class TestDefaultValues:
|
|||||||
"triples",
|
"triples",
|
||||||
"bb",
|
"bb",
|
||||||
"hbp",
|
"hbp",
|
||||||
"so",
|
"so_batter",
|
||||||
"rbi",
|
"rbi",
|
||||||
"runs",
|
"runs",
|
||||||
"sb",
|
"sb",
|
||||||
"cs",
|
"cs",
|
||||||
"games_pitching",
|
"games_pitching",
|
||||||
"outs",
|
"outs",
|
||||||
"k",
|
"so_pitcher",
|
||||||
"bb_allowed",
|
"bb_allowed",
|
||||||
"hits_allowed",
|
"hits_allowed",
|
||||||
"hr_allowed",
|
"hr_allowed",
|
||||||
@ -284,16 +284,16 @@ class TestDeltaUpdatePattern:
|
|||||||
assert updated.games_pitching == 0 # untouched
|
assert updated.games_pitching == 0 # untouched
|
||||||
|
|
||||||
def test_increment_pitching_stats(self):
|
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()
|
rarity = make_rarity()
|
||||||
cardset = make_cardset()
|
cardset = make_cardset()
|
||||||
player = make_player(cardset, rarity)
|
player = make_player(cardset, rarity)
|
||||||
team = make_team()
|
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(
|
PlayerSeasonStats.update(
|
||||||
outs=PlayerSeasonStats.outs + 6,
|
outs=PlayerSeasonStats.outs + 6,
|
||||||
k=PlayerSeasonStats.k + 2,
|
so_pitcher=PlayerSeasonStats.so_pitcher + 2,
|
||||||
).where(
|
).where(
|
||||||
(PlayerSeasonStats.player == player)
|
(PlayerSeasonStats.player == player)
|
||||||
& (PlayerSeasonStats.team == team)
|
& (PlayerSeasonStats.team == team)
|
||||||
@ -302,7 +302,7 @@ class TestDeltaUpdatePattern:
|
|||||||
|
|
||||||
updated = PlayerSeasonStats.get_by_id(row.id)
|
updated = PlayerSeasonStats.get_by_id(row.id)
|
||||||
assert updated.outs == 15
|
assert updated.outs == 15
|
||||||
assert updated.k == 5
|
assert updated.so_pitcher == 5
|
||||||
assert updated.pa == 0 # untouched
|
assert updated.pa == 0 # untouched
|
||||||
|
|
||||||
def test_last_game_fk_is_nullable(self):
|
def test_last_game_fk_is_nullable(self):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user