diff --git a/Dockerfile b/Dockerfile index e84d3d1..582ab12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8 +FROM tiangolo/uvicorn-gunicorn-fastapi:latest WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -COPY . . \ No newline at end of file +COPY ./app /app/app \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db_engine.py b/app/db_engine.py new file mode 100644 index 0000000..c7d50de --- /dev/null +++ b/app/db_engine.py @@ -0,0 +1,1940 @@ +import copy +import math +from typing import Literal + +from peewee import * +from playhouse.shortcuts import model_to_dict + +db = SqliteDatabase( + 'storage/sba_master.db', + pragmas={ + 'journal_mode': 'wal', + 'cache_size': -1 * 64000, + 'synchronous': 0 + } +) + + +""" +Per season updates: + Result: regular_season & post_season - set season length + update_standings - confirm division alignments and records + Standings: recalculate - e_number function, set season length + - wildcard section, set league abbrevs +""" + + +WEEK_NUMS = { + 'regular': { + + } +} + + +def per_season_weeks(season: int, s_type: Literal['regular', 'post', 'total']): + if season == 1: + if s_type == 'regular': + return {'start': 1, 'end': 20} + elif s_type == 'post': + return {'start': 21, 'end': 22} + else: + return {'start': 1, 'end': 22} + elif season in [3, 4, 5, 6, 7]: + if s_type == 'regular': + return {'start': 1, 'end': 22} + elif s_type == 'post': + return {'start': 23, 'end': 25} + else: + return {'start': 1, 'end': 25} + # Season 2, 8, and beyond + else: + if s_type == 'regular': + return {'start': 1, 'end': 18} + elif s_type == 'post': + return {'start': 19, 'end': 21} + else: + return {'start': 1, 'end': 21} + + +def win_pct(this_team_stan): + if this_team_stan.wins + this_team_stan.losses == 0: + return 0 + else: + return (this_team_stan.wins / (this_team_stan.wins + this_team_stan.losses)) + \ + (this_team_stan.run_diff * .000001) + + +def games_back(leader, chaser): + return ((leader.wins - chaser.wins) + (chaser.losses - leader.losses)) / 2 + + +def e_number(leader, chaser): + e_num = 89 - leader.wins - chaser.losses + return e_num if e_num > 0 else 0 + + +class BaseModel(Model): + class Meta: + database = db + + +class Current(BaseModel): + week = IntegerField(default=0) + freeze = BooleanField(default=True) + season = IntegerField() + transcount = IntegerField(default=0) + bstatcount = IntegerField(default=0) + pstatcount = IntegerField(default=0) + bet_week = IntegerField(default=0) + trade_deadline = IntegerField() + pick_trade_start = IntegerField() + pick_trade_end = IntegerField() + playoffs_begin = IntegerField() + injury_count = IntegerField() + + @staticmethod + def latest(): + latest_current = Current.select().order_by(-Current.id).get() + return latest_current + + +class Division(BaseModel): + division_name = CharField() + division_abbrev = CharField() + league_name = CharField(null=True) + league_abbrev = CharField(null=True) + season = IntegerField(default=0) + + def abbrev(self): + league_short = self.league_abbrev + ' ' if self.league_abbrev else '' + return f'{league_short}{self.division_abbrev}' + + def short_name(self): + league_short = self.league_abbrev + ' ' if self.league_abbrev else '' + return f'{league_short}{self.division_name}' + + def full_name(self): + league_long = self.league_name + ' ' if self.league_name else '' + return f'{league_long}{self.division_name}' + + def sort_division(self, season): + div_query = Standings.select_season(season).where(Standings.team.division == self) + div_teams = [team_stan for team_stan in div_query] + div_teams.sort(key=lambda team: win_pct(team), reverse=True) + + # Assign div_gb and e_num + for x in range(len(div_teams)): + # # Used for two playoff teams per divsion + # # Special calculations for the division leader + # if x == 0: + # div_teams[0].div_gb = -games_back(div_teams[0], div_teams[2]) + # div_teams[0].div_e_num = None + # div_teams[0].wc_gb = None + # div_teams[0].wc_e_num = None + # elif x == 1: + # div_teams[1].div_gb = 0 + # div_teams[1].div_e_num = None + # div_teams[1].wc_gb = None + # div_teams[1].wc_e_num = None + # else: + # div_teams[x].div_gb = games_back(div_teams[1], div_teams[x]) + # div_teams[x].div_e_num = e_number(div_teams[1], div_teams[x]) + # Used for one playoff team per division + if x == 0: + div_teams[0].div_gb = -games_back(div_teams[0], div_teams[1]) + div_teams[0].div_e_num = None + div_teams[0].wc_gb = None + div_teams[0].wc_e_num = None + else: + div_teams[x].div_gb = games_back(div_teams[0], div_teams[x]) + div_teams[x].div_e_num = e_number(div_teams[0], div_teams[x]) + + div_teams[x].save() + + @staticmethod + def sort_wildcard(season, league_abbrev): + divisions = Division.select().where(Division.league_abbrev == league_abbrev) + teams_query = Standings.select_season(season).where( + Standings.wc_gb & (Standings.team.division << divisions) + ) + league_teams = [team_stan for team_stan in teams_query] + league_teams.sort(key=lambda team: win_pct(team), reverse=True) + + for x in range(len(league_teams)): + # Special calculations for two wildcard teams + if x == 0: + league_teams[0].wc_gb = -games_back(league_teams[0], league_teams[2]) + league_teams[0].wc_e_num = None + elif x == 1: + league_teams[1].wc_gb = 0 + league_teams[1].wc_e_num = None + else: + league_teams[x].wc_gb = games_back(league_teams[1], league_teams[x]) + league_teams[x].wc_e_num = e_number(league_teams[1], league_teams[x]) + + league_teams[x].save() + + +class Manager(BaseModel): + name = CharField(unique=True) + image = CharField(null=True) + headline = CharField(null=True) + bio = CharField(null=True) + + +class Team(BaseModel): + abbrev = CharField() + sname = CharField() + lname = CharField() + manager_legacy = CharField(null=True) + division_legacy = CharField(null=True) + gmid = IntegerField() + gmid2 = IntegerField(null=True) + manager1 = ForeignKeyField(Manager, null=True) + manager2 = ForeignKeyField(Manager, null=True) + division = ForeignKeyField(Division, null=True) + mascot = CharField(null=True) + stadium = CharField(null=True) + gsheet = CharField(null=True) + thumbnail = CharField(null=True) + color = CharField(null=True) + dice_color = CharField(null=True) + season = IntegerField() + auto_draft = BooleanField() + + @staticmethod + def select_season(num): + return Team.select().where(Team.season == num) + + @staticmethod + def get_by_owner(gmid, season): + team = Team.get_or_none(Team.gmid == gmid, Team.season == season) + if not team: + team = Team.get_or_none(Team.gmid2 == gmid, Team.season == season) + if not team: + return None + return team + + @staticmethod + def get_season(name_or_abbrev, season): + team = Team.get_or_none(fn.Upper(Team.abbrev) == name_or_abbrev.upper(), Team.season == season) + if not team: + team = Team.get_or_none(fn.Lower(Team.sname) == name_or_abbrev.lower(), Team.season == season) + if not team: + team = Team.get_or_none(fn.Lower(Team.lname) == name_or_abbrev.lower(), Team.season == season) + return team + + def get_record(self, week): + wins = Result.select_season(Current.latest().season).where( + (((Result.hometeam == self) & (Result.homescore > Result.awayscore)) | + ((Result.awayteam == self) & (Result.awayscore > Result.homescore))) & (Result.week <= week) + ) + losses = Result.select_season(Current.latest().season).where( + (((Result.awayteam == self) & (Result.homescore > Result.awayscore)) | + ((Result.hometeam == self) & (Result.awayscore > Result.homescore))) & (Result.week <= week) + ) + if wins.count() + losses.count() > 0: + pct = wins.count() / (wins.count() + losses.count()) + else: + pct = 0 + return {'w': wins.count(), 'l': losses.count(), 'pct': pct} + + def get_gms(self): + if self.gmid2: + return [self.gmid, self.gmid2] + else: + return [self.gmid] + + def get_this_week(self): + active_team = Player.select_season(self.season).where(Player.team == self).order_by(Player.wara) + + active_roster = {'C': 0, '1B': 0, '2B': 0, '3B': 0, 'SS': 0, 'LF': 0, 'CF': 0, 'RF': 0, 'DH': 0, + 'SP': 0, 'RP': 0, 'CP': 0, 'WARa': 0, 'players': []} + + combo_pitchers = 0 + for guy in active_team: + active_roster['WARa'] += guy.wara + active_roster['players'].append(guy) + guy_pos = guy.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers += 1 + else: + try: + for pos in guy_pos: + active_roster[pos] += 1 + except KeyError: + # This happens for season 1 without player positions listed + pass + + if combo_pitchers > 0: + if active_roster['SP'] < 5: + if 5 - active_roster['SP'] <= combo_pitchers: + delta = 5 - active_roster['SP'] + else: + delta = combo_pitchers + active_roster['SP'] += delta + combo_pitchers -= delta + + if combo_pitchers > 0: + active_roster['RP'] += combo_pitchers + + short_il = Player.select_season(self.season).join(Team).where(Player.team.abbrev == f'{self.abbrev}IL') + + short_roster = {'C': 0, '1B': 0, '2B': 0, '3B': 0, 'SS': 0, 'LF': 0, 'CF': 0, 'RF': 0, 'DH': 0, + 'SP': 0, 'RP': 0, 'CP': 0, 'WARa': 0, 'players': []} + + combo_pitchers = 0 + for guy in short_il: + short_roster['WARa'] += guy.wara + short_roster['players'].append(guy) + guy_pos = guy.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers += 1 + else: + for pos in guy_pos: + short_roster[pos] += 1 + + if combo_pitchers > 0: + if short_roster['SP'] < 5: + if 5 - short_roster['SP'] <= combo_pitchers: + delta = 5 - short_roster['SP'] + else: + delta = combo_pitchers + short_roster['SP'] += delta + combo_pitchers -= delta + + if combo_pitchers > 0: + short_roster['RP'] += combo_pitchers + + long_il = Player.select_season(self.season).join(Team).where(Player.team.abbrev == f'{self.abbrev}MiL') + + long_roster = {'C': 0, '1B': 0, '2B': 0, '3B': 0, 'SS': 0, 'LF': 0, 'CF': 0, 'RF': 0, 'DH': 0, + 'SP': 0, 'RP': 0, 'CP': 0, 'WARa': 0, 'players': []} + + combo_pitchers = 0 + for guy in long_il: + long_roster['WARa'] += guy.wara + long_roster['players'].append(guy) + guy_pos = guy.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers += 1 + else: + for pos in guy_pos: + long_roster[pos] += 1 + + if combo_pitchers > 0: + if long_roster['SP'] < 5: + if 5 - long_roster['SP'] <= combo_pitchers: + delta = 5 - long_roster['SP'] + else: + delta = combo_pitchers + long_roster['SP'] += delta + combo_pitchers -= delta + + if combo_pitchers > 0: + long_roster['RP'] += combo_pitchers + + return {'active': active_roster, 'shortil': short_roster, 'longil': long_roster} + + def get_next_week(self): + current = Current.latest() + active_team = Player.select_season(current.season).where(Player.team == self) + + active_roster = {'C': 0, '1B': 0, '2B': 0, '3B': 0, 'SS': 0, 'LF': 0, 'CF': 0, 'RF': 0, 'DH': 0, + 'SP': 0, 'RP': 0, 'CP': 0, 'WARa': 0, 'players': []} + + combo_pitchers = 0 + for guy in active_team: + active_roster['WARa'] += guy.wara + active_roster['players'].append(guy) + guy_pos = guy.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers += 1 + else: + for pos in guy_pos: + active_roster[pos] += 1 + + all_drops = Transaction.select_season(Current.latest().season).where( + (Transaction.oldteam == self) & (Transaction.week == current.week + 1) & (Transaction.cancelled == 0) + ) + all_adds = Transaction.select_season(Current.latest().season).where( + (Transaction.newteam == self) & (Transaction.week == current.week + 1) & (Transaction.cancelled == 0) + ) + + for move in all_drops: + guy_pos = move.player.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers -= 1 + else: + for pos in guy_pos: + active_roster[pos] -= 1 + # print(f'dropping {move.player.name} id ({move.player.get_id()}) for {move.player.wara} WARa') + active_roster['WARa'] -= move.player.wara + try: + active_roster['players'].remove(move.player) + except: + print(f'I could not drop {move.player.name}') + + for move in all_adds: + guy_pos = move.player.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers += 1 + else: + for pos in guy_pos: + active_roster[pos] += 1 + # print(f'adding {move.player.name} id ({move.player.get_id()}) for {move.player.wara} WARa') + active_roster['WARa'] += move.player.wara + active_roster['players'].append(move.player) + + if combo_pitchers > 0: + if active_roster['SP'] < 5: + if 5 - active_roster['SP'] <= combo_pitchers: + delta = 5 - active_roster['SP'] + else: + delta = combo_pitchers + active_roster['SP'] += delta + combo_pitchers -= delta + + if combo_pitchers > 0: + active_roster['RP'] += combo_pitchers + + short_il = Player.select_season(current.season).join(Team).where(Player.team.abbrev == f'{self.abbrev}SIL') + + short_roster = {'C': 0, '1B': 0, '2B': 0, '3B': 0, 'SS': 0, 'LF': 0, 'CF': 0, 'RF': 0, 'DH': 0, + 'SP': 0, 'RP': 0, 'CP': 0, 'WARa': 0, 'players': []} + + combo_pitchers = 0 + for guy in short_il: + short_roster['WARa'] += guy.wara + short_roster['players'].append(guy) + guy_pos = guy.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers += 1 + else: + for pos in guy_pos: + short_roster[pos] += 1 + + sil_team = Team.get_season(f'{self.abbrev}SIL', current.season) + all_drops = Transaction.select_season(Current.latest().season).where( + (Transaction.oldteam == sil_team) & (Transaction.week == current.week + 1) & (Transaction.cancelled == 0) + ) + all_adds = Transaction.select_season(Current.latest().season).where( + (Transaction.newteam == sil_team) & (Transaction.week == current.week + 1) & (Transaction.cancelled == 0) + ) + + for move in all_drops: + guy_pos = move.player.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers -= 1 + else: + for pos in guy_pos: + short_roster[pos] -= 1 + short_roster['WARa'] -= move.player.wara + # print(f'SIL dropping {move.player.name} id ({move.player.get_id()}) for {move.player.wara} WARa') + try: + short_roster['players'].remove(move.player) + except: + print(f'I could not drop {move.player.name}') + + for move in all_adds: + guy_pos = move.player.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers += 1 + else: + for pos in guy_pos: + short_roster[pos] += 1 + # print(f'SIL adding {move.player.name} id ({move.player.get_id()}) for {move.player.wara} WARa') + short_roster['WARa'] += move.player.wara + short_roster['players'].append(move.player) + + if combo_pitchers > 0: + if short_roster['SP'] < 5: + if 5 - short_roster['SP'] <= combo_pitchers: + delta = 5 - short_roster['SP'] + else: + delta = combo_pitchers + short_roster['SP'] += delta + combo_pitchers -= delta + + if combo_pitchers > 0: + short_roster['RP'] += combo_pitchers + + long_il = Player.select_season(current.season).join(Team).where(Player.team.abbrev == f'{self.abbrev}MiL') + + long_roster = {'C': 0, '1B': 0, '2B': 0, '3B': 0, 'SS': 0, 'LF': 0, 'CF': 0, 'RF': 0, 'DH': 0, + 'SP': 0, 'RP': 0, 'CP': 0, 'WARa': 0, 'players': []} + + combo_pitchers = 0 + for guy in long_il: + long_roster['WARa'] += guy.wara + long_roster['players'].append(guy) + guy_pos = guy.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers += 1 + else: + for pos in guy_pos: + long_roster[pos] += 1 + + lil_team = Team.get_season(f'{self.abbrev}LIL', current.season) + all_drops = Transaction.select_season(Current.latest().season).where( + (Transaction.oldteam == lil_team) & (Transaction.week == current.week + 1) & (Transaction.cancelled == 0) + ) + all_adds = Transaction.select_season(Current.latest().season).where( + (Transaction.newteam == lil_team) & (Transaction.week == current.week + 1) & (Transaction.cancelled == 0) + ) + + for move in all_drops: + guy_pos = move.player.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers -= 1 + else: + for pos in guy_pos: + long_roster[pos] -= 1 + long_roster['WARa'] -= move.player.wara + # print(f'LIL dropping {move.player.name} id ({move.player.get_id()}) for {move.player.wara} WARa') + try: + long_roster['players'].remove(move.player) + except: + print(f'I could not drop {move.player.name}') + + for move in all_adds: + guy_pos = move.player.get_positions() + if 'SP' in guy_pos and 'RP' in guy_pos: + combo_pitchers += 1 + else: + for pos in guy_pos: + long_roster[pos] += 1 + # print(f'LIL adding {move.player.name} id ({move.player.get_id()}) for {move.player.wara} WARa') + long_roster['WARa'] += move.player.wara + long_roster['players'].append(move.player) + + if combo_pitchers > 0: + if long_roster['SP'] < 5: + if 5 - long_roster['SP'] <= combo_pitchers: + delta = 5 - long_roster['SP'] + else: + delta = combo_pitchers + long_roster['SP'] += delta + combo_pitchers -= delta + + if combo_pitchers > 0: + long_roster['RP'] += combo_pitchers + + return {'active': active_roster, 'shortil': short_roster, 'longil': long_roster} + + def run_pythag_last8(self): + team_stan = Standings.get_or_none(Standings.team == self) + + runs_scored_home = Result.select(fn.SUM(Result.homescore).alias('runs')).where( + Result.hometeam == self + )[0].runs + runs_scored_away = Result.select(fn.SUM(Result.awayscore).alias('runs')).where( + Result.awayteam == self + )[0].runs + runs_allowed_home = Result.select(fn.SUM(Result.homescore).alias('runs')).where( + Result.awayteam == self + )[0].runs + runs_allowed_away = Result.select(fn.SUM(Result.awayscore).alias('runs')).where( + Result.hometeam == self + )[0].runs + + if not runs_scored_home: + runs_scored_home = 0 + if not runs_scored_away: + runs_scored_away = 0 + if not runs_allowed_home: + runs_allowed_home = 0 + if not runs_allowed_away: + runs_allowed_away = 0 + + runs_scored = runs_scored_home + runs_scored_away + runs_allowed = runs_allowed_home + runs_allowed_away + if runs_allowed == 0: + pythag_win_pct = 0 + else: + pythag_win_pct = runs_scored ** 1.83 / ((runs_scored ** 1.83) + (runs_allowed ** 1.83)) + + games_played = team_stan.wins + team_stan.losses + team_stan.pythag_wins = round(games_played * pythag_win_pct) + team_stan.pythag_losses = games_played - team_stan.pythag_wins + + last_games = Result.select_season(self.season).where( + (Result.hometeam == self) | (Result.awayteam == self) + ).order_by(-Result.id).limit(8) + + for game in last_games: + if game.homescore > game.awayscore: + if game.hometeam == self: + team_stan.last8_wins += 1 + else: + team_stan.last8_losses += 1 + else: + if game.hometeam == self: + team_stan.last8_losses += 1 + else: + team_stan.last8_wins += 1 + + return team_stan.save() + + +class Result(BaseModel): + week = IntegerField() + game = IntegerField() + awayteam = ForeignKeyField(Team) + hometeam = ForeignKeyField(Team) + awayscore = IntegerField() + homescore = IntegerField() + season = IntegerField() + scorecard_url = CharField(null=True) + + @staticmethod + def regular_season(num): + if num == 1: + return Result.select().where((Result.season == 1) & (Result.week < 21)) + elif num == 2: + return Result.select().where((Result.season == 2) & (Result.week < 19)) + elif num == 3 or num == 4: + return Result.select().where((Result.season == num) & (Result.week < 23)) + else: + return None + + @staticmethod + def post_season(num): + if num == 1: + return Result.select().where((Result.season == 1) & (Result.week >= 21)) + elif num == 2: + return Result.select().where((Result.season == 2) & (Result.week >= 19)) + elif num == 3 or num == 4: + return Result.select().where((Result.season == num) & (Result.week >= 23)) + else: + return None + + @staticmethod + def select_season(num): + return Result.select().where(Result.season == num) + + def update_standings(self): + away_stan = Standings.get_season(self.awayteam) + home_stan = Standings.get_season(self.hometeam) + away_div = Division.get_by_id(self.awayteam.division.id) + home_div = Division.get_by_id(self.hometeam.division.id) + + if self.homescore > self.awayscore: + # - generic w/l & home/away w/l + home_stan.wins += 1 + home_stan.home_wins += 1 + away_stan.losses += 1 + away_stan.away_losses += 1 + + # - update streak wl and num + if home_stan.streak_wl == 'w': + home_stan.streak_num += 1 + else: + home_stan.streak_wl = 'w' + home_stan.streak_num = 1 + + if away_stan.streak_wl == 'l': + away_stan.streak_num += 1 + else: + away_stan.streak_wl = 'l' + away_stan.streak_num = 1 + + # - if 1-run, tally accordingly + if self.homescore == self.awayscore + 1: + home_stan.one_run_wins += 1 + away_stan.one_run_losses += 1 + + # Used for one league with 3 divisions + # - update record v division + # if away_div.division_abbrev == 'BE': + # home_stan.div1_wins += 1 + # elif away_div.division_abbrev == 'DO': + # home_stan.div2_wins += 1 + # else: + # home_stan.div3_wins += 1 + # + # if home_div.division_abbrev == 'BE': + # away_stan.div1_losses += 1 + # elif home_div.division_abbrev == 'DO': + # away_stan.div2_losses += 1 + # else: + # away_stan.div3_losses += 1 + + # Used for two league plus divisions + if away_div.league_abbrev == 'AL': + if away_div.division_abbrev == 'E': + home_stan.div1_wins += 1 + else: + home_stan.div2_wins += 1 + else: + if away_div.division_abbrev == 'E': + home_stan.div3_wins += 1 + else: + home_stan.div4_wins += 1 + + if home_div.league_abbrev == 'AL': + if home_div.division_abbrev == 'E': + away_stan.div1_losses += 1 + else: + away_stan.div2_losses += 1 + else: + if home_div.division_abbrev == 'E': + away_stan.div3_losses += 1 + else: + away_stan.div4_losses += 1 + + # - adjust run_diff + home_stan.run_diff += self.homescore - self.awayscore + away_stan.run_diff -= self.homescore - self.awayscore + else: + # - generic w/l & home/away w/l + home_stan.losses += 1 + home_stan.home_losses += 1 + away_stan.wins += 1 + away_stan.away_wins += 1 + + # - update streak wl and num + if home_stan.streak_wl == 'l': + home_stan.streak_num += 1 + else: + home_stan.streak_wl = 'l' + home_stan.streak_num = 1 + + if away_stan.streak_wl == 'w': + away_stan.streak_num += 1 + else: + away_stan.streak_wl = 'w' + away_stan.streak_num = 1 + + # - if 1-run, tally accordingly + if self.awayscore == self.homescore + 1: + home_stan.one_run_losses += 1 + away_stan.one_run_wins += 1 + + # Used for one league with 3 divisions + # - update record v division + # if away_div.division_abbrev == 'BE': + # home_stan.div1_losses += 1 + # elif away_div.division_abbrev == 'DO': + # home_stan.div2_losses += 1 + # else: + # home_stan.div3_losses += 1 + # + # if home_div.division_abbrev == 'BE': + # away_stan.div1_wins += 1 + # elif home_div.division_abbrev == 'DO': + # away_stan.div2_wins += 1 + # else: + # away_stan.div3_wins += 1 + + # Used for two league plus divisions + if away_div.league_abbrev == 'AL': + if away_div.division_abbrev == 'E': + home_stan.div1_losses += 1 + else: + home_stan.div2_losses += 1 + else: + if away_div.division_abbrev == 'E': + home_stan.div3_losses += 1 + else: + home_stan.div4_losses += 1 + + if home_div.league_abbrev == 'AL': + if home_div.division_abbrev == 'E': + away_stan.div1_wins += 1 + else: + away_stan.div2_wins += 1 + else: + if home_div.division_abbrev == 'E': + away_stan.div3_wins += 1 + else: + away_stan.div4_wins += 1 + + # - adjust run_diff + home_stan.run_diff -= self.awayscore - self.homescore + away_stan.run_diff += self.awayscore - self.homescore + + home_stan.save() + away_stan.save() + + +class Player(BaseModel): + name = CharField() + wara = FloatField() + image = CharField() + image2 = CharField(null=True) + team = ForeignKeyField(Team) + season = IntegerField() + pitcher_injury = IntegerField(null=True) + pos_1 = CharField() + pos_2 = CharField(null=True) + pos_3 = CharField(null=True) + pos_4 = CharField(null=True) + pos_5 = CharField(null=True) + pos_6 = CharField(null=True) + pos_7 = CharField(null=True) + pos_8 = CharField(null=True) + last_game = CharField(null=True) + last_game2 = CharField(null=True) + il_return = CharField(null=True) + demotion_week = IntegerField(null=True) + headshot = CharField(null=True) + vanity_card = CharField(null=True) + strat_code = CharField(null=True) + bbref_id = CharField(null=True) + injury_rating = CharField(null=True) + + @staticmethod + def select_season(num): + return Player.select().where(Player.season == num) + + @staticmethod + def get_season(name, num): + player = None + try: + player = Player.get(fn.Lower(Player.name) == name.lower(), Player.season == num) + except Exception as e: + print(f'**Error** (db_engine player): {e}') + finally: + return player + + def get_positions(self): + """ + Params: None + Return: List of positions (ex ['1b', '3b']) + """ + pos_list = [] + if self.pos_1: + pos_list.append(self.pos_1) + if self.pos_2: + pos_list.append(self.pos_2) + if self.pos_3: + pos_list.append(self.pos_3) + if self.pos_4: + pos_list.append(self.pos_4) + if self.pos_5: + pos_list.append(self.pos_5) + if self.pos_6: + pos_list.append(self.pos_6) + if self.pos_7: + pos_list.append(self.pos_7) + if self.pos_8: + pos_list.append(self.pos_8) + + return pos_list + + +class Schedule(BaseModel): + week = IntegerField() + awayteam = ForeignKeyField(Team) + hometeam = ForeignKeyField(Team) + gamecount = IntegerField() + season = IntegerField() + + @staticmethod + def select_season(season): + return Schedule.select().where(Schedule.season == season) + + +class Transaction(BaseModel): + week = IntegerField() + player = ForeignKeyField(Player) + oldteam = ForeignKeyField(Team) + newteam = ForeignKeyField(Team) + season = IntegerField() + moveid = IntegerField() + cancelled = BooleanField(default=False) + frozen = BooleanField(default=False) + + @staticmethod + def select_season(num): + return Transaction.select().where(Transaction.season == num) + + +class BattingStat(BaseModel): + player = ForeignKeyField(Player) + team = ForeignKeyField(Team) + pos = CharField() + pa = IntegerField() + ab = IntegerField() + run = IntegerField() + hit = IntegerField() + rbi = IntegerField() + double = IntegerField() + triple = IntegerField() + hr = IntegerField() + bb = IntegerField() + so = IntegerField() + hbp = IntegerField() + sac = IntegerField() + ibb = IntegerField() + gidp = IntegerField() + sb = IntegerField() + cs = IntegerField() + bphr = IntegerField() + bpfo = IntegerField() + bp1b = IntegerField() + bplo = IntegerField() + xba = IntegerField() + xbt = IntegerField() + xch = IntegerField() + xhit = IntegerField() + error = IntegerField() + pb = IntegerField() + sbc = IntegerField() + csc = IntegerField() + roba = IntegerField() + robs = IntegerField() + raa = IntegerField() + rto = IntegerField() + week = IntegerField() + game = IntegerField() + season = IntegerField() + + @staticmethod + def combined_season(season): + """ + Params: season, integer (season number), optional + Return: ModelSelect object for season + """ + return BattingStat.select().where(BattingStat.season == season) + + @staticmethod + def regular_season(season): + """ + Params: num, integer (season number) + Return: ModelSelect object for season's regular season + """ + if season == 1: + return BattingStat.select().where((BattingStat.season == 1) & (BattingStat.week < 21))\ + .order_by(BattingStat.week) + elif season == 2: + return BattingStat.select().where((BattingStat.season == 2) & (BattingStat.week < 19))\ + .order_by(BattingStat.week) + elif season > 2: + return BattingStat.select().where((BattingStat.season == season) & (BattingStat.week < 23))\ + .order_by(BattingStat.week) + else: + return None + + @staticmethod + def post_season(season): + """ + Params: num, integer (season number) + Return: ModelSelect object for season's post season + """ + if season == 1: + return BattingStat.select().where((BattingStat.season == 1) & (BattingStat.week >= 21)) + elif season == 2: + return BattingStat.select().where((BattingStat.season == 2) & (BattingStat.week >= 19)) + elif season > 2: + return BattingStat.select().where((BattingStat.season == season) & (BattingStat.week >= 23)) + else: + return None + + @staticmethod + def team_season(team, season): + b_stats = BattingStat.regular_season(season).join(Player).select( + fn.SUM(BattingStat.pa).alias('pas'), + fn.SUM(BattingStat.ab).alias('abs'), + fn.SUM(BattingStat.run).alias('runs'), + fn.SUM(BattingStat.hit).alias('hits'), + fn.SUM(BattingStat.rbi).alias('rbis'), + fn.SUM(BattingStat.double).alias('doubles'), + fn.SUM(BattingStat.triple).alias('triples'), + fn.SUM(BattingStat.hr).alias('hrs'), + fn.SUM(BattingStat.bb).alias('bbs'), + fn.SUM(BattingStat.so).alias('sos'), + fn.SUM(BattingStat.hbp).alias('hbps'), + fn.SUM(BattingStat.sac).alias('sacs'), + fn.SUM(BattingStat.ibb).alias('ibbs'), + fn.SUM(BattingStat.gidp).alias('gidps'), + fn.SUM(BattingStat.sb).alias('sbs'), + fn.SUM(BattingStat.cs).alias('css'), + fn.SUM(BattingStat.bphr).alias('bphr'), + fn.SUM(BattingStat.bpfo).alias('bpfo'), + fn.SUM(BattingStat.bp1b).alias('bp1b'), + fn.SUM(BattingStat.bplo).alias('bplo'), + # fn.SUM(BattingStat.xba).alias('xba'), + # fn.SUM(BattingStat.xbt).alias('xbt'), + fn.COUNT(BattingStat.game).alias('games'), + ).where(BattingStat.team == team) + + total = { + 'game': b_stats[0].games if b_stats[0].games else 0, + 'pa': b_stats[0].pas if b_stats[0].pas else 0, + 'ab': b_stats[0].abs if b_stats[0].abs else 0, + 'run': b_stats[0].runs if b_stats[0].runs else 0, + 'hit': b_stats[0].hits if b_stats[0].hits else 0, + 'rbi': b_stats[0].rbis if b_stats[0].rbis else 0, + 'double': b_stats[0].doubles if b_stats[0].doubles else 0, + 'triple': b_stats[0].triples if b_stats[0].triples else 0, + 'hr': b_stats[0].hrs if b_stats[0].hrs else 0, + 'bb': b_stats[0].bbs if b_stats[0].bbs else 0, + 'so': b_stats[0].sos if b_stats[0].sos else 0, + 'hbp': b_stats[0].hbps if b_stats[0].hbps else 0, + 'sac': b_stats[0].sacs if b_stats[0].sacs else 0, + 'ibb': b_stats[0].ibbs if b_stats[0].ibbs else 0, + 'gidp': b_stats[0].gidps if b_stats[0].gidps else 0, + 'sb': b_stats[0].sbs if b_stats[0].sbs else 0, + 'cs': b_stats[0].css if b_stats[0].css else 0, + 'ba': 0, + 'obp': 0, + 'slg': 0, + 'woba': 0, + 'kpct': 0, + 'bphr': b_stats[0].bphr if b_stats[0].bphr else 0, + 'bpfo': b_stats[0].bpfo if b_stats[0].bpfo else 0, + 'bp1b': b_stats[0].bp1b if b_stats[0].bp1b else 0, + 'bplo': b_stats[0].bplo if b_stats[0].bplo else 0, + # 'xba': b_stats[0].xba if b_stats[0].xba else 0, + # 'xbt': b_stats[0].xbt if b_stats[0].xbt else 0, + } + + if b_stats[0].abs: + total['ba'] = b_stats[0].hits / b_stats[0].abs + + total['obp'] = ( + (b_stats[0].bbs + b_stats[0].hits + b_stats[0].hbps + b_stats[0].ibbs) / b_stats[0].pas + ) + + total['slg'] = ( + ((b_stats[0].hrs * 4) + (b_stats[0].triples * 3) + (b_stats[0].doubles * 2) + + (b_stats[0].hits - b_stats[0].hrs - b_stats[0].triples - b_stats[0].doubles)) / b_stats[0].abs + ) + + total['woba'] = ( + ((b_stats[0].bbs * .69) + (b_stats[0].hbps * .722) + (b_stats[0].doubles * 1.271) + + (b_stats[0].triples * 1.616) + (b_stats[0].hrs * 2.101) + + ((b_stats[0].hits - b_stats[0].hrs - b_stats[0].triples - b_stats[0].doubles) * .888)) / + (b_stats[0].pas - b_stats[0].ibbs) + ) + + total['kpct'] = (total['so'] * 100) / total['ab'] + + total_innings = PitchingStat.regular_season(season).join(Player).select( + fn.SUM(PitchingStat.ip).alias('ips'), + ).where(PitchingStat.player.team == team) + total['rper9'] = (total['run'] * 9) / total_innings[0].ips + + return total + + @staticmethod + def team_fielding_season(team, season): + f_stats = BattingStat.regular_season(season).select( + fn.SUM(BattingStat.xch).alias('xchs'), + fn.SUM(BattingStat.xhit).alias('xhits'), + fn.SUM(BattingStat.error).alias('errors'), + # fn.SUM(BattingStat.roba).alias('roba'), + # fn.SUM(BattingStat.robs).alias('robs'), + # fn.SUM(BattingStat.raa).alias('raa'), + # fn.SUM(BattingStat.rto).alias('rto'), + fn.SUM(BattingStat.pb).alias('pbs'), + fn.SUM(BattingStat.sbc).alias('sbas'), + fn.SUM(BattingStat.csc).alias('cscs'), + fn.COUNT(BattingStat.game).alias('games'), + ).where(BattingStat.team == team) + + total = { + 'game': f_stats[0].games if f_stats[0].games else 0, + 'xch': f_stats[0].xchs if f_stats[0].xchs else 0, + 'xhit': f_stats[0].xhits if f_stats[0].xhits else 0, + 'error': f_stats[0].errors if f_stats[0].errors else 0, + # 'roba': f_stats[0].roba if f_stats[0].roba else 0, + # 'robs': f_stats[0].robs if f_stats[0].robs else 0, + # 'raa': f_stats[0].raa if f_stats[0].raa else 0, + # 'rto': f_stats[0].rto if f_stats[0].rto else 0, + 'pb': f_stats[0].pbs if f_stats[0].pbs else 0, + 'sbc': f_stats[0].sbas if f_stats[0].sbas else 0, + 'csc': f_stats[0].cscs if f_stats[0].cscs else 0, + 'wfpct': 0, + 'cspct': 0, + } + + if total['xch'] > 0: + total['wfpct'] = (total['xch'] - (total['error'] * .5) - (total['xhit'] * .75)) / (total['xch']) + if total['sbc'] > 0: + total['cspct'] = (total['csc'] / total['sbc']) * 100 + + return total + + +class PitchingStat(BaseModel): + player = ForeignKeyField(Player) + team = ForeignKeyField(Team) + ip = FloatField() + hit = FloatField() + run = FloatField() + erun = FloatField() + so = FloatField() + bb = FloatField() + hbp = FloatField() + wp = FloatField() + balk = FloatField() + hr = FloatField() + ir = FloatField() + irs = FloatField() + gs = FloatField() + win = FloatField() + loss = FloatField() + hold = FloatField() + sv = FloatField() + bsv = FloatField() + week = IntegerField() + game = IntegerField() + season = IntegerField() + + @staticmethod + def select_season(season): + return PitchingStat.select().where(PitchingStat.season == season) + + @staticmethod + def regular_season(season): + if season == 1: + return PitchingStat.select().where((PitchingStat.season == 1) & (PitchingStat.week < 21))\ + .order_by(PitchingStat.week) + elif season == 2: + return PitchingStat.select().where((PitchingStat.season == 2) & (PitchingStat.week < 19))\ + .order_by(PitchingStat.week) + elif season > 2: + return PitchingStat.select().where((PitchingStat.season == season) & (PitchingStat.week < 23))\ + .order_by(PitchingStat.week) + else: + return None + + @staticmethod + def post_season(season): + if season == 1: + return PitchingStat.select().where((PitchingStat.season == 1) & (PitchingStat.week >= 21))\ + .order_by(PitchingStat.week) + elif season == 2: + return PitchingStat.select().where((PitchingStat.season == 2) & (PitchingStat.week >= 19))\ + .order_by(PitchingStat.week) + elif season > 2: + return PitchingStat.select().where((PitchingStat.season == season) & (PitchingStat.week >= 23))\ + .order_by(PitchingStat.week) + else: + return None + + @staticmethod + def team_season(team, season): + p_stats = PitchingStat.regular_season(season).select( + fn.SUM(PitchingStat.ip).alias('ips'), + fn.SUM(PitchingStat.hit).alias('hits'), + fn.SUM(PitchingStat.run).alias('runs'), + fn.SUM(PitchingStat.erun).alias('eruns'), + fn.SUM(PitchingStat.so).alias('sos'), + fn.SUM(PitchingStat.bb).alias('bbs'), + fn.SUM(PitchingStat.hbp).alias('hbps'), + fn.SUM(PitchingStat.wp).alias('wps'), + fn.SUM(PitchingStat.ir).alias('ir'), + fn.SUM(PitchingStat.irs).alias('irs'), + fn.SUM(PitchingStat.balk).alias('balks'), + fn.SUM(PitchingStat.hr).alias('hrs'), + fn.COUNT(PitchingStat.game).alias('games'), + fn.SUM(PitchingStat.gs).alias('gss'), + fn.SUM(PitchingStat.win).alias('wins'), + fn.SUM(PitchingStat.loss).alias('losses'), + fn.SUM(PitchingStat.hold).alias('holds'), + fn.SUM(PitchingStat.sv).alias('saves'), + fn.SUM(PitchingStat.bsv).alias('bsaves'), + ).where(PitchingStat.team == team) + + total = { + 'ip': p_stats[0].ips if p_stats[0].ips else 0, + 'hit': int(p_stats[0].hits) if p_stats[0].hits else 0, + 'run': int(p_stats[0].runs) if p_stats[0].runs else 0, + 'erun': int(p_stats[0].eruns) if p_stats[0].eruns else 0, + 'so': int(p_stats[0].sos) if p_stats[0].sos else 0, + 'bb': int(p_stats[0].bbs) if p_stats[0].bbs else 0, + 'hbp': int(p_stats[0].hbps) if p_stats[0].hbps else 0, + 'wp': int(p_stats[0].wps) if p_stats[0].wps else 0, + 'balk': int(p_stats[0].balks) if p_stats[0].balks else 0, + 'hr': int(p_stats[0].hrs) if p_stats[0].hrs else 0, + 'game': int(p_stats[0].games) if p_stats[0].games else 0, + 'gs': int(p_stats[0].gss) if p_stats[0].gss else 0, + 'win': int(p_stats[0].wins) if p_stats[0].wins else 0, + 'loss': int(p_stats[0].losses) if p_stats[0].losses else 0, + 'hold': int(p_stats[0].holds) if p_stats[0].holds else 0, + 'sv': int(p_stats[0].saves) if p_stats[0].saves else 0, + 'bsv': int(p_stats[0].bsaves) if p_stats[0].bsaves else 0, + 'wl%': 0, + 'era': 0, + 'whip': 0, + 'ir': int(p_stats[0].ir) if p_stats[0].ir else 0, + 'irs': int(p_stats[0].irs) if p_stats[0].irs else 0, + } + + if total['ip']: + total['era'] = (total['erun'] * 9) / total['ip'] + + total['whip'] = (total['bb'] + total['hit']) / total['ip'] + + if total['win'] + total['loss'] > 0: + total['wl%'] = total['win'] / (total['win'] + total['loss']) + + return total + + +class Standings(BaseModel): + team = ForeignKeyField(Team) + wins = IntegerField(default=0) + losses = IntegerField(default=0) + run_diff = IntegerField(default=0) + div_gb = FloatField(default=0.0, null=True) + div_e_num = IntegerField(default=0, null=True) + wc_gb = FloatField(default=99.0, null=True) + wc_e_num = IntegerField(default=99, null=True) + home_wins = IntegerField(default=0) + home_losses = IntegerField(default=0) + away_wins = IntegerField(default=0) + away_losses = IntegerField(default=0) + last8_wins = IntegerField(default=0) + last8_losses = IntegerField(default=0) + streak_wl = CharField(default='w') + streak_num = IntegerField(default=0) + one_run_wins = IntegerField(default=0) + one_run_losses = IntegerField(default=0) + pythag_wins = IntegerField(default=0) + pythag_losses = IntegerField(default=0) + div1_wins = IntegerField(default=0) + div1_losses = IntegerField(default=0) + div2_wins = IntegerField(default=0) + div2_losses = IntegerField(default=0) + div3_wins = IntegerField(default=0) + div3_losses = IntegerField(default=0) + div4_wins = IntegerField(default=0) + div4_losses = IntegerField(default=0) + + @staticmethod + def select_season(season): + return Standings.select().join(Team).where(Standings.team.season == season) + + @staticmethod + def get_season(team): + return Standings.get_or_none(Standings.team == team) + + @staticmethod + def recalculate(season, full_wipe=True): + all_teams = Team.select_season(season).where(Team.division) + if full_wipe: + # Wipe existing data + delete_lines = Standings.select_season(season) + for line in delete_lines: + line.delete_instance() + + # Recreate current season Standings objects + create_teams = [Standings(team=team) for team in all_teams] + with db.atomic(): + Standings.bulk_create(create_teams) + + # Iterate through each individual result + for game in Result.select_season(season).where(Result.week <= 22): + # tally win and loss for each standings object + game.update_standings() + + # Set pythag record and iterate through last 8 games for last8 record + for team in all_teams: + team.run_pythag_last8() + + # Pull each division at a time and sort by win pct + for division in Division.select().where(Division.season == season): + division.sort_division(season) + + # Pull each league (filter by not null wc_gb) and sort by win pct + + # # For one league: + # Division.sort_wildcard(season, 'SBa') + + # For two leagues + Division.sort_wildcard(season, 'AL') + Division.sort_wildcard(season, 'NL') + + +class BattingCareer(BaseModel): + name = CharField() + pa = FloatField(default=0) + ab = FloatField(default=0) + run = FloatField(default=0) + hit = FloatField(default=0) + rbi = FloatField(default=0) + double = FloatField(default=0) + triple = FloatField(default=0) + hr = FloatField(default=0) + bb = FloatField(default=0) + so = FloatField(default=0) + hbp = FloatField(default=0) + sac = FloatField(default=0) + ibb = FloatField(default=0) + gidp = FloatField(default=0) + sb = FloatField(default=0) + cs = FloatField(default=0) + bphr = FloatField(default=0) + bpfo = FloatField(default=0) + bp1b = FloatField(default=0) + bplo = FloatField(default=0) + xba = FloatField(default=0) + xbt = FloatField(default=0) + game = FloatField(default=0) + + @staticmethod + def recalculate(): + # Wipe existing data + delete_lines = BattingCareer.select() + for line in delete_lines: + line.delete_instance() + + # For each seasonstat, find career or create new and increment + for this_season in BattingSeason.select().where(BattingSeason.season_type == 'Regular'): + this_career = BattingCareer.get_or_none(BattingCareer.name == this_season.player.name) + if not this_career: + this_career = BattingCareer(name=this_season.player.name) + this_career.save() + + this_career.pa += this_season.pa + this_career.ab += this_season.ab + this_career.run += this_season.run + this_career.hit += this_season.hit + this_career.rbi += this_season.rbi + this_career.double += this_season.double + this_career.triple += this_season.triple + this_career.hr += this_season.hr + this_career.bb += this_season.bb + this_career.so += this_season.so + this_career.hbp += this_season.hbp + this_career.sac += this_season.sac + this_career.ibb += this_season.ibb + this_career.gidp += this_season.gidp + this_career.sb += this_season.sb + this_career.cs += this_season.cs + this_career.bphr += this_season.bphr + this_career.bpfo += this_season.bpfo + this_career.bp1b += this_season.bp1b + this_career.bplo += this_season.bplo + this_career.xba += this_season.xba + this_career.xbt += this_season.xbt + this_career.save() + + +class PitchingCareer(BaseModel): + name = CharField() + ip = FloatField(default=0) + hit = FloatField(default=0) + run = FloatField(default=0) + erun = FloatField(default=0) + so = FloatField(default=0) + bb = FloatField(default=0) + hbp = FloatField(default=0) + wp = FloatField(default=0) + balk = FloatField(default=0) + hr = FloatField(default=0) + ir = FloatField(default=0) + irs = FloatField(default=0) + gs = FloatField(default=0) + win = FloatField(default=0) + loss = FloatField(default=0) + hold = FloatField(default=0) + sv = FloatField(default=0) + bsv = FloatField(default=0) + game = FloatField(default=0) + + @staticmethod + def recalculate(): + # Wipe existing data + delete_lines = PitchingCareer.select() + for line in delete_lines: + line.delete_instance() + + # For each seasonstat, find career or create new and increment + for this_season in PitchingSeason.select().where(PitchingSeason.season_type == 'Regular'): + this_career = PitchingCareer.get_or_none(PitchingCareer.name == this_season.player.name) + if not this_career: + this_career = PitchingCareer(name=this_season.player.name) + this_career.save() + + this_career.ip += this_season.ip + this_career.hit += this_season.hit + this_career.run += this_season.run + this_career.erun += this_season.erun + this_career.so += this_season.so + this_career.bb += this_season.bb + this_career.hbp += this_season.hbp + this_career.wp += this_season.wp + this_career.balk += this_season.balk + this_career.hr += this_season.hr + this_career.ir += this_season.ir + this_career.irs += this_season.irs + this_career.gs += this_season.gs + this_career.win += this_season.win + this_career.loss += this_season.loss + this_career.hold += this_season.hold + this_career.sv += this_season.sv + this_career.bsv += this_season.bsv + this_career.save() + + +class FieldingCareer(BaseModel): + name = CharField() + pos = CharField() + xch = IntegerField(default=0) + xhit = IntegerField(default=0) + error = IntegerField(default=0) + pb = IntegerField(default=0) + sbc = IntegerField(default=0) + csc = IntegerField(default=0) + roba = IntegerField(default=0) + robs = IntegerField(default=0) + raa = IntegerField(default=0) + rto = IntegerField(default=0) + game = IntegerField(default=0) + + @staticmethod + def recalculate(): + # Wipe existing data + delete_lines = FieldingCareer.select() + for line in delete_lines: + line.delete_instance() + + # For each seasonstat, find career or create new and increment + for this_season in FieldingSeason.select().where(FieldingSeason.season_type == 'Regular'): + this_career = FieldingCareer.get_or_none( + FieldingCareer.name == this_season.player.name, FieldingCareer.pos == this_season.pos + ) + if not this_career: + this_career = FieldingCareer(name=this_season.player.name, pos=this_season.pos) + this_career.save() + + this_career.xch += this_season.xch + this_career.xhit += this_season.xhit + this_career.error += this_season.error + this_career.pb += this_season.pb + this_career.sbc += this_season.sbc + this_career.csc += this_season.csc + this_career.roba += this_season.roba + this_career.robs += this_season.robs + this_career.raa += this_season.raa + this_career.rto += this_season.rto + this_career.save() + + +class BattingSeason(BaseModel): + player = ForeignKeyField(Player) + season = IntegerField() + season_type = CharField(default='Regular') + career = ForeignKeyField(BattingCareer, null=True) + pa = FloatField(default=0) + ab = FloatField(default=0) + run = FloatField(default=0) + hit = FloatField(default=0) + rbi = FloatField(default=0) + double = FloatField(default=0) + triple = FloatField(default=0) + hr = FloatField(default=0) + bb = FloatField(default=0) + so = FloatField(default=0) + hbp = FloatField(default=0) + sac = FloatField(default=0) + ibb = FloatField(default=0) + gidp = FloatField(default=0) + sb = FloatField(default=0) + cs = FloatField(default=0) + bphr = FloatField(default=0) + bpfo = FloatField(default=0) + bp1b = FloatField(default=0) + bplo = FloatField(default=0) + xba = FloatField(default=0) + xbt = FloatField(default=0) + game = FloatField(default=0) + + @staticmethod + def select_season(season): + return BattingSeason.select().where(BattingSeason.season == season) + + # @staticmethod + # def recalculate(season, manager_id): + # # Wipe existing data + # delete_lines = BattingSeason.select_season(season) + # for line in delete_lines: + # line.delete_instance() + # + # # For each battingstat, find season or create new and increment + # for line in BattingStat.select().where( + # (BattingStat.season == season) & (BattingStat.player.team.manager1 == manager_id) + # ): + # if line.season == 1: + # s_type = 'Regular' if line.week < 21 else 'Post' + # elif line.season == 2: + # s_type = 'Regular' if line.week < 19 else 'Post' + # else: + # s_type = 'Regular' if line.week < 23 else 'Post' + # + # this_season = BattingSeason.get_or_none(player=line.player, season_type=s_type) + # if not this_season: + # this_season = BattingSeason(player=line.player, season_type=s_type, season=line.season) + # this_season.save() + # + # this_season.pa += line.pa + # this_season.ab += line.ab + # this_season.run += line.run + # this_season.hit += line.hit + # this_season.rbi += line.rbi + # this_season.double += line.double + # this_season.triple += line.triple + # this_season.hr += line.hr + # this_season.bb += line.bb + # this_season.so += line.so + # this_season.hbp += line.hbp + # this_season.sac += line.sac + # this_season.ibb += line.ibb + # this_season.gidp += line.gidp + # this_season.sb += line.sb + # this_season.cs += line.cs + # this_season.save() + + def recalculate(self): + self.pa = 0 + self.ab = 0 + self.run = 0 + self.hit = 0 + self.rbi = 0 + self.double = 0 + self.triple = 0 + self.hr = 0 + self.bb = 0 + self.so = 0 + self.hbp = 0 + self.sac = 0 + self.ibb = 0 + self.gidp = 0 + self.sb = 0 + self.cs = 0 + self.bphr = 0 + self.bpfo = 0 + self.bp1b = 0 + self.bplo = 0 + self.xba = 0 + self.xbt = 0 + self.game = 0 + + if self.season_type == 'Regular': + all_stats = BattingStat.regular_season(self.season).where(BattingStat.player == self.player) + else: + all_stats = BattingStat.post_season(self.season).where(BattingStat.player == self.player) + for line in all_stats: + self.pa += line.pa + self.ab += line.ab + self.run += line.run + self.hit += line.hit + self.rbi += line.rbi + self.double += line.double + self.triple += line.triple + self.hr += line.hr + self.bb += line.bb + self.so += line.so + self.hbp += line.hbp + self.sac += line.sac + self.ibb += line.ibb + self.gidp += line.gidp + self.sb += line.sb + self.cs += line.cs + self.bphr += line.bphr + self.bpfo += line.bpfo + self.bp1b += line.bp1b + self.bplo += line.bplo + self.xba += line.xba + self.xbt += line.xbt + self.game += 1 + + self.save() + return all_stats.count() + + +class PitchingSeason(BaseModel): + player = ForeignKeyField(Player) + season = IntegerField() + season_type = CharField(default='Regular') + career = ForeignKeyField(PitchingCareer, null=True) + ip = FloatField(default=0) + hit = FloatField(default=0) + run = FloatField(default=0) + erun = FloatField(default=0) + so = FloatField(default=0) + bb = FloatField(default=0) + hbp = FloatField(default=0) + wp = FloatField(default=0) + balk = FloatField(default=0) + hr = FloatField(default=0) + ir = FloatField(default=0) + irs = FloatField(default=0) + gs = FloatField(default=0) + win = FloatField(default=0) + loss = FloatField(default=0) + hold = FloatField(default=0) + sv = FloatField(default=0) + bsv = FloatField(default=0) + game = FloatField(default=0) + + @staticmethod + def select_season(season): + return PitchingSeason.select().where(PitchingSeason.season == season) + + # @staticmethod + # def recalculate(season, manager_id): + # # Wipe existing data + # delete_lines = PitchingSeason.select_season(season) + # for line in delete_lines: + # line.delete_instance() + # + # # For each pitchingstat, find season or create new and increment + # for line in PitchingStat.select().where( + # (PitchingStat.season == season) & (PitchingStat.player.team.manager1 == manager_id) + # ): + # if line.season == 1: + # s_type = 'Regular' if line.week < 21 else 'Post' + # elif line.season == 2: + # s_type = 'Regular' if line.week < 19 else 'Post' + # else: + # s_type = 'Regular' if line.week < 23 else 'Post' + # + # this_season = PitchingSeason.get_or_none(player=line.player, season_type=s_type) + # if not this_season: + # this_season = PitchingSeason(player=line.player, season_type=s_type, season=line.season) + # this_season.save() + # + # this_season.ip += line.ip + # this_season.hit += line.hit + # this_season.run += line.run + # this_season.erun += line.erun + # this_season.so += line.so + # this_season.bb += line.bb + # this_season.hbp += line.hbp + # this_season.wp += line.wp + # this_season.balk += line.balk + # this_season.hr += line.hr + # this_season.gs += line.gs + # this_season.win += line.win + # this_season.loss += line.loss + # this_season.hold += line.hold + # this_season.sv += line.sv + # this_season.bsv += line.bsv + # this_season.game += 1 + # this_season.save() + + def recalculate(self): + self.ip = 0 + self.hit = 0 + self.run = 0 + self.erun = 0 + self.so = 0 + self.bb = 0 + self.hbp = 0 + self.wp = 0 + self.balk = 0 + self.hr = 0 + self.ir = 0 + self.irs = 0 + self.gs = 0 + self.win = 0 + self.loss = 0 + self.hold = 0 + self.sv = 0 + self.bsv = 0 + self.game = 0 + + if self.season_type == 'Regular': + all_stats = PitchingStat.regular_season(self.season).where(PitchingStat.player == self.player) + else: + all_stats = PitchingStat.post_season(self.season).where(PitchingStat.player == self.player) + for line in all_stats: + self.ip += line.ip + self.hit += line.hit + self.run += line.run + self.erun += line.erun + self.so += line.so + self.bb += line.bb + self.hbp += line.hbp + self.wp += line.wp + self.balk += line.balk + self.hr += line.hr + self.ir += line.ir + self.irs += line.irs + self.gs += line.gs + self.win += line.win + self.loss += line.loss + self.hold += line.hold + self.sv += line.sv + self.bsv += line.bsv + self.game += 1 + + self.save() + return all_stats.count() + + +class FieldingSeason(BaseModel): + player = ForeignKeyField(Player) + season = IntegerField() + season_type = CharField(default='Regular') + pos = CharField() + career = ForeignKeyField(FieldingCareer, null=True) + xch = IntegerField(default=0) + xhit = IntegerField(default=0) + error = IntegerField(default=0) + pb = IntegerField(default=0) + sbc = IntegerField(default=0) + csc = IntegerField(default=0) + roba = IntegerField(default=0) + robs = IntegerField(default=0) + raa = IntegerField(default=0) + rto = IntegerField(default=0) + game = IntegerField(default=0) + + @staticmethod + def select_season(season): + return FieldingSeason.select().where(FieldingSeason.season == season) + + # @staticmethod + # def recalculate(season, manager_id): + # # Wipe existing data + # delete_lines = FieldingSeason.select() + # for line in delete_lines: + # line.delete_instance() + # + # # players = Player.select_season(season).where(Player.team) + # + # # For each battingstat, find season or create new and increment + # for line in BattingStat.select().join(Player).join(Team).where( + # (BattingStat.season == season) & (BattingStat.player.team.manager1 == manager_id) + # ): + # if line.season == 1: + # s_type = 'Regular' if line.week < 21 else 'Post' + # elif line.season == 2: + # s_type = 'Regular' if line.week < 19 else 'Post' + # else: + # s_type = 'Regular' if line.week < 23 else 'Post' + # + # this_season = BattingSeason.get_or_none(player=line.player, season_type=s_type, pos=line.pos) + # if not this_season: + # this_season = BattingSeason(player=line.player, season_type=s_type, pos=line.pos, season=line.season) + # this_season.save() + # + # this_season.xch += line.xch + # this_season.xhit += line.xhit + # this_season.error += line.error + # this_season.pb += line.pb + # this_season.sbc += line.sbc + # this_season.csc += line.csc + # this_season.game += 1 + # this_season.save() + + def recalculate(self): + self.xch = 0 + self.xhit = 0 + self.error = 0 + self.pb = 0 + self.sbc = 0 + self.csc = 0 + self.roba = 0 + self.robs = 0 + self.raa = 0 + self.rto = 0 + self.game = 0 + + if self.season_type == 'Regular': + all_stats = BattingStat.regular_season(self.season).where( + (BattingStat.player == self.player) & (BattingStat.pos == self.pos) + ) + else: + all_stats = BattingStat.post_season(self.season).where( + (BattingStat.player == self.player) & (BattingStat.pos == self.pos) + ) + + for line in all_stats: + self.xch += line.xch + self.xhit += line.xhit + self.error += line.error + self.pb += line.pb + self.sbc += line.sbc + self.csc += line.csc + self.roba += line.roba + self.robs += line.robs + self.raa += line.raa + self.rto += line.rto + self.game += 1 + + self.save() + return all_stats.count() + + +class DraftPick(BaseModel): + overall = IntegerField(null=True) + round = IntegerField() + origowner = ForeignKeyField(Team) + owner = ForeignKeyField(Team) + season = IntegerField() + player = ForeignKeyField(Player, null=True) + + @staticmethod + def select_season(num): + return DraftPick.select().where(DraftPick.season == num) + + @staticmethod + def get_season(team, rd, num): + return DraftPick.get(DraftPick.season == num, DraftPick.origowner == team, DraftPick.round == rd) + + +class DraftData(BaseModel): + currentpick = IntegerField() + timer = BooleanField() + pick_deadline = DateTimeField(null=True) + result_channel = IntegerField(null=True) + ping_channel = IntegerField(null=True) + pick_minutes = IntegerField(null=True) + + +class Award(BaseModel): + name = CharField() + season = IntegerField() + timing = CharField(default="In-Season") + manager1 = ForeignKeyField(Manager, null=True) + manager2 = ForeignKeyField(Manager, null=True) + player = ForeignKeyField(Player, null=True) + team = ForeignKeyField(Team, null=True) + image = CharField(null=True) + + +class DiceRoll(BaseModel): + season = IntegerField(default=Current.latest().season) + week = IntegerField(default=Current.latest().week) + team = ForeignKeyField(Team, null=True) + roller = IntegerField() + dsix = IntegerField(null=True) + twodsix = IntegerField(null=True) + threedsix = IntegerField(null=True) + dtwenty = IntegerField(null=True) + + +class DraftList(BaseModel): + season = IntegerField() + team = ForeignKeyField(Team) + rank = IntegerField() + player = ForeignKeyField(Player) + + +class Keeper(BaseModel): + season = IntegerField() + team = ForeignKeyField(Team) + player = ForeignKeyField(Player) + + +class Injury(BaseModel): + season = IntegerField() + player = ForeignKeyField(Player) + total_games = IntegerField() + start_week = IntegerField() + start_game = IntegerField() + end_week = IntegerField() + end_game = IntegerField() + is_active = BooleanField(default=True) + + +class StratGame(BaseModel): + season = IntegerField() + week = IntegerField() + game_num = IntegerField() + season_type = CharField(default='regular') + away_team = ForeignKeyField(Team) + home_team = ForeignKeyField(Team) + away_score = IntegerField(null=True) + home_score = IntegerField(null=True) + + +class StratPlay(BaseModel): + game = ForeignKeyField(StratGame) + play_num = IntegerField() + batter = ForeignKeyField(Player) + pitcher = ForeignKeyField(Player) + on_base_code = IntegerField() + inning_half = CharField() + inning_num = IntegerField() + batting_order = IntegerField() + starting_outs = IntegerField() + away_score = IntegerField() + home_score = IntegerField() + batter_pos = CharField() + + # These _final fields track the base this runner advances to post-play (None) if out + on_first = ForeignKeyField(Player, null=True) + on_first_final = IntegerField(null=True) + on_second = ForeignKeyField(Player, null=True) + on_second_final = IntegerField(null=True) + on_third = ForeignKeyField(Player, null=True) + on_third_final = IntegerField(null=True) + batter_final = IntegerField(null=True) + + pa = IntegerField(default=0) + ab = IntegerField(default=0) + run = IntegerField(default=0) + hit = IntegerField(default=0) + rbi = IntegerField(default=0) + double = IntegerField(default=0) + triple = IntegerField(default=0) + homerun = IntegerField(default=0) + bb = IntegerField(default=0) + so = IntegerField(default=0) + hbp = IntegerField(default=0) + sac = IntegerField(default=0) + ibb = IntegerField(default=0) + gidp = IntegerField(default=0) + bphr = IntegerField(default=0) + bpfo = IntegerField(default=0) + bp1b = IntegerField(default=0) + bplo = IntegerField(default=0) + sb = IntegerField(default=0) + cs = IntegerField(default=0) + outs = IntegerField(default=0) + wpa = FloatField(default=0) + + # These fields are only required if the play is an x-check or baserunning play + catcher = ForeignKeyField(Player, null=True) + defender = ForeignKeyField(Player, null=True) + runner = ForeignKeyField(Player, null=True) + + check_pos = CharField(null=True) + error = IntegerField(default=0) + wild_pitch = IntegerField(default=0) + passed_ball = IntegerField(default=0) + pick_off = IntegerField(default=0) + balk = IntegerField(default=0) + is_go_ahead = BooleanField(default=False) + is_tied = BooleanField(default=False) + is_new_inning = BooleanField(default=False) + + + +# class Streak(BaseModel): +# player = ForeignKeyField(Player) +# streak_type = CharField() +# start_season = IntegerField() +# start_week = IntegerField() +# start_game = IntegerField() +# end_season = IntegerField() +# end_week = IntegerField() +# end_game = IntegerField() +# game_length = IntegerField() +# active = BooleanField() +# +# def recalculate(self): +# # Pitcher streaks +# if self.streak_type in ['win', 'loss', 'save', 'scoreless']: +# all_stats = PitchingStat.select_season(self.start_season).where( +# (PitchingStat.player == self.player) & (PitchingStat.week >= self.start_week) +# ) +# sorted_stats = sorted(all_stats, key=lambda x: f'{x.season:0>2}-{x.week:0>2}-{x.game:}') +# +# for line in sorted_stats: + +db.create_tables([ + Current, Division, Manager, Team, Result, Player, Schedule, Transaction, BattingStat, PitchingStat, Standings, + BattingCareer, PitchingCareer, FieldingCareer, Manager, Award, DiceRoll, DraftList, Keeper +]) +db.close() diff --git a/app/dependencies.py b/app/dependencies.py new file mode 100644 index 0000000..615288f --- /dev/null +++ b/app/dependencies.py @@ -0,0 +1,34 @@ +import datetime +import logging +import os + +from fastapi.security import OAuth2PasswordBearer + +date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}' +LOG_DATA = { + 'filename': f'logs/database/{date}.log', + 'format': '%(asctime)s - database - %(levelname)s - %(message)s', + 'log_level': logging.INFO if os.environ.get('LOG_LEVEL') == 'INFO' else 'WARN' +} + + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +# 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' +# logging.basicConfig( +# filename=f'logs/database/{date}.log', +# format='%(asctime)s - sba-database - %(levelname)s - %(message)s', +# level=log_level +# ) + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +def valid_token(token): + return token == os.environ.get('API_TOKEN') diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..3376bd6 --- /dev/null +++ b/app/main.py @@ -0,0 +1,56 @@ +import datetime +import logging +import os + +from fastapi import Depends, FastAPI, Request +# from fastapi.openapi.docs import get_swagger_ui_html +# from fastapi.openapi.utils import get_openapi + +from .routers_v3 import current, players, results, schedules, standings, teams, transactions, battingstats, \ + pitchingstats, fieldingstats, draftpicks, draftlist, managers, awards, draftdata, keepers + +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' +logging.basicConfig( + filename=f'logs/database/{date}.log', + format='%(asctime)s - sba-database - %(levelname)s - %(message)s', + level=log_level +) + +app = FastAPI( + responses={404: {'description': 'Not found'}} +) + + +app.include_router(current.router) +app.include_router(players.router) +app.include_router(results.router) +app.include_router(schedules.router) +app.include_router(teams.router) +app.include_router(transactions.router) +app.include_router(standings.router) +app.include_router(battingstats.router) +app.include_router(pitchingstats.router) +app.include_router(fieldingstats.router) +app.include_router(draftpicks.router) +app.include_router(draftlist.router) +app.include_router(managers.router) +app.include_router(awards.router) +app.include_router(draftdata.router) +app.include_router(keepers.router) + + +# @app.get("/docs", include_in_schema=False) +# async def get_docs(req: Request): +# print(req.scope) +# return get_swagger_ui_html(openapi_url=req.scope.get('root_path')+'/openapi.json', title='Swagger') +# +# +# @app.get("/openapi.json", include_in_schema=False) +# async def openapi(): +# return get_openapi(title='SBa Dev API', version=f'0.1.1', routes=app.routes) + + +# @app.get("/api") +# async def root(): +# return {"message": "Hello Bigger Applications!"} diff --git a/app/routers_v3/__init__.py b/app/routers_v3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers_v3/awards.py b/app/routers_v3/awards.py new file mode 100644 index 0000000..243bd33 --- /dev/null +++ b/app/routers_v3/awards.py @@ -0,0 +1,162 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional +import logging +import pydantic + +from ..db_engine import db, Award, Team, Player, Manager, model_to_dict, chunked, fn +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/awards', + tags=['awards'] +) + + +class AwardModel(pydantic.BaseModel): + name: str + season: int + timing: Optional[str] = "In-Season" + manager1_id: Optional[int] = None + manager2_id: Optional[int] = None + player_id: Optional[int] = None + team_id: Optional[int] = None + image: Optional[str] = None + + +class AwardList(pydantic.BaseModel): + count: int + awards: List[AwardModel] + + +@router.get('') +async def get_awards( + name: list = Query(default=None), season: Optional[int] = None, timing: Optional[str] = None, + manager_id: list = Query(default=None), player_id: list = Query(default=None), + team_id: list = Query(default=None), short_output: Optional[bool] = False): + all_awards = Award.select() + + if name is not None: + name_list = [x.lower() for x in name] + all_awards = all_awards.where(fn.Lower(Award.name) == name_list) + if season is not None: + all_awards = all_awards.where(Award.season == season) + if timing is not None: + all_awards = all_awards.where(fn.Lower(Award.timing) == timing.lower()) + if manager_id is not None: + managers = Manager.select().where(Manager.id << manager_id) + all_awards = all_awards.where( + (Award.manager1 << managers) | (Award.manager2 << managers) + ) + if player_id is not None: + all_awards = all_awards.where(Award.player_id << player_id) + if team_id is not None: + all_awards = all_awards.where(Award.team_id << team_id) + + return_awards = { + 'count': all_awards.count(), + 'awards': [model_to_dict(x, recurse=not short_output) for x in all_awards] + } + db.close() + return return_awards + + +@router.get('/{award_id}') +async def get_one_award(award_id: int, short_output: Optional[bool] = False): + this_award = Award.get_or_none(Award.id == award_id) + if this_award is None: + db.close() + raise HTTPException(status_code=404, detail=f'Award ID {award_id} not found') + + db.close() + return model_to_dict(this_award, recurse=not short_output) + + +@router.patch('/{award_id}') +async def patch_award( + award_id: int, name: Optional[str] = None, season: Optional[int] = None, timing: Optional[str] = None, + image: Optional[str] = None, manager1_id: Optional[int] = None, manager2_id: Optional[int] = None, + player_id: Optional[int] = None, team_id: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_player - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_award = Award.get_or_none(Award.id == award_id) + if this_award is None: + db.close() + raise HTTPException(status_code=404, detail=f'Award ID {award_id} not found') + + if name is not None: + this_award.name = name + if season is not None: + this_award.season = season + if timing is not None: + this_award.timing = timing + if image is not None: + this_award.image = image + if manager1_id is not None: + this_award.manager1_id = manager1_id + if manager2_id is not None: + this_award.manager2_id = manager2_id + if player_id is not None: + this_award.player_id = player_id + if team_id is not None: + this_award.team_id = team_id + + if this_award.save() == 1: + r_award = model_to_dict(this_award) + db.close() + return r_award + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to patch award {award_id}') + + +@router.post('') +async def post_award(award_list: AwardList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_player - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_awards = [] + for x in award_list.awards: + if x.manager1_id is not None and Manager.get_or_none(Manager.id == x.manager1_id) is None: + raise HTTPException(status_code=404, detail=f'Manager ID {x.manager1_id} not found') + if x.manager2_id is not None and Manager.get_or_none(Manager.id == x.manager2_id) is None: + raise HTTPException(status_code=404, detail=f'Manager ID {x.manager2_id} not found') + if x.player_id is not None and Player.get_or_none(Player.id == x.player_id) is None: + raise HTTPException(status_code=404, detail=f'Player ID {x.player_id} not found') + if x.team_id is not None and Team.get_or_none(Team.id == x.team_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.team_id} not found') + + new_awards.append(x.dict()) + + with db.atomic(): + for batch in chunked(new_awards, 15): + Award.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_awards)} awards' + + +@router.delete('/{award_id}') +async def delete_award(award_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_player - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_award = Award.get_or_none(Award.id == award_id) + if this_award is None: + db.close() + raise HTTPException(status_code=404, detail=f'Award ID {award_id} not found') + + count = this_award.delete_instance() + db.close() + + if count == 1: + return f'Award {award_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Award {award_id} could not be deleted') + + + + diff --git a/app/routers_v3/battingstats.py b/app/routers_v3/battingstats.py new file mode 100644 index 0000000..9c88c96 --- /dev/null +++ b/app/routers_v3/battingstats.py @@ -0,0 +1,306 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional, Literal +import logging +import pydantic + +from ..db_engine import db, BattingStat, Team, Player, Current, model_to_dict, chunked, fn, per_season_weeks +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/battingstats', + tags=['battingstats'] +) + + +class BatStatModel(pydantic.BaseModel): + player_id: int + team_id: int + pos: str + pa: Optional[int] = 0 + ab: Optional[int] = 0 + run: Optional[int] = 0 + hit: Optional[int] = 0 + rbi: Optional[int] = 0 + double: Optional[int] = 0 + triple: Optional[int] = 0 + hr: Optional[int] = 0 + bb: Optional[int] = 0 + so: Optional[int] = 0 + hbp: Optional[int] = 0 + sac: Optional[int] = 0 + ibb: Optional[int] = 0 + gidp: Optional[int] = 0 + sb: Optional[int] = 0 + cs: Optional[int] = 0 + bphr: Optional[int] = 0 + bpfo: Optional[int] = 0 + bp1b: Optional[int] = 0 + bplo: Optional[int] = 0 + xba: Optional[int] = 0 + xbt: Optional[int] = 0 + xch: Optional[int] = 0 + xhit: Optional[int] = 0 + error: Optional[int] = 0 + pb: Optional[int] = 0 + sbc: Optional[int] = 0 + csc: Optional[int] = 0 + roba: Optional[int] = 0 + robs: Optional[int] = 0 + raa: Optional[int] = 0 + rto: Optional[int] = 0 + week: int + game: int + season: int + + +class BatStatList(pydantic.BaseModel): + count: int + stats: List[BatStatModel] + + +@router.get('') +async def get_batstats( + season: int, s_type: Optional[str] = 'regular', team_abbrev: list = Query(default=None), + player_name: list = Query(default=None), player_id: list = Query(default=None), + week_start: Optional[int] = None, week_end: Optional[int] = None, game_num: list = Query(default=None), + position: list = Query(default=None), limit: Optional[int] = None, sort: Optional[str] = None, + short_output: Optional[bool] = True): + if 'post' in s_type.lower(): + all_stats = BattingStat.post_season(season) + if all_stats.count() == 0: + db.close() + return {'count': 0, 'stats': []} + elif s_type.lower() in ['combined', 'total', 'all']: + all_stats = BattingStat.combined_season(season) + if all_stats.count() == 0: + db.close() + return {'count': 0, 'stats': []} + else: + all_stats = BattingStat.regular_season(season) + if all_stats.count() == 0: + db.close() + return {'count': 0, 'stats': []} + + if position is not None: + all_stats = all_stats.where(BattingStat.pos << [x.upper() for x in position]) + if team_abbrev is not None: + t_query = Team.select().where(Team.abbrev << [x.upper() for x in team_abbrev]) + all_stats = all_stats.where(BattingStat.team << t_query) + if player_name is not None or player_id is not None: + if player_id: + all_stats = all_stats.where(BattingStat.player_id << player_id) + else: + p_query = Player.select_season(season).where(fn.Lower(Player.name) << [x.lower() for x in player_name]) + all_stats = all_stats.where(BattingStat.player << p_query) + if game_num: + all_stats = all_stats.where(BattingStat.game == game_num) + + start = 1 + end = Current.get(Current.season == season).week + if week_start is not None: + start = week_start + if week_end is not None: + end = min(week_end, end) + if start > end: + db.close() + raise HTTPException( + status_code=404, + detail=f'Start week {start} is after end week {end} - cannot pull stats' + ) + all_stats = all_stats.where( + (BattingStat.week >= start) & (BattingStat.week <= end) + ) + + if limit: + all_stats = all_stats.limit(limit) + if sort: + if sort == 'newest': + all_stats = all_stats.order_by(-BattingStat.week, -BattingStat.game) + + return_stats = { + 'count': all_stats.count(), + 'stats': [model_to_dict(x, recurse=not short_output) for x in all_stats], + # 'stats': [{'id': x.id} for x in all_stats] + } + + db.close() + return return_stats + + +@router.get('/totals') +async def get_totalstats( + season: int, s_type: Literal['regular', 'post', 'total', None] = None, team_abbrev: list = Query(default=None), + team_id: list = Query(default=None), player_name: list = Query(default=None), + week_start: Optional[int] = None, week_end: Optional[int] = None, game_num: list = Query(default=None), + position: list = Query(default=None), sort: Optional[str] = None, player_id: list = Query(default=None), + group_by: Literal['team', 'player', 'playerteam'] = 'player', short_output: Optional[bool] = False, + min_pa: Optional[int] = 1, week: list = Query(default=None)): + if sum(1 for x in [s_type, (week_start or week_end), week] if x is not None) > 1: + raise HTTPException(status_code=400, detail=f'Only one of s_type, week_start/week_end, or week may be used.') + + all_stats = ( + BattingStat + .select(BattingStat.player, fn.SUM(BattingStat.pa).alias('sum_pa'), fn.SUM(BattingStat.ab).alias('sum_ab'), + fn.SUM(BattingStat.run).alias('sum_run'), fn.SUM(BattingStat.hit).alias('sum_hit'), + fn.SUM(BattingStat.rbi).alias('sum_rbi'), fn.SUM(BattingStat.double).alias('sum_double'), + fn.SUM(BattingStat.triple).alias('sum_triple'), fn.SUM(BattingStat.hr).alias('sum_hr'), + fn.SUM(BattingStat.bb).alias('sum_bb'), fn.SUM(BattingStat.so).alias('sum_so'), + fn.SUM(BattingStat.hbp).alias('sum_hbp'), fn.SUM(BattingStat.sac).alias('sum_sac'), + fn.SUM(BattingStat.ibb).alias('sum_ibb'), fn.SUM(BattingStat.gidp).alias('sum_gidp'), + fn.SUM(BattingStat.sb).alias('sum_sb'), fn.SUM(BattingStat.cs).alias('sum_cs'), + fn.SUM(BattingStat.bphr).alias('sum_bphr'), fn.SUM(BattingStat.bpfo).alias('sum_bpfo'), + fn.SUM(BattingStat.bp1b).alias('sum_bp1b'), fn.SUM(BattingStat.bplo).alias('sum_bplo'), + fn.SUM(BattingStat.xba).alias('sum_xba'), fn.SUM(BattingStat.xbt).alias('sum_xbt'), + fn.SUM(BattingStat.xch).alias('sum_xch'), fn.SUM(BattingStat.xhit).alias('sum_xhit'), + fn.SUM(BattingStat.error).alias('sum_error'), fn.SUM(BattingStat.pb).alias('sum_pb'), + fn.SUM(BattingStat.sbc).alias('sum_sbc'), fn.SUM(BattingStat.csc).alias('sum_csc'), + fn.SUM(BattingStat.roba).alias('sum_roba'), fn.SUM(BattingStat.robs).alias('sum_robs'), + fn.SUM(BattingStat.raa).alias('sum_raa'), fn.SUM(BattingStat.rto).alias('sum_rto'), + BattingStat.team) + .where(BattingStat.season == season) + .having(fn.SUM(BattingStat.pa) >= min_pa) + ) + + if True in [s_type is not None, week_start is not None, week_end is not None]: + weeks = {} + if s_type is not None: + weeks = per_season_weeks(season, s_type) + elif week_start is not None or week_end is not None: + if week_start is None or week_end is None: + raise HTTPException( + status_code=400, detail='Both week_start and week_end must be included if either is used.' + ) + weeks['start'] = week_start + if week_end < weeks['start']: + raise HTTPException(status_code=400, detail='week_end must be greater than or equal to week_start') + else: + weeks['end'] = week_end + + all_stats = all_stats.where( + (BattingStat.week >= weeks['start']) & (BattingStat.week <= weeks['end']) + ) + elif week is not None: + all_stats = all_stats.where(BattingStat.week << week) + + if game_num is not None: + all_stats = all_stats.where(BattingStat.game << game_num) + if position is not None: + p_list = [x.upper() for x in position] + all_players = Player.select().where( + (Player.pos_1 << p_list) | (Player.pos_2 << p_list) | (Player.pos_3 << p_list) | ( Player.pos_4 << p_list) | + (Player.pos_5 << p_list) | (Player.pos_6 << p_list) | (Player.pos_7 << p_list) | ( Player.pos_8 << p_list) + ) + all_stats = all_stats.where(BattingStat.player << all_players) + if sort is not None: + if sort == 'player': + all_stats = all_stats.order_by(BattingStat.player) + elif sort == 'team': + all_stats = all_stats.order_by(BattingStat.team) + if group_by is not None: + if group_by == 'team': + all_stats = all_stats.group_by(BattingStat.team) + elif group_by == 'player': + all_stats = all_stats.group_by(BattingStat.player) + elif group_by == 'playerteam': + all_stats = all_stats.group_by(BattingStat.team, BattingStat.player) + + # if team_abbrev is None and team_id is None and player_name is None and player_id is None: + # raise HTTPException( + # status_code=400, + # detail=f'Must include team_id/team_abbrev and/or player_name/player_id' + # ) + + if team_id is not None: + all_teams = Team.select().where(Team.id << team_id) + all_stats = all_stats.where(BattingStat.team << all_teams) + elif team_abbrev is not None: + all_teams = Team.select().where(fn.Lower(Team.abbrev) << [x.lower() for x in team_abbrev]) + all_stats = all_stats.where(BattingStat.team << all_teams) + + if player_name is not None: + all_players = Player.select().where(fn.Lower(Player.name) << [x.lower() for x in player_name]) + all_stats = all_stats.where(BattingStat.player << all_players) + elif player_id is not None: + all_players = Player.select().where(Player.id << player_id) + all_stats = all_stats.where(BattingStat.player << all_players) + + return_stats = { + 'count': all_stats.count(), + 'stats': [{ + 'player': x.player_id if short_output else model_to_dict(x.player, recurse=False), + 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False), + 'pa': x.sum_pa, + 'ab': x.sum_ab, + 'run': x.sum_run, + 'hit': x.sum_hit, + 'rbi': x.sum_rbi, + 'double': x.sum_double, + 'triple': x.sum_triple, + 'hr': x.sum_hr, + 'bb': x.sum_bb, + 'so': x.sum_so, + 'hbp': x.sum_hbp, + 'sac': x.sum_sac, + 'ibb': x.sum_ibb, + 'gidp': x.sum_gidp, + 'sb': x.sum_sb, + 'cs': x.sum_cs, + 'bphr': x.sum_bphr, + 'bpfo': x.sum_bpfo, + 'bp1b': x.sum_bp1b, + 'bplo': x.sum_bplo + } for x in all_stats] + } + db.close() + return return_stats + + +# @router.get('/career/{player_name}') +# async def get_careerstats( +# s_type: Literal['regular', 'post', 'total'] = 'regular', player_name: list = Query(default=None)): +# pass # Keep Career Stats table and recalculate after posting stats + + +@router.patch('/{stat_id}') +async def patch_batstats(stat_id: int, new_stats: BatStatModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_batstats - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + if BattingStat.get_or_none(BattingStat.id == stat_id) is None: + raise HTTPException(status_code=404, detail=f'Stat ID {stat_id} not found') + + BattingStat.update(**new_stats.dict()).where(BattingStat.id == stat_id).execute() + r_stat = model_to_dict(BattingStat.get_by_id(stat_id)) + db.close() + return r_stat + + +@router.post('') +async def post_batstats(s_list: BatStatList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_batstats - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + all_stats = [] + + for x in s_list.stats: + team = Team.get_or_none(Team.id == x.team_id) + this_player = Player.get_or_none(Player.id == x.player_id) + if team is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.team_id} not found') + if this_player is None: + raise HTTPException(status_code=404, detail=f'Player ID {x.player_id} not found') + + all_stats.append(BattingStat(**x.dict())) + + with db.atomic(): + for batch in chunked(all_stats, 15): + BattingStat.insert_many(batch).on_conflict_replace().execute() + + # Update career stats + + db.close() + return f'Added {len(all_stats)} batting lines' diff --git a/app/routers_v3/current.py b/app/routers_v3/current.py new file mode 100644 index 0000000..8522b2c --- /dev/null +++ b/app/routers_v3/current.py @@ -0,0 +1,118 @@ +from fastapi import APIRouter, Depends, HTTPException +from typing import Optional +import logging +import pydantic + +from ..db_engine import db, Current, model_to_dict +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/current', + tags=['current'] +) + + +class CurrentModel(pydantic.BaseModel): + week: Optional[int] = 0 + freeze: Optional[bool] = False + season: int + transcount: Optional[int] = 0 + bstatcount: Optional[int] = 0 + pstatcount: Optional[int] = 0 + bet_week: Optional[int] = 0 + trade_deadline: int + pick_trade_start: int = 69 + pick_trade_end: int = 420 + playoffs_begin: int + injury_count: Optional[int] = 0 + + +@router.get('') +async def get_current(season: Optional[int] = None): + if season is not None: + current = Current.get_or_none(season=season) + else: + current = Current.latest() + + if current is not None: + r_curr = model_to_dict(current) + db.close() + return r_curr + else: + return None + + +@router.patch('/{current_id}') +async def patch_current( + current_id: int, season: Optional[int] = None, week: Optional[int] = None, freeze: Optional[bool] = None, + transcount: Optional[int] = None, bstatcount: Optional[int] = None, pstatcount: Optional[int] = None, + bet_week: Optional[int] = None, trade_deadline: Optional[int] = None, pick_trade_start: Optional[int] = None, + pick_trade_end: Optional[int] = None, injury_count: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_current - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + try: + current = Current.get_by_id(current_id) + except Exception as e: + raise HTTPException(status_code=404, detail=f'Current id {current_id} not found') + + if week is not None: + current.week = week + if season is not None: + current.season = season + if freeze is not None: + current.freeze = freeze + if transcount is not None: + current.transcount = transcount + if bstatcount is not None: + current.bstatcount = bstatcount + if pstatcount is not None: + current.pstatcount = pstatcount + if bet_week is not None: + current.bet_week = bet_week + if trade_deadline is not None: + current.trade_deadline = trade_deadline + if pick_trade_start is not None: + current.pick_trade_start = pick_trade_start + if pick_trade_end is not None: + current.pick_trade_end = pick_trade_end + if injury_count is not None: + current.injury_count = injury_count + + if current.save(): + r_curr = model_to_dict(current) + db.close() + return r_curr + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to patch current {current_id}') + + +@router.post('') +async def post_current(new_current: CurrentModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_current - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_current = Current(**new_current.dict()) + + if this_current.save(): + r_curr = model_to_dict(this_current) + db.close() + return r_curr + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to post season {new_current.season} current') + + +@router.delete('/{current_id}') +async def delete_current(current_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_current - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + if Current.delete_by_id(current_id) == 1: + return f'Deleted current ID {current_id}' + + raise HTTPException(status_code=500, detail=f'Unable to delete current {current_id}') diff --git a/app/routers_v3/draftdata.py b/app/routers_v3/draftdata.py new file mode 100644 index 0000000..28d98dd --- /dev/null +++ b/app/routers_v3/draftdata.py @@ -0,0 +1,73 @@ +import datetime + +from fastapi import APIRouter, Depends, HTTPException +from typing import Optional +import logging +import pydantic + +from ..db_engine import db, DraftData, model_to_dict +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/draftdata', + tags=['draftdata'] +) + + +class DraftDataModel(pydantic.BaseModel): + currentpick: int + timer: bool + pick_deadline: datetime.datetime + result_channel_id = int + ping_channel_id = int + pick_minutes = int + + +@router.get('') +async def get_draftdata(): + draft_data = DraftData.get_or_none() + + if draft_data is not None: + r_data = model_to_dict(draft_data) + db.close() + return r_data + + raise HTTPException(status_code=404, detail=f'No draft data found') + + +@router.patch('/{data_id}') +async def patch_draftdata( + data_id: int, currentpick: Optional[int] = None, timer: Optional[bool] = None, + pick_deadline: Optional[datetime.datetime] = None, result_channel: Optional[int] = None, + ping_channel: Optional[int] = None, pick_minutes: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_draftdata - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + draft_data = DraftData.get_or_none(DraftData.id == data_id) + if draft_data is None: + db.close() + raise HTTPException(status_code=404, detail=f'No draft data found') + + if currentpick is not None: + draft_data.currentpick = currentpick + if timer is not None: + draft_data.timer = timer + if pick_deadline is not None: + draft_data.pick_deadline = pick_deadline + if result_channel is not None: + draft_data.result_channel = result_channel + if ping_channel is not None: + draft_data.ping_channel = ping_channel + if pick_minutes is not None: + draft_data.pick_minutes = pick_minutes + + saved = draft_data.save() + r_data = model_to_dict(draft_data) + db.close() + + if saved == 1: + return r_data + else: + raise HTTPException(status_code=500, detail='Updating draft data failed') + diff --git a/app/routers_v3/draftlist.py b/app/routers_v3/draftlist.py new file mode 100644 index 0000000..01ca060 --- /dev/null +++ b/app/routers_v3/draftlist.py @@ -0,0 +1,91 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional +import logging +import pydantic + +from ..db_engine import db, DraftList, Team, model_to_dict, chunked +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/draftlist', + tags=['draftlist'] +) + + +class DraftListModel(pydantic.BaseModel): + season: int + team_id: int + rank: int + player_id: int + + +class DraftListList(pydantic.BaseModel): + count: int + draft_list: List[DraftListModel] + + +@router.get('') +async def get_draftlist( + season: Optional[int], team_id: list = Query(default=None), token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'get_draftlist - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + all_list = DraftList.select() + + if season is not None: + all_list = all_list.where(DraftList.season == season) + if team_id is not None: + all_list = all_list.where(DraftList.team_id << team_id) + + r_list = { + 'count': all_list.count(), + 'picks': [model_to_dict(x) for x in all_list] + } + + db.close() + return r_list + + +@router.get('/team/{team_id}') +async def get_team_draftlist(team_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_draftlist - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_team = Team.get_or_none(Team.id == team_id) + if this_team is None: + raise HTTPException(status_code=404, detail=f'Team ID {team_id} not found') + + this_list = DraftList.select().where(DraftList.team == this_team) + r_list = { + 'count': this_list.count(), + 'picks': [model_to_dict(x) for x in this_list] + } + + db.close() + return r_list + + +@router.post('') +async def post_draftlist(draft_list: DraftListList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_draftlist - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_list = [] + this_team = Team.get_or_none(Team.id == draft_list.draft_list[0].team_id) + if this_team is None: + raise HTTPException(status_code=404, detail=f'Team ID {draft_list.draft_list[0].team_id} not found') + + DraftList.delete().where(DraftList.team == this_team).execute() + + for x in draft_list.draft_list: + new_list.append(x.dict()) + + with db.atomic(): + for batch in chunked(new_list, 15): + DraftList.insert_many(batch).on_conflict_replace().execute() + + db.close() + return f'Inserted {len(new_list)} list values' diff --git a/app/routers_v3/draftpicks.py b/app/routers_v3/draftpicks.py new file mode 100644 index 0000000..8cb4a10 --- /dev/null +++ b/app/routers_v3/draftpicks.py @@ -0,0 +1,169 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional +import logging +import pydantic + +from ..db_engine import db, DraftPick, Team, model_to_dict, chunked +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/draftpicks', + tags=['draftpicks'] +) + + +class DraftPickModel(pydantic.BaseModel): + overall: Optional[int] = None + round: int + origowner_id: int + owner_id: Optional[int] = None + season: int + player_id: Optional[int] = None + + +class DraftPickList(pydantic.BaseModel): + picks: List[DraftPickModel] + + +@router.get('') +async def get_picks( + season: int, owner_team_abbrev: list = Query(default=None), orig_team_abbrev: list = Query(default=None), + owner_team_id: list = Query(default=None), orig_team_id: list = Query(default=None), + pick_round_start: Optional[int] = None, pick_round_end: Optional[int] = None, traded: Optional[bool] = None, + overall: Optional[int] = None, overall_start: Optional[int] = None, overall_end: Optional[int] = None, + short_output: Optional[bool] = False, sort: Optional[str] = None, limit: Optional[int] = None, + player_id: list = Query(default=None), player_taken: Optional[bool] = None): + all_picks = DraftPick.select().where(DraftPick.season == season) + + if owner_team_abbrev is not None: + team_list = [] + for x in owner_team_abbrev: + team_list.append(Team.get_season(x, season)) + all_picks = all_picks.where( + (DraftPick.owner << team_list) | (DraftPick.owner << team_list) + ) + + if orig_team_abbrev is not None: + team_list = [] + for x in orig_team_abbrev: + team_list.append(Team.get_season(x, season)) + all_picks = all_picks.where( + (DraftPick.origowner << team_list) | (DraftPick.origowner << team_list) + ) + + if owner_team_id is not None: + all_picks = all_picks.where( + (DraftPick.owner_id << owner_team_id) | (DraftPick.owner_id << owner_team_id) + ) + + if orig_team_id is not None: + all_picks = all_picks.where( + (DraftPick.origowner_id << orig_team_id) | (DraftPick.origowner_id << orig_team_id) + ) + + if pick_round_start is not None and pick_round_end is not None and pick_round_end < pick_round_start: + raise HTTPException(status_code=400, detail=f'pick_round_end must be greater than or equal to pick_round_start') + + if player_id is not None: + all_picks = all_picks.where(DraftPick.player_id << player_id) + if pick_round_start is not None: + all_picks = all_picks.where(DraftPick.round >= pick_round_start) + if pick_round_end is not None: + all_picks = all_picks.where(DraftPick.round <= pick_round_end) + if traded is not None: + all_picks = all_picks.where(DraftPick.origowner != DraftPick.owner) + if overall is not None: + all_picks = all_picks.where(DraftPick.overall == overall) + if overall_start is not None: + all_picks = all_picks.where(DraftPick.overall >= overall_start) + if overall_end is not None: + all_picks = all_picks.where(DraftPick.overall <= overall_end) + if player_taken is not None: + all_picks = all_picks.where(DraftPick.player.is_null(not player_taken)) + if limit is not None: + all_picks = all_picks.limit(limit) + + if sort is not None: + if sort == 'order-asc': + all_picks = all_picks.order_by(DraftPick.overall) + elif sort == 'order-desc': + all_picks = all_picks.order_by(-DraftPick.overall) + + return_picks = {'count': all_picks.count(), 'picks': []} + for line in all_picks: + return_picks['picks'].append(model_to_dict(line, recurse=not short_output)) + + db.close() + return return_picks + + +@router.get('/{pick_id}') +async def get_one_pick(pick_id: int, short_output: Optional[bool] = False): + this_pick = DraftPick.get_or_none(DraftPick.id == pick_id) + if this_pick is not None: + r_pick = model_to_dict(this_pick, recurse=not short_output) + else: + raise HTTPException(status_code=404, detail=f'Pick ID {pick_id} not found') + db.close() + return r_pick + + +@router.patch('/{pick_id}') +async def patch_pick(pick_id: int, new_pick: DraftPickModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_pick - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + if DraftPick.get_or_none(DraftPick.id == pick_id) is None: + raise HTTPException(status_code=404, detail=f'Pick ID {pick_id} not found') + + DraftPick.update(**new_pick.dict()).where(DraftPick.id == pick_id).execute() + r_pick = model_to_dict(DraftPick.get_by_id(pick_id)) + db.close() + return r_pick + + +@router.post('') +async def post_picks(p_list: DraftPickList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_picks - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_picks = [] + for pick in p_list.picks: + dupe = DraftPick.get_or_none(DraftPick.season == pick.season, DraftPick.overall == pick.overall) + if dupe: + db.close() + raise HTTPException( + status_code=500, + detail=f'Pick # {pick.overall} already exists for season {pick.season}' + ) + + new_picks.append(pick.dict()) + + with db.atomic(): + for batch in chunked(new_picks, 15): + DraftPick.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_picks)} picks' + + +@router.delete('/{pick_id}') +async def delete_pick(pick_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_pick - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_pick = DraftPick.get_or_none(DraftPick.id == pick_id) + if this_pick is None: + raise HTTPException(status_code=404, detail=f'Pick ID {pick_id} not found') + + count = this_pick.delete_instance() + db.close() + + if count == 1: + return f'Draft pick {pick_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Draft pick {pick_id} could not be deleted') + diff --git a/app/routers_v3/fieldingstats.py b/app/routers_v3/fieldingstats.py new file mode 100644 index 0000000..2712acd --- /dev/null +++ b/app/routers_v3/fieldingstats.py @@ -0,0 +1,193 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional, Literal +import logging +import pydantic + +from ..db_engine import db, BattingStat, Team, Player, Current, model_to_dict, chunked, fn, per_season_weeks +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/fieldingstats', + tags=['fieldingstats'] +) + + +@router.get('') +async def get_fieldingstats( + season: int, s_type: Optional[str] = 'regular', team_abbrev: list = Query(default=None), + player_name: list = Query(default=None), player_id: list = Query(default=None), + week_start: Optional[int] = None, week_end: Optional[int] = None, game_num: list = Query(default=None), + position: list = Query(default=None), limit: Optional[int] = None, sort: Optional[str] = None, + short_output: Optional[bool] = True): + if 'post' in s_type.lower(): + all_stats = BattingStat.post_season(season) + if all_stats.count() == 0: + db.close() + return {'count': 0, 'stats': []} + elif s_type.lower() in ['combined', 'total', 'all']: + all_stats = BattingStat.combined_season(season) + if all_stats.count() == 0: + db.close() + return {'count': 0, 'stats': []} + else: + all_stats = BattingStat.regular_season(season) + if all_stats.count() == 0: + db.close() + return {'count': 0, 'stats': []} + + all_stats = all_stats.where( + (BattingStat.xch > 0) | (BattingStat.pb > 0) | (BattingStat.sbc > 0) + ) + + if position is not None: + all_stats = all_stats.where(BattingStat.pos << [x.upper() for x in position]) + if team_abbrev is not None: + t_query = Team.select().where(Team.abbrev << [x.upper() for x in team_abbrev]) + all_stats = all_stats.where(BattingStat.team << t_query) + if player_name is not None or player_id is not None: + if player_id: + all_stats = all_stats.where(BattingStat.player_id << player_id) + else: + p_query = Player.select_season(season).where(fn.Lower(Player.name) << [x.lower() for x in player_name]) + all_stats = all_stats.where(BattingStat.player << p_query) + if game_num: + all_stats = all_stats.where(BattingStat.game == game_num) + + start = 1 + end = Current.get(Current.season == season).week + if week_start is not None: + start = week_start + if week_end is not None: + end = min(week_end, end) + if start > end: + db.close() + raise HTTPException( + status_code=404, + detail=f'Start week {start} is after end week {end} - cannot pull stats' + ) + all_stats = all_stats.where( + (BattingStat.week >= start) & (BattingStat.week <= end) + ) + + if limit: + all_stats = all_stats.limit(limit) + if sort: + if sort == 'newest': + all_stats = all_stats.order_by(-BattingStat.week, -BattingStat.game) + + return_stats = { + 'count': all_stats.count(), + 'stats': [{ + 'player': x.player_id if short_output else model_to_dict(x.player, recurse=False), + 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False), + 'pos': x.pos, + 'xch': x.xch, + 'xhit': x.xhit, + 'error': x.error, + 'pb': x.pb, + 'sbc': x.sbc, + 'csc': x.csc, + 'week': x.week, + 'game': x.game, + 'season': x.season + } for x in all_stats] + } + + db.close() + return return_stats + + +@router.get('/totals') +async def get_totalstats( + season: int, s_type: Literal['regular', 'post', 'total', None] = None, team_abbrev: list = Query(default=None), + team_id: list = Query(default=None), player_name: list = Query(default=None), + week_start: Optional[int] = None, week_end: Optional[int] = None, game_num: list = Query(default=None), + position: list = Query(default=None), sort: Optional[str] = None, player_id: list = Query(default=None), + group_by: Literal['team', 'player', 'playerteam'] = 'player', short_output: Optional[bool] = False, + min_ch: Optional[int] = 1, week: list = Query(default=None)): + + all_stats = ( + BattingStat + .select(BattingStat.player, BattingStat.pos, fn.SUM(BattingStat.xch).alias('sum_xch'), + fn.SUM(BattingStat.xhit).alias('sum_xhit'), fn.SUM(BattingStat.error).alias('sum_error'), + fn.SUM(BattingStat.pb).alias('sum_pb'), fn.SUM(BattingStat.sbc).alias('sum_sbc'), + fn.SUM(BattingStat.csc).alias('sum_csc'), BattingStat.team) + .where(BattingStat.season == season) + .having(fn.SUM(BattingStat.xch) >= min_ch) + ) + + if True in [s_type is not None, week_start is not None, week_end is not None]: + weeks = {} + if s_type is not None: + weeks = per_season_weeks(season, s_type) + elif week_start is not None or week_end is not None: + if week_start is None or week_end is None: + raise HTTPException( + status_code=400, detail='Both week_start and week_end must be included if either is used.' + ) + weeks['start'] = week_start + if week_end < weeks['start']: + raise HTTPException(status_code=400, detail='week_end must be greater than or equal to week_start') + else: + weeks['end'] = week_end + + all_stats = all_stats.where( + (BattingStat.week >= weeks['start']) & (BattingStat.week <= weeks['end']) + ) + + elif week is not None: + all_stats = all_stats.where(BattingStat.week << week) + + if game_num is not None: + all_stats = all_stats.where(BattingStat.game << game_num) + if position is not None: + p_list = [x.upper() for x in position] + all_players = Player.select().where( + (Player.pos_1 << p_list) | (Player.pos_2 << p_list) | (Player.pos_3 << p_list) | (Player.pos_4 << p_list) | + (Player.pos_5 << p_list) | (Player.pos_6 << p_list) | (Player.pos_7 << p_list) | (Player.pos_8 << p_list) + ) + all_stats = all_stats.where(BattingStat.player << all_players) + if sort is not None: + if sort == 'player': + all_stats = all_stats.order_by(BattingStat.player) + elif sort == 'team': + all_stats = all_stats.order_by(BattingStat.team) + if group_by is not None: + if group_by == 'team': + all_stats = all_stats.group_by(BattingStat.pos, BattingStat.team) + elif group_by == 'player': + all_stats = all_stats.group_by(BattingStat.pos, BattingStat.player) + elif group_by == 'playerteam': + all_stats = all_stats.group_by(BattingStat.pos, BattingStat.team, BattingStat.player) + if team_id is not None: + all_teams = Team.select().where(Team.id << team_id) + all_stats = all_stats.where(BattingStat.team << all_teams) + elif team_abbrev is not None: + all_teams = Team.select().where(fn.Lower(Team.abbrev) << [x.lower() for x in team_abbrev]) + all_stats = all_stats.where(BattingStat.team << all_teams) + + if player_name is not None: + all_players = Player.select().where(fn.Lower(Player.name) << [x.lower() for x in player_name]) + all_stats = all_stats.where(BattingStat.player << all_players) + elif player_id is not None: + all_players = Player.select().where(Player.id << player_id) + all_stats = all_stats.where(BattingStat.player << all_players) + + return_stats = { + 'count': sum(1 for i in all_stats if i.sum_xch + i.sum_sbc > 0), + 'stats': [{ + 'player': x.player_id if short_output else model_to_dict(x.player, recurse=False), + 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False), + 'pos': x.pos, + 'xch': x.sum_xch, + 'xhit': x.sum_xhit, + 'error': x.sum_error, + 'pb': x.sum_pb, + 'sbc': x.sum_sbc, + 'csc': x.sum_csc + } for x in all_stats if x.sum_xch + x.sum_sbc > 0] + } + db.close() + return return_stats diff --git a/app/routers_v3/keepers.py b/app/routers_v3/keepers.py new file mode 100644 index 0000000..cdc75c2 --- /dev/null +++ b/app/routers_v3/keepers.py @@ -0,0 +1,113 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Response +from typing import List, Optional +import logging +import pydantic + +from ..db_engine import db, Keeper, 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/keepers', + tags=['keepers'] +) + + +class KeeperModel(pydantic.BaseModel): + season: int + team_id: int + player_id: int + + +class KeeperList(pydantic.BaseModel): + count: Optional[int] = None + keepers: List[KeeperModel] + + +@router.get('') +async def get_keepers( + season: list = Query(default=None), team_id: list = Query(default=None), player_id: list = Query(default=None), + short_output: bool = False): + all_keepers = Keeper.select() + + if season is not None: + all_keepers = all_keepers.where(Keeper.season << season) + if team_id is not None: + all_keepers = all_keepers.where(Keeper.team_id << team_id) + if player_id is not None: + all_keepers = all_keepers.where(Keeper.player_id << player_id) + + return_keepers = { + 'count': all_keepers.count(), + 'keepers': [model_to_dict(x, recurse=not short_output) for x in all_keepers] + } + db.close() + return return_keepers + + +@router.patch('/{keeper_id}') +async def patch_keeper( + keeper_id: int, season: Optional[int] = None, team_id: Optional[int] = None, player_id: Optional[int] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_keeper - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_keeper = Keeper.get_or_none(Keeper.id == keeper_id) + if not this_keeper: + raise HTTPException(status_code=404, detail=f'Keeper ID {keeper_id} not found') + + if season is not None: + this_keeper.season = season + if player_id is not None: + this_keeper.player_id = player_id + if team_id is not None: + this_keeper.team_id = team_id + + if this_keeper.save(): + r_keeper = model_to_dict(this_keeper) + db.close() + return r_keeper + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to patch keeper {keeper_id}') + + +@router.post('') +async def post_keepers(k_list: KeeperList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_keepers - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_keepers = [] + for keeper in k_list.keepers: + new_keepers.append(keeper.dict()) + + with db.atomic(): + for batch in chunked(new_keepers, 14): + Keeper.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_keepers)} keepers' + + +@router.delete('/{keeper_id}') +async def delete_keeper(keeper_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_keeper - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_keeper = Keeper.get_or_none(Keeper.id == keeper_id) + if not this_keeper: + raise HTTPException(status_code=404, detail=f'Keeper ID {keeper_id} not found') + + count = this_keeper.delete_instance() + db.close() + + if count == 1: + return f'Keeper ID {keeper_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Keeper ID {keeper_id} could not be deleted') + + diff --git a/app/routers_v3/managers.py b/app/routers_v3/managers.py new file mode 100644 index 0000000..abe8bea --- /dev/null +++ b/app/routers_v3/managers.py @@ -0,0 +1,152 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional +import logging +import pydantic + +from ..db_engine import db, Manager, Team, Current, model_to_dict, fn +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/managers', + tags=['managers'] +) + + +class ManagerModel(pydantic.BaseModel): + name: str + image: Optional[str] = None + headline: Optional[str] = None + bio: Optional[str] = None + + +@router.get('') +async def get_managers( + name: list = Query(default=None), active: Optional[bool] = None, short_output: Optional[bool] = False): + if active is not None: + current = Current.latest() + t_query = Team.select_season(current.season) + t_query = t_query.where( + ~(Team.abbrev.endswith('IL')) & ~(Team.abbrev.endswith('MiL')) + ) + logging.info(f'tquery: {t_query}') + a_mgr = [] + i_mgr = [] + + for x in t_query: + logging.info(f'Team: {x.abbrev} / mgr1: {x.manager1} / mgr2: {x.manager2}') + if x.manager1 is not None: + a_mgr.append(x.manager1) + logging.info(f'appending {x.manager1.name}') + if x.manager2 is not None: + a_mgr.append(x.manager2) + logging.info(f'appending {x.manager2.name}') + + logging.info(f'a_mgr: {a_mgr}') + if active: + final_mgrs = [model_to_dict(y, recurse=not short_output) for y in a_mgr] + else: + logging.info(f'checking inactive') + for z in Manager.select(): + logging.info(f'checking: {z.name}') + if z not in a_mgr: + logging.info(f'+inactive: {z.name}') + i_mgr.append(z) + final_mgrs = [model_to_dict(y, recurse=not short_output) for y in i_mgr] + + return_managers = { + 'count': len(final_mgrs), + 'managers': final_mgrs + } + + else: + all_managers = Manager.select() + if name is not None: + name_list = [x.lower() for x in name] + all_managers = all_managers.where(fn.Lower(Manager.name) << name_list) + + return_managers = { + 'count': all_managers.count(), + 'managers': [model_to_dict(x, recurse=not short_output) for x in all_managers] + } + + db.close() + return return_managers + + +@router.get('/{manager_id}') +async def get_one_manager(manager_id: int, short_output: Optional[bool] = False): + this_manager = Manager.get_or_none(Manager.id == manager_id) + if this_manager is not None: + r_manager = model_to_dict(this_manager, recurse=not short_output) + db.close() + return r_manager + else: + raise HTTPException(status_code=404, detail=f'Manager {manager_id} not found') + + +@router.patch('/{manager_id}') +async def patch_manager( + manager_id: int, name: Optional[str] = None, image: Optional[str] = None, headline: Optional[str] = None, + bio: Optional[str] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_manager - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_manager = Manager.get_or_none(Manager.id == manager_id) + if this_manager is None: + db.close() + raise HTTPException(status_code=404, detail=f'Manager ID {manager_id} not found') + + if name is not None: + this_manager.name = name + if image is not None: + this_manager.image = image + if headline is not None: + this_manager.headline = headline + if bio is not None: + this_manager.bio = bio + + if this_manager.save() == 1: + r_manager = model_to_dict(this_manager) + db.close() + return r_manager + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to patch manager {this_manager}') + + +@router.post('') +async def post_manager(new_manager: ManagerModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_manager - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_manager = Manager(**new_manager.dict()) + + if this_manager.save(): + r_manager = model_to_dict(this_manager) + db.close() + return r_manager + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to post manager {this_manager.name}') + + +@router.delete('/{manager_id}') +async def delete_manager(manager_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_manager - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_manager = Manager.get_or_none(Manager.id == manager_id) + if this_manager is None: + db.close() + raise HTTPException(status_code=404, detail=f'Manager ID {manager_id} not found') + + count = this_manager.delete_instance() + db.close() + + if count == 1: + return f'Manager {manager_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Manager {manager_id} could not be deleted') diff --git a/app/routers_v3/pitchingstats.py b/app/routers_v3/pitchingstats.py new file mode 100644 index 0000000..093476b --- /dev/null +++ b/app/routers_v3/pitchingstats.py @@ -0,0 +1,267 @@ +import datetime +import os + +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional, Literal +import logging +import pydantic + +from ..db_engine import db, PitchingStat, Team, Player, Current, model_to_dict, chunked, fn, per_season_weeks +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/pitchingstats', + tags=['pitchingstats'] +) + + +class PitStatModel(pydantic.BaseModel): + player_id: int + team_id: int + ip: Optional[float] = 0.0 + hit: Optional[int] = 0 + run: Optional[int] = 0 + erun: Optional[int] = 0 + so: Optional[int] = 0 + bb: Optional[int] = 0 + hbp: Optional[int] = 0 + wp: Optional[int] = 0 + balk: Optional[int] = 0 + hr: Optional[int] = 0 + gs: Optional[int] = 0 + win: Optional[int] = 0 + loss: Optional[int] = 0 + hold: Optional[int] = 0 + sv: Optional[int] = 0 + bsv: Optional[int] = 0 + ir: Optional[int] = 0 + irs: Optional[int] = 0 + week: int + game: int + season: int + + +class PitStatList(pydantic.BaseModel): + count: int + stats: List[PitStatModel] + + +@router.get('') +async def get_pitstats( + season: int, s_type: Optional[str] = 'regular', team_abbrev: list = Query(default=None), + player_name: list = Query(default=None), player_id: list = Query(default=None), + week_start: Optional[int] = None, week_end: Optional[int] = None, game_num: list = Query(default=None), + limit: Optional[int] = None, ip_min: Optional[float] = None, sort: Optional[str] = None, + short_output: Optional[bool] = True): + if 'post' in s_type.lower(): + all_stats = PitchingStat.post_season(season) + if all_stats.count() == 0: + db.close() + return {'count': 0, 'stats': []} + elif s_type.lower() in ['combined', 'total', 'all']: + all_stats = PitchingStat.combined_season(season) + if all_stats.count() == 0: + db.close() + return {'count': 0, 'stats': []} + else: + all_stats = PitchingStat.regular_season(season) + if all_stats.count() == 0: + db.close() + return {'count': 0, 'stats': []} + + if team_abbrev is not None: + t_query = Team.select().where(Team.abbrev << [x.upper() for x in team_abbrev]) + all_stats = all_stats.where(PitchingStat.team << t_query) + if player_name is not None or player_id is not None: + if player_id: + all_stats = all_stats.where(PitchingStat.player_id << player_id) + else: + p_query = Player.select_season(season).where(fn.Lower(Player.name) << [x.lower() for x in player_name]) + all_stats = all_stats.where(PitchingStat.player << p_query) + if game_num: + all_stats = all_stats.where(PitchingStat.game == game_num) + if ip_min is not None: + all_stats = all_stats.where(PitchingStat.ip >= ip_min) + + start = 1 + end = Current.get(Current.season == season).week + if week_start is not None: + start = week_start + if week_end is not None: + end = min(week_end, end) + if start > end: + db.close() + raise HTTPException( + status_code=404, + detail=f'Start week {start} is after end week {end} - cannot pull stats' + ) + all_stats = all_stats.where( + (PitchingStat.week >= start) & (PitchingStat.week <= end) + ) + + if limit: + all_stats = all_stats.limit(limit) + if sort: + if sort == 'newest': + all_stats = all_stats.order_by(-PitchingStat.week, -PitchingStat.game) + + return_stats = { + 'count': all_stats.count(), + 'stats': [model_to_dict(x, recurse=not short_output) for x in all_stats] + } + + db.close() + return return_stats + + +@router.get('/totals') +async def get_totalstats( + season: int, s_type: Literal['regular', 'post', 'total', None] = None, team_abbrev: list = Query(default=None), + team_id: list = Query(default=None), player_name: list = Query(default=None), + week_start: Optional[int] = None, week_end: Optional[int] = None, game_num: list = Query(default=None), + is_sp: Optional[bool] = None, ip_min: Optional[float] = 0.25, sort: Optional[str] = None, + player_id: list = Query(default=None), short_output: Optional[bool] = False, + group_by: Literal['team', 'player', 'playerteam'] = 'player', week: list = Query(default=None)): + if sum(1 for x in [s_type, (week_start or week_end), week] if x is not None) > 1: + raise HTTPException(status_code=400, detail=f'Only one of s_type, week_start/week_end, or week may be used.') + + all_stats = ( + PitchingStat + .select(PitchingStat.player, fn.SUM(PitchingStat.ip).alias('sum_ip'), + fn.SUM(PitchingStat.hit).alias('sum_hit'), fn.SUM(PitchingStat.run).alias('sum_run'), + fn.SUM(PitchingStat.erun).alias('sum_erun'), fn.SUM(PitchingStat.so).alias('sum_so'), + fn.SUM(PitchingStat.bb).alias('sum_bb'), fn.SUM(PitchingStat.hbp).alias('sum_hbp'), + fn.SUM(PitchingStat.wp).alias('sum_wp'), fn.SUM(PitchingStat.balk).alias('sum_balk'), + fn.SUM(PitchingStat.hr).alias('sum_hr'), fn.SUM(PitchingStat.ir).alias('sum_ir'), + fn.SUM(PitchingStat.win).alias('sum_win'), fn.SUM(PitchingStat.loss).alias('sum_loss'), + fn.SUM(PitchingStat.hold).alias('sum_hold'), fn.SUM(PitchingStat.sv).alias('sum_sv'), + fn.SUM(PitchingStat.bsv).alias('sum_bsv'), fn.SUM(PitchingStat.irs).alias('sum_irs'), + fn.SUM(PitchingStat.gs).alias('sum_gs'), PitchingStat.team) + .where(PitchingStat.season == season) + .having(fn.SUM(PitchingStat.ip) >= ip_min) + ) + + if True in [s_type is not None, week_start is not None, week_end is not None]: + weeks = {} + if s_type is not None: + weeks = per_season_weeks(season, s_type) + elif week_start is not None or week_end is not None: + if week_start is None or week_end is None: + raise HTTPException( + status_code=400, detail='Both week_start and week_end must be included if either is used.' + ) + weeks['start'] = week_start + if week_end < weeks['start']: + raise HTTPException(status_code=400, detail='week_end must be greater than or equal to week_start') + else: + weeks['end'] = week_end + + all_stats = all_stats.where( + (PitchingStat.week >= weeks['start']) & (PitchingStat.week <= weeks['end']) + ) + + elif week is not None: + all_stats = all_stats.where(PitchingStat.week << week) + + if game_num is not None: + all_stats = all_stats.where(PitchingStat.game << game_num) + if is_sp is not None: + if is_sp: + all_stats = all_stats.where(PitchingStat.gs == 1) + if not is_sp: + all_stats = all_stats.where(PitchingStat.gs == 0) + if sort is not None: + if sort == 'player': + all_stats = all_stats.order_by(PitchingStat.player) + elif sort == 'team': + all_stats = all_stats.order_by(PitchingStat.team) + if group_by is not None: + if group_by == 'team': + all_stats = all_stats.group_by(PitchingStat.team) + elif group_by == 'player': + all_stats = all_stats.group_by(PitchingStat.player) + elif group_by == 'playerteam': + all_stats = all_stats.group_by(PitchingStat.team, PitchingStat.player) + if team_id is not None: + all_teams = Team.select().where(Team.id << team_id) + all_stats = all_stats.where(PitchingStat.team << all_teams) + elif team_abbrev is not None: + all_teams = Team.select().where(fn.Lower(Team.abbrev) << [x.lower() for x in team_abbrev]) + all_stats = all_stats.where(PitchingStat.team << all_teams) + if player_name is not None: + all_players = Player.select().where(fn.Lower(Player.name) << [x.lower() for x in player_name]) + all_stats = all_stats.where(PitchingStat.player << all_players) + elif player_id is not None: + all_players = Player.select().where(Player.id << player_id) + all_stats = all_stats.where(PitchingStat.player << all_players) + + return_stats = { + 'count': all_stats.count(), + 'stats': [{ + 'player': x.player_id if short_output else model_to_dict(x.player, recurse=False), + 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False), + 'ip': x.sum_ip, + 'hit': x.sum_hit, + 'run': x.sum_run, + 'erun': x.sum_erun, + 'so': x.sum_so, + 'bb': x.sum_bb, + 'hbp': x.sum_hbp, + 'wp': x.sum_wp, + 'balk': x.sum_balk, + 'hr': x.sum_hr, + 'ir': x.sum_ir, + 'irs': x.sum_irs, + 'gs': x.sum_gs, + 'win': x.sum_win, + 'loss': x.sum_loss, + 'hold': x.sum_hold, + 'sv': x.sum_sv, + 'bsv': x.sum_bsv + } for x in all_stats] + } + db.close() + return return_stats + + +@router.patch('/{stat_id}') +async def patch_pitstats(stat_id: int, new_stats: PitStatModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_pitstats - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + if PitchingStat.get_or_none(PitchingStat.id == stat_id) is None: + raise HTTPException(status_code=404, detail=f'Stat ID {stat_id} not found') + + PitchingStat.update(**new_stats.dict()).where(PitchingStat.id == stat_id).execute() + r_stat = model_to_dict(PitchingStat.get_by_id(stat_id)) + db.close() + return r_stat + + +@router.post('') +async def post_pitstats(s_list: PitStatList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_pitstats - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + all_stats = [] + + for x in s_list.stats: + team = Team.get_or_none(Team.id == x.team_id) + this_player = Player.get_or_none(Player.id == x.player_id) + if team is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.team_id} not found') + if this_player is None: + raise HTTPException(status_code=404, detail=f'Player ID {x.player_id} not found') + + all_stats.append(PitchingStat(**x.dict())) + + with db.atomic(): + for batch in chunked(all_stats, 15): + PitchingStat.insert_many(batch).on_conflict_replace().execute() + + db.close() + return f'Added {len(all_stats)} batting lines' diff --git a/app/routers_v3/players.py b/app/routers_v3/players.py new file mode 100644 index 0000000..9480f89 --- /dev/null +++ b/app/routers_v3/players.py @@ -0,0 +1,190 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Response +from typing import List, Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, 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/players', + tags=['players'] +) + + +class PlayerModel(pydantic.BaseModel): + name: str + wara: float + image: str + image2: Optional[str] = None + team_id: int + season: int + pitcher_injury: Optional[int] = None + pos_1: str + pos_2: Optional[str] = None + pos_3: Optional[str] = None + pos_4: Optional[str] = None + pos_5: Optional[str] = None + pos_6: Optional[str] = None + pos_7: Optional[str] = None + pos_8: Optional[str] = None + vanity_card: Optional[str] = None + headshot: Optional[str] = None + last_game: Optional[str] = None + last_game2: Optional[str] = None + il_return: Optional[str] = None + demotion_week: Optional[int] = None + strat_code: Optional[str] = None + bbref_id: Optional[str] = None + injury_rating: Optional[str] = None + + +class PlayerList(pydantic.BaseModel): + players: List[PlayerModel] + + +@router.get('') +async def get_players( + season: Optional[int], name: Optional[str] = None, team_id: list = Query(default=None), + pos: list = Query(default=None), strat_code: list = Query(default=None), is_injured: Optional[bool] = None, + sort: Optional[str] = None, short_output: Optional[bool] = False, csv: Optional[bool] = False): + all_players = Player.select_season(season) + + if team_id is not None: + all_players = all_players.where(Player.team_id << team_id) + + if strat_code is not None: + code_list = [x.lower() for x in strat_code] + all_players = all_players.where(fn.Lower(Player.strat_code) << code_list) + + if name is not None: + all_players = all_players.where(fn.lower(Player.name) == name.lower()) + + if pos is not None: + p_list = [x.upper() for x in pos] + all_players = all_players.where( + (Player.pos_1 << p_list) | (Player.pos_2 << p_list) | (Player.pos_3 << p_list) | (Player.pos_4 << p_list) | + (Player.pos_5 << p_list) | (Player.pos_6 << p_list) | (Player.pos_7 << p_list) | (Player.pos_8 << p_list) + ) + + if is_injured is not None: + all_players = all_players.where(Player.il_return.is_null(False)) + + if sort is not None: + if sort == 'cost-asc': + all_players = all_players.order_by(Player.wara) + elif sort == 'cost-desc': + all_players = all_players.order_by(-Player.wara) + elif sort == 'name-asc': + all_players = all_players.order_by(Player.name) + elif sort == 'name-desc': + all_players = all_players.order_by(-Player.name) + + print(f'csv: {csv}') + if csv: + player_list = [ + ['name', 'wara', 'image', 'image2', 'team', 'season', 'pitcher_injury', 'pos_1', 'pos_2', 'pos_3', + 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'last_game', 'last_game2', 'il_return', 'demotion_week', + 'headshot', 'vanity_card', 'strat_code', 'bbref_id', 'injury_rating', 'player_id'] + ] + for line in all_players: + player_list.append( + [ + line.name, line.wara, line.image, line.image2, line.team.abbrev, line.season, line.pitcher_injury, + line.pos_1, line.pos_2, line.pos_3, line.pos_4, line.pos_5, line.pos_6, line.pos_7, line.pos_8, + line.last_game, line.last_game2, line.il_return, line.demotion_week, line.headshot, + line.vanity_card, line.strat_code.replace(",", "-_-"), line.bbref_id, line.injury_rating, line.id + ] + ) + return_players = { + 'count': all_players.count(), + 'players': DataFrame(player_list).to_csv(header=False, index=False), + 'csv': True + } + + db.close() + return Response(content=return_players['players'], media_type='text/csv') + + else: + return_players = { + 'count': all_players.count(), + 'players': [model_to_dict(x, recurse=not short_output) for x in all_players] + } + db.close() + return return_players + + +@router.get('/{player_id}') +async def get_one_player(player_id: int, short_output: Optional[bool] = False): + this_player = Player.get_or_none(Player.id == player_id) + if this_player: + r_player = model_to_dict(this_player, recurse=not short_output) + else: + r_player = None + db.close() + return r_player + + +@router.patch('/{player_id}') +async def patch_player( + player_id: int, new_player: PlayerModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_player - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + if Player.get_or_none(Player.id == player_id) is None: + raise HTTPException(status_code=404, detail=f'Player ID {player_id} not found') + + Player.update(**new_player.dict()).where(Player.id == player_id).execute() + r_player = model_to_dict(Player.get_by_id(player_id)) + db.close() + return r_player + + +@router.post('') +async def post_players(p_list: PlayerList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_players - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_players = [] + for player in p_list.players: + dupe = Player.get_or_none(Player.season == player.season, Player.name == player.name) + if dupe: + db.close() + raise HTTPException( + status_code=500, + detail=f'Player name {player.name} already in use in Season {player.season}' + ) + + new_players.append(player.dict()) + + with db.atomic(): + for batch in chunked(new_players, 15): + Player.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_players)} players' + + +@router.delete('/{player_id}') +async def delete_player(player_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_player - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_player = Player.get_or_none(Player.id == player_id) + if not this_player: + db.close() + raise HTTPException(status_code=404, detail=f'Player ID {player_id} not found') + + count = this_player.delete_instance() + db.close() + + if count == 1: + return f'Player {player_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Player {player_id} could not be deleted') diff --git a/app/routers_v3/results.py b/app/routers_v3/results.py new file mode 100644 index 0000000..fd3ac6e --- /dev/null +++ b/app/routers_v3/results.py @@ -0,0 +1,175 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional +import logging +import pydantic + +from ..db_engine import db, Result, Team, model_to_dict, chunked +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/results', + tags=['results'] +) + + +class ResultModel(pydantic.BaseModel): + week: int + game: int + awayteam_id: int + hometeam_id: int + awayscore: int + homescore: int + season: int + scorecard_url: Optional[str] = None + + +class ResultList(pydantic.BaseModel): + results: List[ResultModel] + + +@router.get('') +async def get_results( + season: int, team_abbrev: list = Query(default=None), week_start: Optional[int] = None, + week_end: Optional[int] = None, game_num: list = Query(default=None), + away_abbrev: list = Query(default=None), home_abbrev: list = Query(default=None), + short_output: Optional[bool] = False): + all_results = Result.select_season(season) + + if team_abbrev is not None: + team_list = [] + for x in team_abbrev: + team_list.append(Team.get_season(x, season)) + all_results = all_results.where( + (Result.awayteam << team_list) | (Result.hometeam << team_list) + ) + + if away_abbrev is not None: + team_list = [] + for x in away_abbrev: + team_list.append(Team.get_season(x, season)) + all_results = all_results.where(Result.awayteam << team_list) + + if home_abbrev is not None: + team_list = [] + for x in home_abbrev: + team_list.append(Team.get_season(x, season)) + all_results = all_results.where(Result.hometeam << team_list) + + if game_num is not None: + all_results = all_results.where(Result.game << game_num) + + if week_start is not None: + all_results = all_results.where(Result.week >= week_start) + + if week_end is not None: + all_results = all_results.where(Result.week <= week_end) + + return_results = { + 'count': all_results.count(), + 'results': [model_to_dict(x, recurse=not short_output) for x in all_results] + } + db.close() + return return_results + + +@router.get('/{result_id}') +async def get_one_result(result_id: int, short_output: Optional[bool] = False): + this_result = Result.get_or_none(Result.id == result_id) + if this_result is not None: + r_result = model_to_dict(this_result, recurse=not short_output) + else: + r_result = None + db.close() + return r_result + + +@router.patch('/{result_id}') +async def patch_result( + result_id: int, week_num: Optional[int] = None, game_num: Optional[int] = None, + away_team_id: Optional[int] = None, home_team_id: Optional[int] = None, away_score: Optional[int] = None, + home_score: Optional[int] = None, season: Optional[int] = None, scorecard_url: Optional[str] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_player - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_result = Result.get_or_none(Result.id == result_id) + if this_result is None: + raise HTTPException(status_code=404, detail=f'Result ID {result_id} not found') + + if week_num is not None: + this_result.week = week_num + + if game_num is not None: + this_result.game = game_num + + if away_team_id is not None: + this_result.awayteam_id = away_team_id + + if home_team_id is not None: + this_result.hometeam_id = home_team_id + + if away_score is not None: + this_result.awayscore = away_score + + if home_score is not None: + this_result.homescore = home_score + + if season is not None: + this_result.season = season + + if scorecard_url is not None: + this_result.scorecard_url = scorecard_url + + if this_result.save() == 1: + r_result = model_to_dict(this_result) + db.close() + return r_result + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to patch result {result_id}') + + +@router.post('') +async def post_results(result_list: ResultList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_player - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_results = [] + for x in result_list.results: + if Team.get_or_none(Team.id == x.awayteam_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.awayteam_id} not found') + if Team.get_or_none(Team.id == x.hometeam_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.hometeam_id} not found') + + new_results.append(x.dict()) + + with db.atomic(): + for batch in chunked(new_results, 15): + Result.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_results)} results' + + +@router.delete('/{result_id}') +async def delete_result(result_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_result - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_result = Result.get_or_none(Result.id == result_id) + if not this_result: + db.close() + raise HTTPException(status_code=404, detail=f'Result ID {result_id} not found') + + count = this_result.delete_instance() + db.close() + + if count == 1: + return f'Result {result_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Result {result_id} could not be deleted') + + diff --git a/app/routers_v3/schedules.py b/app/routers_v3/schedules.py new file mode 100644 index 0000000..a748761 --- /dev/null +++ b/app/routers_v3/schedules.py @@ -0,0 +1,157 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional +import logging +import pydantic + +from ..db_engine import db, Schedule, Team, model_to_dict, chunked +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/schedules', + tags=['schedules'] +) + + +class ScheduleModel(pydantic.BaseModel): + week: int + awayteam_id: int + hometeam_id: int + gamecount: int + season: int + + +class ScheduleList(pydantic.BaseModel): + schedules: List[ScheduleModel] + + +@router.get('') +async def get_schedules( + season: int, team_abbrev: list = Query(default=None), away_abbrev: list = Query(default=None), + home_abbrev: list = Query(default=None), week_start: Optional[int] = None, week_end: Optional[int] = None, + short_output: Optional[bool] = True): + all_sched = Schedule.select_season(season) + + if team_abbrev is not None: + team_list = [] + for x in team_abbrev: + team_list.append(Team.get_season(x, season)) + all_sched = all_sched.where( + (Schedule.awayteam << team_list) | (Schedule.hometeam << team_list) + ) + + if away_abbrev is not None: + team_list = [] + for x in away_abbrev: + team_list.append(Team.get_season(x, season)) + all_sched = all_sched.where(Schedule.awayteam << team_list) + + if home_abbrev is not None: + team_list = [] + for x in home_abbrev: + team_list.append(Team.get_season(x, season)) + all_sched = all_sched.where(Schedule.hometeam << team_list) + + if week_start is not None: + all_sched = all_sched.where(Schedule.week >= week_start) + + if week_end is not None: + all_sched = all_sched.where(Schedule.week <= week_end) + + all_sched = all_sched.order_by(Schedule.id) + + return_sched = { + 'count': all_sched.count(), + 'schedules': [model_to_dict(x, recurse=not short_output) for x in all_sched] + } + db.close() + return return_sched + + +@router.get('/{schedule_id}') +async def get_one_schedule(schedule_id: int): + this_sched = Schedule.get_or_none(Schedule.id == schedule_id) + if this_sched is not None: + r_sched = model_to_dict(this_sched) + else: + r_sched = None + db.close() + return r_sched + + +@router.patch('/{schedule_id}') +async def patch_schedule( + schedule_id: int, week: list = Query(default=None), awayteam_id: Optional[int] = None, + hometeam_id: Optional[int] = None, gamecount: Optional[int] = None, season: Optional[int] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_schedule - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_sched = Schedule.get_or_none(Schedule.id == schedule_id) + if this_sched is None: + raise HTTPException(status_code=404, detail=f'Schedule ID {schedule_id} not found') + + if week is not None: + this_sched.week = week + + if awayteam_id is not None: + this_sched.awayteam_id = awayteam_id + + if hometeam_id is not None: + this_sched.hometeam_id = hometeam_id + + if gamecount is not None: + this_sched.gamecount = gamecount + + if season is not None: + this_sched.season = season + + if this_sched.save() == 1: + r_sched = model_to_dict(this_sched) + db.close() + return r_sched + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to patch schedule {schedule_id}') + + +@router.post('') +async def post_schedules(sched_list: ScheduleList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_schedules - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_sched = [] + for x in sched_list.schedules: + if Team.get_or_none(Team.id == x.awayteam_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.awayteam_id} not found') + if Team.get_or_none(Team.id == x.hometeam_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.hometeam_id} not found') + + new_sched.append(x.dict()) + + with db.atomic(): + for batch in chunked(new_sched, 15): + Schedule.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_sched)} schedules' + + +@router.delete('/{schedule_id}') +async def delete_schedule(schedule_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_schedule - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_sched = Schedule.get_or_none(Schedule.id == schedule_id) + if this_sched is None: + raise HTTPException(status_code=404, detail=f'Schedule ID {schedule_id} not found') + + count = this_sched.delete_instance() + db.close() + + if count == 1: + return f'Schedule {this_sched} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Schedule {this_sched} could not be deleted') diff --git a/app/routers_v3/standings.py b/app/routers_v3/standings.py new file mode 100644 index 0000000..c671f6d --- /dev/null +++ b/app/routers_v3/standings.py @@ -0,0 +1,90 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional +import logging +import pydantic + +from ..db_engine import db, Standings, Team, Division, model_to_dict, chunked, fn +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/standings', + tags=['standings'] +) + + +@router.get('') +async def get_standings( + season: int, team_abbrev: list = Query(default=None), league_abbrev: Optional[str] = None, + division_abbrev: Optional[str] = None, short_output: Optional[bool] = False): + standings = Standings.select_season(season) + + # if standings.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No output for season {season}') + + if team_abbrev is not None: + t_query = Team.select().where(fn.Lower(Team.abbrev) << [x.lower() for x in team_abbrev]) + standings = standings.where(Standings.team << t_query) + + if league_abbrev is not None: + l_query = Division.select().where(fn.Lower(Division.league_abbrev) == league_abbrev.lower()) + standings = standings.where(Standings.team.division << l_query) + + if division_abbrev is not None: + d_query = Division.select().where(fn.Lower(Division.division_abbrev) == division_abbrev.lower()) + standings = standings.where(Standings.team.division << d_query) + + def win_pct(this_team_stan): + if this_team_stan.wins + this_team_stan.losses == 0: + return 0 + else: + return (this_team_stan.wins / (this_team_stan.wins + this_team_stan.losses)) + \ + (this_team_stan.run_diff * .000001) + + div_teams = [x for x in standings] + div_teams.sort(key=lambda team: win_pct(team), reverse=True) + + return_standings = { + 'count': len(div_teams), + 'standings': [model_to_dict(x, recurse=not short_output) for x in div_teams] + } + + db.close() + return return_standings + + +@router.patch('/{stan_id}') +async def patch_standings( + stan_id, wins: Optional[int] = None, losses: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_standings - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + try: + this_stan = Standings.get_by_id(stan_id) + except Exception as e: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {stan_id}') + + if wins: + this_stan.wins = wins + if losses: + this_stan.losses = losses + + this_stan.save() + db.close() + + return model_to_dict(this_stan) + + +@router.post('/s{season}/recalculate') +async def recalculate_standings(season: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'recalculate_standings - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + code = Standings.recalculate(season) + db.close() + if code == 69: + HTTPException(status_code=500, detail=f'Error recreating Standings rows') + raise HTTPException(status_code=200, detail=f'Just recalculated standings for season {season}') diff --git a/app/routers_v3/teams.py b/app/routers_v3/teams.py new file mode 100644 index 0000000..77e8205 --- /dev/null +++ b/app/routers_v3/teams.py @@ -0,0 +1,249 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional, Literal +import copy +import logging +import pydantic + +from ..db_engine import db, Team, Manager, Division, model_to_dict, chunked, fn +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/teams', + tags=['teams'] +) + + +class TeamModel(pydantic.BaseModel): + abbrev: str + sname: str + lname: str + gmid: Optional[int] = None + gmid2: Optional[int] = None + manager1_id: Optional[int] = None + manager2_id: Optional[int] = None + division_id: Optional[int] = None + stadium: Optional[str] = None + thumbnail: Optional[str] = None + color: Optional[str] = None + dice_color: Optional[str] = None + season: int + + +class TeamList(pydantic.BaseModel): + teams: List[TeamModel] + + +@router.get('') +async def get_teams( + season: Optional[int] = None, owner_id: list = Query(default=None), manager_id: list = Query(default=None), + team_abbrev: list = Query(default=None), active_only: Optional[bool] = False, + short_output: Optional[bool] = False): + if season is not None: + all_teams = Team.select_season(season) + else: + all_teams = Team.select() + + if manager_id is not None: + managers = Manager.select().where(Manager.id << manager_id) + all_teams = all_teams.where( + (Team.manager1_id << managers) | (Team.manager2_id << managers) + ) + if owner_id: + all_teams = all_teams.where((Team.gmid << owner_id) | (Team.gmid2 << owner_id)) + if team_abbrev is not None: + team_list = [x.lower() for x in team_abbrev] + all_teams = all_teams.where(fn.lower(Team.abbrev) << team_list) + if active_only: + all_teams = all_teams.where( + ~(Team.abbrev.endswith('IL')) & ~(Team.abbrev.endswith('MiL')) + ) + + return_teams = { + 'count': all_teams.count(), + 'teams': [model_to_dict(x, recurse=not short_output) for x in all_teams] + } + db.close() + return return_teams + + +@router.get('/{team_id}') +async def get_one_team(team_id: int): + this_team = Team.get_or_none(Team.id == team_id) + if this_team: + r_team = model_to_dict(this_team) + else: + r_team = None + db.close() + return r_team + + +@router.get('/{team_id}/roster/{which}') +async def get_team_roster(team_id: int, which: Literal['current', 'next'], sort: Optional[str] = None): + try: + this_team = Team.get_by_id(team_id) + except Exception as e: + raise HTTPException(status_code=404, detail=f'Team ID {team_id} not found') + + if which == 'current': + full_roster = this_team.get_this_week() + else: + full_roster = this_team.get_next_week() + + active_players = copy.deepcopy(full_roster['active']['players']) + sil_players = copy.deepcopy(full_roster['shortil']['players']) + lil_players = copy.deepcopy(full_roster['longil']['players']) + full_roster['active']['players'] = [] + full_roster['shortil']['players'] = [] + full_roster['longil']['players'] = [] + + for player in active_players: + full_roster['active']['players'].append(model_to_dict(player)) + for player in sil_players: + full_roster['shortil']['players'].append(model_to_dict(player)) + for player in lil_players: + full_roster['longil']['players'].append(model_to_dict(player)) + + if sort: + if sort == 'wara-desc': + full_roster['active']['players'].sort(key=lambda p: p["wara"], reverse=True) + full_roster['active']['players'].sort(key=lambda p: p["wara"], reverse=True) + full_roster['active']['players'].sort(key=lambda p: p["wara"], reverse=True) + + db.close() + return full_roster + + +@router.patch('/{team_id}') +async def patch_team( + team_id: int, manager1_id: Optional[int] = None, manager2_id: Optional[int] = None, gmid: Optional[int] = None, + gmid2: Optional[int] = None, mascot: Optional[str] = None, stadium: Optional[str] = None, + thumbnail: Optional[str] = None, color: Optional[str] = None, abbrev: Optional[str] = None, + sname: Optional[str] = None, lname: Optional[str] = None, dice_color: Optional[str] = None, + division_id: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_team - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_team = Team.get_or_none(Team.id == team_id) + if not this_team: + return None + + if abbrev is not None: + this_team.abbrev = abbrev + if manager1_id is not None: + if manager1_id == 0: + this_team.manager1 = None + else: + this_manager = Manager.get_or_none(Manager.id == manager1_id) + if not this_manager: + db.close() + raise HTTPException(status_code=404, detail=f'Manager ID {manager1_id} not found') + this_team.manager1 = this_manager + if manager2_id is not None: + if manager2_id == 0: + this_team.manager2 = None + else: + this_manager = Manager.get_or_none(Manager.id == manager2_id) + if not this_manager: + db.close() + raise HTTPException(status_code=404, detail=f'Manager ID {manager2_id} not found') + this_team.manager2 = this_manager + if gmid is not None: + this_team.gmid = gmid + if gmid2 is not None: + if gmid2 == 0: + this_team.gmid2 = None + else: + this_team.gmid2 = gmid2 + if mascot is not None: + if mascot == 'False': + this_team.mascot = None + else: + this_team.mascot = mascot + if stadium is not None: + this_team.stadium = stadium + if thumbnail is not None: + this_team.thumbnail = thumbnail + if color is not None: + this_team.color = color + if dice_color is not None: + this_team.dice_color = dice_color + if sname is not None: + this_team.sname = sname + if lname is not None: + this_team.lname = lname + if division_id is not None: + if division_id == 0: + this_team.division = None + else: + this_division = Division.get_or_none(Division.id == division_id) + if not this_division: + db.close() + raise HTTPException(status_code=404, detail=f'Division ID {division_id} not found') + this_team.division = this_division + + if this_team.save(): + r_team = model_to_dict(this_team) + db.close() + return r_team + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to patch team {team_id}') + + +@router.post('') +async def post_team(team_list: TeamList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_team - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_teams = [] + for team in team_list.teams: + dupe_team = Team.get_or_none(Team.season == team.season, Team.abbrev == team.abbrev) + if dupe_team: + db.close() + raise HTTPException( + status_code=500, detail=f'Team Abbrev {team.abbrev} already in use in Season {team.season}' + ) + + if team.manager1_id and not Manager.get_or_none(Manager.id == team.manager1_id): + db.close() + raise HTTPException(status_code=404, detail=f'Manager ID {team.manager1_id} not found') + + if team.manager2_id and not Manager.get_or_none(Manager.id == team.manager2_id): + db.close() + raise HTTPException(status_code=404, detail=f'Manager ID {team.manager2_id} not found') + + if team.division_id and not Division.get_or_none(Division.id == team.division_id): + db.close() + raise HTTPException(status_code=404, detail=f'Division ID {team.division_id} not found') + + new_teams.append(team.dict()) + + with db.atomic(): + for batch in chunked(new_teams, 15): + Team.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_teams)} teams' + + +@router.get('/{team_id}') +async def delete_team(team_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_team - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_team = Team.get_or_none(Team.id == team_id) + if not this_team: + db.close() + raise HTTPException(status_code=404, detail=f'Team ID {team_id} not found') + + count = this_team.delete_instance() + db.close() + + if count == 1: + return f'Team {team_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Team {team_id} could not be deleted') + diff --git a/app/routers_v3/transactions.py b/app/routers_v3/transactions.py new file mode 100644 index 0000000..c8edd0f --- /dev/null +++ b/app/routers_v3/transactions.py @@ -0,0 +1,153 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Response +from typing import List, Optional +from pandas import DataFrame +import logging +import pydantic + +from ..db_engine import db, Transaction, Team, Player, model_to_dict, chunked, fn +from ..dependencies import oauth2_scheme, valid_token + +router = APIRouter( + prefix='/api/v3/transactions', + tags=['transactions'] +) + + +class TransactionModel(pydantic.BaseModel): + week: int + player_id: int + oldteam_id: int + newteam_id: int + season: int + moveid: str + cancelled: Optional[bool] = False + frozen: Optional[bool] = False + + +class TransactionList(pydantic.BaseModel): + count: int + moves: List[TransactionModel] + + +@router.get('') +async def get_transactions( + season, team_abbrev: list = Query(default=None), week_start: Optional[int] = 0, + week_end: Optional[int] = None, cancelled: Optional[bool] = None, frozen: Optional[bool] = None, + player_name: list = Query(default=None), player_id: list = Query(default=None), move_id: Optional[str] = None, + is_trade: Optional[bool] = None, short_output: Optional[bool] = False): + if season: + transactions = Transaction.select_season(season) + else: + transactions = Transaction.select() + + if team_abbrev is not None: + t_list = [x.upper() for x in team_abbrev] + these_teams = Team.select().where((Team.abbrev << t_list)) + transactions = transactions.where( + (Transaction.newteam << these_teams) | (Transaction.oldteam << these_teams) + ) + if week_start is not None: + transactions = transactions.where(Transaction.week >= week_start) + if week_end is not None: + transactions = transactions.where(Transaction.week <= week_end) + if move_id: + transactions = transactions.where(Transaction.moveid == move_id) + if player_id or player_name: + if player_id: + p_list = Player.select().where(Player.id << player_id) + transactions = transactions.where(Transaction.player << p_list) + else: + p_list = [x.lower() for x in player_name] + these_players = Player.select().where(fn.Lower(Player.name) << p_list) + transactions = transactions.where(Transaction.player << these_players) + + if cancelled: + transactions = transactions.where(Transaction.cancelled == 1) + else: + transactions = transactions.where(Transaction.cancelled == 0) + + if frozen: + transactions = transactions.where(Transaction.frozen == 1) + else: + transactions = transactions.where(Transaction.frozen == 0) + + if is_trade is not None: + raise HTTPException(status_code=501, detail='The is_trade parameter is not implemented, yet') + + transactions = transactions.order_by(-Transaction.week, Transaction.moveid) + + return_trans = { + 'count': transactions.count(), + 'transactions': [model_to_dict(x, recurse=not short_output) for x in transactions] + } + + db.close() + return return_trans + + +@router.patch('/{move_id}') +async def patch_transactions( + move_id, token: str = Depends(oauth2_scheme), frozen: Optional[bool] = None, cancelled: Optional[bool] = None): + if not valid_token(token): + logging.warning(f'patch_transactions - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + these_moves = Transaction.select().where(Transaction.moveid == move_id) + if these_moves.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'Move ID {move_id} not found') + + if frozen is not None: + for x in these_moves: + x.frozen = frozen + x.save() + if cancelled is not None: + for x in these_moves: + x.cancelled = cancelled + x.save() + + db.close() + raise HTTPException(status_code=200, detail=f'Updated {these_moves.count()} transactions') + + +@router.post('') +async def post_transactions(moves: TransactionList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_transactions - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + all_moves = [] + + for x in moves.moves: + if Team.get_or_none(Team.id == x.oldteam_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.oldteam_id} not found') + if Team.get_or_none(Team.id == x.newteam_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.newteam_id} not found') + if Player.get_or_none(Player.id == x.player_id) is None: + raise HTTPException(status_code=404, detail=f'Player ID {x.player_id} not found') + + all_moves.append(x.dict()) + + with db.atomic(): + for batch in chunked(all_moves, 15): + Transaction.insert_many(batch).on_conflict_replace().execute() + + db.close() + raise HTTPException(status_code=200, detail=f'{len(all_moves)} transactions have been added') + + +@router.delete('/{move_id}') +async def delete_transactions(move_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_transactions - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + delete_query = Transaction.delete().where(Transaction.moveid == move_id) + + count = delete_query.execute() + db.close() + if count > 0: + raise HTTPException(status_code=200, detail=f'Removed {count} transactions') + else: + raise HTTPException(status_code=418, detail=f'Well slap my ass and call me a teapot; ' + f'I did not delete any records') diff --git a/main.py b/main.py index 0916961..ef3c799 100644 --- a/main.py +++ b/main.py @@ -1539,7 +1539,7 @@ async def v1_players_patch( raise HTTPException(status_code=404, detail=f'Team id {team_id} not found') if name: this_player.name = name - if wara: + if wara is not None: this_player.wara = wara if image: this_player.image = image diff --git a/requirements.txt b/requirements.txt index 1e90bfe..6b47181 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -fastapi==0.61.1 -uvicorn==0.12.2 +fastapi +uvicorn peewee==3.13.3 python-multipart pandas