diff --git a/app/db_engine.py b/app/db_engine.py index ac4edec..f090978 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -1851,8 +1851,8 @@ class StratGame(BaseModel): class StratPlay(BaseModel): game = ForeignKeyField(StratGame) play_num = IntegerField() - batter = ForeignKeyField(Player) - batter_team = ForeignKeyField(Team) + batter = ForeignKeyField(Player, null=True) + batter_team = ForeignKeyField(Team, null=True) pitcher = ForeignKeyField(Player) pitcher_team = ForeignKeyField(Team) on_base_code = CharField() @@ -1862,7 +1862,7 @@ class StratPlay(BaseModel): starting_outs = IntegerField() away_score = IntegerField() home_score = IntegerField() - batter_pos = CharField() + batter_pos = CharField(null=True) # These _final fields track the base this runner advances to post-play (None) if out on_first = ForeignKeyField(Player, null=True) @@ -1915,6 +1915,24 @@ class StratPlay(BaseModel): is_new_inning = BooleanField(default=False) +class Decision(BaseModel): + game = ForeignKeyField(StratGame) + season = IntegerField() + week = IntegerField() + game_num = IntegerField() + pitcher = ForeignKeyField(Player) + win = IntegerField() + loss = IntegerField() + hold = IntegerField() + is_save = IntegerField() + b_save = IntegerField() + irunners = IntegerField() + irunners_scored = IntegerField() + rest_ip = IntegerField() + rest_required = IntegerField() + is_start = BooleanField(default=False) + + # class Streak(BaseModel): # player = ForeignKeyField(Player) # streak_type = CharField() @@ -1940,6 +1958,6 @@ class StratPlay(BaseModel): db.create_tables([ Current, Division, Manager, Team, Result, Player, Schedule, Transaction, BattingStat, PitchingStat, Standings, BattingCareer, PitchingCareer, FieldingCareer, Manager, Award, DiceRoll, DraftList, Keeper, StratGame, StratPlay, - Injury + Injury, Decision ]) db.close() diff --git a/app/main.py b/app/main.py index 4818b47..795966b 100644 --- a/app/main.py +++ b/app/main.py @@ -8,7 +8,7 @@ from fastapi import Depends, FastAPI, Request from .routers_v3 import current, players, results, schedules, standings, teams, transactions, battingstats, \ pitchingstats, fieldingstats, draftpicks, draftlist, managers, awards, draftdata, keepers, stratgame, stratplay, \ - injuries + injuries, decisions date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}' log_level = logging.INFO if os.environ.get('LOG_LEVEL') == 'INFO' else 'WARN' @@ -23,6 +23,9 @@ app = FastAPI( ) +logging.info(f'Starting up now...') + + app.include_router(current.router) app.include_router(players.router) app.include_router(results.router) @@ -42,7 +45,9 @@ app.include_router(keepers.router) app.include_router(stratgame.router) app.include_router(stratplay.router) app.include_router(injuries.router) +app.include_router(decisions.router) +logging.info(f'Loaded all routers.') # @app.get("/docs", include_in_schema=False) # async def get_docs(req: Request): diff --git a/app/routers_v3/decisions.py b/app/routers_v3/decisions.py new file mode 100644 index 0000000..18a625f --- /dev/null +++ b/app/routers_v3/decisions.py @@ -0,0 +1,199 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional, Literal +import copy +import logging +import pydantic + +from ..db_engine import db, Decision, StratGame, Player, model_to_dict, chunked, fn +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/v3/decisions', + tags=['decisions'] +) + + +class DecisionModel(pydantic.BaseModel): + game_id: int + season: int + week: int + game_num: int + pitcher_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: int = 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), game_num: list = Query(default=None), + season_type: Literal['regular', 'post', 'all', None] = None, team_id: list = Query(default=None), + week_start: Optional[int] = None, week_end: Optional[int] = 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), + short_output: Optional[bool] = False): + all_dec = Decision.select() + + 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_num is not None: + all_dec = all_dec.where(Decision.game_num << game_num) + if season_type is not None: + all_games = StratGame.select().where(StratGame.season_type == season_type) + all_dec = all_dec.where(Decision.game << all_games) + if season_type is not None: + all_players = Player.select().where(Player.team_id << team_id) + all_dec = all_dec.where(Decision.pitcher << all_players) + if week_start is not None: + all_dec = all_dec.where(Decision.week >= week_start) + if week_end is not None: + all_dec = all_dec.where(Decision.week <= week_end) + 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) + + return_dec = { + 'count': all_dec.count(), + 'decisions': [model_to_dict(x, recurse=not short_output) for x in all_dec] + } + db.close() + return return_dec + + +@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.id == x.pitcher_id) is None: + raise HTTPException(status_code=404, detail=f'Player ID {x.pitcher_id} not found') + + new_dec.append(x.dict()) + + with db.atomic(): + for batch in chunked(new_dec, 10): + Decision.insert_many(batch).on_conflict_replace().execute() + 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') + + + diff --git a/app/routers_v3/stratplay.py b/app/routers_v3/stratplay.py index 31064d6..c0f667d 100644 --- a/app/routers_v3/stratplay.py +++ b/app/routers_v3/stratplay.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query from typing import List, Optional, Literal import copy import logging -import pydantic +from pydantic import BaseModel, validator from ..db_engine import db, StratPlay, StratGame, Team, model_to_dict, chunked, fn from ..dependencies import oauth2_scheme, valid_token, LOG_DATA @@ -21,21 +21,21 @@ router = APIRouter( POS_LIST = Literal['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'P', 'DH', 'PH', 'PR'] -class PlayModel(pydantic.BaseModel): +class PlayModel(BaseModel): game_id: int play_num: int - batter_id: int + batter_id: int = None batter_team_id: int = None pitcher_id: int pitcher_team_id: int = None on_base_code: str - inning_half: Literal['top', 'bot'] + inning_half: Literal['top', 'bot', 'Top', 'Bot'] inning_num: int batting_order: int starting_outs: int away_score: int home_score: int - batter_pos: POS_LIST + batter_pos: POS_LIST = None on_first_id: int = None on_first_final: int = None @@ -85,8 +85,32 @@ class PlayModel(pydantic.BaseModel): is_tied: bool = False is_new_inning: bool = False + @validator('on_first_final') + def no_final_if_no_runner_one(cls, v, values): + if values['on_first_id'] is None: + return None + return v -class PlayList(pydantic.BaseModel): + @validator('on_second_final') + def no_final_if_no_runner_two(cls, v, values): + if values['on_second_id'] is None: + return None + return v + + @validator('on_third_final') + def no_final_if_no_runner_three(cls, v, values): + if values['on_third_id'] is None: + return None + return v + + @validator('batter_final') + def no_final_if_no_batter(cls, v, values): + if values['batter_id'] is None: + return None + return v + + +class PlayList(BaseModel): plays: List[PlayModel] @@ -179,9 +203,10 @@ async def post_plays(p_list: PlayList, token: str = Depends(oauth2_scheme)): for play in p_list.plays: this_play = play + this_play.inning_half = this_play.inning_half.lower() top_half = this_play.inning_half == 'top' - if this_play.batter_team_id is None: + if this_play.batter_team_id is None and this_play.batter_id is not None: this_play.batter_team_id = this_game.away_team.id if top_half else this_game.home_team.id if this_play.pitcher_team_id is None: this_play.pitcher_team_id = this_game.home_team.id if top_half else this_game.away_team.id @@ -191,6 +216,9 @@ async def post_plays(p_list: PlayList, token: str = Depends(oauth2_scheme)): this_play.defender_team_id = this_game.home_team.id if top_half else this_game.away_team.id if this_play.runner_id is not None: this_play.runner_team_id = this_game.away_team.id if top_half else this_game.home_team.id + if this_play.pa == 0: + this_play.batter_final = None + new_plays.append(this_play.dict())