PostgreSQL requires GROUP BY for all non-aggregated columns when using aggregate functions. Added group_by(pitcher, game) to the StratPlay query that calculates pitcher innings in the /decisions/rest endpoint.
331 lines
10 KiB
Python
331 lines
10 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
|
from typing import List, Optional, Literal
|
|
import copy
|
|
import logging
|
|
import pandas as pd
|
|
import pydantic
|
|
|
|
from ..db_engine import (
|
|
db,
|
|
Decision,
|
|
StratGame,
|
|
Player,
|
|
model_to_dict,
|
|
chunked,
|
|
fn,
|
|
Team,
|
|
Card,
|
|
StratPlay,
|
|
)
|
|
from ..db_helpers import upsert_decisions
|
|
from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
|
|
|
|
logging.basicConfig(
|
|
filename=LOG_DATA["filename"],
|
|
format=LOG_DATA["format"],
|
|
level=LOG_DATA["log_level"],
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/v2/decisions", tags=["decisions"])
|
|
|
|
|
|
class DecisionModel(pydantic.BaseModel):
|
|
game_id: int
|
|
season: int
|
|
week: int
|
|
pitcher_id: int
|
|
pitcher_team_id: int
|
|
win: int = 0
|
|
loss: int = 0
|
|
hold: int = 0
|
|
is_save: int = 0
|
|
is_start: bool = False
|
|
b_save: int = 0
|
|
irunners: int = 0
|
|
irunners_scored: int = 0
|
|
rest_ip: float = 0
|
|
rest_required: int = 0
|
|
|
|
|
|
class DecisionList(pydantic.BaseModel):
|
|
decisions: List[DecisionModel]
|
|
|
|
|
|
@router.get("")
|
|
async def get_decisions(
|
|
season: list = Query(default=None),
|
|
week: list = Query(default=None),
|
|
team_id: list = Query(default=None),
|
|
win: Optional[int] = None,
|
|
loss: Optional[int] = None,
|
|
hold: Optional[int] = None,
|
|
save: Optional[int] = None,
|
|
b_save: Optional[int] = None,
|
|
irunners: list = Query(default=None),
|
|
irunners_scored: list = Query(default=None),
|
|
game_type: list = Query(default=None),
|
|
game_id: list = Query(default=None),
|
|
player_id: list = Query(default=None),
|
|
csv: Optional[bool] = False,
|
|
limit: Optional[int] = 100,
|
|
page_num: Optional[int] = 1,
|
|
short_output: Optional[bool] = False,
|
|
):
|
|
all_dec = Decision.select().order_by(-Decision.season, -Decision.week, -Decision.id)
|
|
|
|
if season is not None:
|
|
all_dec = all_dec.where(Decision.season << season)
|
|
if week is not None:
|
|
all_dec = all_dec.where(Decision.week << week)
|
|
if game_id is not None:
|
|
all_dec = all_dec.where(Decision.game_id << game_id)
|
|
if player_id is not None:
|
|
all_dec = all_dec.where(Decision.pitcher_id << player_id)
|
|
if team_id is not None:
|
|
all_dec = all_dec.where(Decision.pitcher_team_id << team_id)
|
|
if win is not None:
|
|
all_dec = all_dec.where(Decision.win == win)
|
|
if loss is not None:
|
|
all_dec = all_dec.where(Decision.loss == loss)
|
|
if hold is not None:
|
|
all_dec = all_dec.where(Decision.hold == hold)
|
|
if save is not None:
|
|
all_dec = all_dec.where(Decision.save == save)
|
|
if b_save is not None:
|
|
all_dec = all_dec.where(Decision.b_save == b_save)
|
|
if irunners is not None:
|
|
all_dec = all_dec.where(Decision.irunners << irunners)
|
|
if irunners_scored is not None:
|
|
all_dec = all_dec.where(Decision.irunners_scored << irunners_scored)
|
|
|
|
if game_type is not None:
|
|
all_types = [x.lower() for x in game_type]
|
|
all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types)
|
|
all_dec = all_dec.where(Decision.game << all_games)
|
|
if limit < 1:
|
|
limit = 1
|
|
if limit > 100:
|
|
limit = 100
|
|
all_dec = all_dec.paginate(page_num, limit)
|
|
|
|
return_dec = {
|
|
"count": all_dec.count(),
|
|
"decisions": [model_to_dict(x, recurse=not short_output) for x in all_dec],
|
|
}
|
|
db.close()
|
|
|
|
if csv:
|
|
return_vals = return_dec["decisions"]
|
|
if len(return_vals) == 0:
|
|
return Response(
|
|
content=pd.DataFrame().to_csv(index=False), media_type="text/csv"
|
|
)
|
|
|
|
for x in return_vals:
|
|
x["game_id"] = x["game"]["id"]
|
|
x["game_type"] = x["game"]["game_type"]
|
|
x["player_id"] = x["pitcher"]["player_id"]
|
|
x["player_name"] = x["pitcher"]["p_name"]
|
|
x["player_cardset"] = x["pitcher"]["cardset"]["name"]
|
|
x["team_id"] = x["pitcher_team"]["id"]
|
|
x["team_abbrev"] = x["pitcher_team"]["abbrev"]
|
|
del x["pitcher"], x["pitcher_team"], x["game"]
|
|
|
|
output = pd.DataFrame(return_vals)
|
|
first = ["player_id", "player_name", "player_cardset", "team_id", "team_abbrev"]
|
|
exclude = first + ["lob_all", "lob_all_rate", "lob_2outs", "rbi%"]
|
|
output = output[first + [col for col in output.columns if col not in exclude]]
|
|
|
|
db.close()
|
|
return Response(
|
|
content=pd.DataFrame(output).to_csv(index=False), media_type="text/csv"
|
|
)
|
|
|
|
return return_dec
|
|
|
|
|
|
@router.get("/rest")
|
|
async def get_decisions_for_rest(
|
|
team_id: int, season: int = None, limit: int = 80, native_rest: bool = False
|
|
):
|
|
all_dec = (
|
|
Decision.select()
|
|
.order_by(-Decision.season, -Decision.week, -Decision.id)
|
|
.paginate(1, limit)
|
|
)
|
|
|
|
if season is not None:
|
|
all_dec = all_dec.where(Decision.season == season)
|
|
if team_id is not None:
|
|
all_dec = all_dec.where(Decision.pitcher_team_id == team_id)
|
|
|
|
return_dec = []
|
|
for x in all_dec:
|
|
this_val = []
|
|
this_card = Card.get_or_none(
|
|
Card.player_id == x.pitcher.player_id, Card.team_id == x.pitcher_team.id
|
|
)
|
|
this_val.append(x.game.id)
|
|
this_val.append(x.pitcher.player_id)
|
|
this_val.append(this_card.id if this_card is not None else -1)
|
|
this_val.append(1 if x.is_start else 0)
|
|
if not native_rest:
|
|
this_line = (
|
|
StratPlay.select(
|
|
StratPlay.pitcher,
|
|
StratPlay.game,
|
|
fn.SUM(StratPlay.outs).alias("sum_outs"),
|
|
)
|
|
.where((StratPlay.game == x.game) & (StratPlay.pitcher == x.pitcher))
|
|
.group_by(StratPlay.pitcher, StratPlay.game)
|
|
)
|
|
logging.info(f"this_line: {this_line[0]}")
|
|
if this_line[0].sum_outs is None:
|
|
this_val.append(0.0)
|
|
else:
|
|
this_val.append(
|
|
float(this_line[0].sum_outs // 3)
|
|
+ (float(this_line[0].sum_outs % 3) * 0.1)
|
|
)
|
|
|
|
return_dec.append(this_val)
|
|
|
|
db.close()
|
|
return Response(
|
|
content=pd.DataFrame(return_dec).to_csv(index=False, header=False),
|
|
media_type="text/csv",
|
|
)
|
|
|
|
|
|
@router.patch("/{decision_id}")
|
|
async def patch_decision(
|
|
decision_id: int,
|
|
win: Optional[int] = None,
|
|
loss: Optional[int] = None,
|
|
hold: Optional[int] = None,
|
|
save: Optional[int] = None,
|
|
b_save: Optional[int] = None,
|
|
irunners: Optional[int] = None,
|
|
irunners_scored: Optional[int] = None,
|
|
rest_ip: Optional[int] = None,
|
|
rest_required: Optional[int] = None,
|
|
token: str = Depends(oauth2_scheme),
|
|
):
|
|
if not valid_token(token):
|
|
logging.warning(f"patch_decision - Bad Token: {token}")
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
|
|
this_dec = Decision.get_or_none(Decision.id == decision_id)
|
|
if this_dec is None:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"Decision ID {decision_id} not found"
|
|
)
|
|
|
|
if win is not None:
|
|
this_dec.win = win
|
|
if loss is not None:
|
|
this_dec.loss = loss
|
|
if hold is not None:
|
|
this_dec.hold = hold
|
|
if save is not None:
|
|
this_dec.is_save = save
|
|
if b_save is not None:
|
|
this_dec.b_save = b_save
|
|
if irunners is not None:
|
|
this_dec.irunners = irunners
|
|
if irunners_scored is not None:
|
|
this_dec.irunners_scored = irunners_scored
|
|
if rest_ip is not None:
|
|
this_dec.rest_ip = rest_ip
|
|
if rest_required is not None:
|
|
this_dec.rest_required = rest_required
|
|
|
|
if this_dec.save() == 1:
|
|
d_result = model_to_dict(this_dec)
|
|
db.close()
|
|
return d_result
|
|
else:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Unable to patch decision {decision_id}"
|
|
)
|
|
|
|
|
|
@router.post("")
|
|
async def post_decisions(dec_list: DecisionList, token: str = Depends(oauth2_scheme)):
|
|
if not valid_token(token):
|
|
logging.warning(f"post_decisions - Bad Token: {token}")
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
|
|
new_dec = []
|
|
for x in dec_list.decisions:
|
|
if StratGame.get_or_none(StratGame.id == x.game_id) is None:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"Game ID {x.game_id} not found"
|
|
)
|
|
if Player.get_or_none(Player.player_id == x.pitcher_id) is None:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"Player ID {x.pitcher_id} not found"
|
|
)
|
|
if Team.get_or_none(Team.id == x.pitcher_team_id) is None:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"Team ID {x.pitcher_team_id} not found"
|
|
)
|
|
|
|
new_dec.append(x.dict())
|
|
|
|
with db.atomic():
|
|
# Use PostgreSQL-compatible upsert helper
|
|
upsert_decisions(new_dec, batch_size=10)
|
|
db.close()
|
|
|
|
return f"Inserted {len(new_dec)} decisions"
|
|
|
|
|
|
@router.delete("/{decision_id}")
|
|
async def delete_decision(decision_id: int, token: str = Depends(oauth2_scheme)):
|
|
if not valid_token(token):
|
|
logging.warning(f"delete_decision - Bad Token: {token}")
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
|
|
this_dec = Decision.get_or_none(Decision.id == decision_id)
|
|
if this_dec is None:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"Decision ID {decision_id} not found"
|
|
)
|
|
|
|
count = this_dec.delete_instance()
|
|
db.close()
|
|
|
|
if count == 1:
|
|
return f"Decision {decision_id} has been deleted"
|
|
else:
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Decision {decision_id} could not be deleted"
|
|
)
|
|
|
|
|
|
@router.delete("/game/{game_id}")
|
|
async def delete_decisions_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
|
if not valid_token(token):
|
|
logging.warning(f"delete_decisions_game - Bad Token: {token}")
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
|
|
this_game = StratGame.get_or_none(StratGame.id == game_id)
|
|
if not this_game:
|
|
db.close()
|
|
raise HTTPException(status_code=404, detail=f"Game ID {game_id} not found")
|
|
|
|
count = Decision.delete().where(Decision.game == this_game).execute()
|
|
db.close()
|
|
|
|
if count > 0:
|
|
return f"Deleted {count} decisions matching Game ID {game_id}"
|
|
else:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"No decisions matching Game ID {game_id} were deleted",
|
|
)
|