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))
|
||||
|
||||
|
||||
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):
|
||||
player = ForeignKeyField(Player)
|
||||
sbaplayer = ForeignKeyField(SbaPlayer, null=True)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
from functools import wraps
|
||||
|
||||
from fastapi import HTTPException
|
||||
@ -197,6 +198,280 @@ def update_season_batting_stats(player_ids, season, db_connection):
|
||||
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):
|
||||
"""
|
||||
Decorator to handle database connection errors and transaction rollbacks.
|
||||
|
||||
@ -5,7 +5,7 @@ import logging
|
||||
import pydantic
|
||||
|
||||
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')
|
||||
|
||||
@ -160,8 +160,25 @@ async def patch_game(
|
||||
|
||||
except Exception as 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
|
||||
|
||||
# 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)
|
||||
return g_result
|
||||
else:
|
||||
|
||||
@ -3,8 +3,8 @@ from typing import List, Literal, Optional
|
||||
import logging
|
||||
import pydantic
|
||||
|
||||
from ..db_engine import SeasonBattingStats, 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 ..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, update_season_pitching_stats
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
|
||||
@ -105,3 +105,105 @@ async def refresh_season_batting_stats(
|
||||
except Exception as e:
|
||||
logger.error(f'Error refreshing season {season}: {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
|
||||
pandas
|
||||
psycopg2-binary>=2.9.0
|
||||
requests
|
||||
|
||||
Loading…
Reference in New Issue
Block a user