From 890625e7705eccdba916170f54950eb3e7a32b3b Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 24 Mar 2026 00:02:08 -0500 Subject: [PATCH 1/3] feat: add limit/pagination to rewards endpoint (#139) Closes #139 Co-Authored-By: Claude Sonnet 4.6 --- app/routers_v2/rewards.py | 111 +++++++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/app/routers_v2/rewards.py b/app/routers_v2/rewards.py index ab6aa43..48a50cc 100644 --- a/app/routers_v2/rewards.py +++ b/app/routers_v2/rewards.py @@ -9,10 +9,7 @@ from ..db_engine import Reward, model_to_dict, fn, DoesNotExist from ..dependencies import oauth2_scheme, valid_token -router = APIRouter( - prefix='/api/v2/rewards', - tags=['rewards'] -) +router = APIRouter(prefix="/api/v2/rewards", tags=["rewards"]) class RewardModel(pydantic.BaseModel): @@ -20,18 +17,25 @@ class RewardModel(pydantic.BaseModel): season: int week: int team_id: int - created: Optional[int] = int(datetime.timestamp(datetime.now())*1000) + created: Optional[int] = int(datetime.timestamp(datetime.now()) * 1000) -@router.get('') +@router.get("") async def get_rewards( - name: Optional[str] = None, in_name: Optional[str] = None, team_id: Optional[int] = None, - season: Optional[int] = None, week: Optional[int] = None, created_after: Optional[int] = None, - flat: Optional[bool] = False, csv: Optional[bool] = None): + name: Optional[str] = None, + in_name: Optional[str] = None, + team_id: Optional[int] = None, + season: Optional[int] = None, + week: Optional[int] = None, + created_after: Optional[int] = None, + flat: Optional[bool] = False, + csv: Optional[bool] = None, + limit: Optional[int] = 100, +): all_rewards = Reward.select().order_by(Reward.id) if all_rewards.count() == 0: - raise HTTPException(status_code=404, detail=f'There are no rewards to filter') + raise HTTPException(status_code=404, detail="There are no rewards to filter") if name is not None: all_rewards = all_rewards.where(fn.Lower(Reward.name) == name.lower()) @@ -49,62 +53,71 @@ async def get_rewards( all_rewards = all_rewards.where(Reward.week == week) if all_rewards.count() == 0: - raise HTTPException(status_code=404, detail=f'No rewards found') + raise HTTPException(status_code=404, detail="No rewards found") + + limit = max(0, min(limit, 500)) + all_rewards = all_rewards.limit(limit) if csv: - data_list = [['id', 'name', 'team', 'daily', 'created']] + data_list = [["id", "name", "team", "daily", "created"]] for line in all_rewards: data_list.append( - [ - line.id, line.name, line.team.id, line.daily, line.created - ] + [line.id, line.name, line.team.id, line.daily, line.created] ) return_val = DataFrame(data_list).to_csv(header=False, index=False) - return Response(content=return_val, media_type='text/csv') + return Response(content=return_val, media_type="text/csv") else: - return_val = {'count': all_rewards.count(), 'rewards': []} + return_val = {"count": all_rewards.count(), "rewards": []} for x in all_rewards: - return_val['rewards'].append(model_to_dict(x, recurse=not flat)) + return_val["rewards"].append(model_to_dict(x, recurse=not flat)) return return_val -@router.get('/{reward_id}') +@router.get("/{reward_id}") async def get_one_reward(reward_id, csv: Optional[bool] = False): try: this_reward = Reward.get_by_id(reward_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}') + raise HTTPException( + status_code=404, detail=f"No reward found with id {reward_id}" + ) if csv: data_list = [ - ['id', 'name', 'card_count', 'description'], - [this_reward.id, this_reward.name, this_reward.team.id, this_reward.daily, this_reward.created] + ["id", "name", "card_count", "description"], + [ + this_reward.id, + this_reward.name, + this_reward.team.id, + this_reward.daily, + this_reward.created, + ], ] return_val = DataFrame(data_list).to_csv(header=False, index=False) - return Response(content=return_val, media_type='text/csv') + return Response(content=return_val, media_type="text/csv") else: return_val = model_to_dict(this_reward) return return_val -@router.post('') +@router.post("") async def post_rewards(reward: RewardModel, token: str = Depends(oauth2_scheme)): if not valid_token(token): - logging.warning('Bad Token: [REDACTED]') + logging.warning("Bad Token: [REDACTED]") raise HTTPException( status_code=401, - detail='You are not authorized to post rewards. This event has been logged.' + detail="You are not authorized to post rewards. This event has been logged.", ) reward_data = reward.dict() # Convert milliseconds timestamp to datetime for PostgreSQL - if reward_data.get('created'): - reward_data['created'] = datetime.fromtimestamp(reward_data['created'] / 1000) + if reward_data.get("created"): + reward_data["created"] = datetime.fromtimestamp(reward_data["created"] / 1000) this_reward = Reward(**reward_data) saved = this_reward.save() @@ -114,24 +127,30 @@ async def post_rewards(reward: RewardModel, token: str = Depends(oauth2_scheme)) else: raise HTTPException( status_code=418, - detail='Well slap my ass and call me a teapot; I could not save that cardset' + detail="Well slap my ass and call me a teapot; I could not save that cardset", ) -@router.patch('/{reward_id}') +@router.patch("/{reward_id}") async def patch_reward( - reward_id, name: Optional[str] = None, team_id: Optional[int] = None, created: Optional[int] = None, - token: str = Depends(oauth2_scheme)): + reward_id, + name: Optional[str] = None, + team_id: Optional[int] = None, + created: Optional[int] = None, + token: str = Depends(oauth2_scheme), +): if not valid_token(token): - logging.warning('Bad Token: [REDACTED]') + logging.warning("Bad Token: [REDACTED]") raise HTTPException( status_code=401, - detail='You are not authorized to patch rewards. This event has been logged.' + detail="You are not authorized to patch rewards. This event has been logged.", ) try: this_reward = Reward.get_by_id(reward_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}') + raise HTTPException( + status_code=404, detail=f"No reward found with id {reward_id}" + ) if name is not None: this_reward.name = name @@ -147,28 +166,32 @@ async def patch_reward( else: raise HTTPException( status_code=418, - detail='Well slap my ass and call me a teapot; I could not save that rarity' + detail="Well slap my ass and call me a teapot; I could not save that rarity", ) -@router.delete('/{reward_id}') +@router.delete("/{reward_id}") async def delete_reward(reward_id, token: str = Depends(oauth2_scheme)): if not valid_token(token): - logging.warning('Bad Token: [REDACTED]') + logging.warning("Bad Token: [REDACTED]") raise HTTPException( status_code=401, - detail='You are not authorized to delete rewards. This event has been logged.' + detail="You are not authorized to delete rewards. This event has been logged.", ) try: this_reward = Reward.get_by_id(reward_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}') + raise HTTPException( + status_code=404, detail=f"No reward found with id {reward_id}" + ) count = this_reward.delete_instance() if count == 1: - raise HTTPException(status_code=200, detail=f'Reward {reward_id} has been deleted') + raise HTTPException( + status_code=200, detail=f"Reward {reward_id} has been deleted" + ) else: - raise HTTPException(status_code=500, detail=f'Reward {reward_id} was not deleted') - - + raise HTTPException( + status_code=500, detail=f"Reward {reward_id} was not deleted" + ) From 0f884a35168ef6ad2882828a832208ee44b6222e Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 24 Mar 2026 01:32:19 -0500 Subject: [PATCH 2/3] feat: add limit/pagination to events endpoint (#147) Closes #147 Co-Authored-By: Claude Sonnet 4.6 --- app/routers_v2/events.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/routers_v2/events.py b/app/routers_v2/events.py index cd989cb..68058f8 100644 --- a/app/routers_v2/events.py +++ b/app/routers_v2/events.py @@ -26,7 +26,7 @@ class EventModel(pydantic.BaseModel): @router.get('') async def v1_events_get( name: Optional[str] = None, in_desc: Optional[str] = None, active: Optional[bool] = None, - csv: Optional[bool] = None): + csv: Optional[bool] = None, limit: Optional[int] = 100): all_events = Event.select().order_by(Event.id) if name is not None: @@ -39,6 +39,8 @@ async def v1_events_get( if active is not None: all_events = all_events.where(Event.active == active) + all_events = all_events.limit(max(0, min(limit, 500))) + if csv: data_list = [['id', 'name', 'short_desc', 'long_desc', 'url', 'thumbnail', 'active']] for line in all_events: From 8c9aa55609cb8fb769a0d6f972de05f05eeb15cc Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 24 Mar 2026 02:32:10 -0500 Subject: [PATCH 3/3] feat: add limit/pagination to pitstats endpoint (#134) Closes #134 Co-Authored-By: Claude Sonnet 4.6 --- app/routers_v2/pitstats.py | 143 ++++++++++++++++++++++++++++--------- 1 file changed, 109 insertions(+), 34 deletions(-) diff --git a/app/routers_v2/pitstats.py b/app/routers_v2/pitstats.py index 82f3883..e540d8b 100644 --- a/app/routers_v2/pitstats.py +++ b/app/routers_v2/pitstats.py @@ -5,14 +5,19 @@ import logging import pydantic from pandas import DataFrame -from ..db_engine import db, PitchingStat, model_to_dict, Card, Player, Current, DoesNotExist +from ..db_engine import ( + db, + PitchingStat, + model_to_dict, + Card, + Player, + Current, + DoesNotExist, +) from ..dependencies import oauth2_scheme, valid_token -router = APIRouter( - prefix='/api/v2/pitstats', - tags=['pitstats'] -) +router = APIRouter(prefix="/api/v2/pitstats", tags=["pitstats"]) class PitStat(pydantic.BaseModel): @@ -40,7 +45,7 @@ class PitStat(pydantic.BaseModel): bsv: Optional[int] = 0 week: int season: int - created: Optional[int] = int(datetime.timestamp(datetime.now())*1000) + created: Optional[int] = int(datetime.timestamp(datetime.now()) * 1000) game_id: int @@ -48,13 +53,23 @@ class PitchingStatModel(pydantic.BaseModel): stats: List[PitStat] -@router.get('') +@router.get("") async def get_pit_stats( - card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None, - season: int = None, week_start: int = None, week_end: int = None, created: int = None, gs: bool = None, - csv: bool = None): + card_id: int = None, + player_id: int = None, + team_id: int = None, + vs_team_id: int = None, + week: int = None, + season: int = None, + week_start: int = None, + week_end: int = None, + created: int = None, + gs: bool = None, + csv: bool = None, + limit: Optional[int] = 100, +): all_stats = PitchingStat.select().join(Card).join(Player).order_by(PitchingStat.id) - logging.debug(f'pit query:\n\n{all_stats}') + logging.debug(f"pit query:\n\n{all_stats}") if season is not None: all_stats = all_stats.where(PitchingStat.season == season) @@ -83,43 +98,99 @@ async def get_pit_stats( if gs is not None: all_stats = all_stats.where(PitchingStat.gs == 1 if gs else 0) + all_stats = all_stats.limit(max(0, min(limit, 500))) + # if all_stats.count() == 0: # db.close() # raise HTTPException(status_code=404, detail=f'No pitching stats found') if csv: - data_list = [['id', 'card_id', 'player_id', 'cardset', 'team', 'vs_team', 'ip', 'hit', 'run', 'erun', 'so', 'bb', 'hbp', - 'wp', 'balk', 'hr', 'ir', 'irs', 'gs', 'win', 'loss', 'hold', 'sv', 'bsv', 'week', 'season', - 'created', 'game_id', 'roster_num']] + data_list = [ + [ + "id", + "card_id", + "player_id", + "cardset", + "team", + "vs_team", + "ip", + "hit", + "run", + "erun", + "so", + "bb", + "hbp", + "wp", + "balk", + "hr", + "ir", + "irs", + "gs", + "win", + "loss", + "hold", + "sv", + "bsv", + "week", + "season", + "created", + "game_id", + "roster_num", + ] + ] for line in all_stats: data_list.append( [ - line.id, line.card.id, line.card.player.player_id, line.card.player.cardset.name, line.team.abbrev, - line.vs_team.abbrev, line.ip, line.hit, - line.run, line.erun, line.so, line.bb, line.hbp, line.wp, line.balk, line.hr, line.ir, line.irs, - line.gs, line.win, line.loss, line.hold, line.sv, line.bsv, line.week, line.season, line.created, - line.game_id, line.roster_num + line.id, + line.card.id, + line.card.player.player_id, + line.card.player.cardset.name, + line.team.abbrev, + line.vs_team.abbrev, + line.ip, + line.hit, + line.run, + line.erun, + line.so, + line.bb, + line.hbp, + line.wp, + line.balk, + line.hr, + line.ir, + line.irs, + line.gs, + line.win, + line.loss, + line.hold, + line.sv, + line.bsv, + line.week, + line.season, + line.created, + line.game_id, + line.roster_num, ] ) return_val = DataFrame(data_list).to_csv(header=False, index=False) - return Response(content=return_val, media_type='text/csv') + return Response(content=return_val, media_type="text/csv") else: - return_val = {'count': all_stats.count(), 'stats': []} + return_val = {"count": all_stats.count(), "stats": []} for x in all_stats: - return_val['stats'].append(model_to_dict(x, recurse=False)) + return_val["stats"].append(model_to_dict(x, recurse=False)) return return_val -@router.post('') +@router.post("") async def post_pitstat(stats: PitchingStatModel, token: str = Depends(oauth2_scheme)): if not valid_token(token): - logging.warning('Bad Token: [REDACTED]') + logging.warning("Bad Token: [REDACTED]") raise HTTPException( status_code=401, - detail='You are not authorized to post stats. This event has been logged.' + detail="You are not authorized to post stats. This event has been logged.", ) new_stats = [] @@ -149,33 +220,37 @@ async def post_pitstat(stats: PitchingStatModel, token: str = Depends(oauth2_sch bsv=x.bsv, week=x.week, season=x.season, - created=datetime.fromtimestamp(x.created / 1000) if x.created else datetime.now(), - game_id=x.game_id + created=datetime.fromtimestamp(x.created / 1000) + if x.created + else datetime.now(), + game_id=x.game_id, ) new_stats.append(this_stat) with db.atomic(): PitchingStat.bulk_create(new_stats, batch_size=15) - raise HTTPException(status_code=200, detail=f'{len(new_stats)} pitching lines have been added') + raise HTTPException( + status_code=200, detail=f"{len(new_stats)} pitching lines have been added" + ) -@router.delete('/{stat_id}') +@router.delete("/{stat_id}") async def delete_pitstat(stat_id, token: str = Depends(oauth2_scheme)): if not valid_token(token): - logging.warning('Bad Token: [REDACTED]') + logging.warning("Bad Token: [REDACTED]") raise HTTPException( status_code=401, - detail='You are not authorized to delete stats. This event has been logged.' + detail="You are not authorized to delete stats. This event has been logged.", ) try: this_stat = PitchingStat.get_by_id(stat_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No stat found with id {stat_id}') + raise HTTPException(status_code=404, detail=f"No stat found with id {stat_id}") count = this_stat.delete_instance() if count == 1: - raise HTTPException(status_code=200, detail=f'Stat {stat_id} has been deleted') + raise HTTPException(status_code=200, detail=f"Stat {stat_id} has been deleted") else: - raise HTTPException(status_code=500, detail=f'Stat {stat_id} was not deleted') + raise HTTPException(status_code=500, detail=f"Stat {stat_id} was not deleted")