Merge pull request 'feat: capture total_count before limit across all paginated endpoints' (#169) from enhancement/total-count-pagination into main
All checks were successful
Build Docker Image / build (push) Successful in 8m22s

This commit is contained in:
cal 2026-03-24 12:45:53 +00:00
commit bc6c23ef2e
14 changed files with 421 additions and 111 deletions

View File

@ -55,6 +55,7 @@ async def get_awards(
all_awards = all_awards.where(Award.image == image)
limit = max(0, min(limit, 500))
total_count = all_awards.count() if not csv else 0
all_awards = all_awards.limit(limit)
if csv:
@ -76,7 +77,7 @@ async def get_awards(
return Response(content=return_val, media_type="text/csv")
else:
return_val = {"count": all_awards.count(), "awards": []}
return_val = {"count": total_count, "awards": []}
for x in all_awards:
return_val["awards"].append(model_to_dict(x))

View File

@ -6,14 +6,20 @@ import logging
import pydantic
from pandas import DataFrame
from ..db_engine import db, BattingStat, model_to_dict, fn, Card, Player, Current, DoesNotExist
from ..db_engine import (
db,
BattingStat,
model_to_dict,
fn,
Card,
Player,
Current,
DoesNotExist,
)
from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA
router = APIRouter(
prefix='/api/v2/batstats',
tags=['Pre-Season 7 Batting Stats']
)
router = APIRouter(prefix="/api/v2/batstats", tags=["Pre-Season 7 Batting Stats"])
class BatStat(pydantic.BaseModel):
@ -50,7 +56,7 @@ class BatStat(pydantic.BaseModel):
csc: 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
@ -63,11 +69,20 @@ class BatStatReturnList(pydantic.BaseModel):
stats: list[BatStat]
@router.get('', response_model=BatStatReturnList)
@router.get("", response_model=BatStatReturnList)
async def get_batstats(
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, csv: bool = None,
limit: Optional[int] = 100):
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,
csv: bool = None,
limit: Optional[int] = 100,
):
all_stats = BattingStat.select().join(Card).join(Player).order_by(BattingStat.id)
if season is not None:
@ -100,43 +115,122 @@ async def get_batstats(
# raise HTTPException(status_code=404, detail=f'No batting stats found')
limit = max(0, min(limit, 500))
total_count = all_stats.count() if not csv else 0
all_stats = all_stats.limit(limit)
if csv:
data_list = [['id', 'card_id', 'player_id', 'cardset', 'team', 'vs_team', 'pos', 'pa', 'ab', 'run', 'hit', 'rbi', 'double',
'triple', 'hr', 'bb', 'so', 'hbp', 'sac', 'ibb', 'gidp', 'sb', 'cs', 'bphr', 'bpfo', 'bp1b',
'bplo', 'xch', 'xhit', 'error', 'pb', 'sbc', 'csc', 'week', 'season', 'created', 'game_id', 'roster_num']]
data_list = [
[
"id",
"card_id",
"player_id",
"cardset",
"team",
"vs_team",
"pos",
"pa",
"ab",
"run",
"hit",
"rbi",
"double",
"triple",
"hr",
"bb",
"so",
"hbp",
"sac",
"ibb",
"gidp",
"sb",
"cs",
"bphr",
"bpfo",
"bp1b",
"bplo",
"xch",
"xhit",
"error",
"pb",
"sbc",
"csc",
"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.pos, line.pa, line.ab, line.run, line.hit, line.rbi, line.double, line.triple, line.hr,
line.bb, line.so, line.hbp, line.sac, line.ibb, line.gidp, line.sb, line.cs, line.bphr, line.bpfo,
line.bp1b, line.bplo, line.xch, line.xhit, line.error, line.pb, line.sbc, line.csc, 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.pos,
line.pa,
line.ab,
line.run,
line.hit,
line.rbi,
line.double,
line.triple,
line.hr,
line.bb,
line.so,
line.hbp,
line.sac,
line.ibb,
line.gidp,
line.sb,
line.cs,
line.bphr,
line.bpfo,
line.bp1b,
line.bplo,
line.xch,
line.xhit,
line.error,
line.pb,
line.sbc,
line.csc,
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": total_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.get('/player/{player_id}', response_model=BatStat)
@router.get("/player/{player_id}", response_model=BatStat)
async def get_player_stats(
player_id: int, team_id: int = None, vs_team_id: int = None, week_start: int = None, week_end: int = None,
csv: bool = None):
all_stats = (BattingStat
.select(fn.COUNT(BattingStat.created).alias('game_count'))
player_id: int,
team_id: int = None,
vs_team_id: int = None,
week_start: int = None,
week_end: int = None,
csv: bool = None,
):
all_stats = (
BattingStat.select(fn.COUNT(BattingStat.created).alias("game_count"))
.join(Card)
.group_by(BattingStat.card)
.where(BattingStat.card.player == player_id)).scalar()
.where(BattingStat.card.player == player_id)
).scalar()
if team_id is not None:
all_stats = all_stats.where(BattingStat.team_id == team_id)
@ -150,37 +244,82 @@ async def get_player_stats(
if csv:
data_list = [
[
'pa', 'ab', 'run', 'hit', 'rbi', 'double', 'triple', 'hr', 'bb', 'so', 'hbp', 'sac', 'ibb', 'gidp',
'sb', 'cs', 'bphr', 'bpfo', 'bp1b', 'bplo', 'xch', 'xhit', 'error', 'pb', 'sbc', 'csc',
],[
all_stats.pa_sum, all_stats.ab_sum, all_stats.run, all_stats.hit_sum, all_stats.rbi_sum,
all_stats.double_sum, all_stats.triple_sum, all_stats.hr_sum, all_stats.bb_sum, all_stats.so_sum,
all_stats.hbp_sum, all_stats.sac, all_stats.ibb_sum, all_stats.gidp_sum, all_stats.sb_sum,
all_stats.cs_sum, all_stats.bphr_sum, all_stats.bpfo_sum, all_stats.bp1b_sum, all_stats.bplo_sum,
all_stats.xch, all_stats.xhit_sum, all_stats.error_sum, all_stats.pb_sum, all_stats.sbc_sum,
all_stats.csc_sum
]
"pa",
"ab",
"run",
"hit",
"rbi",
"double",
"triple",
"hr",
"bb",
"so",
"hbp",
"sac",
"ibb",
"gidp",
"sb",
"cs",
"bphr",
"bpfo",
"bp1b",
"bplo",
"xch",
"xhit",
"error",
"pb",
"sbc",
"csc",
],
[
all_stats.pa_sum,
all_stats.ab_sum,
all_stats.run,
all_stats.hit_sum,
all_stats.rbi_sum,
all_stats.double_sum,
all_stats.triple_sum,
all_stats.hr_sum,
all_stats.bb_sum,
all_stats.so_sum,
all_stats.hbp_sum,
all_stats.sac,
all_stats.ibb_sum,
all_stats.gidp_sum,
all_stats.sb_sum,
all_stats.cs_sum,
all_stats.bphr_sum,
all_stats.bpfo_sum,
all_stats.bp1b_sum,
all_stats.bplo_sum,
all_stats.xch,
all_stats.xhit_sum,
all_stats.error_sum,
all_stats.pb_sum,
all_stats.sbc_sum,
all_stats.csc_sum,
],
]
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:
logging.debug(f'stat pull query: {all_stats}\n')
logging.debug(f"stat pull query: {all_stats}\n")
# logging.debug(f'result 0: {all_stats[0]}\n')
for x in all_stats:
logging.debug(f'this_line: {model_to_dict(x)}')
logging.debug(f"this_line: {model_to_dict(x)}")
return_val = model_to_dict(all_stats[0])
return return_val
@router.post('', include_in_schema=PRIVATE_IN_SCHEMA)
@router.post("", include_in_schema=PRIVATE_IN_SCHEMA)
async def post_batstats(stats: BattingStatModel, 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 = []
@ -219,36 +358,40 @@ async def post_batstats(stats: BattingStatModel, token: str = Depends(oauth2_sch
csc=x.csc,
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():
BattingStat.bulk_create(new_stats, batch_size=15)
raise HTTPException(status_code=200, detail=f'{len(new_stats)} batting lines have been added')
raise HTTPException(
status_code=200, detail=f"{len(new_stats)} batting lines have been added"
)
@router.delete('/{stat_id}', include_in_schema=PRIVATE_IN_SCHEMA)
@router.delete("/{stat_id}", include_in_schema=PRIVATE_IN_SCHEMA)
async def delete_batstat(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 = BattingStat.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")
# @app.get('/api/v1/plays/batting')
@ -453,4 +596,3 @@ async def delete_batstat(stat_id, token: str = Depends(oauth2_scheme)):
# }
# db.close()
# return return_stats

View File

@ -179,6 +179,7 @@ async def get_card_ratings(
)
all_ratings = all_ratings.where(BattingCardRatings.battingcard << set_cards)
total_count = all_ratings.count() if not csv else 0
all_ratings = all_ratings.limit(max(0, min(limit, 500)))
if csv:
@ -195,7 +196,7 @@ async def get_card_ratings(
else:
return_val = {
"count": all_ratings.count(),
"count": total_count,
"ratings": [
model_to_dict(x, recurse=not short_output) for x in all_ratings
],

View File

@ -8,10 +8,7 @@ from ..db_engine import Event, model_to_dict, fn, DoesNotExist
from ..dependencies import oauth2_scheme, valid_token
router = APIRouter(
prefix='/api/v2/events',
tags=['events']
)
router = APIRouter(prefix="/api/v2/events", tags=["events"])
class EventModel(pydantic.BaseModel):
@ -23,78 +20,102 @@ class EventModel(pydantic.BaseModel):
active: Optional[bool] = False
@router.get('')
@router.get("")
async def v1_events_get(
name: Optional[str] = None, in_desc: Optional[str] = None, active: Optional[bool] = None,
csv: Optional[bool] = None, limit: Optional[int] = 100):
name: Optional[str] = None,
in_desc: Optional[str] = None,
active: Optional[bool] = None,
csv: Optional[bool] = None,
limit: Optional[int] = 100,
):
all_events = Event.select().order_by(Event.id)
if name is not None:
all_events = all_events.where(fn.Lower(Event.name) == name.lower())
if in_desc is not None:
all_events = all_events.where(
(fn.Lower(Event.short_desc).contains(in_desc.lower())) |
(fn.Lower(Event.long_desc).contains(in_desc.lower()))
(fn.Lower(Event.short_desc).contains(in_desc.lower()))
| (fn.Lower(Event.long_desc).contains(in_desc.lower()))
)
if active is not None:
all_events = all_events.where(Event.active == active)
total_count = all_events.count() if not csv else 0
all_events = all_events.limit(max(0, min(limit, 500)))
if csv:
data_list = [['id', 'name', 'short_desc', 'long_desc', 'url', 'thumbnail', 'active']]
data_list = [
["id", "name", "short_desc", "long_desc", "url", "thumbnail", "active"]
]
for line in all_events:
data_list.append(
[
line.id, line.name, line.short_desc, line.long_desc, line.url, line.thumbnail, line.active
line.id,
line.name,
line.short_desc,
line.long_desc,
line.url,
line.thumbnail,
line.active,
]
)
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_events.count(), 'events': []}
return_val = {"count": total_count, "events": []}
for x in all_events:
return_val['events'].append(model_to_dict(x))
return_val["events"].append(model_to_dict(x))
return return_val
@router.get('/{event_id}')
@router.get("/{event_id}")
async def v1_events_get_one(event_id, csv: Optional[bool] = False):
try:
this_event = Event.get_by_id(event_id)
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No event found with id {event_id}')
raise HTTPException(
status_code=404, detail=f"No event found with id {event_id}"
)
if csv:
data_list = [
['id', 'name', 'short_desc', 'long_desc', 'url', 'thumbnail', 'active'],
[this_event.id, this_event.name, this_event.short_desc, this_event.long_desc, this_event.url,
this_event.thumbnail, this_event.active]
["id", "name", "short_desc", "long_desc", "url", "thumbnail", "active"],
[
this_event.id,
this_event.name,
this_event.short_desc,
this_event.long_desc,
this_event.url,
this_event.thumbnail,
this_event.active,
],
]
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_event)
return return_val
@router.post('')
@router.post("")
async def v1_events_post(event: EventModel, 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 events. This event has been logged.'
detail="You are not authorized to post events. This event has been logged.",
)
dupe_event = Event.get_or_none(Event.name == event.name)
if dupe_event:
raise HTTPException(status_code=400, detail=f'There is already an event using {event.name}')
raise HTTPException(
status_code=400, detail=f"There is already an event using {event.name}"
)
this_event = Event(
name=event.name,
@ -102,7 +123,7 @@ async def v1_events_post(event: EventModel, token: str = Depends(oauth2_scheme))
long_desc=event.long_desc,
url=event.url,
thumbnail=event.thumbnail,
active=event.active
active=event.active,
)
saved = this_event.save()
@ -112,25 +133,33 @@ async def v1_events_post(event: EventModel, 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('/{event_id}')
@router.patch("/{event_id}")
async def v1_events_patch(
event_id, name: Optional[str] = None, short_desc: Optional[str] = None, long_desc: Optional[str] = None,
url: Optional[str] = None, thumbnail: Optional[str] = None, active: Optional[bool] = None,
token: str = Depends(oauth2_scheme)):
event_id,
name: Optional[str] = None,
short_desc: Optional[str] = None,
long_desc: Optional[str] = None,
url: Optional[str] = None,
thumbnail: Optional[str] = None,
active: Optional[bool] = 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 events. This event has been logged.'
detail="You are not authorized to patch events. This event has been logged.",
)
try:
this_event = Event.get_by_id(event_id)
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No event found with id {event_id}')
raise HTTPException(
status_code=404, detail=f"No event found with id {event_id}"
)
if name is not None:
this_event.name = name
@ -151,26 +180,30 @@ async def v1_events_patch(
else:
raise HTTPException(
status_code=418,
detail='Well slap my ass and call me a teapot; I could not save that event'
detail="Well slap my ass and call me a teapot; I could not save that event",
)
@router.delete('/{event_id}')
@router.delete("/{event_id}")
async def v1_events_delete(event_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 events. This event has been logged.'
detail="You are not authorized to delete events. This event has been logged.",
)
try:
this_event = Event.get_by_id(event_id)
except DoesNotExist:
raise HTTPException(status_code=404, detail=f'No event found with id {event_id}')
raise HTTPException(
status_code=404, detail=f"No event found with id {event_id}"
)
count = this_event.delete_instance()
if count == 1:
raise HTTPException(status_code=200, detail=f'Event {event_id} has been deleted')
raise HTTPException(
status_code=200, detail=f"Event {event_id} has been deleted"
)
else:
raise HTTPException(status_code=500, detail=f'Event {event_id} was not deleted')
raise HTTPException(status_code=500, detail=f"Event {event_id} was not deleted")

View File

@ -43,6 +43,7 @@ async def v1_gamerewards_get(
all_rewards = all_rewards.where(GameRewards.money == money)
limit = max(0, min(limit, 500))
total_count = all_rewards.count() if not csv else 0
all_rewards = all_rewards.limit(limit)
if csv:
@ -61,7 +62,7 @@ async def v1_gamerewards_get(
return Response(content=return_val, media_type="text/csv")
else:
return_val = {"count": all_rewards.count(), "gamerewards": []}
return_val = {"count": total_count, "gamerewards": []}
for x in all_rewards:
return_val["gamerewards"].append(model_to_dict(x))

View File

@ -48,9 +48,10 @@ async def v1_gauntletreward_get(
all_rewards = all_rewards.order_by(-GauntletReward.loss_max, GauntletReward.win_num)
limit = max(0, min(limit, 500))
total_count = all_rewards.count()
all_rewards = all_rewards.limit(limit)
return_val = {"count": all_rewards.count(), "rewards": []}
return_val = {"count": total_count, "rewards": []}
for x in all_rewards:
return_val["rewards"].append(model_to_dict(x))

View File

@ -102,6 +102,7 @@ async def get_players(
if offense_col is not None:
all_players = all_players.where(MlbPlayer.offense_col << offense_col)
total_count = all_players.count() if not csv else 0
all_players = all_players.limit(max(0, min(limit, 500)))
if csv:
@ -109,7 +110,7 @@ async def get_players(
return Response(content=return_val, media_type="text/csv")
return_val = {
"count": all_players.count(),
"count": total_count,
"players": [model_to_dict(x) for x in all_players],
}
return return_val

View File

@ -169,6 +169,7 @@ async def get_card_ratings(
)
all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard << set_cards)
total_count = all_ratings.count() if not csv else 0
all_ratings = all_ratings.limit(max(0, min(limit, 500)))
if csv:
@ -177,7 +178,7 @@ async def get_card_ratings(
else:
return_val = {
"count": all_ratings.count(),
"count": total_count,
"ratings": [
model_to_dict(x, recurse=not short_output) for x in all_ratings
],

View File

@ -98,6 +98,7 @@ async def get_pit_stats(
if gs is not None:
all_stats = all_stats.where(PitchingStat.gs == 1 if gs else 0)
total_count = all_stats.count() if not csv else 0
all_stats = all_stats.limit(max(0, min(limit, 500)))
# if all_stats.count() == 0:
@ -177,7 +178,7 @@ async def get_pit_stats(
return Response(content=return_val, media_type="text/csv")
else:
return_val = {"count": all_stats.count(), "stats": []}
return_val = {"count": total_count, "stats": []}
for x in all_stats:
return_val["stats"].append(model_to_dict(x, recurse=False))

View File

@ -142,6 +142,7 @@ async def get_results(
all_results = all_results.order_by(Result.id)
limit = max(0, min(limit, 500))
total_count = all_results.count() if not csv else 0
all_results = all_results.limit(limit)
# Not functional
# if vs_ai is not None:
@ -201,7 +202,7 @@ async def get_results(
return Response(content=return_val, media_type="text/csv")
else:
return_val = {"count": all_results.count(), "results": []}
return_val = {"count": total_count, "results": []}
for x in all_results:
return_val["results"].append(model_to_dict(x))

View File

@ -34,9 +34,6 @@ async def get_rewards(
):
all_rewards = Reward.select().order_by(Reward.id)
if all_rewards.count() == 0:
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())
if team_id is not None:
@ -52,7 +49,8 @@ async def get_rewards(
if week is not None:
all_rewards = all_rewards.where(Reward.week == week)
if all_rewards.count() == 0:
total_count = all_rewards.count()
if total_count == 0:
raise HTTPException(status_code=404, detail="No rewards found")
limit = max(0, min(limit, 500))
@ -69,7 +67,7 @@ async def get_rewards(
return Response(content=return_val, media_type="text/csv")
else:
return_val = {"count": all_rewards.count(), "rewards": []}
return_val = {"count": total_count, "rewards": []}
for x in all_rewards:
return_val["rewards"].append(model_to_dict(x, recurse=not flat))

View File

@ -30,12 +30,14 @@ async def get_scout_claims(
if claimed_by_team_id is not None:
query = query.where(ScoutClaim.claimed_by_team_id == claimed_by_team_id)
total_count = query.count()
if limit is not None:
limit = max(0, min(limit, 500))
query = query.limit(limit)
results = [model_to_dict(x, recurse=False) for x in query]
return {"count": len(results), "results": results}
return {"count": total_count, "results": results}
@router.get("/{claim_id}")

File diff suppressed because one or more lines are too long

View File

@ -78,6 +78,7 @@ async def get_games(
StratGame.game_type.contains(f"gauntlet-{gauntlet_id}")
)
total_count = all_games.count() if not csv else 0
all_games = all_games.limit(max(0, min(limit, 500)))
if csv:
@ -107,7 +108,7 @@ async def get_games(
return Response(content=output.to_csv(index=False), media_type="text/csv")
return_val = {
"count": all_games.count(),
"count": total_count,
"games": [model_to_dict(x, recurse=not short_output) for x in all_games],
}
return return_val