diff --git a/.claude/sqlite-to-postgres/create_season_pitching_stats.sql b/.claude/sqlite-to-postgres/create_season_pitching_stats.sql new file mode 100644 index 0000000..0168428 --- /dev/null +++ b/.claude/sqlite-to-postgres/create_season_pitching_stats.sql @@ -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); \ No newline at end of file diff --git a/app/db_engine.py b/app/db_engine.py index eecd1e3..5e9a9b4 100644 --- a/app/db_engine.py +++ b/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) diff --git a/app/dependencies.py b/app/dependencies.py index 0d0efb9..33b78a8 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -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. diff --git a/app/routers_v3/stratgame.py b/app/routers_v3/stratgame.py index 672153b..2610b54 100644 --- a/app/routers_v3/stratgame.py +++ b/app/routers_v3/stratgame.py @@ -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: diff --git a/app/routers_v3/views.py b/app/routers_v3/views.py index 09cfab4..f10d895 100644 --- a/app/routers_v3/views.py +++ b/app/routers_v3/views.py @@ -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)}') diff --git a/requirements.txt b/requirements.txt index f4ea62d..784e9d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ peewee==3.13.3 python-multipart pandas psycopg2-binary>=2.9.0 +requests