Added SeasonPitchingStats

This commit is contained in:
Cal Corum 2025-08-26 00:17:57 -05:00
parent 8c492273dc
commit 2c3835c8ac
6 changed files with 594 additions and 3 deletions

View 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);

View File

@ -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)

View File

@ -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.

View File

@ -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:

View File

@ -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)}')

View File

@ -4,3 +4,4 @@ peewee==3.13.3
python-multipart
pandas
psycopg2-binary>=2.9.0
requests