major-domo-database/app/routers_v3/stratplay/plays.py
Cal Corum 86f8495284
All checks were successful
Build Docker Image / build (pull_request) Successful in 2m32s
feat: Add group_by=sbaplayer to batting, pitching, and fielding endpoints
Enables career-total aggregation by real-world player identity (SbaPlayer)
across all seasons. JOINs StratPlay → Player to access Player.sbaplayer FK,
groups by that FK, and excludes players with null sbaplayer. Also refactors
stratplay router from single file into package and adds integration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:42:14 -06:00

215 lines
8.2 KiB
Python

import logging
from typing import Optional, Literal
from fastapi import APIRouter, Query
from ...db_engine import (
db,
StratPlay,
StratGame,
Team,
Player,
model_to_dict,
fn,
)
from ...dependencies import (
handle_db_errors,
add_cache_headers,
cache_result,
)
logger = logging.getLogger("discord_app")
router = APIRouter()
@router.get("/")
@handle_db_errors
@add_cache_headers(max_age=10 * 60)
@cache_result(ttl=5 * 60, key_prefix="plays")
async def get_plays(
game_id: list = Query(default=None),
batter_id: list = Query(default=None),
season: list = Query(default=None),
week: list = Query(default=None),
has_defender: Optional[bool] = None,
has_catcher: Optional[bool] = None,
has_defender_or_catcher: Optional[bool] = None,
is_scoring_play: Optional[bool] = None,
pitcher_id: list = Query(default=None),
obc: list = Query(default=None),
inning: list = Query(default=None),
batting_order: list = Query(default=None),
starting_outs: list = Query(default=None),
batter_pos: list = Query(default=None),
catcher_id: list = Query(default=None),
defender_id: list = Query(default=None),
runner_id: list = Query(default=None),
offense_team_id: list = Query(default=None),
defense_team_id: list = Query(default=None),
hit: Optional[int] = None,
double: Optional[int] = None,
triple: Optional[int] = None,
homerun: Optional[int] = None,
play_num: list = Query(default=None),
error: list = Query(default=None),
sb: Optional[int] = None,
cs: Optional[int] = None,
manager_id: list = Query(default=None),
run: Optional[int] = None,
e_run: Optional[int] = None,
rbi: list = Query(default=None),
outs: list = Query(default=None),
wild_pitch: Optional[int] = None,
is_final_out: Optional[bool] = None,
is_go_ahead: Optional[bool] = None,
is_tied: Optional[bool] = None,
is_new_inning: Optional[bool] = None,
min_wpa: Optional[float] = None,
max_wpa: Optional[float] = None,
pitcher_team_id: list = Query(default=None),
short_output: Optional[bool] = False,
sort: Optional[str] = None,
limit: Optional[int] = 200,
page_num: Optional[int] = 1,
s_type: Literal["regular", "post", "total", None] = None,
):
all_plays = StratPlay.select()
if season is not None:
s_games = StratGame.select().where(StratGame.season << season)
all_plays = all_plays.where(StratPlay.game << s_games)
if week is not None:
w_games = StratGame.select().where(StratGame.week << week)
all_plays = all_plays.where(StratPlay.game << w_games)
if has_defender is not None:
all_plays = all_plays.where(StratPlay.defender.is_null(False))
if has_catcher is not None:
all_plays = all_plays.where(StratPlay.catcher.is_null(False))
if has_defender_or_catcher is not None:
all_plays = all_plays.where(
(StratPlay.catcher.is_null(False)) | (StratPlay.defender.is_null(False))
)
if game_id is not None:
all_plays = all_plays.where(StratPlay.game_id << game_id)
if batter_id is not None:
all_plays = all_plays.where(StratPlay.batter_id << batter_id)
if pitcher_id is not None:
all_plays = all_plays.where(StratPlay.pitcher_id << pitcher_id)
if obc is not None:
all_plays = all_plays.where(StratPlay.on_base_code << obc)
if inning is not None:
all_plays = all_plays.where(StratPlay.inning_num << inning)
if batting_order is not None:
all_plays = all_plays.where(StratPlay.batting_order << batting_order)
if starting_outs is not None:
all_plays = all_plays.where(StratPlay.starting_outs << starting_outs)
if batter_pos is not None:
all_plays = all_plays.where(StratPlay.batter_pos << batter_pos)
if catcher_id is not None:
all_plays = all_plays.where(StratPlay.catcher_id << catcher_id)
if defender_id is not None:
all_plays = all_plays.where(StratPlay.defender_id << defender_id)
if runner_id is not None:
all_plays = all_plays.where(StratPlay.runner_id << runner_id)
if pitcher_team_id is not None:
all_teams = Team.select().where(Team.id << pitcher_team_id)
all_plays = all_plays.where((StratPlay.pitcher_team << all_teams))
if offense_team_id is not None:
all_teams = Team.select().where(Team.id << offense_team_id)
all_plays = all_plays.where(
(StratPlay.batter_team << all_teams) | (StratPlay.runner_team << all_teams)
)
if defense_team_id is not None:
all_teams = Team.select().where(Team.id << defense_team_id)
all_plays = all_plays.where(
(StratPlay.catcher_team << all_teams)
| (StratPlay.defender_team << all_teams)
)
if hit is not None:
all_plays = all_plays.where(StratPlay.hit == hit)
if double is not None:
all_plays = all_plays.where(StratPlay.double == double)
if triple is not None:
all_plays = all_plays.where(StratPlay.triple == triple)
if homerun is not None:
all_plays = all_plays.where(StratPlay.homerun == homerun)
if sb is not None:
all_plays = all_plays.where(StratPlay.sb == sb)
if cs is not None:
all_plays = all_plays.where(StratPlay.cs == cs)
if wild_pitch is not None:
all_plays = all_plays.where(StratPlay.wild_pitch == wild_pitch)
if run is not None:
all_plays = all_plays.where(StratPlay.run == run)
if e_run is not None:
all_plays = all_plays.where(StratPlay.e_run == e_run)
if rbi is not None:
all_plays = all_plays.where(StratPlay.rbi << rbi)
if outs is not None:
all_plays = all_plays.where(StratPlay.outs << outs)
if error is not None:
all_plays = all_plays.where(StratPlay.error << error)
if manager_id is not None:
all_games = StratGame.select().where(
(StratGame.away_manager_id << manager_id)
| (StratGame.home_manager_id << manager_id)
)
all_plays = all_plays.where(StratPlay.game << all_games)
if is_final_out is not None:
all_plays = all_plays.where(StratPlay.starting_outs + StratPlay.outs == 3)
if is_go_ahead is not None:
all_plays = all_plays.where(StratPlay.is_go_ahead == is_go_ahead)
if is_tied is not None:
all_plays = all_plays.where(StratPlay.is_tied == is_tied)
if is_new_inning is not None:
all_plays = all_plays.where(StratPlay.is_new_inning == is_new_inning)
if is_scoring_play is not None:
all_plays = all_plays.where(
(StratPlay.on_first_final == 4)
| (StratPlay.on_second_final == 4)
| (StratPlay.on_third_final == 4)
| (StratPlay.batter_final == 4)
)
if min_wpa is not None:
all_plays = all_plays.where(StratPlay.wpa >= min_wpa)
if max_wpa is not None:
all_plays = all_plays.where(StratPlay.wpa <= max_wpa)
if play_num is not None:
all_plays = all_plays.where(StratPlay.play_num << play_num)
if s_type is not None:
season_games = StratGame.select()
if s_type == "regular":
season_games = season_games.where(StratGame.week <= 18)
elif s_type == "post":
season_games = season_games.where(StratGame.week > 18)
all_plays = all_plays.where(StratPlay.game << season_games)
if limit < 1:
limit = 1
bat_plays = all_plays.paginate(page_num, limit)
if sort == "wpa-desc":
all_plays = all_plays.order_by(-fn.ABS(StratPlay.wpa))
elif sort == "wpa-asc":
all_plays = all_plays.order_by(fn.ABS(StratPlay.wpa))
elif sort == "re24-desc":
all_plays = all_plays.order_by(-fn.ABS(StratPlay.re24_primary))
elif sort == "re24-asc":
all_plays = all_plays.order_by(fn.ABS(StratPlay.re24_primary))
elif sort == "newest":
all_plays = all_plays.order_by(
StratPlay.game_id.desc(), StratPlay.play_num.desc()
)
elif sort == "oldest":
all_plays = all_plays.order_by(StratPlay.game_id, StratPlay.play_num)
all_plays = all_plays.limit(limit)
return_plays = {
"count": all_plays.count(),
"plays": [model_to_dict(x, recurse=not short_output) for x in all_plays],
}
db.close()
return return_plays