Added SeasonPitchingStats
This commit is contained in:
parent
8c492273dc
commit
2c3835c8ac
78
.claude/sqlite-to-postgres/create_season_pitching_stats.sql
Normal file
78
.claude/sqlite-to-postgres/create_season_pitching_stats.sql
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
CREATE TABLE seasonpitchingstats (
|
||||||
|
-- Primary identifiers (composite primary key)
|
||||||
|
player_id INTEGER NOT NULL,
|
||||||
|
season INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- Additional identifiers and metadata
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
sbaplayer_id INTEGER,
|
||||||
|
team_id INTEGER NOT NULL,
|
||||||
|
player_team_id INTEGER NOT NULL,
|
||||||
|
player_team_abbrev VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
-- Counting stats
|
||||||
|
tbf INTEGER NOT NULL DEFAULT 0,
|
||||||
|
outs INTEGER NOT NULL DEFAULT 0,
|
||||||
|
games INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Decision Info
|
||||||
|
gs INTEGER NOT NULL DEFAULT 0,
|
||||||
|
win INTEGER NOT NULL DEFAULT 0,
|
||||||
|
loss INTEGER NOT NULL DEFAULT 0,
|
||||||
|
hold INTEGER NOT NULL DEFAULT 0,
|
||||||
|
saves INTEGER NOT NULL DEFAULT 0,
|
||||||
|
bsave INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ir INTEGER NOT NULL DEFAULT 0,
|
||||||
|
irs INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Counting stats part 2
|
||||||
|
ab INTEGER NOT NULL DEFAULT 0,
|
||||||
|
run INTEGER NOT NULL DEFAULT 0,
|
||||||
|
e_run INTEGER NOT NULL DEFAULT 0,
|
||||||
|
hits INTEGER NOT NULL DEFAULT 0,
|
||||||
|
double INTEGER NOT NULL DEFAULT 0,
|
||||||
|
triple INTEGER NOT NULL DEFAULT 0,
|
||||||
|
homerun INTEGER NOT NULL DEFAULT 0,
|
||||||
|
bb INTEGER NOT NULL DEFAULT 0,
|
||||||
|
so INTEGER NOT NULL DEFAULT 0,
|
||||||
|
hbp INTEGER NOT NULL DEFAULT 0,
|
||||||
|
sac INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ibb INTEGER NOT NULL DEFAULT 0,
|
||||||
|
gidp INTEGER NOT NULL DEFAULT 0,
|
||||||
|
sb INTEGER NOT NULL DEFAULT 0,
|
||||||
|
cs INTEGER NOT NULL DEFAULT 0,
|
||||||
|
bphr INTEGER NOT NULL DEFAULT 0,
|
||||||
|
bpfo INTEGER NOT NULL DEFAULT 0,
|
||||||
|
bp1b INTEGER NOT NULL DEFAULT 0,
|
||||||
|
bplo INTEGER NOT NULL DEFAULT 0,
|
||||||
|
wp INTEGER NOT NULL DEFAULT 0,
|
||||||
|
balk INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Calculated stats
|
||||||
|
wpa REAL NOT NULL DEFAULT 0.000,
|
||||||
|
era REAL NOT NULL DEFAULT 0.00,
|
||||||
|
whip REAL NOT NULL DEFAULT 0.00,
|
||||||
|
avg REAL NOT NULL DEFAULT 0.000,
|
||||||
|
obp REAL NOT NULL DEFAULT 0.000,
|
||||||
|
slg REAL NOT NULL DEFAULT 0.000,
|
||||||
|
ops REAL NOT NULL DEFAULT 0.000,
|
||||||
|
woba REAL NOT NULL DEFAULT 0.000,
|
||||||
|
hper9 REAL NOT NULL DEFAULT 0.0,
|
||||||
|
kper9 REAL NOT NULL DEFAULT 0.0,
|
||||||
|
bbper9 REAL NOT NULL DEFAULT 0.0,
|
||||||
|
kperbb REAL NOT NULL DEFAULT 0.0,
|
||||||
|
lob_2outs REAL NOT NULL DEFAULT 0.000,
|
||||||
|
rbipercent REAL NOT NULL DEFAULT 0.000,
|
||||||
|
re24 REAL NOT NULL DEFAULT 0.000,
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
PRIMARY KEY (player_id, season),
|
||||||
|
FOREIGN KEY (player_id) REFERENCES player (id),
|
||||||
|
FOREIGN KEY (sbaplayer_id) REFERENCES sbaplayer (id),
|
||||||
|
FOREIGN KEY (team_id) REFERENCES team (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for better query performance
|
||||||
|
CREATE INDEX idx_seasonpitchingstats_season ON seasonpitchingstats (season);
|
||||||
|
CREATE INDEX idx_seasonpitchingstats_teamseason ON seasonpitchingstats (season, team_id);
|
||||||
|
CREATE INDEX idx_seasonpitchingstats_sbaplayer ON seasonpitchingstats (sbaplayer_id);
|
||||||
118
app/db_engine.py
118
app/db_engine.py
@ -2413,6 +2413,124 @@ class SeasonBattingStatsView(BaseModel):
|
|||||||
.limit(limit))
|
.limit(limit))
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonPitchingStats(BaseModel):
|
||||||
|
player = ForeignKeyField(Player)
|
||||||
|
sbaplayer = ForeignKeyField(SbaPlayer, null=True)
|
||||||
|
team = ForeignKeyField(Team)
|
||||||
|
season = IntegerField()
|
||||||
|
name = CharField()
|
||||||
|
player_team_id = IntegerField()
|
||||||
|
player_team_abbrev = CharField()
|
||||||
|
|
||||||
|
# Counting stats
|
||||||
|
tbf = IntegerField()
|
||||||
|
outs = IntegerField()
|
||||||
|
games = IntegerField()
|
||||||
|
|
||||||
|
# Decision Info
|
||||||
|
gs = IntegerField()
|
||||||
|
win = IntegerField()
|
||||||
|
loss = IntegerField()
|
||||||
|
hold = IntegerField()
|
||||||
|
saves = IntegerField()
|
||||||
|
bsave = IntegerField()
|
||||||
|
ir = IntegerField()
|
||||||
|
irs = IntegerField()
|
||||||
|
|
||||||
|
# Counting stats part 2
|
||||||
|
ab = IntegerField()
|
||||||
|
run = IntegerField()
|
||||||
|
e_run = IntegerField()
|
||||||
|
hits = IntegerField()
|
||||||
|
double = IntegerField()
|
||||||
|
triple = IntegerField()
|
||||||
|
homerun = IntegerField()
|
||||||
|
bb = IntegerField()
|
||||||
|
so = IntegerField()
|
||||||
|
hbp = IntegerField()
|
||||||
|
sac = IntegerField()
|
||||||
|
ibb = IntegerField()
|
||||||
|
gidp = IntegerField()
|
||||||
|
sb = IntegerField()
|
||||||
|
cs = IntegerField()
|
||||||
|
bphr = IntegerField()
|
||||||
|
bpfo = IntegerField()
|
||||||
|
bp1b = IntegerField()
|
||||||
|
bplo = IntegerField()
|
||||||
|
wp = IntegerField()
|
||||||
|
balk = IntegerField()
|
||||||
|
|
||||||
|
# Calculated stats
|
||||||
|
wpa = FloatField()
|
||||||
|
era = FloatField()
|
||||||
|
whip = FloatField()
|
||||||
|
avg = FloatField()
|
||||||
|
obp = FloatField()
|
||||||
|
slg = FloatField()
|
||||||
|
ops = FloatField()
|
||||||
|
woba = FloatField()
|
||||||
|
hper9 = FloatField()
|
||||||
|
kper9 = FloatField()
|
||||||
|
bbper9 = FloatField()
|
||||||
|
kperbb = FloatField()
|
||||||
|
lob_2outs = FloatField()
|
||||||
|
rbipercent = FloatField()
|
||||||
|
re24 = FloatField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'seasonpitchingstats'
|
||||||
|
primary_key = CompositeKey('player', 'season')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_team_stats(season, team_id):
|
||||||
|
return (SeasonPitchingStats.select()
|
||||||
|
.where(SeasonPitchingStats.season == season,
|
||||||
|
SeasonPitchingStats.player_team_id == team_id))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_top_pitchers(season: Optional[int] = None, stat: str = 'era', limit: Optional[int] = 200,
|
||||||
|
desc: bool = False, team_id: Optional[int] = None, player_id: Optional[int] = None,
|
||||||
|
sbaplayer_id: Optional[int] = None, min_outs: Optional[int] = None, offset: int = 0):
|
||||||
|
"""
|
||||||
|
Get top pitchers by specified stat with optional filtering.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
season: Season to filter by (None for all seasons)
|
||||||
|
stat: Stat field to sort by (default: era)
|
||||||
|
limit: Maximum number of results (None for no limit)
|
||||||
|
desc: Sort descending if True, ascending if False (default False for ERA)
|
||||||
|
team_id: Filter by team ID
|
||||||
|
player_id: Filter by specific player ID
|
||||||
|
sbaplayer_id: Filter by SBA player ID
|
||||||
|
min_outs: Minimum outs pitched filter
|
||||||
|
offset: Number of results to skip for pagination
|
||||||
|
"""
|
||||||
|
stat_field = getattr(SeasonPitchingStats, stat, SeasonPitchingStats.era)
|
||||||
|
order_field = stat_field.desc() if desc else stat_field.asc()
|
||||||
|
|
||||||
|
query = SeasonPitchingStats.select().order_by(order_field)
|
||||||
|
|
||||||
|
# Apply filters
|
||||||
|
if season is not None:
|
||||||
|
query = query.where(SeasonPitchingStats.season == season)
|
||||||
|
if team_id is not None:
|
||||||
|
query = query.where(SeasonPitchingStats.player_team_id == team_id)
|
||||||
|
if player_id is not None:
|
||||||
|
query = query.where(SeasonPitchingStats.player_id == player_id)
|
||||||
|
if sbaplayer_id is not None:
|
||||||
|
query = query.where(SeasonPitchingStats.sbaplayer_id == sbaplayer_id)
|
||||||
|
if min_outs is not None:
|
||||||
|
query = query.where(SeasonPitchingStats.outs >= min_outs)
|
||||||
|
|
||||||
|
# Apply pagination
|
||||||
|
if offset > 0:
|
||||||
|
query = query.offset(offset)
|
||||||
|
if limit is not None and limit > 0:
|
||||||
|
query = query.limit(limit)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
class SeasonBattingStats(BaseModel):
|
class SeasonBattingStats(BaseModel):
|
||||||
player = ForeignKeyField(Player)
|
player = ForeignKeyField(Player)
|
||||||
sbaplayer = ForeignKeyField(SbaPlayer, null=True)
|
sbaplayer = ForeignKeyField(SbaPlayer, null=True)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@ -197,6 +198,280 @@ def update_season_batting_stats(player_ids, season, db_connection):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def update_season_pitching_stats(player_ids, season, db_connection):
|
||||||
|
"""
|
||||||
|
Update season pitching stats for specific players in a given season.
|
||||||
|
Recalculates stats from stratplay and decision data and upserts into seasonpitchingstats table.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not player_ids:
|
||||||
|
logger.warning("update_season_pitching_stats called with empty player_ids list")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert single player_id to list for consistency
|
||||||
|
if isinstance(player_ids, int):
|
||||||
|
player_ids = [player_ids]
|
||||||
|
|
||||||
|
logger.info(f"Updating season pitching stats for {len(player_ids)} players in season {season}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# SQL query to recalculate and upsert pitching stats
|
||||||
|
query = """
|
||||||
|
WITH pitching_stats AS (
|
||||||
|
SELECT
|
||||||
|
p.id AS player_id,
|
||||||
|
p.name,
|
||||||
|
p.sbaplayer_id,
|
||||||
|
p.team_id AS player_team_id,
|
||||||
|
t.abbrev AS player_team_abbrev,
|
||||||
|
sg.season,
|
||||||
|
|
||||||
|
-- Counting statistics (summed from StratPlays)
|
||||||
|
SUM(sp.pa) AS tbf,
|
||||||
|
SUM(sp.ab) AS ab,
|
||||||
|
SUM(sp.run) AS run,
|
||||||
|
SUM(sp.e_run) AS e_run,
|
||||||
|
SUM(sp.hit) AS hits,
|
||||||
|
SUM(sp.double) AS double,
|
||||||
|
SUM(sp.triple) AS triple,
|
||||||
|
SUM(sp.homerun) AS homerun,
|
||||||
|
SUM(sp.bb) AS bb,
|
||||||
|
SUM(sp.so) AS so,
|
||||||
|
SUM(sp.hbp) AS hbp,
|
||||||
|
SUM(sp.sac) AS sac,
|
||||||
|
SUM(sp.ibb) AS ibb,
|
||||||
|
SUM(sp.gidp) AS gidp,
|
||||||
|
SUM(sp.sb) AS sb,
|
||||||
|
SUM(sp.cs) AS cs,
|
||||||
|
SUM(sp.bphr) AS bphr,
|
||||||
|
SUM(sp.bpfo) AS bpfo,
|
||||||
|
SUM(sp.bp1b) AS bp1b,
|
||||||
|
SUM(sp.bplo) AS bplo,
|
||||||
|
SUM(sp.wild_pitch) AS wp,
|
||||||
|
SUM(sp.balk) AS balk,
|
||||||
|
SUM(sp.outs) AS outs,
|
||||||
|
COALESCE(SUM(sp.wpa), 0) AS wpa,
|
||||||
|
COALESCE(SUM(sp.re24_primary), 0) AS re24,
|
||||||
|
|
||||||
|
-- Calculated statistics using formulas
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.outs) > 0
|
||||||
|
THEN ROUND((SUM(sp.e_run) * 27)::DECIMAL / SUM(sp.outs), 2)
|
||||||
|
ELSE 0.00
|
||||||
|
END AS era,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.outs) > 0
|
||||||
|
THEN ROUND(((SUM(sp.bb) + SUM(sp.hit) + SUM(sp.ibb)) * 3)::DECIMAL / SUM(sp.outs), 2)
|
||||||
|
ELSE 0.00
|
||||||
|
END AS whip,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.ab) > 0
|
||||||
|
THEN ROUND(SUM(sp.hit)::DECIMAL / SUM(sp.ab), 3)
|
||||||
|
ELSE 0.000
|
||||||
|
END AS avg,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.pa) > 0
|
||||||
|
THEN ROUND((SUM(sp.hit) + SUM(sp.bb) + SUM(sp.hbp) + SUM(sp.ibb))::DECIMAL / SUM(sp.pa), 3)
|
||||||
|
ELSE 0.000
|
||||||
|
END AS obp,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.ab) > 0
|
||||||
|
THEN ROUND((SUM(sp.hit) + SUM(sp.double) + 2 * SUM(sp.triple) + 3 *
|
||||||
|
SUM(sp.homerun))::DECIMAL / SUM(sp.ab), 3)
|
||||||
|
ELSE 0.000
|
||||||
|
END AS slg,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.pa) > 0 AND SUM(sp.ab) > 0
|
||||||
|
THEN ROUND(
|
||||||
|
((SUM(sp.hit) + SUM(sp.bb) + SUM(sp.hbp) + SUM(sp.ibb))::DECIMAL / SUM(sp.pa)) +
|
||||||
|
((SUM(sp.hit) + SUM(sp.double) + 2 * SUM(sp.triple) + 3 *
|
||||||
|
SUM(sp.homerun))::DECIMAL / SUM(sp.ab)), 3)
|
||||||
|
ELSE 0.000
|
||||||
|
END AS ops,
|
||||||
|
|
||||||
|
-- wOBA calculation (same as batting)
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.pa) > 0
|
||||||
|
THEN ROUND((0.690 * SUM(sp.bb) + 0.722 * SUM(sp.hbp) + 0.888 * (SUM(sp.hit) -
|
||||||
|
SUM(sp.double) - SUM(sp.triple) - SUM(sp.homerun)) +
|
||||||
|
1.271 * SUM(sp.double) + 1.616 * SUM(sp.triple) + 2.101 *
|
||||||
|
SUM(sp.homerun))::DECIMAL / SUM(sp.pa), 3)
|
||||||
|
ELSE 0.000
|
||||||
|
END AS woba,
|
||||||
|
|
||||||
|
-- Rate stats
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.outs) > 0
|
||||||
|
THEN ROUND((SUM(sp.hit) * 9)::DECIMAL / (SUM(sp.outs) / 3.0), 1)
|
||||||
|
ELSE 0.0
|
||||||
|
END AS hper9,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.outs) > 0
|
||||||
|
THEN ROUND((SUM(sp.so) * 9)::DECIMAL / (SUM(sp.outs) / 3.0), 1)
|
||||||
|
ELSE 0.0
|
||||||
|
END AS kper9,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.outs) > 0
|
||||||
|
THEN ROUND((SUM(sp.bb) * 9)::DECIMAL / (SUM(sp.outs) / 3.0), 1)
|
||||||
|
ELSE 0.0
|
||||||
|
END AS bbper9,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN SUM(sp.bb) > 0
|
||||||
|
THEN ROUND(SUM(sp.so)::DECIMAL / SUM(sp.bb), 2)
|
||||||
|
ELSE 0.0
|
||||||
|
END AS kperbb
|
||||||
|
|
||||||
|
FROM stratplay sp
|
||||||
|
JOIN stratgame sg ON sg.id = sp.game_id
|
||||||
|
JOIN player p ON p.id = sp.pitcher_id
|
||||||
|
JOIN team t ON t.id = p.team_id
|
||||||
|
WHERE sg.season = %s AND p.id = ANY(%s) AND sp.pitcher_id IS NOT NULL
|
||||||
|
GROUP BY p.id, p.name, p.sbaplayer_id, p.team_id, t.abbrev, sg.season
|
||||||
|
),
|
||||||
|
decision_stats AS (
|
||||||
|
SELECT
|
||||||
|
d.pitcher_id AS player_id,
|
||||||
|
sg.season,
|
||||||
|
SUM(d.win) AS win,
|
||||||
|
SUM(d.loss) AS loss,
|
||||||
|
SUM(d.hold) AS hold,
|
||||||
|
SUM(d.is_save) AS saves,
|
||||||
|
SUM(d.b_save) AS bsave,
|
||||||
|
SUM(d.irunners) AS ir,
|
||||||
|
SUM(d.irunners_scored) AS irs,
|
||||||
|
SUM(d.is_start::INTEGER) AS gs,
|
||||||
|
COUNT(d.game_id) AS games
|
||||||
|
FROM decision d
|
||||||
|
JOIN stratgame sg ON sg.id = d.game_id
|
||||||
|
WHERE sg.season = %s AND d.pitcher_id = ANY(%s)
|
||||||
|
GROUP BY d.pitcher_id, sg.season
|
||||||
|
)
|
||||||
|
INSERT INTO seasonpitchingstats (
|
||||||
|
player_id, sbaplayer_id, team_id, season, name, player_team_id, player_team_abbrev,
|
||||||
|
tbf, outs, games, gs, win, loss, hold, saves, bsave, ir, irs,
|
||||||
|
ab, run, e_run, hits, double, triple, homerun, bb, so, hbp, sac, ibb, gidp, sb, cs,
|
||||||
|
bphr, bpfo, bp1b, bplo, wp, balk,
|
||||||
|
wpa, era, whip, avg, obp, slg, ops, woba, hper9, kper9, bbper9, kperbb,
|
||||||
|
lob_2outs, rbipercent, re24
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ps.player_id, ps.sbaplayer_id, ps.player_team_id, ps.season, ps.name, ps.player_team_id, ps.player_team_abbrev,
|
||||||
|
ps.tbf, ps.outs, COALESCE(ds.games, 0), COALESCE(ds.gs, 0),
|
||||||
|
COALESCE(ds.win, 0), COALESCE(ds.loss, 0), COALESCE(ds.hold, 0),
|
||||||
|
COALESCE(ds.saves, 0), COALESCE(ds.bsave, 0), COALESCE(ds.ir, 0), COALESCE(ds.irs, 0),
|
||||||
|
ps.ab, ps.run, ps.e_run, ps.hits, ps.double, ps.triple, ps.homerun, ps.bb, ps.so,
|
||||||
|
ps.hbp, ps.sac, ps.ibb, ps.gidp, ps.sb, ps.cs,
|
||||||
|
ps.bphr, ps.bpfo, ps.bp1b, ps.bplo, ps.wp, ps.balk,
|
||||||
|
ps.wpa * -1, ps.era, ps.whip, ps.avg, ps.obp, ps.slg, ps.ops, ps.woba,
|
||||||
|
ps.hper9, ps.kper9, ps.bbper9, ps.kperbb,
|
||||||
|
0.0, 0.0, COALESCE(ps.re24 * -1, 0.0)
|
||||||
|
FROM pitching_stats ps
|
||||||
|
LEFT JOIN decision_stats ds ON ps.player_id = ds.player_id AND ps.season = ds.season
|
||||||
|
ON CONFLICT (player_id, season)
|
||||||
|
DO UPDATE SET
|
||||||
|
sbaplayer_id = EXCLUDED.sbaplayer_id,
|
||||||
|
team_id = EXCLUDED.team_id,
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
player_team_id = EXCLUDED.player_team_id,
|
||||||
|
player_team_abbrev = EXCLUDED.player_team_abbrev,
|
||||||
|
tbf = EXCLUDED.tbf,
|
||||||
|
outs = EXCLUDED.outs,
|
||||||
|
games = EXCLUDED.games,
|
||||||
|
gs = EXCLUDED.gs,
|
||||||
|
win = EXCLUDED.win,
|
||||||
|
loss = EXCLUDED.loss,
|
||||||
|
hold = EXCLUDED.hold,
|
||||||
|
saves = EXCLUDED.saves,
|
||||||
|
bsave = EXCLUDED.bsave,
|
||||||
|
ir = EXCLUDED.ir,
|
||||||
|
irs = EXCLUDED.irs,
|
||||||
|
ab = EXCLUDED.ab,
|
||||||
|
run = EXCLUDED.run,
|
||||||
|
e_run = EXCLUDED.e_run,
|
||||||
|
hits = EXCLUDED.hits,
|
||||||
|
double = EXCLUDED.double,
|
||||||
|
triple = EXCLUDED.triple,
|
||||||
|
homerun = EXCLUDED.homerun,
|
||||||
|
bb = EXCLUDED.bb,
|
||||||
|
so = EXCLUDED.so,
|
||||||
|
hbp = EXCLUDED.hbp,
|
||||||
|
sac = EXCLUDED.sac,
|
||||||
|
ibb = EXCLUDED.ibb,
|
||||||
|
gidp = EXCLUDED.gidp,
|
||||||
|
sb = EXCLUDED.sb,
|
||||||
|
cs = EXCLUDED.cs,
|
||||||
|
bphr = EXCLUDED.bphr,
|
||||||
|
bpfo = EXCLUDED.bpfo,
|
||||||
|
bp1b = EXCLUDED.bp1b,
|
||||||
|
bplo = EXCLUDED.bplo,
|
||||||
|
wp = EXCLUDED.wp,
|
||||||
|
balk = EXCLUDED.balk,
|
||||||
|
wpa = EXCLUDED.wpa,
|
||||||
|
era = EXCLUDED.era,
|
||||||
|
whip = EXCLUDED.whip,
|
||||||
|
avg = EXCLUDED.avg,
|
||||||
|
obp = EXCLUDED.obp,
|
||||||
|
slg = EXCLUDED.slg,
|
||||||
|
ops = EXCLUDED.ops,
|
||||||
|
woba = EXCLUDED.woba,
|
||||||
|
hper9 = EXCLUDED.hper9,
|
||||||
|
kper9 = EXCLUDED.kper9,
|
||||||
|
bbper9 = EXCLUDED.bbper9,
|
||||||
|
kperbb = EXCLUDED.kperbb,
|
||||||
|
lob_2outs = EXCLUDED.lob_2outs,
|
||||||
|
rbipercent = EXCLUDED.rbipercent,
|
||||||
|
re24 = EXCLUDED.re24;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Execute the query with parameters using the passed database connection
|
||||||
|
db_connection.execute_sql(query, [season, player_ids, season, player_ids])
|
||||||
|
|
||||||
|
logger.info(f"Successfully updated season pitching stats for {len(player_ids)} players in season {season}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating season pitching stats: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def send_webhook_message(message: str) -> bool:
|
||||||
|
"""
|
||||||
|
Send a message to Discord via webhook.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The message content to send
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
webhook_url = "https://discord.com/api/webhooks/1408811717424840876/7RXG_D5IqovA3Jwa9YOobUjVcVMuLc6cQyezABcWuXaHo5Fvz1en10M7J43o3OJ3bzGW"
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"content": message
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(webhook_url, json=payload, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
logger.info(f"Webhook message sent successfully: {message[:100]}...")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Failed to send webhook message: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error sending webhook message: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def handle_db_errors(func):
|
def handle_db_errors(func):
|
||||||
"""
|
"""
|
||||||
Decorator to handle database connection errors and transaction rollbacks.
|
Decorator to handle database connection errors and transaction rollbacks.
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import logging
|
|||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
from ..db_engine import db, StratGame, Team, StratPlay, model_to_dict, chunked, fn
|
from ..db_engine import db, StratGame, Team, StratPlay, model_to_dict, chunked, fn
|
||||||
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors, update_season_batting_stats
|
from ..dependencies import oauth2_scheme, send_webhook_message, update_season_pitching_stats, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors, update_season_batting_stats
|
||||||
|
|
||||||
logger = logging.getLogger('discord_app')
|
logger = logging.getLogger('discord_app')
|
||||||
|
|
||||||
@ -160,8 +160,25 @@ async def patch_game(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Failed to update batting stats for game {game_id}: {e}')
|
logger.error(f'Failed to update batting stats for game {game_id}: {e}')
|
||||||
|
send_webhook_message(f'Failed to update season batting stats after game {this_game.game_num} between {this_game.away_team.abbrev} and {this_game.home_team.abbrev}!\nError: {e}')
|
||||||
# Don't fail the patch operation if stats update fails
|
# Don't fail the patch operation if stats update fails
|
||||||
|
|
||||||
|
# Update pitching stats for all pitchers in this game
|
||||||
|
try:
|
||||||
|
# Get all unique pitcher IDs from stratplays in this game
|
||||||
|
pitcher_ids = [row.pitcher_id for row in StratPlay.select(StratPlay.pitcher_id.distinct())
|
||||||
|
.where(StratPlay.game_id == game_id)]
|
||||||
|
|
||||||
|
if pitcher_ids:
|
||||||
|
update_season_pitching_stats(pitcher_ids, this_game.season, db)
|
||||||
|
logger.info(f'Updated pitching stats for {len(pitcher_ids)} players from game {game_id}')
|
||||||
|
else:
|
||||||
|
logger.error(f'No pitchers found for game_id {game_id}')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to update pitching stats for game {game_id}: {e}')
|
||||||
|
send_webhook_message(f'Failed to update season pitching stats after game {this_game.game_num} between {this_game.away_team.abbrev} and {this_game.home_team.abbrev}!\nError: {e}')
|
||||||
|
|
||||||
g_result = model_to_dict(this_game)
|
g_result = model_to_dict(this_game)
|
||||||
return g_result
|
return g_result
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -3,8 +3,8 @@ from typing import List, Literal, Optional
|
|||||||
import logging
|
import logging
|
||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
from ..db_engine import SeasonBattingStats, db, Manager, Team, Current, model_to_dict, fn, query_to_csv, StratPlay, StratGame
|
from ..db_engine import SeasonBattingStats, SeasonPitchingStats, db, Manager, Team, Current, model_to_dict, fn, query_to_csv, StratPlay, StratGame
|
||||||
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors, update_season_batting_stats
|
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors, update_season_batting_stats, update_season_pitching_stats
|
||||||
|
|
||||||
logger = logging.getLogger('discord_app')
|
logger = logging.getLogger('discord_app')
|
||||||
|
|
||||||
@ -105,3 +105,105 @@ async def refresh_season_batting_stats(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Error refreshing season {season}: {e}')
|
logger.error(f'Error refreshing season {season}: {e}')
|
||||||
raise HTTPException(status_code=500, detail=f'Refresh failed: {str(e)}')
|
raise HTTPException(status_code=500, detail=f'Refresh failed: {str(e)}')
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/season-stats/pitching')
|
||||||
|
@handle_db_errors
|
||||||
|
async def get_season_pitching_stats(
|
||||||
|
season: Optional[int] = None,
|
||||||
|
team_id: Optional[int] = None,
|
||||||
|
player_id: Optional[int] = None,
|
||||||
|
sbaplayer_id: Optional[int] = None,
|
||||||
|
min_outs: Optional[int] = None, # Minimum outs pitched
|
||||||
|
sort_by: str = "era", # Default sort field
|
||||||
|
sort_order: Literal['asc', 'desc'] = 'asc', # asc or desc (asc default for ERA)
|
||||||
|
limit: Optional[int] = 200,
|
||||||
|
offset: int = 0,
|
||||||
|
csv: Optional[bool] = False
|
||||||
|
):
|
||||||
|
logger.info(f'Getting season {season} pitching stats - team_id: {team_id}, player_id: {player_id}, min_outs: {min_outs}, sort_by: {sort_by}, sort_order: {sort_order}, limit: {limit}, offset: {offset}')
|
||||||
|
|
||||||
|
# Use the get_top_pitchers method
|
||||||
|
query = SeasonPitchingStats.get_top_pitchers(
|
||||||
|
season=season,
|
||||||
|
stat=sort_by,
|
||||||
|
limit=limit if limit != 0 else None,
|
||||||
|
desc=(sort_order.lower() == 'desc'),
|
||||||
|
team_id=team_id,
|
||||||
|
player_id=player_id,
|
||||||
|
sbaplayer_id=sbaplayer_id,
|
||||||
|
min_outs=min_outs,
|
||||||
|
offset=offset
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build applied filters for response
|
||||||
|
applied_filters = {}
|
||||||
|
if season is not None:
|
||||||
|
applied_filters['season'] = season
|
||||||
|
if team_id is not None:
|
||||||
|
applied_filters['team_id'] = team_id
|
||||||
|
if player_id is not None:
|
||||||
|
applied_filters['player_id'] = player_id
|
||||||
|
if min_outs is not None:
|
||||||
|
applied_filters['min_outs'] = min_outs
|
||||||
|
|
||||||
|
if csv:
|
||||||
|
return_val = query_to_csv(query)
|
||||||
|
return Response(content=return_val, media_type='text/csv')
|
||||||
|
else:
|
||||||
|
stat_list = [model_to_dict(stat) for stat in query]
|
||||||
|
return {
|
||||||
|
'count': len(stat_list),
|
||||||
|
'filters': applied_filters,
|
||||||
|
'stats': stat_list
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/season-stats/pitching/refresh', include_in_schema=PRIVATE_IN_SCHEMA)
|
||||||
|
@handle_db_errors
|
||||||
|
async def refresh_season_pitching_stats(
|
||||||
|
season: int,
|
||||||
|
token: str = Depends(oauth2_scheme)
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Refresh pitching statistics for a specific season by aggregating from individual games.
|
||||||
|
Private endpoint - not included in public API documentation.
|
||||||
|
"""
|
||||||
|
if not valid_token(token):
|
||||||
|
logger.warning(f'refresh_season_batting_stats - Bad Token: {token}')
|
||||||
|
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||||
|
|
||||||
|
logger.info(f'Refreshing season {season} pitching stats')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get all pitcher IDs for this season
|
||||||
|
pitcher_query = (
|
||||||
|
StratPlay
|
||||||
|
.select(StratPlay.pitcher_id)
|
||||||
|
.join(StratGame, on=(StratPlay.game_id == StratGame.id))
|
||||||
|
.where((StratGame.season == season) & (StratPlay.pitcher_id.is_null(False)))
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
pitcher_ids = [row.pitcher_id for row in pitcher_query]
|
||||||
|
|
||||||
|
if not pitcher_ids:
|
||||||
|
logger.warning(f'No pitchers found for season {season}')
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'message': f'No pitchers found for season {season}',
|
||||||
|
'players_updated': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the dependency function to update pitching stats
|
||||||
|
update_season_pitching_stats(pitcher_ids, season, db)
|
||||||
|
|
||||||
|
logger.info(f'Season {season} pitching stats refreshed successfully - {len(pitcher_ids)} players updated')
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'message': f'Season {season} pitching stats refreshed',
|
||||||
|
'players_updated': len(pitcher_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Error refreshing season {season} pitching stats: {e}')
|
||||||
|
raise HTTPException(status_code=500, detail=f'Refresh failed: {str(e)}')
|
||||||
|
|||||||
@ -4,3 +4,4 @@ peewee==3.13.3
|
|||||||
python-multipart
|
python-multipart
|
||||||
pandas
|
pandas
|
||||||
psycopg2-binary>=2.9.0
|
psycopg2-binary>=2.9.0
|
||||||
|
requests
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user