From e7039eb83e00481f48dc78e71b97733a8298bb35 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 13 Sep 2023 14:44:26 -0500 Subject: [PATCH 01/40] Initial Commit with /current --- app/__init__.py | 0 app/db_engine.py | 765 +++++++++++++++++++++++++++++++++++++ app/dependencies.py | 26 ++ app/main.py | 13 + app/routers_v2/__init__.py | 0 app/routers_v2/current.py | 165 ++++++++ requirements.txt | 1 + venv/share/man/man1/ttx.1 | 225 +++++++++++ 8 files changed, 1195 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/db_engine.py create mode 100644 app/dependencies.py create mode 100644 app/main.py create mode 100644 app/routers_v2/__init__.py create mode 100644 app/routers_v2/current.py create mode 100644 venv/share/man/man1/ttx.1 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..e07ad27 --- /dev/null +++ b/app/db_engine.py @@ -0,0 +1,765 @@ +import math +from datetime import datetime +import logging +import os + +from peewee import * +from playhouse.shortcuts import model_to_dict + +db = SqliteDatabase( + 'storage/pd_master.db', + pragmas={ + 'journal_mode': 'wal', + 'cache_size': -1 * 64000, + 'synchronous': 0 + } +) + +date = f'{datetime.now().year}-{datetime.now().month}-{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 - database - %(levelname)s - %(message)s', + level=log_level +) + + +class BaseModel(Model): + class Meta: + database = db + + +class Current(BaseModel): + season = IntegerField() + week = IntegerField(default=0) + gsheet_template = CharField() + gsheet_version = CharField() + live_scoreboard = IntegerField() + + @staticmethod + def latest(): + latest_current = Current.select().order_by(-Current.id).get() + return latest_current + + +db.create_tables([Current]) + + +class Rarity(BaseModel): + value = IntegerField() + name = CharField(unique=True) + color = CharField() + + def __str__(self): + return self.name + + +db.create_tables([Rarity]) + + +class Event(BaseModel): + name = CharField() + short_desc = CharField(null=True) + url = CharField(null=True) + long_desc = CharField(null=True) + thumbnail = CharField(null=True) + active = BooleanField(default=False) + + +db.create_tables([Event]) + + +class Cardset(BaseModel): + name = CharField() + description = CharField() + event = ForeignKeyField(Event, null=True) + for_purchase = BooleanField(default=True) # for_purchase + total_cards = IntegerField() + in_packs = BooleanField(default=True) + ranked_legal = BooleanField(default=True) + + def __str__(self): + return self.name + + +db.create_tables([Cardset]) + + +class Player(BaseModel): + player_id = IntegerField(primary_key=True) + p_name = CharField() + cost = IntegerField(default=0) + image = CharField() + image2 = CharField(null=True) + mlbclub = CharField() + franchise = CharField() + cardset = ForeignKeyField(Cardset) + set_num = IntegerField() + rarity = ForeignKeyField(Rarity) + 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) + headshot = CharField(null=True) + vanity_card = CharField(null=True) + strat_code = CharField(null=True) + bbref_id = CharField(null=True) + fangr_id = CharField(null=True) + description = CharField() + quantity = IntegerField(default=999) + + def __str__(self): + return f'{self.cardset} {self.p_name} ({self.rarity.name})' + + # def __eq__(self, other): + # if self.cardset.id == other.cardset.id and self.name == other.name: + # return True + # else: + # return False + + def __lt__(self, other): + if self.wara < other.wara: + return True + elif self.wara > other.wara: + return False + elif self.name < other.name: + return True + else: + return False + + def get_all_pos(self): + all_pos = [] + + if self.pos_1 and self.pos_1 != 'CP': + all_pos.append(self.pos_1) + if self.pos_2 and self.pos_2 != 'CP': + all_pos.append(self.pos_2) + if self.pos_3 and self.pos_3 != 'CP': + all_pos.append(self.pos_3) + if self.pos_4 and self.pos_4 != 'CP': + all_pos.append(self.pos_4) + if self.pos_5 and self.pos_5 != 'CP': + all_pos.append(self.pos_5) + if self.pos_6 and self.pos_6 != 'CP': + all_pos.append(self.pos_6) + if self.pos_7 and self.pos_7 != 'CP': + all_pos.append(self.pos_7) + if self.pos_8 and self.pos_8 != 'CP': + all_pos.append(self.pos_8) + + return all_pos + + def change_on_sell(self): + # caps = { + # 'replacement': 15, + # 'reserve': 50, + # 'starter': 200, + # 'all-star': 750, + # 'mvp': 2500, + # 'hof': 999999999 + # } + logging.info(f'{self.p_name} cost changing from: {self.cost}') + self.cost = max(math.floor(self.cost * .95), 1) + # if self.quantity != 999: + # self.quantity += 1 + logging.info(f'{self.p_name} cost now: {self.cost}') + self.save() + + def change_on_buy(self): + logging.info(f'{self.p_name} cost changing from: {self.cost}') + self.cost = math.ceil(self.cost * 1.1) + # if self.quantity != 999: + # self.quantity -= 1 + logging.info(f'{self.p_name} cost now: {self.cost}') + self.save() + + +db.create_tables([Player]) + + +class Team(BaseModel): + abbrev = CharField() + sname = CharField() + lname = CharField() + gmid = IntegerField() + gmname = CharField() + gsheet = CharField() + wallet = IntegerField() + team_value = IntegerField() + collection_value = IntegerField() + logo = CharField(null=True) + color = CharField(null=True) + season = IntegerField() + event = ForeignKeyField(Event, null=True) + career = IntegerField(default=0) + ranking = IntegerField(default=1000) + has_guide = BooleanField(default=False) + is_ai = IntegerField(null=True) + + def __str__(self): + return f'S{self.season} {self.lname}' + + @staticmethod + def get_by_owner(gmid, season=None): + if not season: + season = Current.get().season + team = Team.get_or_none((Team.gmid == gmid) & (Team.season == season)) + + if not team: + return None + + return team + + @staticmethod + def select_season(season=None): + if not season: + season = Current.get().season + return Team.select().where(Team.season == season) + + @staticmethod + def get_season(abbrev, season=None): + if not season: + season = Current.get().season + return Team.get_or_none(Team.season == season, Team.abbrev == abbrev.upper()) + + def team_hash(self): + hash_string = f'{self.sname[-1]}{self.gmid / 6950123:.0f}{self.sname[-2]}{self.gmid / 42069123:.0f}' + logging.info(f'string: {hash_string}') + return hash_string + + +db.create_tables([Team]) + + +class PackType(BaseModel): + name = CharField() + card_count = IntegerField() + description = CharField() + cost = IntegerField() + available = BooleanField(default=True) + + +db.create_tables([PackType]) + + +class Pack(BaseModel): + team = ForeignKeyField(Team) + pack_type = ForeignKeyField(PackType) + pack_team = ForeignKeyField(Team, null=True) + pack_cardset = ForeignKeyField(Cardset, null=True) + open_time = DateTimeField(null=True) + + +db.create_tables([Pack]) + + +class Card(BaseModel): + player = ForeignKeyField(Player, null=True) + team = ForeignKeyField(Team, null=True) + pack = ForeignKeyField(Pack, null=True) + value = IntegerField(default=0) + + def __str__(self): + if self.player: + return f'{self.player} - {self.team.sname}' + else: + return f'Blank - {self.team.sname}' + + @staticmethod + def select_season(season): + return Card.select().join(Team).where(Card.team.season == season) + + +db.create_tables([Card]) + + +class Roster(BaseModel): + team = ForeignKeyField(Team) + name = CharField() + roster_num = IntegerField() + card_1 = ForeignKeyField(Card) + card_2 = ForeignKeyField(Card) + card_3 = ForeignKeyField(Card) + card_4 = ForeignKeyField(Card) + card_5 = ForeignKeyField(Card) + card_6 = ForeignKeyField(Card) + card_7 = ForeignKeyField(Card) + card_8 = ForeignKeyField(Card) + card_9 = ForeignKeyField(Card) + card_10 = ForeignKeyField(Card) + card_11 = ForeignKeyField(Card) + card_12 = ForeignKeyField(Card) + card_13 = ForeignKeyField(Card) + card_14 = ForeignKeyField(Card) + card_15 = ForeignKeyField(Card) + card_16 = ForeignKeyField(Card) + card_17 = ForeignKeyField(Card) + card_18 = ForeignKeyField(Card) + card_19 = ForeignKeyField(Card) + card_20 = ForeignKeyField(Card) + card_21 = ForeignKeyField(Card) + card_22 = ForeignKeyField(Card) + card_23 = ForeignKeyField(Card) + card_24 = ForeignKeyField(Card) + card_25 = ForeignKeyField(Card) + card_26 = ForeignKeyField(Card) + + def __str__(self): + return f'{self.team} Roster' + + # def get_cards(self, team): + # all_cards = Card.select().where(Card.roster == self) + # this_roster = [] + # return [this_roster.card1, this_roster.card2, this_roster.card3, this_roster.card4, this_roster.card5, + # this_roster.card6, this_roster.card7, this_roster.card8, this_roster.card9, this_roster.card10, + # this_roster.card11, this_roster.card12, this_roster.card13, this_roster.card14, this_roster.card15, + # this_roster.card16, this_roster.card17, this_roster.card18, this_roster.card19, this_roster.card20, + # this_roster.card21, this_roster.card22, this_roster.card23, this_roster.card24, this_roster.card25, + # this_roster.card26] + + +class BattingStat(BaseModel): + card = ForeignKeyField(Card) + team = ForeignKeyField(Team) + roster_num = IntegerField() + vs_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() + xch = IntegerField() + xhit = IntegerField() + error = IntegerField() + pb = IntegerField() + sbc = IntegerField() + csc = IntegerField() + week = IntegerField() + season = IntegerField() + created = DateTimeField() + game_id = IntegerField() + + +class PitchingStat(BaseModel): + card = ForeignKeyField(Card) + team = ForeignKeyField(Team) + roster_num = IntegerField() + vs_team = ForeignKeyField(Team) + ip = FloatField() + hit = IntegerField() + run = IntegerField() + erun = IntegerField() + so = IntegerField() + bb = IntegerField() + hbp = IntegerField() + wp = IntegerField() + balk = IntegerField() + hr = IntegerField() + ir = IntegerField() + irs = IntegerField() + gs = IntegerField() + win = IntegerField() + loss = IntegerField() + hold = IntegerField() + sv = IntegerField() + bsv = IntegerField() + week = IntegerField() + season = IntegerField() + created = DateTimeField() + game_id = IntegerField() + + +class Result(BaseModel): + away_team = ForeignKeyField(Team) + home_team = ForeignKeyField(Team) + away_score = IntegerField() + home_score = IntegerField() + away_team_value = IntegerField(null=True) + home_team_value = IntegerField(null=True) + away_team_ranking = IntegerField(null=True) + home_team_ranking = IntegerField(null=True) + scorecard = CharField() + week = IntegerField() + season = IntegerField() + ranked = BooleanField() + short_game = BooleanField() + game_type = CharField(null=True) + + @staticmethod + def select_season(season=None): + if not season: + season = Current.get().season + return Result.select().where(Result.season == season) + + +class Award(BaseModel): + name = CharField() + season = IntegerField() + timing = CharField(default="In-Season") + card = ForeignKeyField(Card, null=True) + team = ForeignKeyField(Team, null=True) + image = CharField(null=True) + + +class Paperdex(BaseModel): + team = ForeignKeyField(Team) + player = ForeignKeyField(Player) + created = DateTimeField(default=int(datetime.timestamp(datetime.now())*1000)) + + # def add_to_paperdex(self, team, cards: list): + # for x in players: + # if not isinstance(x, Card): + # raise TypeError(f'The Pokedex can only take a list of Player or Card objects') + # + # Paperdex.get_or_create(team=team, player=player) + + +class Reward(BaseModel): + name = CharField(null=True) + season = IntegerField() + week = IntegerField() + team = ForeignKeyField(Team) + created = DateTimeField() + + +class GameRewards(BaseModel): + name = CharField() + pack_type = ForeignKeyField(PackType, null=True) + player = ForeignKeyField(Player, null=True) + money = IntegerField(null=True) + + +class Notification(BaseModel): + created = DateTimeField() + title = CharField() + desc = CharField(null=True) + field_name = CharField() + message = CharField() + about = CharField() # f'{Topic}-{Object ID}' + ack = BooleanField(default=False) + + +class GauntletReward(BaseModel): + name = CharField() + gauntlet = ForeignKeyField(Event) + reward = ForeignKeyField(GameRewards) + win_num = IntegerField() + loss_max = IntegerField(default=1) + + +class GauntletRun(BaseModel): + team = ForeignKeyField(Team) + gauntlet = ForeignKeyField(Event) + wins = IntegerField(default=0) + losses = IntegerField(default=0) + gsheet = CharField(null=True) + created = DateTimeField(default=int(datetime.timestamp(datetime.now())*1000)) + ended = DateTimeField(default=0) + + +db.create_tables([ + Roster, BattingStat, PitchingStat, Result, Award, Paperdex, Reward, GameRewards, Notification, GauntletReward, + GauntletRun +]) + + +class BattingCard(BaseModel): + player = ForeignKeyField(Player) + steal_low = IntegerField() + steal_high = IntegerField() + steal_auto = BooleanField() + steal_jump = FloatField() + bunting = CharField() + hit_and_run = CharField() + running = IntegerField() + offense_col = IntegerField() + hand = CharField(default='R') + + +class BattingCardRatings(BaseModel): + battingcard = ForeignKeyField(BattingCard) + vs_hand = FloatField() + homerun = FloatField() + bp_homerun = FloatField() + triple = FloatField() + double_three = FloatField() + double_two = FloatField() + double_pull = FloatField() + single_two = FloatField() + single_one = FloatField() + single_center = FloatField() + bp_single = FloatField() + hbp = FloatField() + walk = FloatField() + strikeout = FloatField() + lineout = FloatField() + popout = FloatField() + flyout_a = FloatField() + flyout_bq = FloatField() + flyout_lf_b = FloatField() + flyout_rf_b = FloatField() + groundout_a = FloatField() + groundout_b = FloatField() + groundout_c = FloatField() + avg = FloatField(null=True) + obp = FloatField(null=True) + slg = FloatField(null=True) + + +class PitchingCard(BaseModel): + player = ForeignKeyField(Player) + balk = IntegerField() + wild_pitch = IntegerField(null=True) + hold = CharField() + starter_rating = IntegerField() + relief_rating = IntegerField() + closer_rating = IntegerField(null=True) + batting = CharField(null=True) + + +class PitchingCardRatings(BaseModel): + pitchingcard = ForeignKeyField(PitchingCard) + vs_hand = CharField() + homerun = FloatField() + bp_homerun = FloatField() + triple = FloatField() + double_three = FloatField() + double_two = FloatField() + double_cf = FloatField() + single_two = FloatField() + single_one = FloatField() + single_center = FloatField() + bp_single = FloatField() + hbp = FloatField() + walk = FloatField() + strikeout = FloatField() + fo_slap = FloatField() + fo_center = FloatField() + groundout_a = FloatField() + groundout_b = FloatField() + xcheck_p = FloatField() + xcheck_c = FloatField() + xcheck_1b = FloatField() + xcheck_2b = FloatField() + xcheck_3b = FloatField() + xcheck_ss = FloatField() + xcheck_lf = FloatField() + xcheck_cf = FloatField() + xcheck_rf = FloatField() + avg = FloatField(null=True) + obp = FloatField(null=True) + slg = FloatField(null=True) + + +class CardPosition(BaseModel): + player = ForeignKeyField(Player) + batting = ForeignKeyField(BattingCard, null=True) + pitching = ForeignKeyField(PitchingCard, null=True) + position = CharField() + innings = IntegerField() + range = IntegerField() + error = IntegerField() + arm = IntegerField(null=True) + pb = IntegerField(null=True) + overthrow = IntegerField(null=True) + + +db.create_tables([BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition]) + + +db.close() + +# scout_db = SqliteDatabase( +# 'storage/card_creation.db', +# pragmas={ +# 'journal_mode': 'wal', +# 'cache_size': -1 * 64000, +# 'synchronous': 0 +# } +# ) +# +# +# class BaseModelScout(Model): +# class Meta: +# database = scout_db +# +# +# class ScoutCardset(BaseModelScout): +# set_title = CharField() +# set_subtitle = CharField(null=True) +# +# +# class ScoutPlayer(BaseModelScout): +# sba_id = IntegerField(primary_key=True) +# name = CharField() +# fg_id = IntegerField() +# br_id = CharField() +# offense_col = IntegerField() +# hand = CharField(default='R') +# +# +# scout_db.create_tables([ScoutCardset, ScoutPlayer]) +# +# +# class BatterRatings(BaseModelScout): +# id = CharField(unique=True, primary_key=True) +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# vs_hand = FloatField() +# is_prep = BooleanField() +# homerun = FloatField() +# bp_homerun = FloatField() +# triple = FloatField() +# double_three = FloatField() +# double_two = FloatField() +# double_pull = FloatField() +# single_two = FloatField() +# single_one = FloatField() +# single_center = FloatField() +# bp_single = FloatField() +# hbp = FloatField() +# walk = FloatField() +# strikeout = FloatField() +# lineout = FloatField() +# popout = FloatField() +# flyout_a = FloatField() +# flyout_bq = FloatField() +# flyout_lf_b = FloatField() +# flyout_rf_b = FloatField() +# groundout_a = FloatField() +# groundout_b = FloatField() +# groundout_c = FloatField() +# avg = FloatField(null=True) +# obp = FloatField(null=True) +# slg = FloatField(null=True) +# +# +# class PitcherRatings(BaseModelScout): +# id = CharField(unique=True, primary_key=True) +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# vs_hand = CharField() +# is_prep = BooleanField() +# homerun = FloatField() +# bp_homerun = FloatField() +# triple = FloatField() +# double_three = FloatField() +# double_two = FloatField() +# double_cf = FloatField() +# single_two = FloatField() +# single_one = FloatField() +# single_center = FloatField() +# bp_single = FloatField() +# hbp = FloatField() +# walk = FloatField() +# strikeout = FloatField() +# fo_slap = FloatField() +# fo_center = FloatField() +# groundout_a = FloatField() +# groundout_b = FloatField() +# xcheck_p = FloatField() +# xcheck_c = FloatField() +# xcheck_1b = FloatField() +# xcheck_2b = FloatField() +# xcheck_3b = FloatField() +# xcheck_ss = FloatField() +# xcheck_lf = FloatField() +# xcheck_cf = FloatField() +# xcheck_rf = FloatField() +# avg = FloatField(null=True) +# obp = FloatField(null=True) +# slg = FloatField(null=True) +# +# +# # scout_db.create_tables([BatterRatings, PitcherRatings]) +# +# +# class CardColumns(BaseModelScout): +# id = CharField(unique=True, primary_key=True) +# player = ForeignKeyField(ScoutPlayer) +# hand = CharField() +# b_ratings = ForeignKeyField(BatterRatings, null=True) +# p_ratings = ForeignKeyField(PitcherRatings, null=True) +# one_dice = CharField() +# one_results = CharField() +# one_splits = CharField() +# two_dice = CharField() +# two_results = CharField() +# two_splits = CharField() +# three_dice = CharField() +# three_results = CharField() +# three_splits = CharField() +# +# +# class Position(BaseModelScout): +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# position = CharField() +# innings = IntegerField() +# range = IntegerField() +# error = IntegerField() +# arm = CharField(null=True) +# pb = IntegerField(null=True) +# overthrow = IntegerField(null=True) +# +# +# class BatterData(BaseModelScout): +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# stealing = CharField() +# st_low = IntegerField() +# st_high = IntegerField() +# st_auto = BooleanField() +# st_jump = FloatField() +# bunting = CharField(null=True) +# hit_and_run = CharField(null=True) +# running = CharField() +# +# +# class PitcherData(BaseModelScout): +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# balk = IntegerField(null=True) +# wild_pitch = IntegerField(null=True) +# hold = CharField() +# starter_rating = IntegerField() +# relief_rating = IntegerField() +# closer_rating = IntegerField(null=True) +# batting = CharField(null=True) +# +# +# scout_db.create_tables([CardColumns, Position, BatterData, PitcherData]) +# +# +# class CardOutput(BaseModelScout): +# name = CharField() +# hand = CharField() +# positions = CharField() +# stealing = CharField() +# bunting = CharField() +# hitandrun = CharField() +# running = CharField() +# +# +# scout_db.close() + diff --git a/app/dependencies.py b/app/dependencies.py new file mode 100644 index 0000000..8386e66 --- /dev/null +++ b/app/dependencies.py @@ -0,0 +1,26 @@ +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'] +) + + +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..fc7999a --- /dev/null +++ b/app/main.py @@ -0,0 +1,13 @@ +import datetime +import logging +import os + +from fastapi import FastAPI + +from.routers_v2 import current + +app = FastAPI( + responses={404: {'description': 'Not found'}} +) + +app.include_router(current.router) diff --git a/app/routers_v2/__init__.py b/app/routers_v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers_v2/current.py b/app/routers_v2/current.py new file mode 100644 index 0000000..f3d1dbf --- /dev/null +++ b/app/routers_v2/current.py @@ -0,0 +1,165 @@ +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, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/current', + tags=['current'] +) + + +class CurrentModel(pydantic.BaseModel): + season: int + week: int + gsheet_template: str + gsheet_version: str + + +@router.get('') +async def get_current(season: Optional[int] = None, csv: Optional[bool] = False): + if season: + current = Current.get_or_none(season=season) + else: + current = Current.latest() + + if csv: + current_list = [ + ['id', 'season', 'week'], + [current.id, current.season, current.week] + ] + return_val = DataFrame(current_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + else: + return_val = model_to_dict(current) + db.close() + return return_val + + +@router.get('/{current_id}') +async def get_one_current(current_id, csv: Optional[bool] = False): + try: + current = Current.get_by_id(current_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No current found with id {current_id}') + + if csv: + current_list = [ + ['id', 'season', 'week'], + [current.id, current.season, current.week] + ] + return_val = DataFrame(current_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + else: + return_val = model_to_dict(current) + db.close() + return return_val + + +@router.post('') +async def post_current(current: CurrentModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post current. This event has been logged.' + ) + + dupe_curr = Current.get_or_none(Current.season == current.season) + if dupe_curr: + db.close() + raise HTTPException(status_code=400, detail=f'There is already a current for season {current.season}') + + this_curr = Current( + season=current.season, + week=current.week, + gsheet_template=current.gsheet_template, + gsheet_version=current.gsheet_version + ) + + saved = this_curr.save() + if saved == 1: + return_val = model_to_dict(this_curr) + db.close() + return return_val + else: + raise HTTPException(status_code=418, detail='Well slap my ass and call me a teapot; I could not save that team') + + +@router.patch('/{current_id}') +async def patch_current( + current_id: int, season: Optional[int] = None, week: Optional[int] = None, + gsheet_template: Optional[str] = None, gsheet_version: Optional[str] = None, + live_scoreboard: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch current. This event has been logged.' + ) + try: + current = Current.get_by_id(current_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No current found with id {current_id}') + + if season is not None: + current.season = season + if week is not None: + current.week = week + if gsheet_template is not None: + current.gsheet_template = gsheet_template + if gsheet_version is not None: + current.gsheet_version = gsheet_version + if live_scoreboard is not None: + current.live_scoreboard = live_scoreboard + + if current.save() == 1: + return_val = model_to_dict(current) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that current' + ) + + +@router.delete('/{current_id}') +async def delete_current(current_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete current. This event has been logged.' + ) + try: + this_curr = Current.get_by_id(current_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No current found with id {current_id}') + + count = this_curr.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Current {current_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Current {current_id} was not deleted') diff --git a/requirements.txt b/requirements.txt index ecccb5d..4ac1ae5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ peewee python-multipart pandas pygsheets +pybaseball diff --git a/venv/share/man/man1/ttx.1 b/venv/share/man/man1/ttx.1 new file mode 100644 index 0000000..bba23b5 --- /dev/null +++ b/venv/share/man/man1/ttx.1 @@ -0,0 +1,225 @@ +.Dd May 18, 2004 +.\" ttx is not specific to any OS, but contrary to what groff_mdoc(7) +.\" seems to imply, entirely omitting the .Os macro causes 'BSD' to +.\" be used, so I give a zero-width space as its argument. +.Os \& +.\" The "FontTools Manual" argument apparently has no effect in +.\" groff 1.18.1. I think it is a bug in the -mdoc groff package. +.Dt TTX 1 "FontTools Manual" +.Sh NAME +.Nm ttx +.Nd tool for manipulating TrueType and OpenType fonts +.Sh SYNOPSIS +.Nm +.Bk +.Op Ar option ... +.Ek +.Bk +.Ar file ... +.Ek +.Sh DESCRIPTION +.Nm +is a tool for manipulating TrueType and OpenType fonts. It can convert +TrueType and OpenType fonts to and from an +.Tn XML Ns -based format called +.Tn TTX . +.Tn TTX +files have a +.Ql .ttx +extension. +.Pp +For each +.Ar file +argument it is given, +.Nm +detects whether it is a +.Ql .ttf , +.Ql .otf +or +.Ql .ttx +file and acts accordingly: if it is a +.Ql .ttf +or +.Ql .otf +file, it generates a +.Ql .ttx +file; if it is a +.Ql .ttx +file, it generates a +.Ql .ttf +or +.Ql .otf +file. +.Pp +By default, every output file is created in the same directory as the +corresponding input file and with the same name except for the +extension, which is substituted appropriately. +.Nm +never overwrites existing files; if necessary, it appends a suffix to +the output file name before the extension, as in +.Pa Arial#1.ttf . +.Ss "General options" +.Bl -tag -width ".Fl t Ar table" +.It Fl h +Display usage information. +.It Fl d Ar dir +Write the output files to directory +.Ar dir +instead of writing every output file to the same directory as the +corresponding input file. +.It Fl o Ar file +Write the output to +.Ar file +instead of writing it to the same directory as the +corresponding input file. +.It Fl v +Be verbose. Write more messages to the standard output describing what +is being done. +.It Fl a +Allow virtual glyphs ID's on compile or decompile. +.El +.Ss "Dump options" +The following options control the process of dumping font files +(TrueType or OpenType) to +.Tn TTX +files. +.Bl -tag -width ".Fl t Ar table" +.It Fl l +List table information. Instead of dumping the font to a +.Tn TTX +file, display minimal information about each table. +.It Fl t Ar table +Dump table +.Ar table . +This option may be given multiple times to dump several tables at +once. When not specified, all tables are dumped. +.It Fl x Ar table +Exclude table +.Ar table +from the list of tables to dump. This option may be given multiple +times to exclude several tables from the dump. The +.Fl t +and +.Fl x +options are mutually exclusive. +.It Fl s +Split tables. Dump each table to a separate +.Tn TTX +file and write (under the name that would have been used for the output +file if the +.Fl s +option had not been given) one small +.Tn TTX +file containing references to the individual table dump files. This +file can be used as input to +.Nm +as long as the referenced files can be found in the same directory. +.It Fl i +.\" XXX: I suppose OpenType programs (exist and) are also affected. +Don't disassemble TrueType instructions. When this option is specified, +all TrueType programs (glyph programs, the font program and the +pre-program) are written to the +.Tn TTX +file as hexadecimal data instead of +assembly. This saves some time and results in smaller +.Tn TTX +files. +.It Fl y Ar n +When decompiling a TrueType Collection (TTC) file, +decompile font number +.Ar n , +starting from 0. +.El +.Ss "Compilation options" +The following options control the process of compiling +.Tn TTX +files into font files (TrueType or OpenType): +.Bl -tag -width ".Fl t Ar table" +.It Fl m Ar fontfile +Merge the input +.Tn TTX +file +.Ar file +with +.Ar fontfile . +No more than one +.Ar file +argument can be specified when this option is used. +.It Fl b +Don't recalculate glyph bounding boxes. Use the values in the +.Tn TTX +file as is. +.El +.Sh "THE TTX FILE FORMAT" +You can find some information about the +.Tn TTX +file format in +.Pa documentation.html . +In particular, you will find in that file the list of tables understood by +.Nm +and the relations between TrueType GlyphIDs and the glyph names used in +.Tn TTX +files. +.Sh EXAMPLES +In the following examples, all files are read from and written to the +current directory. Additionally, the name given for the output file +assumes in every case that it did not exist before +.Nm +was invoked. +.Pp +Dump the TrueType font contained in +.Pa FreeSans.ttf +to +.Pa FreeSans.ttx : +.Pp +.Dl ttx FreeSans.ttf +.Pp +Compile +.Pa MyFont.ttx +into a TrueType or OpenType font file: +.Pp +.Dl ttx MyFont.ttx +.Pp +List the tables in +.Pa FreeSans.ttf +along with some information: +.Pp +.Dl ttx -l FreeSans.ttf +.Pp +Dump the +.Sq cmap +table from +.Pa FreeSans.ttf +to +.Pa FreeSans.ttx : +.Pp +.Dl ttx -t cmap FreeSans.ttf +.Sh NOTES +On MS\-Windows and MacOS, +.Nm +is available as a graphical application to which files can be dropped. +.Sh SEE ALSO +.Pa documentation.html +.Pp +.Xr fontforge 1 , +.Xr ftinfo 1 , +.Xr gfontview 1 , +.Xr xmbdfed 1 , +.Xr Font::TTF 3pm +.Sh AUTHORS +.Nm +was written by +.An -nosplit +.An "Just van Rossum" Aq just@letterror.com . +.Pp +This manual page was written by +.An "Florent Rougon" Aq f.rougon@free.fr +for the Debian GNU/Linux system based on the existing FontTools +documentation. It may be freely used, modified and distributed without +restrictions. +.\" For Emacs: +.\" Local Variables: +.\" fill-column: 72 +.\" sentence-end: "[.?!][]\"')}]*\\($\\| $\\| \\| \\)[ \n]*" +.\" sentence-end-double-space: t +.\" End: \ No newline at end of file From 0872362869d0dd0545c1d8fbddb2e41430bbafbb Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 13 Sep 2023 15:12:44 -0500 Subject: [PATCH 02/40] Added teams & rarity --- app/dependencies.py | 4 + app/main.py | 4 +- app/routers_v2/rarity.py | 187 ++++++++++++ app/routers_v2/teams.py | 613 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 807 insertions(+), 1 deletion(-) create mode 100644 app/routers_v2/rarity.py create mode 100644 app/routers_v2/teams.py diff --git a/app/dependencies.py b/app/dependencies.py index 8386e66..eec6f22 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -24,3 +24,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") def valid_token(token): return token == os.environ.get('API_TOKEN') + + +def int_timestamp(datetime_obj: datetime) -> int: + return int(datetime.timestamp(datetime_obj) * 1000) diff --git a/app/main.py b/app/main.py index fc7999a..8261785 100644 --- a/app/main.py +++ b/app/main.py @@ -4,10 +4,12 @@ import os from fastapi import FastAPI -from.routers_v2 import current +from.routers_v2 import current, teams, rarity app = FastAPI( responses={404: {'description': 'Not found'}} ) app.include_router(current.router) +app.include_router(teams.router) +app.include_router(rarity.router) diff --git a/app/routers_v2/rarity.py b/app/routers_v2/rarity.py new file mode 100644 index 0000000..6078e29 --- /dev/null +++ b/app/routers_v2/rarity.py @@ -0,0 +1,187 @@ +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Rarity, model_to_dict, 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/v2/rarities', + tags=['rarities'] +) + + +class RarityModel(pydantic.BaseModel): + value: int + name: str + color: str + + +@router.get('') +async def get_rarities(value: Optional[int] = None, name: Optional[str] = None, min_value: Optional[int] = None, + max_value: Optional[int] = None, csv: Optional[bool] = None): + all_rarities = Rarity.select() + + if all_rarities.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'There are no rarities to filter') + + if value is not None: + all_rarities = all_rarities.where(Rarity.value == value) + if name is not None: + all_rarities = all_rarities.where(fn.Lower(Rarity.name) == name.lower()) + if min_value is not None: + all_rarities = all_rarities.where(Rarity.value >= min_value) + if max_value is not None: + all_rarities = all_rarities.where(Rarity.value <= max_value) + + if all_rarities.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'No rarities found') + + if csv: + data_list = [['id', 'value', 'name']] + for line in all_rarities: + data_list.append( + [ + line.id, line.value, line.name + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_rarities.count(), 'rarities': []} + for x in all_rarities: + return_val['rarities'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{rarity_id}') +async def get_one_rarity(rarity_id, csv: Optional[bool] = False): + try: + this_rarity = Rarity.get_by_id(rarity_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}') + + if csv: + data_list = [['id', 'value', 'name']] + for line in this_rarity: + data_list.append( + [ + line.id, line.value, line.name + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + else: + return_val = model_to_dict(this_rarity) + db.close() + return return_val + + +@router.post('') +async def post_rarity(rarity: RarityModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post rarities. This event has been logged.' + ) + + dupe_team = Rarity.get_or_none(Rarity.name) + if dupe_team: + db.close() + raise HTTPException(status_code=400, detail=f'There is already a rarity using {rarity.name}') + + this_rarity = Rarity( + value=rarity.value, + name=rarity.name, + color=rarity.color + ) + + saved = this_rarity.save() + if saved == 1: + return_val = model_to_dict(this_rarity) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.patch('/{rarity_id}') +async def patch_rarity( + rarity_id, value: Optional[int] = None, name: Optional[str] = None, color: Optional[str] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch rarities. This event has been logged.' + ) + try: + this_rarity = Rarity.get_by_id(rarity_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}') + + if value is not None: + this_rarity.value = value + if name is not None: + this_rarity.name = name + if color is not None: + this_rarity.color = color + + if this_rarity.save() == 1: + return_val = model_to_dict(this_rarity) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.delete('/{rarity_id}') +async def v1_rarities_delete(rarity_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete rarities. This event has been logged.' + ) + try: + this_rarity = Rarity.get_by_id(rarity_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}') + + count = this_rarity.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Rarity {rarity_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Rarity {rarity_id} was not deleted') diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py new file mode 100644 index 0000000..2a0a930 --- /dev/null +++ b/app/routers_v2/teams.py @@ -0,0 +1,613 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import List, Optional, Literal +import copy +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Team, model_to_dict, chunked, fn, Pack, Card, Player, Paperdex, Notification, PackType, \ + Rarity, Current +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/teams', + tags=['teams'] +) + + +class TeamModel(pydantic.BaseModel): + abbrev: str + sname: str + lname: str + gmid: int + gmname: str + wallet: int = 0 + gsheet: str + team_value: int = 0 + collection_value: int = 0 + logo: Optional[str] = None + color: Optional[str] = None + season: int + ps_shiny: Optional[int] = 0 + ranking: Optional[int] = 1000 + has_guide: Optional[bool] = False + is_ai: Optional[bool] = False + + +@router.get('') +async def get_teams( + season: Optional[int] = None, gm_id: Optional[int] = None, abbrev: Optional[str] = None, + tv_min: Optional[int] = None, tv_max: Optional[int] = None, cv_min: Optional[int] = None, + cv_max: Optional[int] = None, ps_shiny_min: Optional[int] = None, ps_shiny_max: Optional[int] = None, + ranking_min: Optional[int] = None, ranking_max: Optional[int] = None, has_guide: Optional[bool] = None, + sname: Optional[str] = None, lname: Optional[str] = None, is_ai: Optional[bool] = None, + event_id: Optional[int] = None, limit: Optional[int] = None, csv: Optional[bool] = False): + """ + Param: season: int + Param: team_abbrev: string + Param: owner_id: int + """ + if season: + all_teams = Team.select_season(season) + else: + all_teams = Team.select() + + # if all_teams.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'There are no teams to filter') + + if gm_id is not None: + all_teams = all_teams.where(Team.gmid == gm_id) + + if abbrev is not None: + all_teams = all_teams.where(fn.Lower(Team.abbrev) == abbrev.lower()) + + if sname is not None: + all_teams = all_teams.where(fn.Lower(Team.sname) == sname.lower()) + + if lname is not None: + all_teams = all_teams.where(fn.Lower(Team.lname) == lname.lower()) + + if tv_min is not None: + all_teams = all_teams.where(Team.team_value >= tv_min) + + if tv_max is not None: + all_teams = all_teams.where(Team.team_value <= tv_max) + + if cv_min is not None: + all_teams = all_teams.where(Team.collection_value >= cv_min) + + if cv_max is not None: + all_teams = all_teams.where(Team.collection_value <= cv_max) + + if ps_shiny_min is not None: + all_teams = all_teams.where(Team.career >= ps_shiny_min) + + if ps_shiny_max is not None: + all_teams = all_teams.where(Team.career <= ps_shiny_max) + + if ranking_min is not None: + all_teams = all_teams.where(Team.ranking >= ranking_min) + + if ranking_max is not None: + all_teams = all_teams.where(Team.ranking <= ranking_max) + + if ranking_max is not None: + all_teams = all_teams.where(Team.ranking <= ranking_max) + + if has_guide is not None: + if not has_guide: + all_teams = all_teams.where(Team.has_guide == 0) + else: + all_teams = all_teams.where(Team.has_guide == 1) + + if is_ai is not None: + all_teams = all_teams.where(Team.is_ai) + + if event_id is not None: + all_teams = all_teams.where(Team.event_id == event_id) + + if limit is not None: + all_teams = all_teams.limit(limit) + + # if all_teams.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No teams found') + + if csv: + data_list = [[ + 'id', 'abbrev', 'sname', 'lname', 'gmid', 'gmname', 'wallet', 'gsheet', 'team_value', + 'collection_value', 'logo', 'color', 'season', 'ranking' + ]] + for line in all_teams: + data_list.append( + [ + line.id, line.abbrev, line.sname, line.lname, line.gmid, line.gmname, line.wallet, line.gsheet, + line.team_value, line.collection_value, line.logo, f'\'{line.color}', line.season, line.ranking + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_teams = {'count': all_teams.count(), 'teams': []} + for x in all_teams: + return_teams['teams'].append(model_to_dict(x)) + + db.close() + return return_teams + + +@router.get('/{team_id}') +async def get_one_team(team_id, csv: Optional[bool] = False): + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + + if csv: + team_packs = Pack.select().where((Pack.team == this_team) & (Pack.open_time.is_null(True))) + data_list = [ + ['id', 'abbrev', 'sname', 'lname', 'gmid', 'gmname', 'wallet', 'ranking', 'gsheet', 'sealed_packs', + 'collection_value', 'logo', 'color', 'season'], + [this_team.id, this_team.abbrev, this_team.sname, this_team.lname, this_team.gmid, this_team.gmname, + this_team.wallet, this_team.ranking, this_team.gsheet, team_packs.count(), this_team.collection_value, + this_team.logo, this_team.color, this_team.season] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + else: + return_val = model_to_dict(this_team) + db.close() + return return_val + + +@router.get('/{team_id}/buy/players') +async def team_buy_players(team_id: int, ids: str, ts: str): + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + + if ts != this_team.team_hash(): + logging.warning(f'Bad Team Secret: {ts} ({this_team.team_hash()})') + db.close() + raise HTTPException( + status_code=401, + detail=f'You are not authorized to buy {this_team.abbrev} cards. This event has been logged.' + ) + + last_card = Card.select(Card.id).order_by(-Card.id).limit(1) + lc_id = last_card[0].id + + all_ids = ids.split(',') + conf_message = '' + total_cost = 0 + for player_id in all_ids: + if player_id != '': + try: + this_player = Player.get_by_id(player_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No player found with id {player_id} /// ' + f'{conf_message} purchased') + + # check wallet balance + if this_team.wallet < this_player.cost: + logging.info(f'{this_player} was not purchased. {this_team.lname} only has {this_team.wallet}₼, but ' + f'{this_player} costs {this_player.cost}₼.') + db.close() + raise HTTPException( + 200, + detail=f'{this_player} was not purchased. {this_team.lname} only has {this_team.wallet}₼, but ' + f'{this_player} costs {this_player.cost}₼. /// {conf_message} purchased' + ) + + # Create player card and update cost + buy_price = this_player.cost + total_cost += buy_price + this_card = Card( + player_id=this_player.player_id, + team_id=this_team.id, + value=buy_price + ) + Paperdex.get_or_create(team_id=team_id, player_id=this_player.player_id) + this_card.save() + this_player.change_on_buy() + + # Deduct card cost from team + logging.info(f'{this_team.abbrev} starting wallet: {this_team.wallet}') + this_team.wallet -= buy_price + this_team.save() + logging.info(f'{this_team.abbrev} ending wallet: {this_team.wallet}') + + # Post a notification + if this_player.rarity.value >= 2: + new_notif = Notification( + created=int_timestamp(datetime.now()), + title=f'Price Change', + desc='Modified by buying and selling', + field_name=f'{this_player.description}', + message=f'From {buy_price}₼ 📈 to **{this_player.cost}**₼', + about=f'Player-{this_player.player_id}' + ) + new_notif.save() + + conf_message += f'{buy_price}₼ for {this_player.rarity.name} {this_player.p_name} ' \ + f'({this_player.cardset.name}), ' + + # sheets.post_new_cards(SHEETS_AUTH, lc_id) + + raise HTTPException(status_code=200, detail=f'{conf_message} purchased. /// Total Cost: {total_cost}₼ /// ' + f'Final Wallet: {this_team.wallet}') + + +@router.get('/{team_id}/buy/pack/{packtype_id}') +async def team_buy_packs(team_id: int, packtype_id: int, ts: str, quantity: Optional[int] = 1): + try: + this_packtype = PackType.get_by_id(packtype_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No pack type found with id {packtype_id}') + + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + + if ts != this_team.team_hash(): + logging.warning(f'Bad Team Secret: {ts} ({this_team.team_hash()})') + db.close() + logging.warning(f'team: {this_team} / pack_type: {this_packtype} / secret: {ts} / ' + f'actual: {this_team.team_hash()}') + raise HTTPException( + status_code=401, + detail=f'You are not authorized to buy {this_team.abbrev} packs. This event has been logged.' + ) + + # check wallet balance + total_cost = this_packtype.cost * quantity + if this_team.wallet < total_cost: + db.close() + raise HTTPException( + 200, + detail=f'{this_packtype} was not purchased. {this_team.lname} only has {this_team.wallet} bucks, but ' + f'{this_packtype} costs {this_packtype.cost}.' + ) + + all_packs = [] + for i in range(quantity): + all_packs.append(Pack(team_id=this_team.id, pack_type_id=this_packtype.id)) + + # Deduct card cost from team + logging.info(f'{this_team.abbrev} starting wallet: {this_team.wallet}') + this_team.wallet -= total_cost + this_team.save() + logging.info(f'{this_team.abbrev} ending wallet: {this_team.wallet}') + + with db.atomic(): + Pack.bulk_create(all_packs, batch_size=15) + db.close() + + raise HTTPException( + status_code=200, + detail=f'Quantity {quantity} {this_packtype.name} pack{"s" if quantity > 1 else ""} have been purchased by ' + f'{this_team.lname} for {total_cost} bucks. You may close this window.' + ) + + +@router.get('/{team_id}/sell/cards') +async def team_sell_cards(team_id: int, ids: str, ts: str): + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + + if ts != this_team.team_hash(): + logging.warning(f'Bad Team Secret: {ts} ({this_team.team_hash()})') + db.close() + raise HTTPException( + status_code=401, + detail=f'You are not authorized to sell {this_team.abbrev} cards. This event has been logged.' + ) + + all_ids = ids.split(',') + del_ids = [] + conf_message = '' + total_cost = 0 + for card_id in all_ids: + if card_id != '': + try: + this_card = Card.get_by_id(card_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No card found with id {card_id}') + + del_ids.append(card_id) + this_player = this_card.player + + if this_card.team != this_team: + raise HTTPException(status_code=401, + detail=f'Card id {card_id} ({this_player.p_name}) belongs to ' + f'{this_card.team.abbrev} and cannot be sold. /// {conf_message} sold') + + orig_price = this_player.cost + sell_price = round(this_player.cost * .5) + total_cost += sell_price + + # credit selling team's wallet + if this_team.wallet is None: + this_team.wallet = sell_price + else: + this_team.wallet += sell_price + this_team.save() + + # decrease price of player + this_player.change_on_sell() + this_card.delete_instance() + + # post a notification + if this_player.rarity.value >= 2: + new_notif = Notification( + created=int_timestamp(datetime.now()), + title=f'Price Change', + desc='Modified by buying and selling', + field_name=f'{this_player.description}', + message=f'From {orig_price}₼ 📉 to **{this_player.cost}**₼', + about=f'Player-{this_player.id}' + ) + new_notif.save() + + conf_message += f'{sell_price}₼ for {this_player.rarity.name} {this_player.p_name} ' \ + f'({this_player.cardset.name}), ' + + # sheets.post_deletion(SHEETS_AUTH, del_ids) + raise HTTPException(status_code=200, detail=f'{conf_message} sold. /// Total Earned: {total_cost}₼ /// ' + f'Final Wallet: {this_team.wallet}') + + +@router.get('/{team_id}/cards') +async def get_team_cards(team_id, csv: Optional[bool] = True): + """ + CSV output specifically targeting team roster sheet + + Parameters + ---------- + team_id + csv + """ + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + + if not csv: + db.close() + raise HTTPException( + status_code=400, + detail='The /teams/{team_id}/cards endpoint only supports csv output.' + ) + + all_cards = (Card + .select() + .join(Player) + .join(Rarity) + .where(Card.team == this_team) + .order_by(-Card.player.rarity.value, Card.player.p_name) + ) + if all_cards.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'No cards found') + + data_list = [[ + 'cardset', 'player', 'rarity', 'image', 'image2', 'pos_1', 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', + 'pos_7', 'pos_8', 'cost', 'mlbclub', 'franchise', 'set_num', 'bbref_id', 'player_id', 'card_id' + ]] + for line in all_cards: + data_list.append( + [ + line.player.cardset, line.player.p_name, line.player.rarity, line.player.image, line.player.image2, + line.player.pos_1, line.player.pos_2, line.player.pos_3, line.player.pos_4, line.player.pos_5, + line.player.pos_6, line.player.pos_7, line.player.pos_8, line.player.cost, line.player.mlbclub, + line.player.franchise, line.player.set_num, line.player.bbref_id, line.player.player_id, line.id + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + +@router.post('') +async def post_team(team: TeamModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post teams. This event has been logged.' + ) + + dupe_team = Team.get_or_none(Team.season == team.season, Team.abbrev == team.abbrev) + if dupe_team: + db.close() + raise HTTPException(status_code=400, detail=f'There is already a season {team.season} team using {team.abbrev}') + + this_team = Team( + abbrev=team.abbrev, + sname=team.sname, + lname=team.lname, + gmid=team.gmid, + gmname=team.gmname, + wallet=team.wallet, + gsheet=team.gsheet, + team_value=team.team_value, + collection_value=team.collection_value, + logo=team.logo, + color=team.color, + ranking=team.ranking, + season=team.season, + career=team.ps_shiny, + has_guide=team.has_guide, + is_ai=team.is_ai + ) + + saved = this_team.save() + if saved == 1: + return_team = model_to_dict(this_team) + db.close() + return return_team + else: + raise HTTPException(status_code=418, detail='Well slap my ass and call me a teapot; I could not save that team') + + +@router.post('/new-season/{new_season}') +async def team_season_update(new_season: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post teams. This event has been logged.' + ) + + r_query = Team.update(ranking=1000, season=new_season, wallet=Team.wallet + 250).execute() + current = Current.latest() + current.season = new_season + current.save() + db.close() + + return {'detail': f'Team rankings, season, and wallet updated for season {new_season}'} + + +@router.post('/{team_id}/money/{delta}') +async def team_update_money(team_id: int, delta: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to adjust wallets. This event has been logged.' + ) + + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + + this_team.wallet += delta + + if this_team.save() == 1: + return_team = model_to_dict(this_team) + db.close() + return return_team + else: + raise HTTPException(status_code=418, detail='Well slap my ass and call me a teapot; I could not save that team') + + +@router.patch('/{team_id}') +async def patch_team( + team_id, sname: Optional[str] = None, lname: Optional[str] = None, gmid: Optional[int] = None, + gmname: Optional[str] = None, gsheet: Optional[str] = None, team_value: Optional[int] = None, + collection_value: Optional[int] = None, logo: Optional[str] = None, color: Optional[str] = None, + season: Optional[int] = None, ps_shiny: Optional[int] = None, wallet_delta: Optional[int] = None, + has_guide: Optional[bool] = None, is_ai: Optional[bool] = None, ranking: Optional[int] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete teams. This event has been logged.' + ) + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + + if sname is not None: + this_team.sname = sname + if lname is not None: + this_team.lname = lname + if gmid is not None: + this_team.gmid = gmid + if gmname is not None: + this_team.gmname = gmname + if gsheet is not None: + this_team.gsheet = gsheet + if team_value is not None: + this_team.team_value = team_value + if collection_value is not None: + this_team.collection_value = collection_value + if logo is not None: + this_team.logo = logo + if color is not None: + this_team.color = color + if season is not None: + this_team.season = season + if ps_shiny is not None: + this_team.career = ps_shiny + if ranking is not None: + this_team.ranking = ranking + if wallet_delta is not None: + this_team.wallet += wallet_delta + if has_guide is not None: + if has_guide: + this_team.has_guide = 1 + else: + this_team.has_guide = 0 + if is_ai is not None: + if is_ai: + this_team.is_ai = 1 + else: + this_team.is_ai = 0 + + if this_team.save() == 1: + return_team = model_to_dict(this_team) + db.close() + return return_team + else: + raise HTTPException(status_code=418, detail='Well slap my ass and call me a teapot; I could not save that team') + + +@router.delete('/{team_id}') +async def delete_team(team_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete teams. This event has been logged.' + ) + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + + count = this_team.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Team {team_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Team {team_id} was not deleted') From 6325698a10e7f2c5d7828913228bd36951f657b0 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 13 Sep 2023 16:19:56 -0500 Subject: [PATCH 03/40] Added cardsets, packs, packtypes, players --- app/main.py | 6 +- app/routers_v2/cardsets.py | 204 ++++++++++++++ app/routers_v2/packs.py | 259 +++++++++++++++++ app/routers_v2/packtypes.py | 194 +++++++++++++ app/routers_v2/players.py | 540 ++++++++++++++++++++++++++++++++++++ 5 files changed, 1202 insertions(+), 1 deletion(-) create mode 100644 app/routers_v2/cardsets.py create mode 100644 app/routers_v2/packs.py create mode 100644 app/routers_v2/packtypes.py create mode 100644 app/routers_v2/players.py diff --git a/app/main.py b/app/main.py index 8261785..9c43bf2 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,7 @@ import os from fastapi import FastAPI -from.routers_v2 import current, teams, rarity +from.routers_v2 import current, teams, rarity, cardsets, players, packtypes, packs app = FastAPI( responses={404: {'description': 'Not found'}} @@ -13,3 +13,7 @@ app = FastAPI( app.include_router(current.router) app.include_router(teams.router) app.include_router(rarity.router) +app.include_router(cardsets.router) +app.include_router(players.router) +app.include_router(packtypes.router) +app.include_router(packs.router) diff --git a/app/routers_v2/cardsets.py b/app/routers_v2/cardsets.py new file mode 100644 index 0000000..b9806d4 --- /dev/null +++ b/app/routers_v2/cardsets.py @@ -0,0 +1,204 @@ +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Cardset, model_to_dict, fn, Event +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/cardsets', + tags=['cardsets'] +) + + +class CardsetModel(pydantic.BaseModel): + name: str + description: str + event_id: Optional[int] = None + in_packs: Optional[bool] = True + total_cards: int = 0 + for_purchase: Optional[bool] = True + ranked_legal: Optional[bool] = True + + +@router.get('') +async def get_cardsets( + name: Optional[str] = None, in_desc: Optional[str] = None, event_id: Optional[int] = None, + in_packs: Optional[bool] = None, ranked_legal: Optional[bool] = None, csv: Optional[bool] = None): + all_cardsets = Cardset.select() + + if all_cardsets.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'There are no cardsets to filter') + + if name is not None: + all_cardsets = all_cardsets.where(fn.Lower(Cardset.name) == name.lower()) + if in_desc is not None: + all_cardsets = all_cardsets.where(fn.Lower(Cardset.description).contains(in_desc.lower())) + if event_id is not None: + try: + this_event = Event.get_by_id(event_id) + all_cardsets = all_cardsets.where(Cardset.event == this_event) + except Exception as e: + logging.error(f'Failed to find event {event_id}: {e}') + raise HTTPException(status_code=404, detail=f'Event id {event_id} not found') + if in_packs is not None: + all_cardsets = all_cardsets.where(Cardset.in_packs == in_packs) + if ranked_legal is not None: + all_cardsets = all_cardsets.where(Cardset.ranked_legal == ranked_legal) + + if all_cardsets.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'No cardsets found') + + if csv: + data_list = [[ + 'id', 'name', 'description', 'event_id', 'in_packs', 'for_purchase', 'total_cards', 'ranked_legal' + ]] + for line in all_cardsets: + data_list.append( + [ + line.id, line.name, line.description, line.event.id if line.event else '', line.in_packs, + line.for_purchase, line.total_cards, line.ranked_legal + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_cardsets.count(), 'cardsets': []} + for x in all_cardsets: + return_val['cardsets'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{cardset_id}') +async def get_one_cardset(cardset_id, csv: Optional[bool] = False): + try: + this_cardset = Cardset.get_by_id(cardset_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}') + + if csv: + data_list = [ + ['id', 'name', 'description'], + [this_cardset.id, this_cardset.name, this_cardset.description] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + else: + return_val = model_to_dict(this_cardset) + db.close() + return return_val + + +@router.post('') +async def post_cardsets(cardset: CardsetModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post cardsets. This event has been logged.' + ) + + dupe_set = Cardset.get_or_none(Cardset.name == cardset.name) + if dupe_set: + db.close() + raise HTTPException(status_code=400, detail=f'There is already a cardset using {cardset.name}') + + this_cardset = Cardset(**cardset.__dict__) + + saved = this_cardset.save() + if saved == 1: + return_val = model_to_dict(this_cardset) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that cardset' + ) + + +@router.patch('/{cardset_id}') +async def patch_cardsets( + cardset_id, name: Optional[str] = None, description: Optional[str] = None, in_packs: Optional[bool] = None, + for_purchase: Optional[bool] = None, total_cards: Optional[int] = None, ranked_legal: Optional[bool] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch cardsets. This event has been logged.' + ) + try: + this_cardset = Cardset.get_by_id(cardset_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}') + + if name is not None: + this_cardset.name = name + if description is not None: + this_cardset.description = description + if in_packs is not None: + this_cardset.in_packs = in_packs + if for_purchase is not None: + this_cardset.for_purchase = for_purchase + if total_cards is not None: + this_cardset.total_cards = total_cards + if ranked_legal is not None: + this_cardset.ranked_legal = ranked_legal + + if this_cardset.save() == 1: + return_val = model_to_dict(this_cardset) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.delete('/{cardset_id}') +async def delete_cardsets(cardset_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete cardsets. This event has been logged.' + ) + try: + this_cardset = Cardset.get_by_id(cardset_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}') + + count = this_cardset.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Cardset {cardset_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Cardset {cardset_id} was not deleted') + + diff --git a/app/routers_v2/packs.py b/app/routers_v2/packs.py new file mode 100644 index 0000000..38a01c0 --- /dev/null +++ b/app/routers_v2/packs.py @@ -0,0 +1,259 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional, List +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Cardset, model_to_dict, fn, Pack, Team, PackType +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/packs', + tags=['packs'] +) + + +class PackPydantic(pydantic.BaseModel): + team_id: int + pack_type_id: int + pack_team_id: Optional[int] = None + pack_cardset_id: Optional[int] = None + open_time: Optional[str] = None + + +class PackModel(pydantic.BaseModel): + packs: List[PackPydantic] + + +@router.get('') +async def get_packs( + team_id: Optional[int] = None, pack_type_id: Optional[int] = None, opened: Optional[bool] = None, + limit: Optional[int] = None, new_to_old: Optional[bool] = None, pack_team_id: Optional[int] = None, + pack_cardset_id: Optional[int] = None, exact_match: Optional[bool] = False, csv: Optional[bool] = None): + all_packs = Pack.select() + + if all_packs.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'There are no packs to filter') + + if team_id is not None: + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + all_packs = all_packs.where(Pack.team == this_team) + if pack_type_id is not None: + try: + this_pack_type = PackType.get_by_id(pack_type_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No pack type found with id {pack_type_id}') + all_packs = all_packs.where(Pack.pack_type == this_pack_type) + + if pack_team_id is not None: + try: + this_pack_team = Team.get_by_id(pack_team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {pack_team_id}') + all_packs = all_packs.where(Pack.pack_team == this_pack_team) + elif exact_match: + all_packs = all_packs.where(Pack.pack_team == None) + + if pack_cardset_id is not None: + try: + this_pack_cardset = Cardset.get_by_id(pack_cardset_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No cardset found with id {pack_cardset_id}') + all_packs = all_packs.where(Pack.pack_cardset == this_pack_cardset) + elif exact_match: + all_packs = all_packs.where(Pack.pack_cardset == None) + + if opened is not None: + all_packs = all_packs.where(Pack.open_time.is_null(not opened)) + if limit is not None: + all_packs = all_packs.limit(limit) + if new_to_old is not None: + all_packs = all_packs.order_by(-Pack.id) + + # if all_packs.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No packs found') + + if csv: + data_list = [['id', 'team', 'pack_type', 'open_time']] + for line in all_packs: + data_list.append( + [ + line.id, line.team.abbrev, line.pack_type.name, + datetime.fromtimestamp(line.open_time) if line.open_time else None + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_packs.count(), 'packs': []} + for x in all_packs: + return_val['packs'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{pack_id}') +async def get_one_pack(pack_id, csv: Optional[bool] = False): + try: + this_pack = Pack.get_by_id(pack_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}') + + if csv: + data_list = [ + ['id', 'team', 'pack_type', 'open_time'], + [this_pack.id, this_pack.team.abbrev, this_pack.pack_type.name, + datetime.fromtimestamp(this_pack.open_time) if this_pack.open_time else None] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_pack) + db.close() + return return_val + + +@router.post('') +async def post_pack(packs: PackModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post packs. This event has been logged.' + ) + + new_packs = [] + for x in packs.packs: + this_player = Pack( + team_id=x.team_id, + pack_type_id=x.pack_type_id, + pack_team_id=x.pack_team_id, + pack_cardset_id=x.pack_cardset_id, + open_time=x.open_time if x.open_time != "" else None + ) + new_packs.append(this_player) + + with db.atomic(): + Pack.bulk_create(new_packs, batch_size=15) + db.close() + + raise HTTPException(status_code=200, detail=f'{len(new_packs)} packs have been added') + + +@router.post('/one') +async def post_one_pack(pack: PackPydantic, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post packs. This event has been logged.' + ) + + this_pack = Pack( + team_id=pack.team_id, + pack_type_id=pack.pack_type_id, + pack_team_id=pack.pack_team_id, + pack_cardset_id=pack.pack_cardset_id, + open_time=pack.open_time + ) + + saved = this_pack.save() + if saved == 1: + return_val = model_to_dict(this_pack) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that cardset' + ) + + +@router.patch('/{pack_id}') +async def patch_pack( + pack_id, team_id: Optional[int] = None, pack_type_id: Optional[int] = None, open_time: Optional[int] = None, + pack_team_id: Optional[int] = None, pack_cardset_id: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch packs. This event has been logged.' + ) + try: + this_pack = Pack.get_by_id(pack_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}') + + if team_id is not None: + this_pack.team_id = team_id + if pack_type_id is not None: + this_pack.pack_type_id = pack_type_id + if pack_team_id is not None: + this_pack.pack_team_id = pack_team_id + if pack_cardset_id is not None: + this_pack.pack_cardset_id = pack_cardset_id + if open_time is not None: + this_pack.open_time = open_time + + if this_pack.save() == 1: + return_val = model_to_dict(this_pack) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.delete('/{pack_id}') +async def delete_pack(pack_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete packs. This event has been logged.' + ) + try: + this_pack = Pack.get_by_id(pack_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No packs found with id {pack_id}') + + count = this_pack.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Pack {pack_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Pack {pack_id} was not deleted') diff --git a/app/routers_v2/packtypes.py b/app/routers_v2/packtypes.py new file mode 100644 index 0000000..98ae1e2 --- /dev/null +++ b/app/routers_v2/packtypes.py @@ -0,0 +1,194 @@ +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, PackType, model_to_dict, 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/v2/packtypes', + tags=['packtypes'] +) + + +class PacktypeModel(pydantic.BaseModel): + name: str + card_count: int + description: str + cost: int + available: Optional[bool] = True + + +@router.get('') +async def get_packtypes( + name: Optional[str] = None, card_count: Optional[int] = None, in_desc: Optional[str] = None, + available: Optional[bool] = None, csv: Optional[bool] = None): + all_packtypes = PackType.select() + + if all_packtypes.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'There are no packtypes to filter') + + if name is not None: + all_packtypes = all_packtypes.where(fn.Lower(PackType.name) == name.lower()) + if card_count is not None: + all_packtypes = all_packtypes.where(PackType.card_count == card_count) + if in_desc is not None: + all_packtypes = all_packtypes.where(fn.Lower(PackType.description).contains(in_desc.lower())) + if available is not None: + all_packtypes = all_packtypes.where(PackType.available == available) + + # if all_packtypes.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No packtypes found') + + if csv: + data_list = [['id', 'name', 'card_count', 'description']] + for line in all_packtypes: + data_list.append( + [ + line.id, line.name, line.card_count, line.description + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_packtypes.count(), 'packtypes': []} + for x in all_packtypes: + return_val['packtypes'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{packtype_id}') +async def get_one_packtype(packtype_id, csv: Optional[bool] = False): + try: + this_packtype = PackType.get_by_id(packtype_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No packtype found with id {packtype_id}') + + if csv: + data_list = [ + ['id', 'name', 'card_count', 'description'], + [this_packtype.id, this_packtype.name, this_packtype.card_count, this_packtype.description] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_packtype) + db.close() + return return_val + + +@router.post('') +async def post_packtypes(packtype: PacktypeModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post packtypes. This event has been logged.' + ) + + dupe_packtype = PackType.get_or_none(PackType.name == packtype.name) + if dupe_packtype: + db.close() + raise HTTPException(status_code=400, detail=f'There is already a packtype using {packtype.name}') + + this_packtype = PackType( + name=packtype.name, + card_count=packtype.card_count, + description=packtype.description, + cost=packtype.cost, + available=packtype.available + ) + + saved = this_packtype.save() + if saved == 1: + return_val = model_to_dict(this_packtype) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that cardset' + ) + + +@router.patch('/{packtype_id}') +async def patch_packtype( + packtype_id, name: Optional[str] = None, card_count: Optional[int] = None, description: Optional[str] = None, + cost: Optional[int] = None, available: Optional[bool] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch packtypes. This event has been logged.' + ) + try: + this_packtype = PackType.get_by_id(packtype_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No packtype found with id {packtype_id}') + + if name is not None: + this_packtype.name = name + if card_count is not None: + this_packtype.card_count = card_count + if description is not None: + this_packtype.description = description + if cost is not None: + this_packtype.cost = cost + if available is not None: + this_packtype.available = available + + if this_packtype.save() == 1: + return_val = model_to_dict(this_packtype) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.delete('/{packtype_id}') +async def delete_packtype(packtype_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete packtypes. This event has been logged.' + ) + try: + this_packtype = PackType.get_by_id(packtype_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No packtype found with id {packtype_id}') + + count = this_packtype.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Packtype {packtype_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Packtype {packtype_id} was not deleted') diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py new file mode 100644 index 0000000..b05af66 --- /dev/null +++ b/app/routers_v2/players.py @@ -0,0 +1,540 @@ +from fastapi import APIRouter, Depends, HTTPException, Response, Query +from typing import Optional, List +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Player, model_to_dict, fn, chunked, Paperdex, Cardset, Rarity +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/players', + tags=['players'] +) + + +class PlayerPydantic(pydantic.BaseModel): + player_id: int + p_name: str + cost: int + image: str + image2: Optional[str] = None + mlbclub: str + franchise: str + cardset_id: int + set_num: int + rarity_id: int + 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 + headshot: Optional[str] = None + vanity_card: Optional[str] = None + strat_code: Optional[str] = None + bbref_id: Optional[str] = None + fangr_id: Optional[str] = None + description: str + quantity: Optional[int] = 999 + + +class PlayerModel(pydantic.BaseModel): + players: List[PlayerPydantic] + + +@router.get('') +async def get_players( + name: Optional[str] = None, value: Optional[int] = None, min_cost: Optional[int] = None, + max_cost: Optional[int] = None, has_image2: Optional[bool] = None, mlbclub: Optional[str] = None, + franchise: Optional[str] = None, cardset_id: list = Query(default=None), rarity_id: list = Query(default=None), + pos_include: list = Query(default=None), pos_exclude: list = Query(default=None), has_headshot: Optional[bool] = None, + has_vanity_card: Optional[bool] = None, strat_code: Optional[str] = None, bbref_id: Optional[str] = None, + fangr_id: Optional[str] = None, inc_dex: Optional[bool] = True, in_desc: Optional[str] = None, + flat: Optional[bool] = False, sort_by: Optional[str] = False, cardset_id_exclude: list = Query(default=None), + limit: Optional[int] = None, csv: Optional[bool] = None): + all_players = Player.select() + if all_players.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'There are no players to filter') + + if name is not None: + all_players = all_players.where(fn.Lower(Player.p_name) == name.lower()) + if value is not None: + all_players = all_players.where(Player.cost == value) + if min_cost is not None: + all_players = all_players.where(Player.cost >= min_cost) + if max_cost is not None: + all_players = all_players.where(Player.cost <= max_cost) + if has_image2 is not None: + all_players = all_players.where(Player.image2.is_null(not has_image2)) + if mlbclub is not None: + all_players = all_players.where(fn.Lower(Player.mlbclub) == mlbclub.lower()) + if franchise is not None: + all_players = all_players.where(fn.Lower(Player.franchise) == franchise.lower()) + if cardset_id is not None: + all_players = all_players.where(Player.cardset_id << cardset_id) + if cardset_id_exclude is not None: + all_players = all_players.where(Player.cardset_id.not_in(cardset_id_exclude)) + if rarity_id is not None: + all_players = all_players.where(Player.rarity_id << rarity_id) + if pos_include is not None: + p_list = [x.upper() for x in pos_include] + 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 has_headshot is not None: + all_players = all_players.where(Player.headshot.is_null(not has_headshot)) + if has_vanity_card is not None: + all_players = all_players.where(Player.vanity_card.is_null(not has_vanity_card)) + if strat_code is not None: + all_players = all_players.where(Player.strat_code == strat_code) + if bbref_id is not None: + all_players = all_players.where(Player.bbref_id == bbref_id) + if fangr_id is not None: + all_players = all_players.where(Player.fangr_id == fangr_id) + if in_desc is not None: + all_players = all_players.where(fn.Lower(Player.description).contains(in_desc.lower())) + + if sort_by is not None: + if sort_by == 'cost-desc': + all_players = all_players.order_by(-Player.cost) + elif sort_by == 'cost-asc': + all_players = all_players.order_by(Player.cost) + elif sort_by == 'name-asc': + all_players = all_players.order_by(Player.p_name) + elif sort_by == 'name-desc': + all_players = all_players.order_by(-Player.p_name) + elif sort_by == 'rarity-desc': + all_players = all_players.order_by(Player.rarity) + elif sort_by == 'rarity-asc': + all_players = all_players.order_by(-Player.rarity) + + final_players = [] + # logging.info(f'pos_exclude: {type(pos_exclude)} - {pos_exclude} - is None: {pos_exclude is None}') + for x in all_players: + if pos_exclude is not None and set([x.upper() for x in pos_exclude]).intersection(x.get_all_pos()): + pass + else: + final_players.append(x) + + if limit is not None and len(final_players) >= limit: + break + + # if len(final_players) == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No players found') + + if csv: + all_players.order_by(-Player.rarity.value, Player.p_name) + data_list = [['id', 'name', 'value', 'image', 'image2', 'mlbclub', 'franchise', 'cardset', 'rarity', 'pos_1', + 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card', + 'strat_code', 'bbref_id', 'description', 'for_purchase', 'ranked_legal']] + for line in final_players: + data_list.append( + [ + line.player_id, line.p_name, line.cost, line.image, line.image2, line.mlbclub, line.franchise, + line.cardset, line.rarity, 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.headshot, line.vanity_card, line.strat_code, line.bbref_id, + line.description, line.cardset.for_purchase, line.cardset.ranked_legal + # line.description, line.cardset.in_packs, line.quantity + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': len(final_players), 'players': []} + for x in final_players: + + this_record = model_to_dict(x, recurse=not flat) + + if inc_dex: + this_dex = Paperdex.select().where(Paperdex.player == x) + this_record['paperdex'] = {'count': this_dex.count(), 'paperdex': []} + for y in this_dex: + this_record['paperdex']['paperdex'].append(model_to_dict(y, recurse=False)) + + return_val['players'].append(this_record) + + # return_val['players'].append(model_to_dict(x, recurse=not flat)) + + db.close() + return return_val + + +@router.get('/random') +async def get_random_player( + min_cost: Optional[int] = None, max_cost: Optional[int] = None, in_packs: Optional[bool] = None, + min_rarity: Optional[int] = None, max_rarity: Optional[int] = None, limit: Optional[int] = None, + pos_include: Optional[str] = None, pos_exclude: Optional[str] = None, franchise: Optional[str] = None, + mlbclub: Optional[str] = None, cardset_id: list = Query(default=None), pos_inc: list = Query(default=None), + pos_exc: list = Query(default=None), csv: Optional[bool] = None): + all_players = (Player + .select() + .join(Cardset) + .switch(Player) + .join(Rarity) + .order_by(fn.Random())) + + if min_cost is not None: + all_players = all_players.where(Player.cost >= min_cost) + if max_cost is not None: + all_players = all_players.where(Player.cost <= max_cost) + if in_packs is not None: + if in_packs: + all_players = all_players.where(Player.cardset.in_packs) + if min_rarity is not None: + all_players = all_players.where(Player.rarity.value >= min_rarity) + if max_rarity is not None: + all_players = all_players.where(Player.rarity.value <= max_rarity) + if pos_include is not None: + all_players = all_players.where( + (fn.lower(Player.pos_1) == pos_include.lower()) | (fn.lower(Player.pos_2) == pos_include.lower()) | + (fn.lower(Player.pos_3) == pos_include.lower()) | (fn.lower(Player.pos_4) == pos_include.lower()) | + (fn.lower(Player.pos_5) == pos_include.lower()) | (fn.lower(Player.pos_6) == pos_include.lower()) | + (fn.lower(Player.pos_7) == pos_include.lower()) | (fn.lower(Player.pos_8) == pos_include.lower()) + ) + if franchise is not None: + all_players = all_players.where(fn.Lower(Player.franchise) == franchise.lower()) + if mlbclub is not None: + all_players = all_players.where(fn.Lower(Player.mlbclub) == mlbclub.lower()) + if cardset_id is not None: + all_players = all_players.where(Player.cardset_id << cardset_id) + if pos_inc is not None: + p_list = [x.upper() for x in pos_inc] + 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 pos_exc is not None: + # p_list = [x.upper() for x in pos_exc] + # logging.info(f'starting query: {all_players}\n\np_list: {p_list}\n\n') + # all_players = all_players.where( + # Player.pos_1.not_in(p_list) & Player.pos_2.not_in(p_list) & Player.pos_3.not_in(p_list) & + # Player.pos_4.not_in(p_list) & Player.pos_5.not_in(p_list) & Player.pos_6.not_in(p_list) & + # Player.pos_7.not_in(p_list) & Player.pos_8.not_in(p_list) + # ) + # logging.info(f'post pos query: {all_players}') + + if pos_exclude is not None and pos_exc is None: + final_players = [x for x in all_players if pos_exclude not in x.get_all_pos()] + elif pos_exc is not None and pos_exclude is None: + final_players = [] + p_list = [x.upper() for x in pos_exc] + for x in all_players: + if limit is not None and len(final_players) >= limit: + break + if not set(p_list).intersection(x.get_all_pos()): + final_players.append(x) + else: + final_players = all_players + + if limit is not None: + final_players = final_players[:limit] + + # if len(final_players) == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No players found') + + if csv: + data_list = [['id', 'name', 'cost', 'image', 'image2', 'mlbclub', 'franchise', 'cardset', 'rarity', 'pos_1', + 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card', + 'strat_code', 'bbref_id', 'description']] + for line in final_players: + data_list.append( + [ + line.id, line.p_name, line.cost, line.image, line.image2, + line.mlbclub, line.franchise, line.cardset.name, line.rarity.name, + 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.headshot, line.vanity_card, + line.strat_code, line.bbref_id, line.description + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': len(final_players), 'players': []} + for x in final_players: + this_record = model_to_dict(x) + + this_dex = Paperdex.select().where(Paperdex.player == x) + this_record['paperdex'] = {'count': this_dex.count(), 'paperdex': []} + for y in this_dex: + this_record['paperdex']['paperdex'].append(model_to_dict(y, recurse=False)) + + return_val['players'].append(this_record) + # return_val['players'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{player_id}') +async def get_one_player(player_id, csv: Optional[bool] = False): + try: + this_player = Player.get_by_id(player_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No player found with id {player_id}') + + if csv: + data_list = [['id', 'name', 'cost', 'image', 'image2', 'mlbclub', 'franchise', 'cardset', 'rarity', 'pos_1', + 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card', + 'strat_code', 'bbref_id', 'description']] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + data_list.append( + [ + this_player.id, this_player.p_name, this_player.cost, this_player.image, this_player.image2, + this_player.mlbclub, this_player.franchise, this_player.cardset.name, this_player.rarity.name, + this_player.pos_1, this_player.pos_2, this_player.pos_3, this_player.pos_4, this_player.pos_5, + this_player.pos_6, this_player.pos_7, this_player.pos_8, this_player.headshot, this_player.vanity_card, + this_player.strat_code, this_player.bbref_id, this_player.description + ] + ) + + db.close() + return Response(content=return_val, media_type='text/csv') + else: + return_val = model_to_dict(this_player) + this_dex = Paperdex.select().where(Paperdex.player == this_player) + return_val['paperdex'] = {'count': this_dex.count(), 'paperdex': []} + for x in this_dex: + return_val['paperdex']['paperdex'].append(model_to_dict(x, recurse=False)) + db.close() + return return_val + + +@router.patch('/{player_id}') +async def v1_players_patch( + player_id, name: Optional[str] = None, image: Optional[str] = None, image2: Optional[str] = None, + mlbclub: Optional[str] = None, franchise: Optional[str] = None, cardset_id: Optional[int] = None, + rarity_id: Optional[int] = None, pos_1: Optional[str] = None, 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, + headshot: Optional[str] = None, vanity_card: Optional[str] = None, strat_code: Optional[str] = None, + bbref_id: Optional[str] = None, description: Optional[str] = None, cost: Optional[int] = None, + fangr_id: Optional[str] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch players. This event has been logged.' + ) + + try: + this_player = Player.get_by_id(player_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No player found with id {player_id}') + + if cost is not None: + this_player.cost = cost + if name is not None: + this_player.p_name = name + if image is not None: + this_player.image = image + if image2 is not None: + if image2.lower() == 'false': + this_player.image2 = None + else: + this_player.image2 = image2 + if mlbclub is not None: + this_player.mlbclub = mlbclub + if franchise is not None: + this_player.franchise = franchise + if cardset_id is not None: + try: + this_cardset = Cardset.get_by_id(cardset_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}') + this_player.cardset = this_cardset + if rarity_id is not None: + try: + this_rarity = Rarity.get_by_id(rarity_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}') + this_player.rarity = this_rarity + if pos_1 is not None: + if pos_1 == 'False': + this_player.pos_1 = None + else: + this_player.pos_1 = pos_1 + if pos_2 is not None: + if pos_2 == 'False': + this_player.pos_2 = None + else: + this_player.pos_2 = pos_2 + if pos_3 is not None: + if pos_3 == 'False': + this_player.pos_3 = None + else: + this_player.pos_3 = pos_3 + if pos_4 is not None: + if pos_4 == 'False': + this_player.pos_4 = None + else: + this_player.pos_4 = pos_4 + if pos_5 is not None: + if pos_5 == 'False': + this_player.pos_5 = None + else: + this_player.pos_5 = pos_5 + if pos_6 is not None: + if pos_6 == 'False': + this_player.pos_6 = None + else: + this_player.pos_6 = pos_6 + if pos_7 is not None: + if pos_7 == 'False': + this_player.pos_7 = None + else: + this_player.pos_7 = pos_7 + if pos_8 is not None: + if pos_8 == 'False': + this_player.pos_8 = None + else: + this_player.pos_8 = pos_8 + if headshot is not None: + this_player.headshot = headshot + if vanity_card is not None: + this_player.vanity_card = vanity_card + if strat_code is not None: + this_player.strat_code = strat_code + if bbref_id is not None: + this_player.bbref_id = bbref_id + if fangr_id is not None: + this_player.fangr_id = fangr_id + if description is not None: + this_player.description = description + + if this_player.save() == 1: + return_val = model_to_dict(this_player) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.put('') +async def v1_players_put(players: PlayerModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post players. This event has been logged.' + ) + + new_players = [] + for x in players.players: + # this_player = Player( + # player_id=x.player_id, + # p_name=x.p_name, + # cost=x.cost, + # image=x.image, + # image2=x.image2, + # mlbclub=x.mlbclub, + # franchise=x.franchise, + # cardset_id=x.cardset_id, + # rarity_id=x.rarity_id, + # set_num=x.set_num, + # pos_1=x.pos_1, + # pos_2=x.pos_2, + # pos_3=x.pos_3, + # pos_4=x.pos_4, + # pos_5=x.pos_5, + # pos_6=x.pos_6, + # pos_7=x.pos_7, + # pos_8=x.pos_8, + # headshot=x.headshot, + # vanity_card=x.vanity_card, + # strat_code=x.strat_code, + # fangr_id=x.fangr_id, + # bbref_id=x.bbref_id, + # description=x.description + # ) + # new_players.append(this_player) + new_players.append({ + 'player_id': x.player_id, + 'p_name': x.p_name, + 'cost': x.cost, + 'image': x.image, + 'image2': x.image2, + 'mlbclub': x.mlbclub.title(), + 'franchise': x.franchise.title(), + 'cardset_id': x.cardset_id, + 'rarity_id': x.rarity_id, + 'set_num': x.set_num, + 'pos_1': x.pos_1, + 'pos_2': x.pos_2, + 'pos_3': x.pos_3, + 'pos_4': x.pos_4, + 'pos_5': x.pos_5, + 'pos_6': x.pos_6, + 'pos_7': x.pos_7, + 'pos_8': x.pos_8, + 'headshot': x.headshot, + 'vanity_card': x.vanity_card, + 'strat_code': x.strat_code, + 'fangr_id': x.fangr_id, + 'bbref_id': x.bbref_id, + 'description': x.description + }) + + logging.info(f'new_players: {new_players}') + + with db.atomic(): + # Player.bulk_create(new_players, batch_size=15) + for batch in chunked(new_players, 15): + logging.info(f'batch: {batch}') + Player.insert_many(batch).on_conflict_replace().execute() + db.close() + + # sheets.update_all_players(SHEETS_AUTH) + raise HTTPException(status_code=200, detail=f'{len(new_players)} players have been added') + + +@router.delete('/{player_id}') +async def v1_players_delete(player_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete players. This event has been logged.' + ) + + try: + this_player = Player.get_by_id(player_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No player found with id {player_id}') + + count = this_player.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Player {player_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Player {player_id} was not deleted') From 7f008f9d981a174114cc3eb630057f70def53d0b Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 13 Sep 2023 16:42:14 -0500 Subject: [PATCH 04/40] Added cards and events --- app/main.py | 4 +- app/routers_v2/cards.py | 355 +++++++++++++++++++++++++++++++++++++++ app/routers_v2/events.py | 198 ++++++++++++++++++++++ 3 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 app/routers_v2/cards.py create mode 100644 app/routers_v2/events.py diff --git a/app/main.py b/app/main.py index 9c43bf2..85676bf 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,7 @@ import os from fastapi import FastAPI -from.routers_v2 import current, teams, rarity, cardsets, players, packtypes, packs +from.routers_v2 import current, teams, rarity, cardsets, players, packtypes, packs, cards, events app = FastAPI( responses={404: {'description': 'Not found'}} @@ -17,3 +17,5 @@ app.include_router(cardsets.router) app.include_router(players.router) app.include_router(packtypes.router) app.include_router(packs.router) +app.include_router(cards.router) +app.include_router(events.router) diff --git a/app/routers_v2/cards.py b/app/routers_v2/cards.py new file mode 100644 index 0000000..f1f8e7a --- /dev/null +++ b/app/routers_v2/cards.py @@ -0,0 +1,355 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response, Query +from typing import Optional, List +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Card, model_to_dict, fn, Team, Player, Pack, Paperdex +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/cards', + tags=['cards'] +) + + +class CardPydantic(pydantic.BaseModel): + player_id: int + team_id: int + pack_id: int + value: Optional[int] = 0 + + +class CardModel(pydantic.BaseModel): + cards: List[CardPydantic] + + +@router.get('') +async def get_cards( + player_id: Optional[int] = None, team_id: Optional[int] = None, pack_id: Optional[int] = None, + value: Optional[int] = None, min_value: Optional[int] = None, max_value: Optional[int] = None, + order_by: Optional[str] = None, limit: Optional[int] = None, dupes: Optional[bool] = None, + csv: Optional[bool] = None): + all_cards = Card.select() + + # if all_cards.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'There are no cards to filter') + + if team_id is not None: + try: + this_team = Team.get_by_id(team_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + all_cards = all_cards.where(Card.team == this_team) + if player_id is not None: + try: + this_player = Player.get_by_id(player_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No player found with id {player_id}') + all_cards = all_cards.where(Card.player == this_player) + if pack_id is not None: + try: + this_pack = Pack.get_by_id(pack_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}') + all_cards = all_cards.where(Card.pack == this_pack) + if value is not None: + all_cards = all_cards.where(Card.value == value) + if min_value is not None: + all_cards = all_cards.where(Card.value >= min_value) + if max_value is not None: + all_cards = all_cards.where(Card.value <= max_value) + if order_by is not None: + if order_by.lower() == 'new': + all_cards = all_cards.order_by(-Card.id) + if limit is not None: + all_cards = all_cards.limit(limit) + if dupes: + if team_id is None: + raise HTTPException(status_code=400, detail='Dupe checking must include a team_id') + logging.info(f'dupe check') + p_query = Card.select(Card.player).where(Card.team_id == team_id) + seen = set() + dupes = [] + for x in p_query: + if x.player.player_id in seen: + dupes.append(x.player_id) + else: + seen.add(x.player_id) + all_cards = all_cards.where(Card.player_id << dupes) + + # if all_cards.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No cards found') + + if csv: + data_list = [['id', 'player', 'cardset', 'rarity', 'team', 'pack', 'value']] + for line in all_cards: + data_list.append( + [ + line.id, line.player.p_name, line.player.cardset, line.player.rarity, line.team.abbrev, line.pack, + line.value + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_cards.count(), 'cards': []} + for x in all_cards: + + this_record = model_to_dict(x) + logging.debug(f'this_record: {this_record}') + + this_dex = Paperdex.select().where(Paperdex.player == x) + this_record['player']['paperdex'] = {'count': this_dex.count(), 'paperdex': []} + for y in this_dex: + this_record['player']['paperdex']['paperdex'].append(model_to_dict(y, recurse=False)) + + return_val['cards'].append(this_record) + + # return_val['cards'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{card_id}') +async def v1_cards_get_one(card_id, csv: Optional[bool] = False): + try: + this_card = Card.get_by_id(card_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No card found with id {card_id}') + + if csv: + data_list = [ + ['id', 'player', 'team', 'pack', 'value', 'roster1', 'roster2', 'roster3'], + [this_card.id, this_card.player, this_card.team.abbrev, this_card.pack, this_card.value, + this_card.roster1.name, this_card.roster2.name, this_card.roster3.name] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_card) + db.close() + return return_val + + +@router.post('') +async def v1_cards_post(cards: CardModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post cards. This event has been logged.' + ) + last_card = Card.select(Card.id).order_by(-Card.id).limit(1) + lc_id = last_card[0].id + + new_cards = [] + player_ids = [] + inc_dex = True + this_team = Team.get_by_id(cards.cards[0].team_id) + if this_team.is_ai or 'Gauntlet' in this_team.abbrev: + inc_dex = False + + # new_dex = [] + # now = int(datetime.timestamp(datetime.now()) * 1000) + for x in cards.cards: + this_card = Card( + player_id=x.player_id, + team_id=x.team_id, + pack_id=x.pack_id, + value=x.value + ) + if inc_dex: + Paperdex.get_or_create(team_id=x.team_id, player_id=x.player_id) + player_ids.append(x.player_id) + new_cards.append(this_card) + + with db.atomic(): + Card.bulk_create(new_cards, batch_size=15) + cost_query = Player.update(cost=Player.cost + 1).where(Player.player_id << player_ids) + cost_query.execute() + # sheets.post_new_cards(SHEETS_AUTH, lc_id) + db.close() + + raise HTTPException(status_code=200, detail=f'{len(new_cards)} cards have been added') + + +# @router.post('/ai-update') +# async def v1_cards_ai_update(token: str = Depends(oauth2_scheme)): +# if not valid_token(token): +# logging.warning(f'Bad Token: {token}') +# db.close() +# raise HTTPException( +# status_code=401, +# detail='You are not authorized to update AI cards. This event has been logged.' +# ) +# +# sheets.send_ai_cards(SHEETS_AUTH) +# raise HTTPException(status_code=200, detail=f'Just sent AI cards to sheets') + + +@router.post('/legal-check/{rarity_name}') +async def v1_cards_legal_check( + rarity_name: str, card_id: list = Query(default=None), token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='Unauthorized' + ) + if rarity_name not in ['ranked']: + return f'Rarity name {rarity_name} not a valid check' + + bad_cards = [] + all_cards = Card.select().where(Card.id << card_id) + + for x in all_cards: + if x.player.cardset_id not in [3, 4, 9, 10]: + bad_cards.append(x.player.description) + + return {'count': len(bad_cards), 'bad_cards': bad_cards} + + +@router.post('/post-update/{starting_id}') +async def v1_cards_post_update(starting_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to update card lists. This event has been logged.' + ) + + # sheets.post_new_cards(SHEETS_AUTH, starting_id) + db.close() + raise HTTPException(status_code=200, detail=f'Just sent cards to sheets starting at ID {starting_id}') + + +@router.post('/post-delete') +async def v1_cards_post_delete(del_ids: str, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete card lists. This event has been logged.' + ) + + logging.info(f'del_ids: {del_ids} / type: {type(del_ids)}') + # sheets.post_deletion(SHEETS_AUTH, del_ids.split(',')) + + +@router.post('/wipe-team/{team_id}') +async def v1_cards_wipe_team(team_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to wipe teams. This event has been logged.' + ) + + try: + this_team = Team.get_by_id(team_id) + except Exception as e: + logging.error(f'/cards/wipe-team/{team_id} - could not find team') + raise HTTPException(status_code=404, detail=f'Team {team_id} not found') + + t_query = Card.update(team=None).where(Card.team == this_team).execute() + db.close() + return f'Wiped {t_query} cards' + + +@router.patch('/{card_id}') +async def v1_cards_patch( + card_id, player_id: Optional[int] = None, team_id: Optional[int] = None, pack_id: Optional[int] = None, + value: Optional[int] = None, roster1_id: Optional[int] = None, roster2_id: Optional[int] = None, + roster3_id: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch cards. This event has been logged.' + ) + try: + this_card = Card.get_by_id(card_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No card found with id {card_id}') + + if player_id is not None: + this_card.player_id = player_id + if team_id is not None: + if team_id == 0: + this_card.team_id = None + else: + this_card.team_id = team_id + if pack_id is not None: + this_card.pack_id = pack_id + if value is not None: + this_card.value = value + if roster1_id is not None: + this_card.roster1_id = roster1_id + if roster2_id is not None: + this_card.roster2_id = roster2_id + if roster3_id is not None: + this_card.roster3_id = roster3_id + + if this_card.save() == 1: + return_val = model_to_dict(this_card) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.delete('/{card_id}') +async def v1_cards_delete(card_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete packs. This event has been logged.' + ) + try: + this_card = Card.get_by_id(card_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No cards found with id {card_id}') + + count = this_card.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Card {card_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Card {card_id} was not deleted') diff --git a/app/routers_v2/events.py b/app/routers_v2/events.py new file mode 100644 index 0000000..87f77b4 --- /dev/null +++ b/app/routers_v2/events.py @@ -0,0 +1,198 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import List, Optional, Literal +import copy +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Event, model_to_dict, chunked, fn +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/events', + tags=['events'] +) + + +class EventModel(pydantic.BaseModel): + name: str + short_desc: str + long_desc: str + url: Optional[str] = None + thumbnail: Optional[str] = None + active: Optional[bool] = False + + +@router.get('') +async def v1_events_get( + name: Optional[str] = None, in_desc: Optional[str] = None, active: Optional[bool] = None, + csv: Optional[bool] = None): + all_events = Event.select() + + if name is not None: + all_events = all_events.where(fn.Lower(Event.name) == name.lower()) + if in_desc is not None: + all_events = all_events.where( + (fn.Lower(Event.short_desc).contains(in_desc.lower())) | + (fn.Lower(Event.long_desc).contains(in_desc.lower())) + ) + if active is not None: + all_events = all_events.where(Event.active == active) + + if csv: + data_list = [['id', 'name', 'short_desc', 'long_desc', 'url', 'thumbnail', 'active']] + for line in all_events: + data_list.append( + [ + line.id, line.name, line.short_desc, line.long_desc, line.url, line.thumbnail, line.active + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_events.count(), 'events': []} + for x in all_events: + return_val['events'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{event_id}') +async def v1_events_get_one(event_id, csv: Optional[bool] = False): + try: + this_event = Event.get_by_id(event_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No event found with id {event_id}') + + if csv: + data_list = [ + ['id', 'name', 'short_desc', 'long_desc', 'url', 'thumbnail', 'active'], + [this_event.id, this_event.name, this_event.short_desc, this_event.long_desc, this_event.url, + this_event.thumbnail, this_event.active] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_event) + db.close() + return return_val + + +@router.post('') +async def v1_events_post(event: EventModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post events. This event has been logged.' + ) + + dupe_event = Event.get_or_none(Event.name == event.name) + if dupe_event: + db.close() + raise HTTPException(status_code=400, detail=f'There is already an event using {event.name}') + + this_event = Event( + name=event.name, + short_desc=event.short_desc, + long_desc=event.long_desc, + url=event.url, + thumbnail=event.thumbnail, + active=event.active + ) + + saved = this_event.save() + if saved == 1: + return_val = model_to_dict(this_event) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that cardset' + ) + + +@router.patch('/{event_id}') +async def v1_events_patch( + event_id, name: Optional[str] = None, short_desc: Optional[str] = None, long_desc: Optional[str] = None, + url: Optional[str] = None, thumbnail: Optional[str] = None, active: Optional[bool] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch events. This event has been logged.' + ) + try: + this_event = Event.get_by_id(event_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No event found with id {event_id}') + + if name is not None: + this_event.name = name + if short_desc is not None: + this_event.short_desc = short_desc + if long_desc is not None: + this_event.long_desc = long_desc + if url is not None: + this_event.url = url + if thumbnail is not None: + this_event.thumbnail = thumbnail + if active is not None: + this_event.active = active + + if this_event.save() == 1: + return_val = model_to_dict(this_event) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that event' + ) + + +@router.delete('/{event_id}') +async def v1_events_delete(event_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete events. This event has been logged.' + ) + try: + this_event = Event.get_by_id(event_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No event found with id {event_id}') + + count = this_event.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Event {event_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Event {event_id} was not deleted') From 177ca2c58581f3d2617f97dfba517206abcc8ece Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Thu, 14 Sep 2023 15:48:41 -0500 Subject: [PATCH 05/40] Added results, rewards, batstats, and pitstats --- app/main.py | 7 +- app/routers_v2/awards.py | 155 +++++++++++++ app/routers_v2/batstats.py | 455 +++++++++++++++++++++++++++++++++++++ app/routers_v2/pitstats.py | 188 +++++++++++++++ app/routers_v2/results.py | 433 +++++++++++++++++++++++++++++++++++ app/routers_v2/rewards.py | 188 +++++++++++++++ 6 files changed, 1425 insertions(+), 1 deletion(-) create mode 100644 app/routers_v2/awards.py create mode 100644 app/routers_v2/batstats.py create mode 100644 app/routers_v2/pitstats.py create mode 100644 app/routers_v2/results.py create mode 100644 app/routers_v2/rewards.py diff --git a/app/main.py b/app/main.py index 85676bf..7e90f53 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,8 @@ import os from fastapi import FastAPI -from.routers_v2 import current, teams, rarity, cardsets, players, packtypes, packs, cards, events +from.routers_v2 import current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, \ + batstats, pitstats app = FastAPI( responses={404: {'description': 'Not found'}} @@ -19,3 +20,7 @@ app.include_router(packtypes.router) app.include_router(packs.router) app.include_router(cards.router) app.include_router(events.router) +app.include_router(results.router) +app.include_router(rewards.router) +app.include_router(batstats.router) +app.include_router(pitstats.router) diff --git a/app/routers_v2/awards.py b/app/routers_v2/awards.py new file mode 100644 index 0000000..040126e --- /dev/null +++ b/app/routers_v2/awards.py @@ -0,0 +1,155 @@ +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Award, model_to_dict, 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/v2/awards', + tags=['awards'] +) + + +class AwardModel(pydantic.BaseModel): + name: str + season: int + timing: str = 'In-Season' + card_id: Optional[int] = None + team_id: Optional[int] = None + image: Optional[str] = None + + +@app.get('/api/v1/awards') +async def get_awards( + name: Optional[str] = None, season: Optional[int] = None, timing: Optional[str] = None, + card_id: Optional[int] = None, team_id: Optional[int] = None, image: Optional[str] = None, + csv: Optional[bool] = None): + all_awards = Award.select() + + if all_awards.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'There are no awards to filter') + + if name is not None: + all_awards = all_awards.where(Award.name == name) + if season is not None: + all_awards = all_awards.where(Award.season == season) + if timing is not None: + all_awards = all_awards.where(Award.timing == timing) + if card_id is not None: + all_awards = all_awards.where(Award.card_id == card_id) + if team_id is not None: + all_awards = all_awards.where(Award.team_id == team_id) + if image is not None: + all_awards = all_awards.where(Award.image == image) + + if csv: + data_list = [['id', 'name', 'season', 'timing', 'card', 'team', 'image']] + for line in all_awards: + data_list.append([ + line.id, line.name, line.season, line.timing, line.card, line.team, line.image + ]) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_awards.count(), 'awards': []} + for x in all_awards: + return_val['awards'].append(model_to_dict(x)) + + db.close() + return return_val + + +@app.get('/api/v1/awards/{award_id}') +async def get_one_award(award_id, csv: Optional[bool] = None): + try: + this_award = Award.get_by_id(award_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No award found with id {award_id}') + + if csv: + data_list = [ + ['id', 'name', 'season', 'timing', 'card', 'team', 'image'], + [this_award.id, this_award.name, this_award.season, this_award.timing, this_award.card, + this_award.team, this_award.image] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_award) + db.close() + return return_val + + +@app.post('/api/v1/awards') +async def post_awards(award: AwardModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post awards. This event has been logged.' + ) + + this_award = Award( + name=award.name, + season=award.season, + timing=award.season, + card_id=award.card_id, + team_id=award.team_id, + image=award.image + ) + + saved = this_award.save() + if saved == 1: + return_val = model_to_dict(this_award) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that roster' + ) + + +@app.delete('/api/v1/awards/{award_id}') +async def delete_award(award_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete awards. This event has been logged.' + ) + try: + this_award = Award.get_by_id(award_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No award found with id {award_id}') + + count = this_award.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Award {award_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Award {award_id} was not deleted') + + diff --git a/app/routers_v2/batstats.py b/app/routers_v2/batstats.py new file mode 100644 index 0000000..580ceff --- /dev/null +++ b/app/routers_v2/batstats.py @@ -0,0 +1,455 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional, List +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, BattingStat, model_to_dict, fn, Card +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/batstats', + tags=['batstats'] +) + + +class BatStat(pydantic.BaseModel): + card_id: int + team_id: int + roster_num: int + vs_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 + xch: Optional[int] = 0 + xhit: Optional[int] = 0 + error: Optional[int] = 0 + pb: Optional[int] = 0 + sbc: Optional[int] = 0 + csc: Optional[int] = 0 + week: int + season: int + created: Optional[int] = int(datetime.timestamp(datetime.now())*100000) + game_id: int + + +class BattingStatModel(pydantic.BaseModel): + stats: List[BatStat] + + +@router.get('') +async def get_batstats( + card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None, + season: int = None, week_start: int = None, week_end: int = None, created: int = None, csv: bool = None): + all_stats = BattingStat.select().join(Card).join(Player) + + if card_id is not None: + all_stats = all_stats.where(BattingStat.card_id == card_id) + if player_id is not None: + all_stats = all_stats.where(BattingStat.card.player.player_id == player_id) + if team_id is not None: + all_stats = all_stats.where(BattingStat.team_id == team_id) + if vs_team_id is not None: + all_stats = all_stats.where(BattingStat.vs_team_id == vs_team_id) + if week is not None: + all_stats = all_stats.where(BattingStat.week == week) + if season is not None: + all_stats = all_stats.where(BattingStat.season == season) + if week_start is not None: + all_stats = all_stats.where(BattingStat.week >= week_start) + if week_end is not None: + all_stats = all_stats.where(BattingStat.week <= week_end) + if created is not None: + all_stats = all_stats.where(BattingStat.created == created) + + # if all_stats.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No batting stats found') + + if csv: + data_list = [['id', 'card_id', 'player_id', 'cardset', 'team', 'vs_team', 'pos', 'pa', 'ab', 'run', 'hit', 'rbi', 'double', + 'triple', 'hr', 'bb', 'so', 'hbp', 'sac', 'ibb', 'gidp', 'sb', 'cs', 'bphr', 'bpfo', 'bp1b', + 'bplo', 'xch', 'xhit', 'error', 'pb', 'sbc', 'csc', 'week', 'season', 'created', 'game_id', 'roster_num']] + for line in all_stats: + data_list.append( + [ + line.id, line.card.id, line.card.player.player_id, line.card.player.cardset.name, line.team.abbrev, line.vs_team.abbrev, + line.pos, line.pa, line.ab, line.run, line.hit, line.rbi, line.double, line.triple, line.hr, + line.bb, line.so, line.hbp, line.sac, line.ibb, line.gidp, line.sb, line.cs, line.bphr, line.bpfo, + line.bp1b, line.bplo, line.xch, line.xhit, line.error, line.pb, line.sbc, line.csc, line.week, + line.season, line.created, line.game_id, line.roster_num + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_stats.count(), 'stats': []} + for x in all_stats: + return_val['stats'].append(model_to_dict(x, recurse=False)) + + db.close() + return return_val + + +@router.get('/player/{player_id}') +async def get_player_stats( + player_id: int, team_id: int = None, vs_team_id: int = None, week_start: int = None, week_end: int = None, + csv: bool = None): + all_stats = (BattingStat + .select(fn.COUNT(BattingStat.created).alias('game_count')) + .join(Card) + .group_by(BattingStat.card) + .where(BattingStat.card.player == player_id)).scalar() + + if team_id is not None: + all_stats = all_stats.where(BattingStat.team_id == team_id) + if vs_team_id is not None: + all_stats = all_stats.where(BattingStat.vs_team_id == vs_team_id) + if week_start is not None: + all_stats = all_stats.where(BattingStat.week >= week_start) + if week_end is not None: + all_stats = all_stats.where(BattingStat.week <= week_end) + + if csv: + data_list = [ + [ + 'pa', 'ab', 'run', 'hit', 'rbi', 'double', 'triple', 'hr', 'bb', 'so', 'hbp', 'sac', 'ibb', 'gidp', + 'sb', 'cs', 'bphr', 'bpfo', 'bp1b', 'bplo', 'xch', 'xhit', 'error', 'pb', 'sbc', 'csc', + ],[ + all_stats.pa_sum, all_stats.ab_sum, all_stats.run, all_stats.hit_sum, all_stats.rbi_sum, + all_stats.double_sum, all_stats.triple_sum, all_stats.hr_sum, all_stats.bb_sum, all_stats.so_sum, + all_stats.hbp_sum, all_stats.sac, all_stats.ibb_sum, all_stats.gidp_sum, all_stats.sb_sum, + all_stats.cs_sum, all_stats.bphr_sum, all_stats.bpfo_sum, all_stats.bp1b_sum, all_stats.bplo_sum, + all_stats.xch, all_stats.xhit_sum, all_stats.error_sum, all_stats.pb_sum, all_stats.sbc_sum, + all_stats.csc_sum + ] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + logging.debug(f'stat pull query: {all_stats}\n') + # logging.debug(f'result 0: {all_stats[0]}\n') + for x in all_stats: + logging.debug(f'this_line: {model_to_dict(x)}') + return_val = model_to_dict(all_stats[0]) + db.close() + return return_val + + +@router.post('') +async def post_batstats(stats: BattingStatModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post stats. This event has been logged.' + ) + + new_stats = [] + for x in stats.stats: + this_stat = BattingStat( + card_id=x.card_id, + team_id=x.team_id, + roster_num=x.roster_num, + vs_team_id=x.vs_team_id, + pos=x.pos, + pa=x.pa, + ab=x.ab, + run=x.run, + hit=x.hit, + rbi=x.rbi, + double=x.double, + triple=x.triple, + hr=x.hr, + bb=x.bb, + so=x.so, + hbp=x.hbp, + sac=x.sac, + ibb=x.ibb, + gidp=x.gidp, + sb=x.sb, + cs=x.cs, + bphr=x.bphr, + bpfo=x.bpfo, + bp1b=x.bp1b, + bplo=x.bplo, + xch=x.xch, + xhit=x.xhit, + error=x.error, + pb=x.pb, + sbc=x.sbc, + csc=x.csc, + week=x.week, + season=x.season, + created=x.created, + game_id=x.game_id + ) + new_stats.append(this_stat) + + with db.atomic(): + BattingStat.bulk_create(new_stats, batch_size=15) + db.close() + + raise HTTPException(status_code=200, detail=f'{len(new_stats)} batting lines have been added') + + +@router.delete('/{stat_id}') +async def delete_batstat(stat_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete stats. This event has been logged.' + ) + try: + this_stat = BattingStat.get_by_id(stat_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No stat found with id {stat_id}') + + count = this_stat.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Stat {stat_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Stat {stat_id} was not deleted') + + +# @app.get('/api/v1/plays/batting') +# async def get_batting_totals( +# player_id: list = Query(default=None), team_id: list = Query(default=None), min_pa: Optional[int] = 1, +# season: list = Query(default=None), position: list = Query(default=None), +# group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league'] = 'player', +# sort: Optional[str] = None, limit: Optional[int] = None, short_output: Optional[bool] = False): +# all_stats = BattingStat.select( +# BattingStat.card, BattingStat.game_id, BattingStat.team, BattingStat.vs_team, BattingStat.pos, +# BattingStat.card.player.alias('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.so).alias('sum_so'), +# 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.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') +# ).having( +# fn.SUM(BattingStat.pa) >= min_pa +# ).join(Card) +# +# if player_id is not None: +# # all_players = Player.select().where(Player.id << player_id) +# all_cards = Card.select().where(Card.player_id << player_id) +# all_stats = all_stats.where(BattingStat.card << all_cards) +# if team_id is not None: +# all_teams = Team.select().where(Team.id << team_id) +# all_stats = all_stats.where(BattingStat.team << all_teams) +# if season is not None: +# all_stats = all_stats.where(BattingStat.season << season) +# if position is not None: +# all_stats = all_stats.where(BattingStat.pos << position) +# +# if group_by == 'player': +# all_stats = all_stats.group_by(SQL('player')) +# elif group_by == 'playerteam': +# all_stats = all_stats.group_by(SQL('player'), BattingStat.team) +# elif group_by == 'playergame': +# all_stats = all_stats.group_by(SQL('player'), BattingStat.game_id) +# elif group_by == 'team': +# all_stats = all_stats.group_by(BattingStat.team) +# elif group_by == 'teamgame': +# all_stats = all_stats.group_by(BattingStat.team, BattingStat.game_id) +# elif group_by == 'league': +# all_stats = all_stats.group_by(BattingStat.season) +# +# if sort == 'pa-desc': +# all_stats = all_stats.order_by(SQL('sum_pa').desc()) +# elif sort == 'newest': +# all_stats = all_stats.order_by(-BattingStat.game_id) +# elif sort == 'oldest': +# all_stats = all_stats.order_by(BattingStat.game_id) +# +# if limit is not None: +# if limit < 1: +# limit = 1 +# all_stats = all_stats.limit(limit) +# +# logging.info(f'bat_plays query: {all_stats}') +# +# return_stats = { +# 'count': all_stats.count(), +# 'stats': [{ +# 'player': x.card.player_id if short_output else model_to_dict(x.card.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, +# 'avg': x.sum_hit / max(x.sum_ab, 1), +# 'obp': (x.sum_hit + x.sum_bb + x.sum_hbp + x.sum_ibb) / max(x.sum_pa, 1), +# 'slg': (x.sum_hr * 4 + x.sum_triple * 3 + x.sum_double * 2 + +# (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr)) / max(x.sum_ab, 1), +# 'ops': ((x.sum_hit + x.sum_bb + x.sum_hbp + x.sum_ibb) / max(x.sum_pa, 1)) + +# ((x.sum_hr * 4 + x.sum_triple * 3 + x.sum_double * 2 + +# (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr)) / max(x.sum_ab, 1)), +# 'woba': (.69 * x.sum_bb + .72 * x.sum_hbp + .89 * (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr) + +# 1.27 * x.sum_double + 1.62 * x.sum_triple + 2.1 * x.sum_hr) / max(x.sum_pa - x.sum_ibb, 1), +# 'game': x.game_id +# } for x in all_stats] +# } +# +# db.close() +# return return_stats +# +# +# @app.get('/api/v1/plays/pitching') +# async def get_pitching_totals( +# player_id: list = Query(default=None), team_id: list = Query(default=None), season: list = Query(default=None), +# group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league'] = 'player', +# min_pa: Optional[int] = 1, +# sort: Optional[str] = None, limit: Optional[int] = None, short_output: Optional[bool] = False): +# all_stats = PitchingStat.select( +# PitchingStat.card, PitchingStat.team, PitchingStat.game_id, PitchingStat.vs_team, +# PitchingStat.card.player.alias('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.irs).alias('sum_irs'), fn.SUM(PitchingStat.gs).alias('sum_gs'), +# 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.COUNT(PitchingStat.game_id).alias('sum_games') +# ).having( +# fn.SUM(PitchingStat.ip) >= max(min_pa / 3, 1) +# ).join(Card) +# +# if player_id is not None: +# all_cards = Card.select().where(Card.player_id << player_id) +# all_stats = all_stats.where(PitchingStat.card << all_cards) +# if team_id is not None: +# all_teams = Team.select().where(Team.id << team_id) +# all_stats = all_stats.where(PitchingStat.team << all_teams) +# if season is not None: +# all_stats = all_stats.where(PitchingStat.season << season) +# +# if group_by == 'player': +# all_stats = all_stats.group_by(SQL('player')) +# elif group_by == 'playerteam': +# all_stats = all_stats.group_by(SQL('player'), PitchingStat.team) +# elif group_by == 'playergame': +# all_stats = all_stats.group_by(SQL('player'), PitchingStat.game_id) +# elif group_by == 'team': +# all_stats = all_stats.group_by(PitchingStat.team) +# elif group_by == 'teamgame': +# all_stats = all_stats.group_by(PitchingStat.team, PitchingStat.game_id) +# elif group_by == 'league': +# all_stats = all_stats.group_by(PitchingStat.season) +# +# if sort == 'pa-desc': +# all_stats = all_stats.order_by(SQL('sum_pa').desc()) +# elif sort == 'newest': +# all_stats = all_stats.order_by(-PitchingStat.game_id) +# elif sort == 'oldest': +# all_stats = all_stats.order_by(PitchingStat.game_id) +# +# if limit is not None: +# if limit < 1: +# limit = 1 +# all_stats = all_stats.limit(limit) +# +# logging.info(f'bat_plays query: {all_stats}') +# +# return_stats = { +# 'count': all_stats.count(), +# 'stats': [{ +# 'player': x.card.player_id if short_output else model_to_dict(x.card.player, recurse=False), +# 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False), +# 'tbf': None, +# 'outs': round(x.sum_ip * 3), +# 'games': x.sum_games, +# 'gs': x.sum_gs, +# 'win': x.sum_win, +# 'loss': x.sum_loss, +# 'hold': x.sum_hold, +# 'save': x.sum_sv, +# 'bsave': x.sum_bsv, +# 'ir': x.sum_ir, +# 'ir_sc': x.sum_irs, +# 'runs': x.sum_run, +# 'e_runs': x.sum_erun, +# 'hits': x.sum_hit, +# 'hr': x.sum_hr, +# 'bb': x.sum_bb, +# 'so': x.sum_so, +# 'hbp': x.sum_hbp, +# 'wp': x.sum_wp, +# 'balk': x.sum_balk, +# 'era': (x.sum_erun * 27) / round(x.sum_ip * 3), +# 'whip': (x.sum_bb + x.sum_hit) / x.sum_ip, +# 'avg': None, +# 'obp': None, +# 'woba': None, +# 'k/9': x.sum_so * 9 / x.sum_ip, +# 'bb/9': x.sum_bb * 9 / x.sum_ip, +# 'k/bb': x.sum_so / max(x.sum_bb, .1), +# 'game': None, +# 'lob_2outs': None, +# 'rbi%': None +# } for x in all_stats] +# } +# db.close() +# return return_stats + diff --git a/app/routers_v2/pitstats.py b/app/routers_v2/pitstats.py new file mode 100644 index 0000000..b873520 --- /dev/null +++ b/app/routers_v2/pitstats.py @@ -0,0 +1,188 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional, List +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, PitchingStat, model_to_dict, fn, Card, Player +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/pitstats', + tags=['pitstats'] +) + + +class PitStat(pydantic.BaseModel): + card_id: int + team_id: int + vs_team_id: int + roster_num: int + ip: float + 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 + ir: Optional[int] = 0 + irs: 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 + week: int + season: int + created: Optional[int] = int(datetime.timestamp(datetime.now())*100000) + game_id: int + + +class PitchingStatModel(pydantic.BaseModel): + stats: List[PitStat] + + +@router.get('') +async def get_pit_stats( + card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None, + season: int = None, week_start: int = None, week_end: int = None, created: int = None, gs: bool = None, + csv: bool = None): + all_stats = PitchingStat.select().join(Card).join(Player) + logging.debug(f'pit query:\n\n{all_stats}') + + if card_id is not None: + all_stats = all_stats.where(PitchingStat.card_id == card_id) + if player_id is not None: + all_stats = all_stats.where(PitchingStat.card.player.player_id == player_id) + if team_id is not None: + all_stats = all_stats.where(PitchingStat.team_id == team_id) + if vs_team_id is not None: + all_stats = all_stats.where(PitchingStat.vs_team_id == vs_team_id) + if week is not None: + all_stats = all_stats.where(PitchingStat.week == week) + if season is not None: + all_stats = all_stats.where(PitchingStat.season == season) + if week_start is not None: + all_stats = all_stats.where(PitchingStat.week >= week_start) + if week_end is not None: + all_stats = all_stats.where(PitchingStat.week <= week_end) + if created is not None: + all_stats = all_stats.where(PitchingStat.created <= created) + if gs is not None: + all_stats = all_stats.where(PitchingStat.gs == 1 if gs else 0) + + # if all_stats.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No pitching stats found') + + if csv: + data_list = [['id', 'card_id', 'player_id', 'cardset', 'team', 'vs_team', 'ip', 'hit', 'run', 'erun', 'so', 'bb', 'hbp', + 'wp', 'balk', 'hr', 'ir', 'irs', 'gs', 'win', 'loss', 'hold', 'sv', 'bsv', 'week', 'season', + 'created', 'game_id', 'roster_num']] + for line in all_stats: + data_list.append( + [ + line.id, line.card.id, line.card.player.player_id, line.card.player.cardset.name, line.team.abbrev, + line.vs_team.abbrev, line.ip, line.hit, + line.run, line.erun, line.so, line.bb, line.hbp, line.wp, line.balk, line.hr, line.ir, line.irs, + line.gs, line.win, line.loss, line.hold, line.sv, line.bsv, line.week, line.season, line.created, + line.game_id, line.roster_num + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_stats.count(), 'stats': []} + for x in all_stats: + return_val['stats'].append(model_to_dict(x, recurse=False)) + + db.close() + return return_val + + +@router.post('') +async def post_pitstat(stats: PitchingStatModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post stats. This event has been logged.' + ) + + new_stats = [] + for x in stats.stats: + this_stat = PitchingStat( + card_id=x.card_id, + team_id=x.team_id, + vs_team_id=x.vs_team_id, + roster_num=x.roster_num, + ip=x.ip, + hit=x.hit, + run=x.run, + erun=x.erun, + so=x.so, + bb=x.bb, + hbp=x.hbp, + wp=x.wp, + balk=x.balk, + hr=x.hr, + ir=x.ir, + irs=x.irs, + gs=x.gs, + win=x.win, + loss=x.loss, + hold=x.hold, + sv=x.sv, + bsv=x.bsv, + week=x.week, + season=x.season, + created=x.created, + game_id=x.game_id + ) + new_stats.append(this_stat) + + with db.atomic(): + PitchingStat.bulk_create(new_stats, batch_size=15) + db.close() + + raise HTTPException(status_code=200, detail=f'{len(new_stats)} pitching lines have been added') + + +@router.delete('/{stat_id}') +async def delete_pitstat(stat_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete stats. This event has been logged.' + ) + try: + this_stat = PitchingStat.get_by_id(stat_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No stat found with id {stat_id}') + + count = this_stat.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Stat {stat_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Stat {stat_id} was not deleted') diff --git a/app/routers_v2/results.py b/app/routers_v2/results.py new file mode 100644 index 0000000..379fbb7 --- /dev/null +++ b/app/routers_v2/results.py @@ -0,0 +1,433 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional, List +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Result, model_to_dict, fn, Team, DataError +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/results', + tags=['results'] +) + + +class ResultModel(pydantic.BaseModel): + away_team_id: int + home_team_id: int + away_score: int + home_score: int + away_team_value: Optional[int] = None + home_team_value: Optional[int] = None + away_team_ranking: Optional[int] = None + home_team_ranking: Optional[int] = None + scorecard: str + week: int + season: int + ranked: bool + short_game: bool + game_type: str + + +@router.get('') +async def get_results( + away_team_id: Optional[int] = None, home_team_id: Optional[int] = None, team_one_id: Optional[int] = None, + team_two_id: Optional[int] = None, away_score_min: Optional[int] = None, away_score_max: Optional[int] = None, + home_score_min: Optional[int] = None, home_score_max: Optional[int] = None, bothscore_min: Optional[int] = None, + bothscore_max: Optional[int] = None, season: Optional[int] = None, week: Optional[int] = None, + week_start: Optional[int] = None, week_end: Optional[int] = None, ranked: Optional[bool] = None, + short_game: Optional[bool] = None, game_type: Optional[str] = None, vs_ai: Optional[bool] = None, + csv: Optional[bool] = None): + all_results = Result.select() + + # if all_results.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'There are no results to filter') + + if away_team_id is not None: + try: + this_team = Team.get_by_id(away_team_id) + all_results = all_results.where(Result.away_team == this_team) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {away_team_id}') + + if home_team_id is not None: + try: + this_team = Team.get_by_id(home_team_id) + all_results = all_results.where(Result.home_team == this_team) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {home_team_id}') + + if team_one_id is not None: + try: + this_team = Team.get_by_id(team_one_id) + all_results = all_results.where((Result.home_team == this_team) | (Result.away_team == this_team)) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_one_id}') + + if team_two_id is not None: + try: + this_team = Team.get_by_id(team_two_id) + all_results = all_results.where((Result.home_team == this_team) | (Result.away_team == this_team)) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No team found with id {team_two_id}') + + if away_score_min is not None: + all_results = all_results.where(Result.away_score >= away_score_min) + + if away_score_max is not None: + all_results = all_results.where(Result.away_score <= away_score_max) + + if home_score_min is not None: + all_results = all_results.where(Result.home_score >= home_score_min) + + if home_score_max is not None: + all_results = all_results.where(Result.home_score <= home_score_max) + + if bothscore_min is not None: + all_results = all_results.where((Result.home_score >= bothscore_min) & (Result.away_score >= bothscore_min)) + + if bothscore_max is not None: + all_results = all_results.where((Result.home_score <= bothscore_max) & (Result.away_score <= bothscore_max)) + + if season is not None: + all_results = all_results.where(Result.season == season) + + if week is not None: + all_results = all_results.where(Result.week == week) + + if ranked is not None: + all_results = all_results.where(Result.ranked == ranked) + + if short_game is not None: + all_results = all_results.where(Result.short_game == short_game) + + 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) + + if game_type is not None: + all_results = all_results.where(Result.game_type == game_type) + + all_results = all_results.order_by(Result.id) + # Not functional + # if vs_ai is not None: + # AwayTeam = Team.alias() + # all_results = all_results.join( + # Team, on=Result.home_team + # ).switch(Result).join( + # Team, on=(AwayTeam.id == Result.away_team).alias('a_team') + # ) + # + # if vs_ai: + # all_results = all_results.where( + # (Result.home_team.is_ai == 1) | (Result.a_team.is_ai == 1) + # ) + # else: + # all_results = all_results.where( + # (Result.home_team.is_ai == 0) & (Result.a_team.is_ai == 0) + # ) + # logging.info(f'Result Query:\n\n{all_results}') + + if csv: + data_list = [['id', 'away_abbrev', 'home_abbrev', 'away_score', 'home_score', 'away_tv', 'home_tv', + 'game_type', 'season', 'week', 'short_game', 'ranked']] + for line in all_results: + data_list.append([ + line.id, line.away_team.abbrev, line.home_team.abbrev, line.away_score, line.home_score, + line.away_team_value, line.home_team_value, line.game_type if line.game_type else 'minor-league', + line.season, line.week, line.short_game, line.ranked + ]) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_results.count(), 'results': []} + for x in all_results: + return_val['results'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{result_id}') +async def get_one_results(result_id, csv: Optional[bool] = None): + try: + this_result = Result.get_by_id(result_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No result found with id {result_id}') + + if csv: + data_list = [ + ['id', 'away_abbrev', 'home_abbrev', 'away_score', 'home_score', 'away_tv', 'home_tv', 'game_type', + 'season', 'week', 'game_type'], + [this_result.id, this_result.away_team.abbrev, this_result.away_team.abbrev, this_result.away_score, + this_result.home_score, this_result.away_team_value, this_result.home_team_value, + this_result.game_type if this_result.game_type else 'minor-league', + this_result.season, this_result.week, this_result.game_type] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_result) + db.close() + return return_val + + +@router.get('/team/{team_id}') +async def get_team_results( + team_id: int, season: Optional[int] = None, week: Optional[int] = None, csv: Optional[bool] = False): + all_results = Result.select().where((Result.away_team_id == team_id) | (Result.home_team_id == team_id)) + try: + this_team = Team.get_by_id(team_id) + except Exception as e: + logging.error(f'Unknown team id {team_id} trying to pull team results') + raise HTTPException(404, f'Team id {team_id} not found') + + if season is not None: + all_results = all_results.where(Result.season == season) + else: + all_results = all_results.where(Result.season == this_team.season) + + if week is not None: + all_results = all_results.where(Result.week == week) + + r_wins, r_loss, c_wins, c_loss = 0, 0, 0, 0 + for x in all_results: + if x.away_team_id == team_id: + if x.away_score > x.home_score: + if x.ranked: + r_wins += 1 + else: + c_wins += 1 + else: + if x.ranked: + r_loss += 1 + else: + c_loss += 1 + elif x.home_team_id == team_id: + if x.away_score > x.home_score: + if x.ranked: + r_loss += 1 + else: + c_loss += 1 + else: + if x.ranked: + r_wins += 1 + else: + c_wins += 1 + + if csv: + data_list = [ + ['team_id', 'ranked_wins', 'ranked_losses', 'casual_wins', 'casual_losses', 'team_ranking'], + [team_id, r_wins, r_loss, c_wins, c_loss, this_team.ranking] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = { + 'team': model_to_dict(this_team), + 'ranked_wins': r_wins, + 'ranked_losses': r_loss, + 'casual_wins': c_wins, + 'casual_losses': c_loss, + } + db.close() + return return_val + + +@router.post('') +async def post_result(result: ResultModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post results. This event has been logged.' + ) + + this_result = Result(**result.__dict__) + saved = this_result.save() + + if result.ranked: + if not result.away_team_ranking: + db.close() + error = f'Ranked game did not include away team ({result.away_team_id}) ranking.' + logging.error(error) + raise DataError(error) + if not result.home_team_ranking: + db.close() + error = f'Ranked game did not include home team ({result.home_team_id}) ranking.' + logging.error(error) + raise DataError(error) + + k_value = 20 if result.short_game else 60 + ratio = (result.home_team_ranking - result.away_team_ranking) / 400 + exp_score = 1 / (1 + (10 ** ratio)) + away_win = True if result.away_score > result.home_score else False + total_delta = k_value * exp_score + high_delta = total_delta * exp_score if exp_score > .5 else total_delta * (1 - exp_score) + low_delta = total_delta - high_delta + + # exp_score > .5 means away team is favorite + if exp_score > .5 and away_win: + final_delta = low_delta + away_delta = low_delta * 3 + home_delta = -low_delta + elif away_win: + final_delta = high_delta + away_delta = high_delta * 3 + home_delta = -high_delta + elif exp_score <= .5 and not away_win: + final_delta = low_delta + away_delta = -low_delta + home_delta = low_delta * 3 + elif not away_win: + final_delta = high_delta + away_delta = -high_delta + home_delta = high_delta * 3 + else: + final_delta = 0 + away_delta = 0 + home_delta = 0 + + logging.debug(f'/results ranking deltas\n\nk_value: {k_value} / ratio: {ratio} / ' + f'exp_score: {exp_score} / away_win: {away_win} / total_delta: {total_delta} / ' + f'high_delta: {high_delta} / low_delta: {low_delta} / final_delta: {final_delta} / ') + + away_team = Team.get_by_id(result.away_team_id) + away_team.ranking += away_delta + away_team.save() + logging.info(f'Just updated {away_team.abbrev} ranking to {away_team.ranking}') + home_team = Team.get_by_id(result.home_team_id) + home_team.ranking += home_delta + home_team.save() + logging.info(f'Just updated {home_team.abbrev} ranking to {home_team.ranking}') + + if saved == 1: + return_val = model_to_dict(this_result) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that roster' + ) + + +@app.patch('/api/v1/results/{result_id}') +async def patch_result( + result_id, away_team_id: Optional[int] = None, home_team_id: Optional[int] = None, + away_score: Optional[int] = None, home_score: Optional[int] = None, away_team_value: Optional[int] = None, + home_team_value: Optional[int] = None, scorecard: Optional[str] = None, week: Optional[int] = None, + season: Optional[int] = None, short_game: Optional[bool] = None, game_type: Optional[str] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch results. This event has been logged.' + ) + try: + this_result = Result.get_by_id(result_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No result found with id {result_id}') + + if away_team_id is not None: + this_result.away_team_id = away_team_id + + if home_team_id is not None: + this_result.home_team_id = home_team_id + + if away_score is not None: + this_result.away_score = away_score + + if home_score is not None: + this_result.home_score = home_score + + if away_team_value is not None: + this_result.away_team_value = away_team_value + + if home_team_value is not None: + this_result.home_team_value = home_team_value + + if scorecard is not None: + this_result.scorecard = scorecard + + if week is not None: + this_result.week = week + + if season is not None: + this_result.season = season + + if game_type is not None: + this_result.game_type = game_type + + if short_game is not None: + if not short_game: + this_result.short_game = None + else: + this_result.short_game = short_game + + if this_result.save() == 1: + return_val = model_to_dict(this_result) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that event' + ) + + +@app.delete('/api/v1/results/{result_id}') +async def delete_result(result_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post results. This event has been logged.' + ) + try: + this_result = Result.get_by_id(result_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No result found with id {result_id}') + + count = this_result.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Result {result_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Result {result_id} was not deleted') + diff --git a/app/routers_v2/rewards.py b/app/routers_v2/rewards.py new file mode 100644 index 0000000..2d8abf2 --- /dev/null +++ b/app/routers_v2/rewards.py @@ -0,0 +1,188 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Reward, model_to_dict, 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/v2/rewards', + tags=['rewards'] +) + + +class RewardModel(pydantic.BaseModel): + name: str + season: int + week: int + team_id: int + created: Optional[int] = int(datetime.timestamp(datetime.now())*1000) + + +@router.get('') +async def get_rewards( + name: Optional[str] = None, in_name: Optional[str] = None, team_id: Optional[int] = None, + season: Optional[int] = None, week: Optional[int] = None, created_after: Optional[int] = None, + flat: Optional[bool] = False, csv: Optional[bool] = None): + all_rewards = Reward.select() + + if all_rewards.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'There are no rewards to filter') + + if name is not None: + all_rewards = all_rewards.where(fn.Lower(Reward.name) == name.lower()) + if team_id is not None: + all_rewards = all_rewards.where(Reward.team_id == team_id) + if created_after is not None: + all_rewards = all_rewards.where(Reward.created >= created_after) + if in_name is not None: + all_rewards = all_rewards.where(fn.Lower(Reward.name).contains(in_name.lower())) + if season is not None: + all_rewards = all_rewards.where(Reward.season == season) + if week is not None: + all_rewards = all_rewards.where(Reward.week == week) + + if all_rewards.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'No rewards found') + + if csv: + data_list = [['id', 'name', 'team', 'daily', 'created']] + for line in all_rewards: + data_list.append( + [ + line.id, line.name, line.team.id, line.daily, line.created + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_rewards.count(), 'rewards': []} + for x in all_rewards: + return_val['rewards'].append(model_to_dict(x, recurse=not flat)) + + db.close() + return return_val + + +@router.get('/{reward_id}') +async def get_one_reward(reward_id, csv: Optional[bool] = False): + try: + this_reward = Reward.get_by_id(reward_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}') + + if csv: + data_list = [ + ['id', 'name', 'card_count', 'description'], + [this_reward.id, this_reward.name, this_reward.team.id, this_reward.daily, this_reward.created] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_reward) + db.close() + return return_val + + +@router.post('') +async def post_rewards(reward: RewardModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post rewards. This event has been logged.' + ) + + this_reward = Reward(**reward.dict()) + + saved = this_reward.save() + if saved == 1: + return_val = model_to_dict(this_reward) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that cardset' + ) + + +@router.patch('/{reward_id}') +async def patch_reward( + reward_id, name: Optional[str] = None, team_id: Optional[int] = None, created: Optional[int] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch rewards. This event has been logged.' + ) + try: + this_reward = Reward.get_by_id(reward_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}') + + if name is not None: + this_reward.name = name + if team_id is not None: + this_reward.team_id = team_id + if created is not None: + this_reward.created = created + + if this_reward.save() == 1: + return_val = model_to_dict(this_reward) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.delete('/{reward_id}') +async def delete_reward(reward_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete rewards. This event has been logged.' + ) + try: + this_reward = Reward.get_by_id(reward_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}') + + count = this_reward.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Reward {reward_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Reward {reward_id} was not deleted') + + From 144ced68751fc4f5d665a0e002780fd24241166e Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Thu, 14 Sep 2023 22:05:51 -0500 Subject: [PATCH 06/40] Added admin, gamerewards, gauntletrewards, gauntletruns, notifications, paperdex --- app/main.py | 7 +- app/routers_v2/admin.py | 40 ++++++ app/routers_v2/gamerewards.py | 194 ++++++++++++++++++++++++++++ app/routers_v2/gauntletrewards.py | 139 ++++++++++++++++++++ app/routers_v2/gauntletruns.py | 170 ++++++++++++++++++++++++ app/routers_v2/notifications.py | 203 +++++++++++++++++++++++++++++ app/routers_v2/paperdex.py | 208 ++++++++++++++++++++++++++++++ 7 files changed, 960 insertions(+), 1 deletion(-) create mode 100644 app/routers_v2/admin.py create mode 100644 app/routers_v2/gamerewards.py create mode 100644 app/routers_v2/gauntletrewards.py create mode 100644 app/routers_v2/gauntletruns.py create mode 100644 app/routers_v2/notifications.py create mode 100644 app/routers_v2/paperdex.py diff --git a/app/main.py b/app/main.py index 7e90f53..374aeff 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ import os from fastapi import FastAPI from.routers_v2 import current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, \ - batstats, pitstats + batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns app = FastAPI( responses={404: {'description': 'Not found'}} @@ -24,3 +24,8 @@ app.include_router(results.router) app.include_router(rewards.router) app.include_router(batstats.router) app.include_router(pitstats.router) +app.include_router(notifications.router) +app.include_router(paperdex.router) +app.include_router(gamerewards.router) +app.include_router(gauntletrewards.router) +app.include_router(gauntletruns.router) diff --git a/app/routers_v2/admin.py b/app/routers_v2/admin.py new file mode 100644 index 0000000..540b8d6 --- /dev/null +++ b/app/routers_v2/admin.py @@ -0,0 +1,40 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Player, model_to_dict, 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/v2/admin', + tags=['admin'] +) + + +@router.post('/stl-fix') +async def stl_cardinals_fix(token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post. This event has been logged.' + ) + + p_query = Player.update(mlbclub='St Louis Cardinals', franchise='St Louis Cardinals').where( + Player.mlbclub == 'St. Louis Cardinals' + ).execute() + db.close() + + return {'detail': f'Removed the period from St Louis'} + diff --git a/app/routers_v2/gamerewards.py b/app/routers_v2/gamerewards.py new file mode 100644 index 0000000..04cb328 --- /dev/null +++ b/app/routers_v2/gamerewards.py @@ -0,0 +1,194 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, GameRewards, model_to_dict, 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/v2/gamerewards', + tags=['gamerewards'] +) + + +class GameRewardModel(pydantic.BaseModel): + name: str + pack_type_id: Optional[int] = None + player_id: Optional[int] = None + money: Optional[int] = None + + +@router.get('') +async def v1_gamerewards_get( + name: Optional[str] = None, pack_type_id: Optional[int] = None, player_id: Optional[int] = None, + money: Optional[int] = None, csv: Optional[bool] = None): + all_rewards = GameRewards.select() + + # if all_rewards.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'There are no awards to filter') + + if name is not None: + all_rewards = all_rewards.where(GameRewards.name == name) + if pack_type_id is not None: + all_rewards = all_rewards.where(GameRewards.pack_type_id == pack_type_id) + if player_id is not None: + all_rewards = all_rewards.where(GameRewards.player_id == player_id) + if money is not None: + all_rewards = all_rewards.where(GameRewards.money == money) + + if csv: + data_list = [['id', 'pack_type_id', 'player_id', 'money']] + for line in all_rewards: + data_list.append([ + line.id, line.pack_type_id if line.pack_type else None, line.player_id if line.player else None, + line.money + ]) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_rewards.count(), 'gamerewards': []} + for x in all_rewards: + return_val['gamerewards'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{gameaward_id}') +async def v1_gamerewards_get_one(gamereward_id, csv: Optional[bool] = None): + try: + this_game_reward = GameRewards.get_by_id(gamereward_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No game reward found with id {gamereward_id}') + + if csv: + data_list = [ + ['id', 'pack_type_id', 'player_id', 'money'], + [this_game_reward.id, this_game_reward.pack_type_id if this_game_reward.pack_type else None, + this_game_reward.player_id if this_game_reward.player else None, this_game_reward.money] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_game_reward) + db.close() + return return_val + + +@router.post('') +async def v1_gamerewards_post(game_reward: GameRewardModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post game rewards. This event has been logged.' + ) + + this_award = GameRewards( + name=game_reward.name, + pack_type_id=game_reward.pack_type_id, + player_id=game_reward.player_id, + money=game_reward.money + ) + + saved = this_award.save() + if saved == 1: + return_val = model_to_dict(this_award) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that roster' + ) + + +@router.patch('/{game_reward_id}') +async def v1_gamerewards_patch( + game_reward_id: int, name: Optional[str] = None, pack_type_id: Optional[int] = None, + player_id: Optional[int] = None, money: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch gamerewards. This event has been logged.' + ) + try: + this_game_reward = GameRewards.get_by_id(game_reward_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No game reward found with id {game_reward_id}') + + if name is not None: + this_game_reward.name = name + if pack_type_id is not None: + if not pack_type_id: + this_game_reward.pack_type_id = None + else: + this_game_reward.pack_type_id = pack_type_id + if player_id is not None: + if not player_id: + this_game_reward.player_id = None + else: + this_game_reward.player_id = player_id + if money is not None: + if not money: + this_game_reward.money = None + else: + this_game_reward.money = money + + if this_game_reward.save() == 1: + return_val = model_to_dict(this_game_reward) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.delete('/{gamereward_id}') +async def v1_gamerewards_delete(gamereward_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete awards. This event has been logged.' + ) + try: + this_award = GameRewards.get_by_id(gamereward_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No award found with id {gamereward_id}') + + count = this_award.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Game Reward {gamereward_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Game Reward {gamereward_id} was not deleted') + diff --git a/app/routers_v2/gauntletrewards.py b/app/routers_v2/gauntletrewards.py new file mode 100644 index 0000000..ce80750 --- /dev/null +++ b/app/routers_v2/gauntletrewards.py @@ -0,0 +1,139 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import Optional, List +import logging +import pydantic + +from ..db_engine import db, GauntletReward, model_to_dict, chunked, DatabaseError +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/gauntletrewards', + tags=['gauntletrewards'] +) + + +class GauntletRewardModel(pydantic.BaseModel): + name: str + gauntlet_id: Optional[int] = 0 + reward_id: Optional[int] = 0 + win_num: Optional[int] = 0 + loss_max: Optional[int] = 1 + + +class GauntletRewardList(pydantic.BaseModel): + rewards: List[GauntletRewardModel] + + +@router.get('/api/v1/gauntletrewards') +async def v1_gauntletreward_get( + name: Optional[str] = None, gauntlet_id: Optional[int] = None, reward_id: list = Query(default=None), + win_num: Optional[int] = None, loss_max: Optional[int] = None): + all_rewards = GauntletReward.select() + + if name is not None: + all_rewards = all_rewards.where(GauntletReward.name == name) + if gauntlet_id is not None: + all_rewards = all_rewards.where(GauntletReward.gauntlet_id == gauntlet_id) + if reward_id is not None: + all_rewards = all_rewards.where(GauntletReward.reward_id << reward_id) + if win_num is not None: + all_rewards = all_rewards.where(GauntletReward.win_num == win_num) + if loss_max is not None: + all_rewards = all_rewards.where(GauntletReward.loss_max >= loss_max) + + all_rewards = all_rewards.order_by(-GauntletReward.loss_max, GauntletReward.win_num) + + return_val = {'count': all_rewards.count(), 'rewards': []} + for x in all_rewards: + return_val['rewards'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/api/v1/gauntletrewards/{gauntletreward_id}') +async def v1_gauntletreward_get_one(gauntletreward_id): + try: + this_reward = GauntletReward.get_by_id(gauntletreward_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No gauntlet reward found with id {gauntletreward_id}') + + return_val = model_to_dict(this_reward) + db.close() + return return_val + + +@router.patch('/api/v1/gauntletrewards/{gauntletreward_id}') +async def v1_gauntletreward_patch( + gauntletreward_id, name: Optional[str] = None, gauntlet_id: Optional[int] = None, + reward_id: Optional[int] = None, win_num: Optional[int] = None, loss_max: Optional[int] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch gauntlet rewards. This event has been logged.' + ) + + this_reward = GauntletReward.get_or_none(GauntletReward.id == gauntletreward_id) + if this_reward is None: + db.close() + raise KeyError(f'Gauntlet Reward ID {gauntletreward_id} not found') + + if gauntlet_id is not None: + this_reward.gauntlet_id = gauntlet_id + if reward_id is not None: + this_reward.reward_id = reward_id + if win_num is not None: + this_reward.win_num = win_num + if loss_max is not None: + this_reward.loss_max = loss_max + if name is not None: + this_reward.name = name + + if this_reward.save(): + r_curr = model_to_dict(this_reward) + db.close() + return r_curr + else: + db.close() + raise DatabaseError(f'Unable to patch gauntlet reward {gauntletreward_id}') + + +@router.post('/api/v1/gauntletrewards') +async def v1_gauntletreward_post(gauntletreward: GauntletRewardList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post gauntlets. This event has been logged.' + ) + + all_rewards = [] + for x in gauntletreward.rewards: + all_rewards.append(x.dict()) + + with db.atomic(): + for batch in chunked(all_rewards, 15): + GauntletReward.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(all_rewards)} gauntlet rewards' + + +@router.delete('/api/v1/gauntletrewards/{gauntletreward_id}') +async def v1_gauntletreward_delete(gauntletreward_id): + if GauntletReward.delete_by_id(gauntletreward_id) == 1: + return f'Deleted gauntlet reward ID {gauntletreward_id}' + + raise DatabaseError(f'Unable to delete gauntlet run {gauntletreward_id}') + diff --git a/app/routers_v2/gauntletruns.py b/app/routers_v2/gauntletruns.py new file mode 100644 index 0000000..5b61e33 --- /dev/null +++ b/app/routers_v2/gauntletruns.py @@ -0,0 +1,170 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import Optional +import logging +import pydantic + +from ..db_engine import db, GauntletRun, model_to_dict, DatabaseError +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/gauntletruns', + tags=['notifs'] +) + + +class GauntletRunModel(pydantic.BaseModel): + team_id: int + gauntlet_id: int + wins: Optional[int] = 0 + losses: Optional[int] = 0 + gsheet: Optional[str] = None + created: Optional[int] = int(datetime.timestamp(datetime.now())*1000) + ended: Optional[int] = 0 + + +@router.get('') +async def get_gauntletruns( + team_id: list = Query(default=None), wins: Optional[int] = None, wins_min: Optional[int] = None, + wins_max: Optional[int] = None, losses: Optional[int] = None, losses_min: Optional[int] = None, + losses_max: Optional[int] = None, gsheet: Optional[str] = None, created_after: Optional[int] = None, + created_before: Optional[int] = None, ended_after: Optional[int] = None, ended_before: Optional[int] = None, + is_active: Optional[bool] = None, gauntlet_id: list = Query(default=None), season: list = Query(default=None)): + all_gauntlets = GauntletRun.select() + + if team_id is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.team_id << team_id) + if wins is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.wins == wins) + if wins_min is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.wins >= wins_min) + if wins_max is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.wins <= wins_max) + if losses is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.losses == losses) + if losses_min is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.losses >= losses_min) + if losses_max is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.losses <= losses_max) + if gsheet is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.gsheet == gsheet) + if created_after is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.created >= created_after) + if created_before is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.created <= created_before) + if ended_after is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.ended >= ended_after) + if ended_before is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.ended <= ended_before) + if is_active is not None: + if is_active is True: + all_gauntlets = all_gauntlets.where(GauntletRun.ended == 0) + else: + all_gauntlets = all_gauntlets.where(GauntletRun.ended != 0) + if gauntlet_id is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.gauntlet_id << gauntlet_id) + if season is not None: + all_gauntlets = all_gauntlets.where(GauntletRun.team.season << season) + + return_val = {'count': all_gauntlets.count(), 'runs': []} + for x in all_gauntlets: + return_val['runs'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{gauntletrun_id}') +async def get_one_gauntletrun(gauntletrun_id): + try: + this_gauntlet = GauntletRun.get_by_id(gauntletrun_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No gauntlet found with id {gauntletrun_id}') + + return_val = model_to_dict(this_gauntlet) + db.close() + return return_val + + +@router.patch('/{gauntletrun_id}') +async def patch_gauntletrun( + gauntletrun_id, team_id: Optional[int] = None, wins: Optional[int] = None, losses: Optional[int] = None, + gsheet: Optional[str] = None, created: Optional[bool] = None, ended: Optional[bool] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch gauntlet runs. This event has been logged.' + ) + + this_run = GauntletRun.get_or_none(GauntletRun.id == gauntletrun_id) + if this_run is None: + db.close() + raise KeyError(f'Gauntlet Run ID {gauntletrun_id} not found') + + if team_id is not None: + this_run.team_id = team_id + if wins is not None: + this_run.wins = wins + if losses is not None: + this_run.losses = losses + if gsheet is not None: + this_run.gsheet = gsheet + if created is not None: + if created is True: + this_run.created = int(datetime.timestamp(datetime.now())*1000) + else: + this_run.created = None + if ended is not None: + if ended is True: + this_run.ended = int(datetime.timestamp(datetime.now())*1000) + else: + this_run.ended = 0 + + if this_run.save(): + r_curr = model_to_dict(this_run) + db.close() + return r_curr + else: + db.close() + raise DatabaseError(f'Unable to patch gauntlet run {gauntletrun_id}') + + +@router.post('') +async def post_gauntletrun(gauntletrun: GauntletRunModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post gauntlets. This event has been logged.' + ) + + this_run = GauntletRun(**gauntletrun.dict()) + + if this_run.save(): + r_run = model_to_dict(this_run) + db.close() + return r_run + else: + db.close() + raise DatabaseError(f'Unable to post gauntlet run') + + +@router.delete('/{gauntletrun_id}') +async def delete_gauntletrun(gauntletrun_id): + if GauntletRun.delete_by_id(gauntletrun_id) == 1: + return f'Deleted gauntlet run ID {gauntletrun_id}' + + raise DatabaseError(f'Unable to delete gauntlet run {gauntletrun_id}') + diff --git a/app/routers_v2/notifications.py b/app/routers_v2/notifications.py new file mode 100644 index 0000000..8276b09 --- /dev/null +++ b/app/routers_v2/notifications.py @@ -0,0 +1,203 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Notification, model_to_dict, 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/v2/notifs', + tags=['notifs'] +) + + +class NotifModel(pydantic.BaseModel): + created: int + title: str + desc: Optional[str] = None + field_name: str + message: str + about: Optional[str] = 'blank' + ack: Optional[bool] = False + + +@router.get('') +async def get_notifs( + created_after: Optional[int] = None, title: Optional[str] = None, desc: Optional[str] = None, + field_name: Optional[str] = None, in_desc: Optional[str] = None, about: Optional[str] = None, + ack: Optional[bool] = None, csv: Optional[bool] = None): + all_notif = Notification.select() + + if all_notif.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'There are no notifications to filter') + + if created_after is not None: + all_notif = all_notif.where(Notification.created < created_after) + if title is not None: + all_notif = all_notif.where(Notification.title == title) + if desc is not None: + all_notif = all_notif.where(Notification.desc == desc) + if field_name is not None: + all_notif = all_notif.where(Notification.field_name == field_name) + if in_desc is not None: + all_notif = all_notif.where(fn.Lower(Notification.desc).contains(in_desc.lower())) + if about is not None: + all_notif = all_notif.where(Notification.about == about) + if ack is not None: + all_notif = all_notif.where(Notification.ack == ack) + + if csv: + data_list = [['id', 'created', 'title', 'desc', 'field_name', 'message', 'about', 'ack']] + for line in all_notif: + data_list.append([ + line.id, line.created, line.title, line.desc, line.field_name, line.message, line.about, line.ack + ]) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_notif.count(), 'notifs': []} + for x in all_notif: + return_val['notifs'].append(model_to_dict(x)) + + db.close() + return return_val + + +@router.get('/{notif_id}') +async def get_one_notif(notif_id, csv: Optional[bool] = None): + try: + this_notif = Notification.get_by_id(notif_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No notification found with id {notif_id}') + + if csv: + data_list = [ + ['id', 'created', 'title', 'desc', 'field_name', 'message', 'about', 'ack'], + [this_notif.id, this_notif.created, this_notif.title, this_notif.desc, this_notif.field_name, + this_notif.message, this_notif.about, this_notif.ack] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_notif) + db.close() + return return_val + + +@router.post('') +async def post_notif(notif: NotifModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post notifications. This event has been logged.' + ) + + logging.info(f'new notif: {notif}') + this_notif = Notification( + created=notif.created, + title=notif.title, + desc=notif.desc, + field_name=notif.field_name, + message=notif.message, + about=notif.about, + ) + + saved = this_notif.save() + if saved == 1: + return_val = model_to_dict(this_notif) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that notification' + ) + + +@router.patch('/{notif_id}') +async def patch_notif( + notif_id, created: Optional[int] = None, title: Optional[str] = None, desc: Optional[str] = None, + field_name: Optional[str] = None, message: Optional[str] = None, about: Optional[str] = None, + ack: Optional[bool] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch notifications. This event has been logged.' + ) + try: + this_notif = Notification.get_by_id(notif_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No notification found with id {notif_id}') + + if title is not None: + this_notif.title = title + if desc is not None: + this_notif.desc = desc + if field_name is not None: + this_notif.field_name = field_name + if message is not None: + this_notif.message = message + if about is not None: + this_notif.about = about + if ack is not None: + this_notif.ack = ack + if created is not None: + this_notif.created = created + + if this_notif.save() == 1: + return_val = model_to_dict(this_notif) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.delete('/{notif_id}') +async def delete_notif(notif_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete notifications. This event has been logged.' + ) + try: + this_notif = Notification.get_by_id(notif_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No notification found with id {notif_id}') + + count = this_notif.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Notification {notif_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Notification {notif_id} was not deleted') diff --git a/app/routers_v2/paperdex.py b/app/routers_v2/paperdex.py new file mode 100644 index 0000000..665ae47 --- /dev/null +++ b/app/routers_v2/paperdex.py @@ -0,0 +1,208 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, Paperdex, model_to_dict, fn, Player, Cardset, Team +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/paperdex', + tags=['paperdex'] +) + + +class PaperdexModel(pydantic.BaseModel): + team_id: int + player_id: int + created: Optional[int] = int(datetime.timestamp(datetime.now())*1000) + + +@router.get('') +async def get_paperdex( + team_id: Optional[int] = None, player_id: Optional[int] = None, created_after: Optional[int] = None, + cardset_id: Optional[int] = None, created_before: Optional[int] = None, flat: Optional[bool] = False, + csv: Optional[bool] = None): + all_dex = Paperdex.select().join(Player).join(Cardset) + + if all_dex.count() == 0: + db.close() + raise HTTPException(status_code=404, detail=f'There are no paperdex to filter') + + if team_id is not None: + all_dex = all_dex.where(Paperdex.team_id == team_id) + if player_id is not None: + all_dex = all_dex.where(Paperdex.player_id == player_id) + if cardset_id is not None: + all_sets = Cardset.select().where(Cardset.id == cardset_id) + all_dex = all_dex.where(Paperdex.player.cardset.id == cardset_id) + if created_after is not None: + all_dex = all_dex.where(Paperdex.created >= created_after) + if created_before is not None: + all_dex = all_dex.where(Paperdex.created <= created_before) + + # if all_dex.count() == 0: + # db.close() + # raise HTTPException(status_code=404, detail=f'No paperdex found') + + if csv: + data_list = [['id', 'team_id', 'player_id', 'created']] + for line in all_dex: + data_list.append( + [ + line.id, line.team.id, line.player.player_id, line.created + ] + ) + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_dex.count(), 'paperdex': []} + for x in all_dex: + return_val['paperdex'].append(model_to_dict(x, recurse=not flat)) + + db.close() + return return_val + + +@router.get('/{paperdex_id}') +async def get_one_paperdex(paperdex_id, csv: Optional[bool] = False): + try: + this_dex = Paperdex.get_by_id(paperdex_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}') + + if csv: + data_list = [ + ['id', 'team_id', 'player_id', 'created'], + [this_dex.id, this_dex.team.id, this_dex.player.id, this_dex.created] + ] + return_val = DataFrame(data_list).to_csv(header=False, index=False) + + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = model_to_dict(this_dex) + db.close() + return return_val + + +@router.post('') +async def post_paperdex(paperdex: PaperdexModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post paperdex. This event has been logged.' + ) + + dupe_dex = Paperdex.get_or_none(Paperdex.team_id == paperdex.team_id, Paperdex.player_id == paperdex.player_id) + if dupe_dex: + return_val = model_to_dict(dupe_dex) + db.close() + return return_val + + this_dex = Paperdex( + team_id=paperdex.team_id, + player_id=paperdex.player_id, + created=paperdex.created + ) + + saved = this_dex.save() + if saved == 1: + return_val = model_to_dict(this_dex) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that dex' + ) + + +@router.patch('/{paperdex_id}') +async def patch_paperdex( + paperdex_id, team_id: Optional[int] = None, player_id: Optional[int] = None, created: Optional[int] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch paperdex. This event has been logged.' + ) + try: + this_dex = Paperdex.get_by_id(paperdex_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}') + + if team_id is not None: + this_dex.team_id = team_id + if player_id is not None: + this_dex.player_id = player_id + if created is not None: + this_dex.created = created + + if this_dex.save() == 1: + return_val = model_to_dict(this_dex) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that rarity' + ) + + +@router.delete('/{paperdex_id}') +async def delete_paperdex(paperdex_id, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete rewards. This event has been logged.' + ) + try: + this_dex = Paperdex.get_by_id(paperdex_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}') + + count = this_dex.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Paperdex {this_dex} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Paperdex {this_dex} was not deleted') + + +@router.post('/wipe-ai') +async def wipe_ai_paperdex(token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='Unauthorized' + ) + + g_teams = Team.select().where(Team.abbrev.contains('Gauntlet')) + count = Paperdex.delete().where(Paperdex.team << g_teams).execute() + return f'Deleted {count} records' From 4a7a8ad39707ac893a856591b6627ea4da8a1cc4 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Thu, 14 Sep 2023 22:11:31 -0500 Subject: [PATCH 07/40] Import cleanup --- app/main.py | 4 ---- app/routers_v2/admin.py | 9 ++------- app/routers_v2/awards.py | 2 +- app/routers_v2/cards.py | 4 +--- app/routers_v2/events.py | 9 +++------ app/routers_v2/gamerewards.py | 4 +--- app/routers_v2/gauntletruns.py | 1 - app/routers_v2/notifications.py | 2 -- app/routers_v2/packs.py | 2 +- app/routers_v2/paperdex.py | 3 +-- app/routers_v2/pitstats.py | 3 +-- app/routers_v2/results.py | 6 ++---- app/routers_v2/rewards.py | 1 - app/routers_v2/teams.py | 6 ++---- 14 files changed, 15 insertions(+), 41 deletions(-) diff --git a/app/main.py b/app/main.py index 374aeff..58d089b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,3 @@ -import datetime -import logging -import os - from fastapi import FastAPI from.routers_v2 import current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, \ diff --git a/app/routers_v2/admin.py b/app/routers_v2/admin.py index 540b8d6..b3b84be 100644 --- a/app/routers_v2/admin.py +++ b/app/routers_v2/admin.py @@ -1,12 +1,7 @@ -from datetime import datetime - -from fastapi import APIRouter, Depends, HTTPException, Response -from typing import Optional +from fastapi import APIRouter, Depends, HTTPException import logging -import pydantic -from pandas import DataFrame -from ..db_engine import db, Player, model_to_dict, fn +from ..db_engine import db, Player from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( diff --git a/app/routers_v2/awards.py b/app/routers_v2/awards.py index 040126e..8d5b85a 100644 --- a/app/routers_v2/awards.py +++ b/app/routers_v2/awards.py @@ -4,7 +4,7 @@ import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Award, model_to_dict, fn +from ..db_engine import db, Award, model_to_dict from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( diff --git a/app/routers_v2/cards.py b/app/routers_v2/cards.py index f1f8e7a..ca2d64d 100644 --- a/app/routers_v2/cards.py +++ b/app/routers_v2/cards.py @@ -1,12 +1,10 @@ -from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Response, Query from typing import Optional, List import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Card, model_to_dict, fn, Team, Player, Pack, Paperdex +from ..db_engine import db, Card, model_to_dict, Team, Player, Pack, Paperdex from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( diff --git a/app/routers_v2/events.py b/app/routers_v2/events.py index 87f77b4..539014d 100644 --- a/app/routers_v2/events.py +++ b/app/routers_v2/events.py @@ -1,14 +1,11 @@ -from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Response -from typing import List, Optional, Literal -import copy +from typing import Optional import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Event, model_to_dict, chunked, fn -from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp +from ..db_engine import db, Event, model_to_dict, fn +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( filename=LOG_DATA['filename'], diff --git a/app/routers_v2/gamerewards.py b/app/routers_v2/gamerewards.py index 04cb328..5fbf6bc 100644 --- a/app/routers_v2/gamerewards.py +++ b/app/routers_v2/gamerewards.py @@ -1,12 +1,10 @@ -from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Response from typing import Optional import logging import pydantic from pandas import DataFrame -from ..db_engine import db, GameRewards, model_to_dict, fn +from ..db_engine import db, GameRewards, model_to_dict from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( diff --git a/app/routers_v2/gauntletruns.py b/app/routers_v2/gauntletruns.py index 5b61e33..c8fecf9 100644 --- a/app/routers_v2/gauntletruns.py +++ b/app/routers_v2/gauntletruns.py @@ -1,5 +1,4 @@ from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Query from typing import Optional import logging diff --git a/app/routers_v2/notifications.py b/app/routers_v2/notifications.py index 8276b09..4fcfa3d 100644 --- a/app/routers_v2/notifications.py +++ b/app/routers_v2/notifications.py @@ -1,5 +1,3 @@ -from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Response from typing import Optional import logging diff --git a/app/routers_v2/packs.py b/app/routers_v2/packs.py index 38a01c0..aec29df 100644 --- a/app/routers_v2/packs.py +++ b/app/routers_v2/packs.py @@ -6,7 +6,7 @@ import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Cardset, model_to_dict, fn, Pack, Team, PackType +from ..db_engine import db, Cardset, model_to_dict, Pack, Team, PackType from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( diff --git a/app/routers_v2/paperdex.py b/app/routers_v2/paperdex.py index 665ae47..5ea709e 100644 --- a/app/routers_v2/paperdex.py +++ b/app/routers_v2/paperdex.py @@ -1,12 +1,11 @@ from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Response from typing import Optional import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Paperdex, model_to_dict, fn, Player, Cardset, Team +from ..db_engine import db, Paperdex, model_to_dict, Player, Cardset, Team from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( diff --git a/app/routers_v2/pitstats.py b/app/routers_v2/pitstats.py index b873520..79e5e88 100644 --- a/app/routers_v2/pitstats.py +++ b/app/routers_v2/pitstats.py @@ -1,12 +1,11 @@ from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Response from typing import Optional, List import logging import pydantic from pandas import DataFrame -from ..db_engine import db, PitchingStat, model_to_dict, fn, Card, Player +from ..db_engine import db, PitchingStat, model_to_dict, Card, Player from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( diff --git a/app/routers_v2/results.py b/app/routers_v2/results.py index 379fbb7..7ec0de2 100644 --- a/app/routers_v2/results.py +++ b/app/routers_v2/results.py @@ -1,12 +1,10 @@ -from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Response -from typing import Optional, List +from typing import Optional import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Result, model_to_dict, fn, Team, DataError +from ..db_engine import db, Result, model_to_dict, Team, DataError from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( diff --git a/app/routers_v2/rewards.py b/app/routers_v2/rewards.py index 2d8abf2..26c8670 100644 --- a/app/routers_v2/rewards.py +++ b/app/routers_v2/rewards.py @@ -1,5 +1,4 @@ from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Response from typing import Optional import logging diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index 2a0a930..4280492 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -1,13 +1,11 @@ from datetime import datetime - from fastapi import APIRouter, Depends, HTTPException, Response -from typing import List, Optional, Literal -import copy +from typing import Optional import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Team, model_to_dict, chunked, fn, Pack, Card, Player, Paperdex, Notification, PackType, \ +from ..db_engine import db, Team, model_to_dict, fn, Pack, Card, Player, Paperdex, Notification, PackType, \ Rarity, Current from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp From be5bda66528d4081f3e6f07adf931c7b815d74f7 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 15 Sep 2023 00:01:30 -0500 Subject: [PATCH 08/40] Add battingcards & bugfixes --- app/dependencies.py | 2 +- app/routers_v2/battingcard.py | 166 ++++++++++++++++++++++++++++++++++ app/routers_v2/results.py | 4 +- 3 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 app/routers_v2/battingcard.py diff --git a/app/dependencies.py b/app/dependencies.py index eec6f22..8bce8f8 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -27,4 +27,4 @@ def valid_token(token): def int_timestamp(datetime_obj: datetime) -> int: - return int(datetime.timestamp(datetime_obj) * 1000) + return int(datetime.datetime.timestamp(datetime_obj) * 1000) diff --git a/app/routers_v2/battingcard.py b/app/routers_v2/battingcard.py new file mode 100644 index 0000000..6eea906 --- /dev/null +++ b/app/routers_v2/battingcard.py @@ -0,0 +1,166 @@ +from fastapi import APIRouter, Depends, HTTPException, Response, Query +from typing import Literal, Optional, List +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, BattingCard, model_to_dict, fn, chunked +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/battingcard', + tags=['battingcard'] +) + + +class BattingCardModel(pydantic.BaseModel): + player_id: int + steal_low: int = 3 + steal_high: int = 20 + steal_auto: bool = False + steal_jump: float = 0 + bunting: str = 'C' + hit_and_run: str = 'C' + running: int = 10 + offense_col: int + hand: Literal['R', 'L', 'S'] + + +class BattingCardList(pydantic.BaseModel): + cards: List[BattingCardModel] + + +@router.get('') +async def get_batting_cards(player_id: list = Query(default=None), short_output: bool = False): + all_cards = BattingCard.select() + if player_id is not None: + all_cards = all_cards.where(BattingCard.player_id << player_id) + + return_val = {'count': all_cards.count(), 'cards': [ + {model_to_dict(x, recurse=not short_output)} for x in all_cards + ]} + db.close() + return return_val + + +@router.get('/{card_id}') +async def get_one_card(card_id: int): + this_card = BattingCard.get_or_none(BattingCard.id == card_id) + if this_card is None: + db.close() + raise HTTPException(status_code=404, detail=f'BattingCard id {card_id} not found') + + r_card = model_to_dict(this_card) + db.close() + return r_card + + +@router.get('/player/{player_id}') +async def get_player_cards(player_id: int, variant: list = Query(default=None), short_output: bool = False): + all_cards = BattingCard.select().where(BattingCard.player_id == player_id).order_by(BattingCard.variant) + if variant is not None: + all_cards = all_cards.where(BattingCard.variant << variant) + + return_val = {'count': all_cards.count(), 'cards': [ + {model_to_dict(x, recurse=not short_output) for x in all_cards} + ]} + db.close() + return return_val + + +@router.put('') +async def put_cards(cards: BattingCardList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post batting cards. This event has been logged.' + ) + + new_cards = [x.dict() for x in cards.cards] + with db.atomic(): + for batch in chunked(new_cards, 30): + BattingCard.insert_many(batch).on_conflict_replace().execute() + + db.close() + return f'Inserted {len(new_cards)} batting cards' + + +@router.patch('/{card_id}') +async def patch_card( + card_id: int, steal_low: Optional[int] = None, steal_high: Optional[int] = None, + steal_auto: Optional[bool] = None, steal_jump: Optional[float] = None, bunting: Optional[str] = None, + hit_and_run: Optional[str] = None, running: Optional[int] = None, offense_col: Optional[int] = None, + hand: Literal['R', 'L', 'S'] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch batting cards. This event has been logged.' + ) + + this_card = BattingCard.get_or_none(BattingCard.id == card_id) + if this_card is None: + db.close() + raise HTTPException(status_code=404, detail=f'BattingCard id {card_id} not found') + + if steal_low is not None: + this_card.steal_low = steal_low + if steal_high is not None: + this_card.steal_high = steal_high + if steal_auto is not None: + this_card.steal_auto = steal_auto + if steal_jump is not None: + this_card.steal_jump = steal_jump + if bunting is not None: + this_card.bunting = bunting + if hit_and_run is not None: + this_card.hit_and_run = hit_and_run + if running is not None: + this_card.running = running + if offense_col is not None: + this_card.offense_col = offense_col + if hand is not None: + this_card.hand = hand + + if this_card.save() == 1: + return_val = model_to_dict(this_card) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that card' + ) + + +@router.delete('/{card_id}') +async def delete_card(card_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch batting cards. This event has been logged.' + ) + + this_card = BattingCard.get_or_none(BattingCard.id == card_id) + if this_card is None: + db.close() + raise HTTPException(status_code=404, detail=f'BattingCard id {card_id} not found') + + count = this_card.delete_instance() + db.close() + + if count == 1: + return f'Card {this_card} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Card {this_card} could not be deleted') diff --git a/app/routers_v2/results.py b/app/routers_v2/results.py index 7ec0de2..557e04e 100644 --- a/app/routers_v2/results.py +++ b/app/routers_v2/results.py @@ -338,7 +338,7 @@ async def post_result(result: ResultModel, token: str = Depends(oauth2_scheme)): ) -@app.patch('/api/v1/results/{result_id}') +@router.patch('/{result_id}') async def patch_result( result_id, away_team_id: Optional[int] = None, home_team_id: Optional[int] = None, away_score: Optional[int] = None, home_score: Optional[int] = None, away_team_value: Optional[int] = None, @@ -406,7 +406,7 @@ async def patch_result( ) -@app.delete('/api/v1/results/{result_id}') +@router.delete('/{result_id}') async def delete_result(result_id, token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'Bad Token: {token}') From 88b723d9f2eeee9852788075d3277fa92a71ab19 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 15 Sep 2023 00:01:59 -0500 Subject: [PATCH 09/40] Added query_to_csv helper --- app/db_engine.py | 28 ++++++++++++++++++++++++++++ app/main.py | 3 ++- app/routers_v2/teams.py | 27 +++++++++++++-------------- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/app/db_engine.py b/app/db_engine.py index e07ad27..25df88a 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -1,9 +1,12 @@ import math from datetime import datetime +from typing import List import logging import os +from pandas import DataFrame from peewee import * +from peewee import ModelSelect from playhouse.shortcuts import model_to_dict db = SqliteDatabase( @@ -24,6 +27,27 @@ logging.basicConfig( ) +def model_csv_headers(this_obj, exclude=None) -> List: + if this_obj is None: + return ['None'] + + data = model_to_dict(this_obj, recurse=False, exclude=exclude) + return [x for x in data.keys()] + + +def model_to_csv(this_obj, exclude=None) -> List: + data = model_to_dict(this_obj, recurse=False, exclude=exclude) + return [x for x in data.values()] + + +def query_to_csv(all_items: ModelSelect, exclude=None): + data_list = [model_csv_headers(all_items[0], exclude=exclude)] + for x in all_items: + data_list.append(model_to_csv(x, exclude=exclude)) + + return DataFrame(data_list).to_csv(header=False, index=False) + + class BaseModel(Model): class Meta: database = db @@ -485,6 +509,7 @@ db.create_tables([ class BattingCard(BaseModel): player = ForeignKeyField(Player) + variant = IntegerField() steal_low = IntegerField() steal_high = IntegerField() steal_auto = BooleanField() @@ -496,6 +521,9 @@ class BattingCard(BaseModel): hand = CharField(default='R') +BattingCard.add_index(BattingCard.player, BattingCard.variant) + + class BattingCardRatings(BaseModel): battingcard = ForeignKeyField(BattingCard) vs_hand = FloatField() diff --git a/app/main.py b/app/main.py index 58d089b..d972853 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from.routers_v2 import current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, \ - batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns + batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcard app = FastAPI( responses={404: {'description': 'Not found'}} @@ -25,3 +25,4 @@ app.include_router(paperdex.router) app.include_router(gamerewards.router) app.include_router(gauntletrewards.router) app.include_router(gauntletruns.router) +app.include_router(battingcard.router) diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index 4280492..5badf72 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -6,7 +6,7 @@ import pydantic from pandas import DataFrame from ..db_engine import db, Team, model_to_dict, fn, Pack, Card, Player, Paperdex, Notification, PackType, \ - Rarity, Current + Rarity, Current, query_to_csv from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp logging.basicConfig( @@ -121,19 +121,18 @@ async def get_teams( # raise HTTPException(status_code=404, detail=f'No teams found') if csv: - data_list = [[ - 'id', 'abbrev', 'sname', 'lname', 'gmid', 'gmname', 'wallet', 'gsheet', 'team_value', - 'collection_value', 'logo', 'color', 'season', 'ranking' - ]] - for line in all_teams: - data_list.append( - [ - line.id, line.abbrev, line.sname, line.lname, line.gmid, line.gmname, line.wallet, line.gsheet, - line.team_value, line.collection_value, line.logo, f'\'{line.color}', line.season, line.ranking - ] - ) - return_val = DataFrame(data_list).to_csv(header=False, index=False) - + # data_list = [[ + # 'id', 'abbrev', 'sname', 'lname', 'gmid', 'gmname', 'wallet', 'gsheet', 'team_value', + # 'collection_value', 'logo', 'color', 'season', 'ranking' + # ]] + # for line in all_teams: + # data_list.append( + # [ + # line.id, line.abbrev, line.sname, line.lname, line.gmid, line.gmname, line.wallet, line.gsheet, + # line.team_value, line.collection_value, line.logo, f'\'{line.color}', line.season, line.ranking + # ] + # ) + return_val = query_to_csv(all_teams, exclude=[Team.career]) db.close() return Response(content=return_val, media_type='text/csv') From 22cc01d2005ded2dfc4ec837c77e2921c26a247a Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 15 Sep 2023 22:38:15 -0500 Subject: [PATCH 10/40] Added battingcards and ratings --- app/db_engine.py | 58 +++++- app/main.py | 9 +- app/routers_v2/battingcardratings.py | 182 ++++++++++++++++++ .../{battingcard.py => battingcards.py} | 22 ++- app/routers_v2/teams.py | 39 +--- 5 files changed, 260 insertions(+), 50 deletions(-) create mode 100644 app/routers_v2/battingcardratings.py rename app/routers_v2/{battingcard.py => battingcards.py} (87%) diff --git a/app/db_engine.py b/app/db_engine.py index 25df88a..a8884fe 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -28,9 +28,6 @@ logging.basicConfig( def model_csv_headers(this_obj, exclude=None) -> List: - if this_obj is None: - return ['None'] - data = model_to_dict(this_obj, recurse=False, exclude=exclude) return [x for x in data.keys()] @@ -41,9 +38,47 @@ def model_to_csv(this_obj, exclude=None) -> List: def query_to_csv(all_items: ModelSelect, exclude=None): - data_list = [model_csv_headers(all_items[0], exclude=exclude)] - for x in all_items: - data_list.append(model_to_csv(x, exclude=exclude)) + if all_items.count() == 0: + data_list = [['No data found']] + else: + data_list = [model_csv_headers(all_items[0], exclude=exclude)] + for x in all_items: + data_list.append(model_to_csv(x, exclude=exclude)) + + return DataFrame(data_list).to_csv(header=False, index=False) + + +def complex_data_to_csv(complex_data: List): + if len(complex_data) == 0: + data_list = [['No data found']] + else: + data_list = [[x for x in complex_data[0].keys()]] + for line in complex_data: + logging.info(f'line: {line}') + this_row = [] + for key in line: + logging.info(f'key: {key}') + if line[key] is None: + this_row.append('') + + elif isinstance(line[key], dict): + if 'name' in line[key]: + this_row.append(line[key]['name']) + elif 'abbrev' in line[key]: + this_row.append(line[key]['abbrev']) + else: + this_row.append(line[key]['id']) + + elif isinstance(line[key], int) and line[key] > 100000000: + this_row.append(f"'{line[key]}") + + elif isinstance(line[key], str) and ',' in line[key]: + this_row.append(line[key].replace(",", "-_-")) + + else: + this_row.append(line[key]) + + data_list.append(this_row) return DataFrame(data_list).to_csv(header=False, index=False) @@ -526,7 +561,7 @@ BattingCard.add_index(BattingCard.player, BattingCard.variant) class BattingCardRatings(BaseModel): battingcard = ForeignKeyField(BattingCard) - vs_hand = FloatField() + vs_hand = CharField(default='R') homerun = FloatField() bp_homerun = FloatField() triple = FloatField() @@ -554,8 +589,12 @@ class BattingCardRatings(BaseModel): slg = FloatField(null=True) +BattingCardRatings.add_index(BattingCardRatings.battingcard, BattingCardRatings.vs_hand) + + class PitchingCard(BaseModel): player = ForeignKeyField(Player) + variant = IntegerField() balk = IntegerField() wild_pitch = IntegerField(null=True) hold = CharField() @@ -565,9 +604,12 @@ class PitchingCard(BaseModel): batting = CharField(null=True) +PitchingCard.add_index(PitchingCard.player, PitchingCard.variant) + + class PitchingCardRatings(BaseModel): pitchingcard = ForeignKeyField(PitchingCard) - vs_hand = CharField() + vs_hand = CharField(default='R') homerun = FloatField() bp_homerun = FloatField() triple = FloatField() diff --git a/app/main.py b/app/main.py index d972853..dc69140 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,9 @@ from fastapi import FastAPI -from.routers_v2 import current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, \ - batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcard +from.routers_v2 import ( + current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, + batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards, + battingcardratings) app = FastAPI( responses={404: {'description': 'Not found'}} @@ -25,4 +27,5 @@ app.include_router(paperdex.router) app.include_router(gamerewards.router) app.include_router(gauntletrewards.router) app.include_router(gauntletruns.router) -app.include_router(battingcard.router) +app.include_router(battingcards.router) +app.include_router(battingcardratings.router) diff --git a/app/routers_v2/battingcardratings.py b/app/routers_v2/battingcardratings.py new file mode 100644 index 0000000..0fe649b --- /dev/null +++ b/app/routers_v2/battingcardratings.py @@ -0,0 +1,182 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import Literal, Optional, List +import logging +import pydantic +from pydantic import validator, root_validator + +from ..db_engine import db, BattingCardRatings, model_to_dict, chunked, BattingCard +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/battingcardratings', + tags=['battingcardratings'] +) + + +class BattingCardRatingsModel(pydantic.BaseModel): + battingcard_id: int + vs_hand: Literal['R', 'L', 'vR', 'vL'] + homerun: float = 0.0 + bp_homerun: float = 0.0 + triple: float = 0.0 + double_three: float = 0.0 + double_two: float = 0.0 + double_pull: float = 0.0 + single_two: float = 0.0 + single_one: float = 0.0 + single_center: float = 0.0 + bp_single: float = 0.0 + hbp: float = 0.0 + walk: float = 0.0 + strikeout: float = 0.0 + lineout: float = 0.0 + popout: float = 0.0 + flyout_a: float = 0.0 + flyout_bq: float = 0.0 + flyout_lf_b: float = 0.0 + flyout_rf_b: float = 0.0 + groundout_a: float = 0.0 + groundout_b: float = 0.0 + groundout_c: float = 0.0 + avg: float = 0.0 + obp: float = 0.0 + slg: float = 0.0 + + @validator("avg", always=True) + def avg_validator(cls, v, values, **kwargs): + return (values['homerun'] + values['bp_homerun'] / 2 + values['triple'] + values['double_three'] + + values['double_two'] + values['double_pull'] + values['single_two'] + values['single_one'] + + values['single_center'] + values['bp_single'] / 2) / 108 + + @validator("obp", always=True) + def obp_validator(cls, v, values, **kwargs): + return ((values['hbp'] + values['walk']) / 108) + values['avg'] + + @validator("slg", always=True) + def slg_validator(cls, v, values, **kwargs): + return (values['homerun'] * 4 + values['bp_homerun'] * 2 + values['triple'] * 3 + values['double_three'] * 2 + + values['double_two'] * 2 + values['double_pull'] * 2 + values['single_two'] + values['single_one'] + + values['single_center'] + values['bp_single'] / 2) / 108 + + @root_validator + def validate_chance_total(cls, values): + total_chances = ( + values['homerun'] + values['bp_homerun'] + values['triple'] + values['double_three'] + + values['double_two'] + values['double_pull'] + values['single_two'] + values['single_one'] + + values['single_center'] + values['bp_single'] + values['hbp'] + values['walk'] + + values['strikeout'] + values['lineout'] + values['popout'] + values['flyout_a'] + + values['flyout_bq'] + values['flyout_lf_b'] + values['flyout_rf_b'] + values['groundout_a'] + + values['groundout_b'] + values['groundout_c']) + + if total_chances != 108: + raise ValueError("Must have exactly 108 chances on the card") + return values + + +class RatingsList(pydantic.BaseModel): + ratings: List[BattingCardRatingsModel] + + +@router.get('') +async def get_card_ratings( + battingcard_id: list = Query(default=None), vs_hand: Literal['R', 'L', 'vR', 'vL'] = None, + short_output: bool = False, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to pull card ratings.' + ) + + all_ratings = BattingCardRatings.select() + + if battingcard_id is not None: + all_ratings = all_ratings.where(BattingCardRatings.battingcard_id << battingcard_id) + if vs_hand is not None: + all_ratings = all_ratings.where(BattingCardRatings.vs_hand << vs_hand[-1]) + + return_val = {'count': all_ratings.count(), 'ratings': [ + model_to_dict(x, recurse=not short_output) for x in all_ratings + ]} + db.close() + return return_val + + +@router.get('/{ratings_id}') +async def get_one_rating(ratings_id: int): + this_rating = BattingCardRatings.get_or_none(BattingCardRatings.id == ratings_id) + if this_rating is None: + db.close() + raise HTTPException(status_code=404, detail=f'BattingCardRating id {ratings_id} not found') + + r_data = model_to_dict(this_rating) + db.close() + return r_data + + +@router.get('') +async def get_player_ratings(player_id: int, variant: list = Query(default=None), short_output: bool = False): + all_cards = BattingCard.select().where(BattingCard.player_id == player_id).order_by(BattingCard.variant) + if variant is not None: + all_cards = all_cards.where(BattingCard.variant << variant) + + all_ratings = BattingCardRatings.select().where(BattingCardRatings.battingcard << all_cards) + + return_val = {'count': all_ratings.count(), 'ratings': [ + model_to_dict(x, recurse=not short_output) for x in all_ratings + ]} + db.close() + return return_val + + +@router.put('') +async def put_ratings(ratings: RatingsList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post card ratings.' + ) + + new_ratings = [x.dict() for x in ratings.ratings] + with db.atomic(): + for batch in chunked(new_ratings, 30): + BattingCardRatings.insert_many(batch).on_conflict_replace().execute() + + db.close() + return f'Inserted {len(new_ratings)} batting ratings' + + +@router.delete('/{ratings_id}') +async def put_one_rating( + ratings_id: int, this_rating: BattingCardRatingsModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post card ratings.' + ) + + this_rating = BattingCardRatings.get_or_none(BattingCardRatings.id == ratings_id) + if this_rating is None: + db.close() + raise HTTPException(status_code=404, detail=f'BattingCardRating id {ratings_id} not found') + + count = this_rating.delete_instance() + db.close() + + if count == 1: + return f'Rating {this_rating} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Rating {this_rating} could not be deleted') + + diff --git a/app/routers_v2/battingcard.py b/app/routers_v2/battingcards.py similarity index 87% rename from app/routers_v2/battingcard.py rename to app/routers_v2/battingcards.py index 6eea906..7753e45 100644 --- a/app/routers_v2/battingcard.py +++ b/app/routers_v2/battingcards.py @@ -1,10 +1,9 @@ -from fastapi import APIRouter, Depends, HTTPException, Response, Query +from fastapi import APIRouter, Depends, HTTPException, Query from typing import Literal, Optional, List import logging import pydantic -from pandas import DataFrame -from ..db_engine import db, BattingCard, model_to_dict, fn, chunked +from ..db_engine import db, BattingCard, model_to_dict, chunked, Player from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -14,13 +13,14 @@ logging.basicConfig( ) router = APIRouter( - prefix='/api/v2/battingcard', - tags=['battingcard'] + prefix='/api/v2/battingcards', + tags=['battingcards'] ) class BattingCardModel(pydantic.BaseModel): player_id: int + variant: int = 0 steal_low: int = 3 steal_high: int = 20 steal_auto: bool = False @@ -29,7 +29,7 @@ class BattingCardModel(pydantic.BaseModel): hit_and_run: str = 'C' running: int = 10 offense_col: int - hand: Literal['R', 'L', 'S'] + hand: Literal['R', 'L', 'S'] = 'R' class BattingCardList(pydantic.BaseModel): @@ -37,13 +37,17 @@ class BattingCardList(pydantic.BaseModel): @router.get('') -async def get_batting_cards(player_id: list = Query(default=None), short_output: bool = False): +async def get_batting_cards( + player_id: list = Query(default=None), cardset_id: list = Query(default=None), short_output: bool = False): all_cards = BattingCard.select() if player_id is not None: all_cards = all_cards.where(BattingCard.player_id << player_id) + if cardset_id is not None: + all_players = Player.select().where(Player.cardset_id << cardset_id) + all_cards = all_cards.where(BattingCard.player << all_players) return_val = {'count': all_cards.count(), 'cards': [ - {model_to_dict(x, recurse=not short_output)} for x in all_cards + model_to_dict(x, recurse=not short_output) for x in all_cards ]} db.close() return return_val @@ -68,7 +72,7 @@ async def get_player_cards(player_id: int, variant: list = Query(default=None), all_cards = all_cards.where(BattingCard.variant << variant) return_val = {'count': all_cards.count(), 'cards': [ - {model_to_dict(x, recurse=not short_output) for x in all_cards} + model_to_dict(x, recurse=not short_output) for x in all_cards ]} db.close() return return_val diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index 5badf72..7c83881 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -6,7 +6,7 @@ import pydantic from pandas import DataFrame from ..db_engine import db, Team, model_to_dict, fn, Pack, Card, Player, Paperdex, Notification, PackType, \ - Rarity, Current, query_to_csv + Rarity, Current, query_to_csv, complex_data_to_csv from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp logging.basicConfig( @@ -116,22 +116,7 @@ async def get_teams( if limit is not None: all_teams = all_teams.limit(limit) - # if all_teams.count() == 0: - # db.close() - # raise HTTPException(status_code=404, detail=f'No teams found') - if csv: - # data_list = [[ - # 'id', 'abbrev', 'sname', 'lname', 'gmid', 'gmname', 'wallet', 'gsheet', 'team_value', - # 'collection_value', 'logo', 'color', 'season', 'ranking' - # ]] - # for line in all_teams: - # data_list.append( - # [ - # line.id, line.abbrev, line.sname, line.lname, line.gmid, line.gmname, line.wallet, line.gsheet, - # line.team_value, line.collection_value, line.logo, f'\'{line.color}', line.season, line.ranking - # ] - # ) return_val = query_to_csv(all_teams, exclude=[Team.career]) db.close() return Response(content=return_val, media_type='text/csv') @@ -153,23 +138,17 @@ async def get_one_team(team_id, csv: Optional[bool] = False): db.close() raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + p_query = Pack.select().where((Pack.team == this_team) & (Pack.open_time.is_null(True))) if csv: - team_packs = Pack.select().where((Pack.team == this_team) & (Pack.open_time.is_null(True))) - data_list = [ - ['id', 'abbrev', 'sname', 'lname', 'gmid', 'gmname', 'wallet', 'ranking', 'gsheet', 'sealed_packs', - 'collection_value', 'logo', 'color', 'season'], - [this_team.id, this_team.abbrev, this_team.sname, this_team.lname, this_team.gmid, this_team.gmname, - this_team.wallet, this_team.ranking, this_team.gsheet, team_packs.count(), this_team.collection_value, - this_team.logo, this_team.color, this_team.season] - ] - return_val = DataFrame(data_list).to_csv(header=False, index=False) - - db.close() - return Response(content=return_val, media_type='text/csv') + data = model_to_dict(this_team) + data['sealed_packs'] = p_query.count() + return_val = complex_data_to_csv([data]) else: return_val = model_to_dict(this_team) - db.close() - return return_val + return_val['sealed_packs'] = [model_to_dict(x) for x in p_query] + + db.close() + return return_val @router.get('/{team_id}/buy/players') From 51a5251c9276bbd8c88d5997136bc576318159cf Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 16 Sep 2023 00:05:10 -0500 Subject: [PATCH 11/40] Add pitchercards and ratings --- app/db_engine.py | 19 ++- app/main.py | 4 +- app/routers_v2/battingcardratings.py | 14 +- app/routers_v2/battingcards.py | 1 + app/routers_v2/pitchingcardratings.py | 194 ++++++++++++++++++++++++++ app/routers_v2/pitchingcards.py | 166 ++++++++++++++++++++++ 6 files changed, 389 insertions(+), 9 deletions(-) create mode 100644 app/routers_v2/pitchingcardratings.py create mode 100644 app/routers_v2/pitchingcards.py diff --git a/app/db_engine.py b/app/db_engine.py index a8884fe..1a1f6d9 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -556,7 +556,8 @@ class BattingCard(BaseModel): hand = CharField(default='R') -BattingCard.add_index(BattingCard.player, BattingCard.variant) +bc_index = ModelIndex(BattingCard, (BattingCard.player, BattingCard.variant), unique=True) +BattingCard.add_index(bc_index) class BattingCardRatings(BaseModel): @@ -589,22 +590,26 @@ class BattingCardRatings(BaseModel): slg = FloatField(null=True) -BattingCardRatings.add_index(BattingCardRatings.battingcard, BattingCardRatings.vs_hand) +bcr_index = ModelIndex(BattingCardRatings, (BattingCardRatings.battingcard, BattingCardRatings.vs_hand), unique=True) +BattingCardRatings.add_index(bcr_index) class PitchingCard(BaseModel): player = ForeignKeyField(Player) variant = IntegerField() balk = IntegerField() - wild_pitch = IntegerField(null=True) - hold = CharField() + wild_pitch = IntegerField() + hold = IntegerField() starter_rating = IntegerField() relief_rating = IntegerField() closer_rating = IntegerField(null=True) batting = CharField(null=True) + offense_col = IntegerField() + hand = CharField(default='R') -PitchingCard.add_index(PitchingCard.player, PitchingCard.variant) +pc_index = ModelIndex(PitchingCard, (PitchingCard.player, PitchingCard.variant), unique=True) +PitchingCard.add_index(bc_index) class PitchingCardRatings(BaseModel): @@ -641,6 +646,10 @@ class PitchingCardRatings(BaseModel): slg = FloatField(null=True) +pcr_index = ModelIndex(PitchingCardRatings, (PitchingCardRatings.pitchingcard, PitchingCardRatings.vs_hand), unique=True) +PitchingCardRatings.add_index(pcr_index) + + class CardPosition(BaseModel): player = ForeignKeyField(Player) batting = ForeignKeyField(BattingCard, null=True) diff --git a/app/main.py b/app/main.py index dc69140..5293faa 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI from.routers_v2 import ( current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards, - battingcardratings) + battingcardratings, pitchingcards, pitchingcardratings) app = FastAPI( responses={404: {'description': 'Not found'}} @@ -29,3 +29,5 @@ app.include_router(gauntletrewards.router) app.include_router(gauntletruns.router) app.include_router(battingcards.router) app.include_router(battingcardratings.router) +app.include_router(pitchingcards.router) +app.include_router(pitchingcardratings.router) diff --git a/app/routers_v2/battingcardratings.py b/app/routers_v2/battingcardratings.py index 0fe649b..a378014 100644 --- a/app/routers_v2/battingcardratings.py +++ b/app/routers_v2/battingcardratings.py @@ -110,7 +110,15 @@ async def get_card_ratings( @router.get('/{ratings_id}') -async def get_one_rating(ratings_id: int): +async def get_one_rating(ratings_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to pull card ratings.' + ) + this_rating = BattingCardRatings.get_or_none(BattingCardRatings.id == ratings_id) if this_rating is None: db.close() @@ -121,7 +129,7 @@ async def get_one_rating(ratings_id: int): return r_data -@router.get('') +@router.get('/{player_id}') async def get_player_ratings(player_id: int, variant: list = Query(default=None), short_output: bool = False): all_cards = BattingCard.select().where(BattingCard.player_id == player_id).order_by(BattingCard.variant) if variant is not None: @@ -157,7 +165,7 @@ async def put_ratings(ratings: RatingsList, token: str = Depends(oauth2_scheme)) @router.delete('/{ratings_id}') async def put_one_rating( - ratings_id: int, this_rating: BattingCardRatingsModel, token: str = Depends(oauth2_scheme)): + ratings_id: int, token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'Bad Token: {token}') db.close() diff --git a/app/routers_v2/battingcards.py b/app/routers_v2/battingcards.py index 7753e45..85730fa 100644 --- a/app/routers_v2/battingcards.py +++ b/app/routers_v2/battingcards.py @@ -140,6 +140,7 @@ async def patch_card( db.close() return return_val else: + db.close() raise HTTPException( status_code=418, detail='Well slap my ass and call me a teapot; I could not save that card' diff --git a/app/routers_v2/pitchingcardratings.py b/app/routers_v2/pitchingcardratings.py new file mode 100644 index 0000000..b95e613 --- /dev/null +++ b/app/routers_v2/pitchingcardratings.py @@ -0,0 +1,194 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import Literal, Optional, List +import logging +import pydantic +from pydantic import validator, root_validator + +from ..db_engine import db, PitchingCardRatings, model_to_dict, chunked, PitchingCard +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/pitchingcardratings', + tags=['pitchingcardratings'] +) + + +class PitchingCardRatingsModel(pydantic.BaseModel): + pitchingcard_id: int + vs_hand: Literal['R', 'L', 'vR', 'vL'] + homerun: float = 0.0 + bp_homerun: float = 0.0 + triple: float = 0.0 + double_three: float = 0.0 + double_two: float = 0.0 + double_cf: float = 0.0 + single_two: float = 0.0 + single_one: float = 0.0 + single_center: float = 0.0 + bp_single: float = 0.0 + hbp: float = 0.0 + walk: float = 0.0 + strikeout: float = 0.0 + fo_slap: float = 0.0 + fo_center: float = 0.0 + groundout_a: float = 0.0 + groundout_b: float = 0.0 + xcheck_p: float = 0.0 + xcheck_c: float = 0.0 + xcheck_1b: float = 0.0 + xcheck_2b: float = 0.0 + xcheck_3b: float = 0.0 + xcheck_ss: float = 0.0 + xcheck_lf: float = 0.0 + xcheck_cf: float = 0.0 + xcheck_rf: float = 0.0 + avg: float = 0.0 + obp: float = 0.0 + slg: float = 0.0 + + @validator("avg", always=True) + def avg_validator(cls, v, values, **kwargs): + return (values['homerun'] + values['bp_homerun'] / 2 + values['triple'] + values['double_three'] + + values['double_two'] + values['double_cf'] + values['single_two'] + values['single_one'] + + values['single_center'] + values['bp_single'] / 2) / 108 + + @validator("obp", always=True) + def obp_validator(cls, v, values, **kwargs): + return ((values['hbp'] + values['walk']) / 108) + values['avg'] + + @validator("slg", always=True) + def slg_validator(cls, v, values, **kwargs): + return (values['homerun'] * 4 + values['bp_homerun'] * 2 + values['triple'] * 3 + values['double_three'] * 2 + + values['double_two'] * 2 + values['double_cf'] * 2 + values['single_two'] + values['single_one'] + + values['single_center'] + values['bp_single'] / 2) / 108 + + @root_validator + def validate_chance_total(cls, values): + total_chances = ( + values['homerun'] + values['bp_homerun'] + values['triple'] + values['double_three'] + + values['double_two'] + values['double_cf'] + values['single_two'] + values['single_one'] + + values['single_center'] + values['bp_single'] + values['hbp'] + values['walk'] + + values['strikeout'] + values['fo_slap'] + values['fo_center'] + values['groundout_a'] + + values['groundout_b'] + values['xcheck_p'] + values['xcheck_c'] + values['xcheck_1b'] + + values['xcheck_2b'] + values['xcheck_3b'] + values['xcheck_ss'] + values['xcheck_lf'] + + values['xcheck_cf'] + values['xcheck_rf']) + + if total_chances != 108: + raise ValueError("Must have exactly 108 chances on the card") + return values + + +class RatingsList(pydantic.BaseModel): + ratings: List[PitchingCardRatingsModel] + + +@router.get('') +async def get_card_ratings( + pitchingcard_id: list = Query(default=None), vs_hand: Literal['R', 'L', 'vR', 'vL'] = None, + short_output: bool = False, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to pull card ratings.' + ) + + all_ratings = PitchingCardRatings.select() + + if pitchingcard_id is not None: + all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard_id << pitchingcard_id) + if vs_hand is not None: + all_ratings = all_ratings.where(PitchingCardRatings.vs_hand << vs_hand[-1]) + + return_val = {'count': all_ratings.count(), 'ratings': [ + model_to_dict(x, recurse=not short_output) for x in all_ratings + ]} + db.close() + return return_val + + +@router.get('/{ratings_id}') +async def get_one_rating(ratings_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to pull card ratings.' + ) + + this_rating = PitchingCardRatings.get_or_none(PitchingCardRatings.id == ratings_id) + if this_rating is None: + db.close() + raise HTTPException(status_code=404, detail=f'PitchingCardRating id {ratings_id} not found') + + r_data = model_to_dict(this_rating) + db.close() + return r_data + + +@router.get('/{player_id}') +async def get_player_ratings(player_id: int, variant: list = Query(default=None), short_output: bool = False): + all_cards = PitchingCard.select().where(PitchingCard.player_id == player_id).order_by(PitchingCard.variant) + if variant is not None: + all_cards = all_cards.where(PitchingCard.variant << variant) + + all_ratings = PitchingCardRatings.select().where(PitchingCardRatings.PitchingCard << all_cards) + + return_val = {'count': all_ratings.count(), 'ratings': [ + model_to_dict(x, recurse=not short_output) for x in all_ratings + ]} + db.close() + return return_val + + +@router.put('') +async def put_ratings(ratings: RatingsList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post card ratings.' + ) + + new_ratings = [x.dict() for x in ratings.ratings] + with db.atomic(): + for batch in chunked(new_ratings, 30): + PitchingCardRatings.insert_many(batch).on_conflict_replace().execute() # TODO: replace gives new ID which breaks links + + db.close() + return f'Inserted {len(new_ratings)} batting ratings' + + +@router.delete('/{ratings_id}') +async def put_one_rating( + ratings_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post card ratings.' + ) + + this_rating = PitchingCardRatings.get_or_none(PitchingCardRatings.id == ratings_id) + if this_rating is None: + db.close() + raise HTTPException(status_code=404, detail=f'PitchingCardRating id {ratings_id} not found') + + count = this_rating.delete_instance() + db.close() + + if count == 1: + return f'Rating {this_rating} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Rating {this_rating} could not be deleted') + diff --git a/app/routers_v2/pitchingcards.py b/app/routers_v2/pitchingcards.py new file mode 100644 index 0000000..5c57d8b --- /dev/null +++ b/app/routers_v2/pitchingcards.py @@ -0,0 +1,166 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import Literal, Optional, List +import logging +import pydantic + +from ..db_engine import db, PitchingCard, model_to_dict, chunked, Player +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/pitchingcards', + tags=['pitchingcards'] +) + + +class PitchingCardModel(pydantic.BaseModel): + player_id: int + variant: int = 0 + balk: int = 0 + wild_pitch: int = 0 + hold: int = 0 + starter_rating: int = 1 + relief_rating: int = 0 + closer_rating: int = None + batting: str = "#1WR-C" + offense_col: int + hand: Literal['R', 'L', 'S'] = 'R' + + +class PitchingCardList(pydantic.BaseModel): + cards: List[PitchingCardModel] + + +@router.get('') +async def get_pitching_cards( + player_id: list = Query(default=None), cardset_id: list = Query(default=None), short_output: bool = False): + all_cards = PitchingCard.select() + if player_id is not None: + all_cards = all_cards.where(PitchingCard.player_id << player_id) + if cardset_id is not None: + all_players = Player.select().where(Player.cardset_id << cardset_id) + all_cards = all_cards.where(PitchingCard.player << all_players) + + return_val = {'count': all_cards.count(), 'cards': [ + model_to_dict(x, recurse=not short_output) for x in all_cards + ]} + db.close() + return return_val + + +@router.get('/{card_id}') +async def get_one_card(card_id: int): + this_card = PitchingCard.get_or_none(PitchingCard.id == card_id) + if this_card is None: + db.close() + raise HTTPException(status_code=404, detail=f'PitchingCard id {card_id} not found') + + r_card = model_to_dict(this_card) + db.close() + return r_card + + +@router.get('/player/{player_id}') +async def get_player_cards(player_id: int, variant: list = Query(default=None), short_output: bool = False): + all_cards = PitchingCard.select().where(PitchingCard.player_id == player_id).order_by(PitchingCard.variant) + if variant is not None: + all_cards = all_cards.where(PitchingCard.variant << variant) + + return_val = {'count': all_cards.count(), 'cards': [ + model_to_dict(x, recurse=not short_output) for x in all_cards + ]} + db.close() + return return_val + + +@router.put('') +async def put_cards(cards: PitchingCardList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post batting cards. This event has been logged.' + ) + + new_cards = [x.dict() for x in cards.cards] + with db.atomic(): + for batch in chunked(new_cards, 30): + PitchingCard.insert_many(batch).on_conflict_replace().execute() + + db.close() + return f'Inserted {len(new_cards)} batting cards' + + +@router.patch('/{card_id}') +async def patch_card( + card_id: int, balk: Optional[int] = None, wild_pitch: Optional[int] = None, hold: Optional[int] = None, + starter_rating: Optional[int] = None, relief_rating: Optional[int] = None, closer_rating: Optional[int] = None, + batting: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch pitching cards. This event has been logged.' + ) + + this_card = PitchingCard.get_or_none(PitchingCard.id == card_id) + if this_card is None: + db.close() + raise HTTPException(status_code=404, detail=f'PitchingCard id {card_id} not found') + + if balk is not None: + this_card.balk = balk + if wild_pitch is not None: + this_card.wild_pitch = wild_pitch + if hold is not None: + this_card.hold = hold + if starter_rating is not None: + this_card.starter_rating = starter_rating + if relief_rating is not None: + this_card.relief_rating = relief_rating + if closer_rating is not None: + this_card.closer_rating = closer_rating + if batting is not None: + this_card.batting = batting + + if this_card.save() == 1: + return_val = model_to_dict(this_card) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that card' + ) + + +@router.delete('/{card_id}') +async def delete_card(card_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch batting cards. This event has been logged.' + ) + + this_card = PitchingCard.get_or_none(PitchingCard.id == card_id) + if this_card is None: + db.close() + raise HTTPException(status_code=404, detail=f'BattingCard id {card_id} not found') + + count = this_card.delete_instance() + db.close() + + if count == 1: + return f'Card {this_card} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Card {this_card} could not be deleted') From 6183a125bc5efa1532ecaa1e4e83f42e039935cc Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 16 Sep 2023 17:21:01 -0500 Subject: [PATCH 12/40] Cards and ratings updates --- app/routers_v2/battingcardratings.py | 20 ++++++++++++++++---- app/routers_v2/battingcards.py | 17 +++++++++++++++-- app/routers_v2/pitchingcardratings.py | 24 ++++++++++++++++++------ app/routers_v2/pitchingcards.py | 17 +++++++++++++++-- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/app/routers_v2/battingcardratings.py b/app/routers_v2/battingcardratings.py index a378014..987825f 100644 --- a/app/routers_v2/battingcardratings.py +++ b/app/routers_v2/battingcardratings.py @@ -129,7 +129,7 @@ async def get_one_rating(ratings_id: int, token: str = Depends(oauth2_scheme)): return r_data -@router.get('/{player_id}') +@router.get('/player/{player_id}') async def get_player_ratings(player_id: int, variant: list = Query(default=None), short_output: bool = False): all_cards = BattingCard.select().where(BattingCard.player_id == player_id).order_by(BattingCard.variant) if variant is not None: @@ -154,17 +154,29 @@ async def put_ratings(ratings: RatingsList, token: str = Depends(oauth2_scheme)) detail='You are not authorized to post card ratings.' ) - new_ratings = [x.dict() for x in ratings.ratings] + new_ratings = [] + updates = 0 + for x in ratings.ratings: + try: + BattingCardRatings.get( + (BattingCardRatings.battingcard_id == x.battingcard_id) & (BattingCardRatings.vs_hand == x.vs_hand) + ) + updates += BattingCardRatings.update(x.dict()).where( + (BattingCardRatings.battingcard_id == x.battingcard_id) & (BattingCardRatings.vs_hand == x.vs_hand) + ).execute() + except BattingCardRatings.DoesNotExist: + new_ratings.append(x.dict()) + with db.atomic(): for batch in chunked(new_ratings, 30): BattingCardRatings.insert_many(batch).on_conflict_replace().execute() db.close() - return f'Inserted {len(new_ratings)} batting ratings' + return f'Updated ratings: {updates}; new ratings: {len(new_ratings)}' @router.delete('/{ratings_id}') -async def put_one_rating( +async def delete_rating( ratings_id: int, token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'Bad Token: {token}') diff --git a/app/routers_v2/battingcards.py b/app/routers_v2/battingcards.py index 85730fa..63fdb64 100644 --- a/app/routers_v2/battingcards.py +++ b/app/routers_v2/battingcards.py @@ -88,13 +88,26 @@ async def put_cards(cards: BattingCardList, token: str = Depends(oauth2_scheme)) detail='You are not authorized to post batting cards. This event has been logged.' ) - new_cards = [x.dict() for x in cards.cards] + new_cards = [] + updates = 0 + + for x in cards.cards: + try: + BattingCard.get( + (BattingCard.player_id == x.player_id) & (BattingCard.variant == x.variant) + ) + updates += BattingCard.update(x.dict()).where( + (BattingCard.player_id == x.player_id) & (BattingCard.variant == x.variant) + ).execute() + except BattingCard.DoesNotExist: + new_cards.append(x.dict()) + with db.atomic(): for batch in chunked(new_cards, 30): BattingCard.insert_many(batch).on_conflict_replace().execute() db.close() - return f'Inserted {len(new_cards)} batting cards' + return f'Updated cards: {updates}; new cards: {len(new_cards)}' @router.patch('/{card_id}') diff --git a/app/routers_v2/pitchingcardratings.py b/app/routers_v2/pitchingcardratings.py index b95e613..2fb7125 100644 --- a/app/routers_v2/pitchingcardratings.py +++ b/app/routers_v2/pitchingcardratings.py @@ -134,13 +134,13 @@ async def get_one_rating(ratings_id: int, token: str = Depends(oauth2_scheme)): return r_data -@router.get('/{player_id}') +@router.get('/player/{player_id}') async def get_player_ratings(player_id: int, variant: list = Query(default=None), short_output: bool = False): all_cards = PitchingCard.select().where(PitchingCard.player_id == player_id).order_by(PitchingCard.variant) if variant is not None: all_cards = all_cards.where(PitchingCard.variant << variant) - all_ratings = PitchingCardRatings.select().where(PitchingCardRatings.PitchingCard << all_cards) + all_ratings = PitchingCardRatings.select().where(PitchingCardRatings.pitchingcard << all_cards) return_val = {'count': all_ratings.count(), 'ratings': [ model_to_dict(x, recurse=not short_output) for x in all_ratings @@ -159,17 +159,29 @@ async def put_ratings(ratings: RatingsList, token: str = Depends(oauth2_scheme)) detail='You are not authorized to post card ratings.' ) - new_ratings = [x.dict() for x in ratings.ratings] + new_ratings = [] + updates = 0 + for x in ratings.ratings: + try: + PitchingCardRatings.get( + (PitchingCardRatings.pitchingcard_id == x.pitchingcard_id) & (PitchingCardRatings.vs_hand == x.vs_hand) + ) + updates += PitchingCardRatings.update(x.dict()).where( + (PitchingCardRatings.pitchingcard_id == x.pitchingcard_id) & (PitchingCardRatings.vs_hand == x.vs_hand) + ).execute() + except PitchingCardRatings.DoesNotExist: + new_ratings.append(x.dict()) + with db.atomic(): for batch in chunked(new_ratings, 30): - PitchingCardRatings.insert_many(batch).on_conflict_replace().execute() # TODO: replace gives new ID which breaks links + PitchingCardRatings.insert_many(batch).on_conflict_replace().execute() db.close() - return f'Inserted {len(new_ratings)} batting ratings' + return f'Updated ratings: {updates}; new ratings: {len(new_ratings)}' @router.delete('/{ratings_id}') -async def put_one_rating( +async def delete_rating( ratings_id: int, token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'Bad Token: {token}') diff --git a/app/routers_v2/pitchingcards.py b/app/routers_v2/pitchingcards.py index 5c57d8b..d055815 100644 --- a/app/routers_v2/pitchingcards.py +++ b/app/routers_v2/pitchingcards.py @@ -88,13 +88,26 @@ async def put_cards(cards: PitchingCardList, token: str = Depends(oauth2_scheme) detail='You are not authorized to post batting cards. This event has been logged.' ) - new_cards = [x.dict() for x in cards.cards] + new_cards = [] + updates = 0 + + for x in cards.cards: + try: + PitchingCard.get( + (PitchingCard.player_id == x.player_id) & (PitchingCard.variant == x.variant) + ) + updates += PitchingCard.update(x.dict()).where( + (PitchingCard.player_id == x.player_id) & (PitchingCard.variant == x.variant) + ).execute() + except PitchingCard.DoesNotExist: + new_cards.append(x.dict()) + with db.atomic(): for batch in chunked(new_cards, 30): PitchingCard.insert_many(batch).on_conflict_replace().execute() db.close() - return f'Inserted {len(new_cards)} batting cards' + return f'Updated cards: {updates}; new cards: {len(new_cards)}' @router.patch('/{card_id}') From 995735d878dada7378d67c4832c2d7ffe1fed7f2 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 16 Sep 2023 18:36:15 -0500 Subject: [PATCH 13/40] Added cardpositions --- app/db_engine.py | 17 +++- app/main.py | 3 +- app/routers_v2/cardpositions.py | 148 ++++++++++++++++++++++++++++++++ app/routers_v2/pitchingcards.py | 2 +- 4 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 app/routers_v2/cardpositions.py diff --git a/app/db_engine.py b/app/db_engine.py index 1a1f6d9..0494716 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -590,7 +590,9 @@ class BattingCardRatings(BaseModel): slg = FloatField(null=True) -bcr_index = ModelIndex(BattingCardRatings, (BattingCardRatings.battingcard, BattingCardRatings.vs_hand), unique=True) +bcr_index = ModelIndex( + BattingCardRatings, (BattingCardRatings.battingcard, BattingCardRatings.vs_hand), unique=True +) BattingCardRatings.add_index(bcr_index) @@ -646,14 +648,15 @@ class PitchingCardRatings(BaseModel): slg = FloatField(null=True) -pcr_index = ModelIndex(PitchingCardRatings, (PitchingCardRatings.pitchingcard, PitchingCardRatings.vs_hand), unique=True) +pcr_index = ModelIndex( + PitchingCardRatings, (PitchingCardRatings.pitchingcard, PitchingCardRatings.vs_hand), unique=True +) PitchingCardRatings.add_index(pcr_index) class CardPosition(BaseModel): player = ForeignKeyField(Player) - batting = ForeignKeyField(BattingCard, null=True) - pitching = ForeignKeyField(PitchingCard, null=True) + variant = IntegerField() position = CharField() innings = IntegerField() range = IntegerField() @@ -663,6 +666,12 @@ class CardPosition(BaseModel): overthrow = IntegerField(null=True) +pos_index = ModelIndex( + CardPosition, (CardPosition.player, CardPosition.variant, CardPosition.position), unique=True +) +PitchingCardRatings.add_index(pos_index) + + db.create_tables([BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition]) diff --git a/app/main.py b/app/main.py index 5293faa..9d32bd9 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI from.routers_v2 import ( current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards, - battingcardratings, pitchingcards, pitchingcardratings) + battingcardratings, pitchingcards, pitchingcardratings, cardpositions) app = FastAPI( responses={404: {'description': 'Not found'}} @@ -31,3 +31,4 @@ app.include_router(battingcards.router) app.include_router(battingcardratings.router) app.include_router(pitchingcards.router) app.include_router(pitchingcardratings.router) +app.include_router(cardpositions.router) diff --git a/app/routers_v2/cardpositions.py b/app/routers_v2/cardpositions.py new file mode 100644 index 0000000..d9f927a --- /dev/null +++ b/app/routers_v2/cardpositions.py @@ -0,0 +1,148 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import Literal, Optional, List +import logging +import pydantic +from pydantic import root_validator + +from ..db_engine import db, CardPosition, model_to_dict, chunked, Player +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/cardpositions', + tags=['cardpositions'] +) + + +class CardPositionModel(pydantic.BaseModel): + player_id: int + variant: int = 0 + position: Literal['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'] + innings: int = 1 + range: int = 5 + error: int = 0 + arm: Optional[int] = None + pb: Optional[int] = None + overthrow: Optional[int] = None + + @root_validator + def position_validator(cls, values): + if values['position'] in ['C', 'LF', 'CF', 'RF'] and values['arm'] is None: + raise ValueError(f'{values["position"]} must have an arm rating') + if values['position'] == 'C' and (values['pb'] is None or values['overthrow'] is None): + raise ValueError('Catchers must have a pb and overthrow rating') + return values + + +class PositionList(pydantic.BaseModel): + positions: List[CardPositionModel] + + +@router.get('') +async def get_card_positions( + player_id: Optional[int] = None, position: list = Query(default=None), min_innings: Optional[int] = 1, + r: list = Query(default=None), e: list = Query(default=None), arm: list = Query(default=None), + pb: list = Query(default=None), overthrow: list = Query(default=None), cardset_id: list = Query(default=None), + short_output: Optional[bool] = False): + all_pos = CardPosition.select().where(CardPosition.innings >= min_innings).order_by( + CardPosition.player, CardPosition.position, CardPosition.variant + ) + + if player_id is not None: + all_pos = all_pos.where(CardPosition.player_id << player_id) + if position is not None: + all_pos = all_pos.where(CardPosition.position << position) + if r is not None: + all_pos = all_pos.where(CardPosition.range << r) + if e is not None: + all_pos = all_pos.where(CardPosition.error << e) + if arm is not None: + all_pos = all_pos.where(CardPosition.arm << arm) + if pb is not None: + all_pos = all_pos.where(CardPosition.pb << pb) + if overthrow is not None: + all_pos = all_pos.where(CardPosition.overthrow << overthrow) + if position is not None: + all_players = Player.select().where(Player.cardset_id << cardset_id) + all_pos = all_pos.where(CardPosition.player << all_players) + + return_val = {'count': all_pos.count(), 'positions': [ + model_to_dict(x, recurse=not short_output) for x in all_pos + ]} + db.close() + return return_val + + +@router.get('/{position_id}') +async def get_one_position(position_id: int): + this_pos = CardPosition.get_or_none(CardPosition.id == position_id) + if this_pos is None: + db.close() + raise HTTPException(status_code=404, detail=f'CardPosition id {position_id} not found') + + r_data = model_to_dict(this_pos) + db.close() + return r_data + + +@router.put('') +async def put_positions(positions: PositionList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post card positions. This event has been logged.' + ) + + new_cards = [] + updates = 0 + + for x in positions.positions: + try: + CardPosition.get( + (CardPosition.player_id == x.player_id) & (CardPosition.variant == x.variant) & + (CardPosition.position == x.position) + ) + updates += CardPosition.update(x.dict()).where( + (CardPosition.player_id == x.player_id) & (CardPosition.variant == x.variant) & + (CardPosition.position == x.position) + ).execute() + except CardPosition.DoesNotExist: + new_cards.append(x.dict()) + + with db.atomic(): + for batch in chunked(new_cards, 30): + CardPosition.insert_many(batch).on_conflict_replace().execute() + + db.close() + return f'Updated cards: {updates}; new cards: {len(new_cards)}' + + +@router.delete('/{position_id}') +async def delete_position(position_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete card positions. This event has been logged.' + ) + + this_pos = CardPosition.get_or_none(CardPosition.id == position_id) + if this_pos is None: + db.close() + raise HTTPException(status_code=404, detail=f'CardPosition id {position_id} not found') + + count = this_pos.delete_instance() + db.close() + + if count == 1: + return f'Card Position {this_pos} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Card Position {this_pos} could not be deleted') diff --git a/app/routers_v2/pitchingcards.py b/app/routers_v2/pitchingcards.py index d055815..7190011 100644 --- a/app/routers_v2/pitchingcards.py +++ b/app/routers_v2/pitchingcards.py @@ -162,7 +162,7 @@ async def delete_card(card_id: int, token: str = Depends(oauth2_scheme)): db.close() raise HTTPException( status_code=401, - detail='You are not authorized to patch batting cards. This event has been logged.' + detail='You are not authorized to delete batting cards. This event has been logged.' ) this_card = PitchingCard.get_or_none(PitchingCard.id == card_id) From d5386f86f81e9d6ec02561d24d0a3b3a98b6b2ad Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 17 Sep 2023 00:25:42 -0500 Subject: [PATCH 14/40] Added /scouting --- app/main.py | 3 ++- app/player_scouting.py | 24 +++++++++++++++++++++++ app/routers_v2/scouting.py | 39 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 app/player_scouting.py create mode 100644 app/routers_v2/scouting.py diff --git a/app/main.py b/app/main.py index 9d32bd9..0dd9006 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI from.routers_v2 import ( current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards, - battingcardratings, pitchingcards, pitchingcardratings, cardpositions) + battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting) app = FastAPI( responses={404: {'description': 'Not found'}} @@ -32,3 +32,4 @@ app.include_router(battingcardratings.router) app.include_router(pitchingcards.router) app.include_router(pitchingcardratings.router) app.include_router(cardpositions.router) +app.include_router(scouting.router) diff --git a/app/player_scouting.py b/app/player_scouting.py new file mode 100644 index 0000000..775d7c6 --- /dev/null +++ b/app/player_scouting.py @@ -0,0 +1,24 @@ +from typing import Literal, Optional +from pybaseball import playerid_reverse_lookup + +import pydantic + + +class PlayerIds(pydantic.BaseModel): + bbref: str = None + fangraphs: int = None + retro: str = None + mlbam: int = None + + +def get_player_ids(player_id: str, id_type: Literal['bbref', 'fangraphs']) -> PlayerIds | None: + q = playerid_reverse_lookup([player_id], key_type=id_type) + if len(q.values) == 0: + return None + else: + return PlayerIds( + bbref=q.loc[0].key_bbref, + fangraphs=q.loc[0].key_fangraphs, + retro=q.loc[0].key_retro, + mlbam=q.loc[0].key_mlbam + ) diff --git a/app/routers_v2/scouting.py b/app/routers_v2/scouting.py new file mode 100644 index 0000000..9f438d1 --- /dev/null +++ b/app/routers_v2/scouting.py @@ -0,0 +1,39 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, HTTPException, Response, Query +from typing import Optional +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, model_to_dict, fn, query_to_csv, complex_data_to_csv, Player +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp +from ..player_scouting import get_player_ids + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/scouting', + tags=['scouting'] +) + + +@router.get('/playerkeys') +async def get_player_keys(player_id: list = Query(default=None)): + all_keys = [] + for x in player_id: + this_player = Player.get_or_none(Player.player_id == x) + if this_player is not None: + this_keys = get_player_ids(this_player.bbref_id, id_type='bbref') + if this_keys is not None: + all_keys.append(this_keys) + + return_val = {'count': len(all_keys), 'keys': [ + dict(x) for x in all_keys + ]} + db.close() + return return_val + From 8a0d094227641f59236e7975d197d8c5167b9eff Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 22 Sep 2023 01:29:35 -0500 Subject: [PATCH 15/40] Scouting tables added to db --- app/db_engine.py | 14 +++ app/dependencies.py | 167 ++++++++++++++++++++++++++++- app/main.py | 5 +- app/player_scouting.py | 4 + app/routers_v2/battingcards.py | 50 ++++++++- app/routers_v2/mlbplayers.py | 189 +++++++++++++++++++++++++++++++++ app/routers_v2/players.py | 37 ++++++- app/routers_v2/scouting.py | 67 +++++++++++- db_engine.py | 14 +++ db_migrations.py | 10 +- requirements.txt | 2 + 11 files changed, 541 insertions(+), 18 deletions(-) create mode 100644 app/routers_v2/mlbplayers.py diff --git a/app/db_engine.py b/app/db_engine.py index 0494716..dac9051 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -144,6 +144,19 @@ class Cardset(BaseModel): db.create_tables([Cardset]) +class MlbPlayer(BaseModel): + first_name = CharField() + last_name = CharField() + key_fangraphs = IntegerField(null=True) + key_bbref = CharField(null=True) + key_retro = CharField(null=True) + key_mlbam = IntegerField(null=True) + offense_col = IntegerField(default=1) + + +db.create_tables([MlbPlayer]) + + class Player(BaseModel): player_id = IntegerField(primary_key=True) p_name = CharField() @@ -170,6 +183,7 @@ class Player(BaseModel): fangr_id = CharField(null=True) description = CharField() quantity = IntegerField(default=999) + mlb_player = ForeignKeyField(MlbPlayer, null=True) def __str__(self): return f'{self.cardset} {self.p_name} ({self.rarity.name})' diff --git a/app/dependencies.py b/app/dependencies.py index 8bce8f8..df7cc1f 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -2,6 +2,7 @@ import datetime import logging import os +import requests from fastapi.security import OAuth2PasswordBearer date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}' @@ -20,11 +21,175 @@ logging.basicConfig( oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +master_debug = False +DB_URL = 'https://pd.manticorum.com/api/' +AUTH_TOKEN = f'{os.environ.get("API_TOKEN")}' +AUTH_HEADER = {'Authorization': f'Bearer {AUTH_TOKEN}'} + + +if os.environ.get('TESTING') == 'False': + DB_URL = 'https://pddev.manticorum.com/api/' def valid_token(token): - return token == os.environ.get('API_TOKEN') + return token == AUTH_TOKEN def int_timestamp(datetime_obj: datetime) -> int: return int(datetime.datetime.timestamp(datetime_obj) * 1000) + + +def mround(x, prec=2, base=.05): + return round(base * round(float(x) / base), prec) + + +def param_char(other_params): + if other_params: + return '&' + else: + return '?' + + +def get_req_url(endpoint: str, api_ver: int = 2, object_id: int = None, params: list = None): + req_url = f'{DB_URL}/v{api_ver}/{endpoint}{"/" if object_id is not None else ""}{object_id if object_id is not None else ""}' + + if params: + other_params = False + for x in params: + req_url += f'{param_char(other_params)}{x[0]}={x[1]}' + other_params = True + + return req_url + + +async def db_get(endpoint: str, api_ver: int = 2, object_id: int = None, params: list = None, none_okay: bool = True, + timeout: int = 3): + req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params) + log_string = f'get:\n{endpoint} id: {object_id} params: {params}' + logging.info(log_string) if master_debug else logging.debug(log_string) + + retries = 0 + while True: + try: + resp = requests.get(req_url, timeout=timeout) + break + except requests.ReadTimeout as e: + logging.error(f'Get Timeout: {req_url} / retries: {retries} / timeout: {timeout}') + if retries > 1: + raise ConnectionError(f'DB: The internet was a bit too slow for me to grab the data I needed. Please ' + f'hang on a few extra seconds and try again.') + timeout += [2, 5][retries] + retries += 1 + + if resp.status_code == 200: + data = resp.json() + log_string = f'{data}' + if master_debug: + logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + else: + logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + return data + elif none_okay: + data = resp.json() + log_string = f'{data}' + if master_debug: + logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + else: + logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + return None + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def db_patch(endpoint: str, object_id: int, params: list, api_ver: int = 2, timeout: int = 3): + req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params) + log_string = f'patch:\n{endpoint} {params}' + logging.info(log_string) if master_debug else logging.debug(log_string) + + retries = 0 + while True: + try: + resp = requests.patch(req_url, headers=AUTH_HEADER, timeout=timeout) + break + except requests.Timeout as e: + logging.error(f'Patch Timeout: {req_url} / retries: {retries} / timeout: {timeout}') + if retries > 1: + raise ConnectionError(f'DB: The internet was a bit too slow for me to grab the data I needed. Please ' + f'hang on a few extra seconds and try again.') + timeout += [min(3, timeout), min(5, timeout)][retries] + retries += 1 + + if resp.status_code == 200: + data = resp.json() + log_string = f'{data}' + if master_debug: + logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + else: + logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + return data + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def db_post(endpoint: str, api_ver: int = 2, payload: dict = None, timeout: int = 3): + req_url = get_req_url(endpoint, api_ver=api_ver) + log_string = f'post:\n{endpoint} payload: {payload}\ntype: {type(payload)}' + logging.info(log_string) if master_debug else logging.debug(log_string) + + retries = 0 + while True: + try: + resp = requests.post(req_url, json=payload, headers=AUTH_HEADER, timeout=timeout) + break + except requests.Timeout as e: + logging.error(f'Post Timeout: {req_url} / retries: {retries} / timeout: {timeout}') + if retries > 1: + raise ConnectionError(f'DB: The internet was a bit too slow for me to grab the data I needed. Please ' + f'hang on a few extra seconds and try again.') + timeout += [min(3, timeout), min(5, timeout)][retries] + retries += 1 + + if resp.status_code == 200: + data = resp.json() + log_string = f'{data}' + if master_debug: + logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + else: + logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + return data + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def db_delete(endpoint: str, object_id: int, api_ver: int = 2, timeout=3): + req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id) + log_string = f'delete:\n{endpoint} {object_id}' + logging.info(log_string) if master_debug else logging.debug(log_string) + + retries = 0 + while True: + try: + resp = requests.delete(req_url, headers=AUTH_HEADER, timeout=timeout) + break + except requests.ReadTimeout as e: + logging.error(f'Delete Timeout: {req_url} / retries: {retries} / timeout: {timeout}') + if retries > 1: + raise ConnectionError(f'DB: The internet was a bit too slow for me to grab the data I needed. Please ' + f'hang on a few extra seconds and try again.') + timeout += [min(3, timeout), min(5, timeout)][retries] + retries += 1 + + if resp.status_code == 200: + data = resp.json() + log_string = f'{data}' + if master_debug: + logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + else: + logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}') + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') diff --git a/app/main.py b/app/main.py index 0dd9006..f187f3b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,9 @@ from fastapi import FastAPI -from.routers_v2 import ( +from .routers_v2 import ( current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards, - battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting) + battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers) app = FastAPI( responses={404: {'description': 'Not found'}} @@ -33,3 +33,4 @@ app.include_router(pitchingcards.router) app.include_router(pitchingcardratings.router) app.include_router(cardpositions.router) app.include_router(scouting.router) +app.include_router(mlbplayers.router) diff --git a/app/player_scouting.py b/app/player_scouting.py index 775d7c6..2c33217 100644 --- a/app/player_scouting.py +++ b/app/player_scouting.py @@ -1,5 +1,6 @@ from typing import Literal, Optional from pybaseball import playerid_reverse_lookup +from .card_creation import batter_calcs, pitcher_calcs, defense_calcs import pydantic @@ -22,3 +23,6 @@ def get_player_ids(player_id: str, id_type: Literal['bbref', 'fangraphs']) -> Pl retro=q.loc[0].key_retro, mlbam=q.loc[0].key_mlbam ) + + + diff --git a/app/routers_v2/battingcards.py b/app/routers_v2/battingcards.py index 63fdb64..146680a 100644 --- a/app/routers_v2/battingcards.py +++ b/app/routers_v2/battingcards.py @@ -1,9 +1,11 @@ +import random + from fastapi import APIRouter, Depends, HTTPException, Query from typing import Literal, Optional, List import logging import pydantic -from ..db_engine import db, BattingCard, model_to_dict, chunked, Player +from ..db_engine import db, BattingCard, model_to_dict, fn, chunked, Player, MlbPlayer from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -28,7 +30,7 @@ class BattingCardModel(pydantic.BaseModel): bunting: str = 'C' hit_and_run: str = 'C' running: int = 10 - offense_col: int + offense_col: int = None hand: Literal['R', 'L', 'S'] = 'R' @@ -38,13 +40,21 @@ class BattingCardList(pydantic.BaseModel): @router.get('') async def get_batting_cards( - player_id: list = Query(default=None), cardset_id: list = Query(default=None), short_output: bool = False): + player_id: list = Query(default=None), player_name: list = Query(default=None), + cardset_id: list = Query(default=None), short_output: bool = False, limit: Optional[int] = None): all_cards = BattingCard.select() if player_id is not None: all_cards = all_cards.where(BattingCard.player_id << player_id) if cardset_id is not None: all_players = Player.select().where(Player.cardset_id << cardset_id) all_cards = all_cards.where(BattingCard.player << all_players) + if player_name is not None: + name_list = [x.lower() for x in player_name] + all_players = Player.select().where(fn.lower(Player.p_name) << name_list) + all_cards = all_cards.where(BattingCard.player << all_players) + + if limit is not None: + all_cards = all_cards.limit(limit) return_val = {'count': all_cards.count(), 'cards': [ model_to_dict(x, recurse=not short_output) for x in all_cards @@ -90,16 +100,30 @@ async def put_cards(cards: BattingCardList, token: str = Depends(oauth2_scheme)) new_cards = [] updates = 0 + logging.info(f'here!') for x in cards.cards: try: - BattingCard.get( + old = BattingCard.get( (BattingCard.player_id == x.player_id) & (BattingCard.variant == x.variant) ) + + if x.offense_col is None: + x.offense_col = old.offense_col updates += BattingCard.update(x.dict()).where( (BattingCard.player_id == x.player_id) & (BattingCard.variant == x.variant) ).execute() except BattingCard.DoesNotExist: + if x.offense_col is None: + this_player = Player.get_or_none(Player.player_id == x.player_id) + mlb_player = MlbPlayer.get_or_none(MlbPlayer.key_bbref == this_player.bbref_id) + if mlb_player is not None: + logging.info(f'setting offense_col to {mlb_player.offense_col} for {this_player.p_name}') + x.offense_col = mlb_player.offense_col + else: + logging.info(f'randomly setting offense_col for {this_player.p_name}') + x.offense_col = random.randint(1, 3) + logging.info(f'x.dict(): {x.dict()}') new_cards.append(x.dict()) with db.atomic(): @@ -167,7 +191,7 @@ async def delete_card(card_id: int, token: str = Depends(oauth2_scheme)): db.close() raise HTTPException( status_code=401, - detail='You are not authorized to patch batting cards. This event has been logged.' + detail='You are not authorized to delete batting cards. This event has been logged.' ) this_card = BattingCard.get_or_none(BattingCard.id == card_id) @@ -182,3 +206,19 @@ async def delete_card(card_id: int, token: str = Depends(oauth2_scheme)): return f'Card {this_card} has been deleted' else: raise HTTPException(status_code=500, detail=f'Card {this_card} could not be deleted') + + +@router.delete('') +async def delete_all_cards(token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete batting cards. This event has been logged.' + ) + + d_query = BattingCard.delete() + d_query.execute() + + return f'Deleted {d_query.count()} batting cards' diff --git a/app/routers_v2/mlbplayers.py b/app/routers_v2/mlbplayers.py new file mode 100644 index 0000000..cf8092c --- /dev/null +++ b/app/routers_v2/mlbplayers.py @@ -0,0 +1,189 @@ +from fastapi import APIRouter, Depends, HTTPException, Response, Query +from typing import Optional, List +import logging +import pydantic +from pandas import DataFrame + +from ..db_engine import db, MlbPlayer, model_to_dict, fn, chunked, query_to_csv +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/mlbplayers', + tags=['mlbplayers'] +) + + +class PlayerModel(pydantic.BaseModel): + first_name: str + last_name: str + key_fangraphs: int = None + key_bbref: str = None + key_retro: str = None + key_mlbam: int = None + offense_col: int = None + + +class PlayerList(pydantic.BaseModel): + players: List[PlayerModel] + + +@router.get('') +async def get_players( + full_name: list = Query(default=None), first_name: list = Query(default=None), + last_name: list = Query(default=None), key_fangraphs: list = Query(default=None), + key_bbref: list = Query(default=None), key_retro: list = Query(default=None), + key_mlbam: list = Query(default=None), offense_col: list = Query(default=None), csv: Optional[bool] = False): + all_players = MlbPlayer.select() + + if full_name is not None: + name_list = [x.lower() for x in full_name] + all_players = all_players.where( + fn.lower(MlbPlayer.first_name) + ' ' + fn.lower(MlbPlayer.last_name) << name_list + ) + if first_name is not None: + all_players = all_players.where(MlbPlayer.first_name << first_name) + if first_name is not None: + all_players = all_players.where(MlbPlayer.first_name << first_name) + if last_name is not None: + all_players = all_players.where(MlbPlayer.last_name << last_name) + if key_fangraphs is not None: + all_players = all_players.where(MlbPlayer.key_fangraphs << key_fangraphs) + if key_bbref is not None: + all_players = all_players.where(MlbPlayer.key_bbref << key_bbref) + if key_retro is not None: + all_players = all_players.where(MlbPlayer.key_retro << key_retro) + if key_mlbam is not None: + all_players = all_players.where(MlbPlayer.key_mlbam << key_mlbam) + if offense_col is not None: + all_players = all_players.where(MlbPlayer.offense_col << offense_col) + + if csv: + return_val = query_to_csv(all_players) + db.close() + return Response(content=return_val, media_type='text/csv') + + return_val = {'count': all_players.count(), 'players': [ + model_to_dict(x) for x in all_players + ]} + db.close() + return return_val + + +@router.get('/{player_id}') +async def get_one_player(player_id: int): + this_player = MlbPlayer.get_or_none(MlbPlayer.id == player_id) + if this_player is None: + db.close() + raise HTTPException(status_code=404, detail=f'MlbPlayer id {player_id} not found') + + r_data = model_to_dict(this_player) + db.close() + return r_data + + +@router.patch('/{player_id}') +async def patch_player( + player_id: int, first_name: Optional[str] = None, last_name: Optional[str] = None, + key_fangraphs: Optional[str] = None, key_bbref: Optional[str] = None, key_retro: Optional[str] = None, + key_mlbam: Optional[str] = None, offense_col: Optional[str] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to patch mlb players. This event has been logged.' + ) + + this_player = MlbPlayer.get_or_none(MlbPlayer.id == player_id) + if this_player is None: + db.close() + raise HTTPException(status_code=404, detail=f'MlbPlayer id {player_id} not found') + + if first_name is not None: + this_player.first_name = first_name + if last_name is not None: + this_player.last_name = last_name + if key_fangraphs is not None: + this_player.key_fangraphs = key_fangraphs + if key_bbref is not None: + this_player.key_bbref = key_bbref + if key_retro is not None: + this_player.key_retro = key_retro + if key_mlbam is not None: + this_player.key_mlbam = key_mlbam + if offense_col is not None: + this_player.offense_col = offense_col + + if this_player.save() == 1: + return_val = model_to_dict(this_player) + db.close() + return return_val + else: + db.close() + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that player' + ) + + +@router.post('') +async def post_players(players: PlayerList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post mlb players. This event has been logged.' + ) + + new_players = [] + for x in players.players: + dupes = MlbPlayer.select().where( + (MlbPlayer.key_fangraphs == x.key_fangraphs) | (MlbPlayer.key_mlbam == x.key_mlbam) | + (MlbPlayer.key_retro == x.key_retro) | (MlbPlayer.key_bbref == x.key_bbref) + ) + if dupes.count() > 0: + db.close() + raise HTTPException( + status_code=400, + detail=f'{x.first_name} {x.last_name} has a key already in the database' + ) + + new_players.append(x.dict()) + + with db.atomic(): + for batch in chunked(new_players, 15): + MlbPlayer.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_players)} new MLB 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'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete mlb players. This event has been logged.' + ) + + this_player = MlbPlayer.get_or_none(MlbPlayer.id == player_id) + if this_player is None: + db.close() + raise HTTPException(status_code=404, detail=f'MlbPlayer id {player_id} not found') + + count = this_player.delete_instance() + db.close() + + if count == 1: + raise HTTPException(status_code=200, detail=f'Player {player_id} has been deleted') + else: + raise HTTPException(status_code=500, detail=f'Player {player_id} was not deleted') diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py index b05af66..d2f00ff 100644 --- a/app/routers_v2/players.py +++ b/app/routers_v2/players.py @@ -20,7 +20,7 @@ router = APIRouter( class PlayerPydantic(pydantic.BaseModel): - player_id: int + player_id: int = None p_name: str cost: int image: str @@ -437,7 +437,7 @@ async def v1_players_patch( @router.put('') -async def v1_players_put(players: PlayerModel, token: str = Depends(oauth2_scheme)): +async def put_players(players: PlayerModel, token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'Bad Token: {token}') db.close() @@ -515,8 +515,39 @@ async def v1_players_put(players: PlayerModel, token: str = Depends(oauth2_schem raise HTTPException(status_code=200, detail=f'{len(new_players)} players have been added') +@router.post('') +async def post_players(new_player: PlayerPydantic, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to post players. This event has been logged.' + ) + + dupe_query = Player.select().where( + (fn.Lower(Player.p_name) == new_player.p_name.lower()) & (Player.cardset_id == new_player.cardset_id) + ) + if dupe_query.count() != 0: + db.close() + raise HTTPException( + status_code=400, + detail=f'This appears to be a duplicate with player {dupe_query[0].player_id}' + ) + + p_query = Player.select(Player.player_id).order_by(-Player.player_id).limit(1) + new_id = p_query[0].player_id + 1 + + new_player.player_id = new_id + p_id = Player.insert(new_player.dict()).execute() + + return_val = model_to_dict(Player.get_by_id(p_id)) + db.close() + return return_val + + @router.delete('/{player_id}') -async def v1_players_delete(player_id, token: str = Depends(oauth2_scheme)): +async def delete_player(player_id, token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'Bad Token: {token}') db.close() diff --git a/app/routers_v2/scouting.py b/app/routers_v2/scouting.py index 9f438d1..b412ae6 100644 --- a/app/routers_v2/scouting.py +++ b/app/routers_v2/scouting.py @@ -1,11 +1,12 @@ +import csv from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Response, Query from typing import Optional import logging import pydantic -from pandas import DataFrame +import pandas as pd -from ..db_engine import db, model_to_dict, fn, query_to_csv, complex_data_to_csv, Player +from ..db_engine import db, model_to_dict, fn, query_to_csv, complex_data_to_csv, Player, BattingCardRatings from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp from ..player_scouting import get_player_ids @@ -21,6 +22,21 @@ router = APIRouter( ) +class BattingFiles(pydantic.BaseModel): + vl_basic: str = 'vl-basic.csv' + vl_rate: str = 'vl-rate.csv' + vr_basic: str = 'vr-basic.csv' + vr_rate: str = 'vr-rate.csv' + running: str = 'running.csv' + + +# def csv_file_to_dataframe(filename: str) -> pd.DataFrame | None: +# with open(filename, 'r', encoding='utf8') as file: +# reader = csv.reader(file) +# +# for row in reader: + + @router.get('/playerkeys') async def get_player_keys(player_id: list = Query(default=None)): all_keys = [] @@ -37,3 +53,50 @@ async def get_player_keys(player_id: list = Query(default=None)): db.close() return return_val + +@router.post('/live-update/batting') +def live_update_batting(files: BattingFiles, cardset_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to initiate live updates.' + ) + + data = {} # : { 'vL': [combined vl stat data], 'vR': [combined vr stat data] } + for row in files.vl_basic: + if row['pa'] >= 20: + data[row['fgid']]['vL'] = row + for row in files.vl_rate: + if row['fgid'] in data.keys(): + data[row['fgid']]['vL'].extend(row) + + for row in files.vr_basic: + if row['pa'] >= 40 and row['fgid'] in data.keys(): + data[row['fgid']]['vR'] = row + for row in files.vr_rate: + if row['fgid'] in data.keys(): + data[row['fgid']]['vR'].extend(row) + + for x in data.items(): + pass + # Create BattingCardRating object for vL + # Create BattingCardRating object for vR + + # Read running stats and create/update BattingCard object + + return files.dict() + + +@router.post('/live-update/pitching') +def live_update_pitching(files: BattingFiles, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to initiate live updates.' + ) + + return files.dict() diff --git a/db_engine.py b/db_engine.py index e106c64..1b98dcd 100644 --- a/db_engine.py +++ b/db_engine.py @@ -84,6 +84,19 @@ class Cardset(BaseModel): db.create_tables([Cardset]) +class MlbPlayer(BaseModel): + first_name = CharField() + last_name = CharField() + key_fangraphs = IntegerField(null=True) + key_bbref = CharField(null=True) + key_retro = CharField(null=True) + key_mlbam = IntegerField(null=True) + offense_col = IntegerField(default=1) + + +db.create_tables([MlbPlayer]) + + class Player(BaseModel): player_id = IntegerField(primary_key=True) p_name = CharField() @@ -110,6 +123,7 @@ class Player(BaseModel): fangr_id = CharField(null=True) description = CharField() quantity = IntegerField(default=999) + mlb_player = ForeignKeyField(MlbPlayer, null=True) def __str__(self): return f'{self.cardset} {self.p_name} ({self.rarity.name})' diff --git a/db_migrations.py b/db_migrations.py index 7559e0a..418bc8b 100644 --- a/db_migrations.py +++ b/db_migrations.py @@ -11,23 +11,23 @@ migrator = SqliteMigrator(db_engine.db) # pitcher_injury = IntegerField(null=True) -# pos_1 = CharField(default='None') +# offense_col = IntegerField(null=True) # pos_2 = CharField(null=True) # last_game = CharField(null=True) # game_type = CharField(null=True) -# pack_type = ForeignKeyField(PackType, default=1, to_field='id', field_type=int) +mlb_player = ForeignKeyField(db_engine.MlbPlayer, field=db_engine.MlbPlayer.id, null=True) # active_theme = ForeignKeyField(PackTheme, to_field='id', field_type=int, null=True) # active_theme = ForeignKeyField(db_engine.PackTheme, field=db_engine.PackTheme.id, null=True) # for careers # game_type = CharField(null=True) # pack_team = ForeignKeyField(db_engine.Team, field=db_engine.Team.id, null=True) -pack_cardset = ForeignKeyField(db_engine.Cardset, field=db_engine.Cardset.id, null=True) +# pack_cardset = ForeignKeyField(db_engine.Cardset, field=db_engine.Cardset.id, null=True) migrate( # migrator.add_column('current', 'active_theme_id', active_theme), # migrator.add_column('pack', 'pack_team_id', pack_team), - migrator.add_column('pack', 'pack_cardset_id', pack_cardset), + migrator.add_column('player', 'mlb_player_id', mlb_player), # migrator.rename_column('cardset', 'available', 'for_purchase') - # migrator.add_column('player', 'pos_1', pos_1), + # migrator.add_column('player', 'offense_col', offense_col), # migrator.add_column('comment_tbl', 'comment', comment_field), # migrator.rename_column('story', 'pub_date', 'publish_date'), # migrator.drop_column('story', 'some_old_field'), diff --git a/requirements.txt b/requirements.txt index 4ac1ae5..e6c901c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ python-multipart pandas pygsheets pybaseball +python-multipart +requests From 89aebd441de69d4c0a6ff64892561107d797d399 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 24 Sep 2023 18:59:32 -0500 Subject: [PATCH 16/40] Phase 1 card images --- app/card_creation.py | 41 +++++++++++++++ app/main.py | 5 ++ app/routers_v2/battingcardratings.py | 31 +++++++---- app/routers_v2/players.py | 79 ++++++++++++++++++++++++++-- requirements.txt | 2 + 5 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 app/card_creation.py diff --git a/app/card_creation.py b/app/card_creation.py new file mode 100644 index 0000000..7f0f8fe --- /dev/null +++ b/app/card_creation.py @@ -0,0 +1,41 @@ +import pandas as pd + +chance_df = pd.DataFrame( + { + 'd20-1': [0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.25, 0.2, 0.15, 0.1, 0.05], + 'd20-2': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1], + 'd20-3': [0.15, 0.3, 0.45, 0.6, 0.75, 0.9, 0.75, 0.6, 0.45, 0.3, 0.15], + 'd20-4': [0.2, 0.4, 0.6, 0.8, 1, 1.2, 1, 0.8, 0.6, 0.4, 0.2], + 'd20-5': [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.25, 1, 0.75, 0.5, 0.25], + 'd20-6': [0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 1.5, 1.2, 0.9, 0.6, 0.3], + 'd20-7': [0.35, 0.7, 1.05, 1.4, 1.75, 2.1, 1.75, 1.4, 1.05, 0.7, 0.35], + 'd20-8': [0.4, 0.8, 1.2, 1.6, 2, 2.4, 2, 1.6, 1.2, 0.8, 0.4], + 'd20-9': [0.45, 0.9, 1.35, 1.8, 2.25, 2.7, 2.25, 1.8, 1.35, 0.9, 0.45], + 'd20-10': [0.5, 1, 1.5, 2, 2.5, 3, 2.5, 2, 1.5, 1, 0.5], + 'd20-11': [0.55, 1.1, 1.65, 2.2, 2.75, 3.3, 2.75, 2.2, 1.65, 1.1, 0.55], + 'd20-12': [0.6, 1.2, 1.8, 2.4, 3, 3.6, 3, 2.4, 1.8, 1.2, 0.6], + 'd20-13': [0.65, 1.3, 1.95, 2.6, 3.25, 3.9, 3.25, 2.6, 1.95, 1.3, 0.65], + 'd20-14': [0.7, 1.4, 2.1, 2.8, 3.5, 4.2, 3.5, 2.8, 2.1, 1.4, 0.7], + 'd20-15': [0.75, 1.5, 2.25, 3, 3.75, 4.5, 3.75, 3, 2.25, 1.5, 0.75], + 'd20-16': [0.8, 1.6, 2.4, 3.2, 4, 4.8, 4, 3.2, 2.4, 1.6, 0.8], + 'd20-17': [0.85, 1.7, 2.55, 3.4, 4.25, 5.1, 4.25, 3.4, 2.55, 1.7, 0.85], + 'd20-18': [0.9, 1.8, 2.7, 3.6, 4.5, 5.4, 4.5, 3.6, 2.7, 1.8, 0.9], + 'd20-19': [0.95, 1.9, 2.85, 3.8, 4.75, 5.7, 4.75, 3.8, 2.85, 1.9, 0.95], + 'd20-20': [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1] + }, + index=[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] +) + + +def get_batter_card_html(request, player, batting_card, ratings_vl, ratings_vr): + """ + create header_data + create column data + return data to be templated + """ + return f'

{player.p_name}

' + + +def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr) -> dict: + + return {} diff --git a/app/main.py b/app/main.py index f187f3b..423c32a 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,7 @@ +import os + from fastapi import FastAPI +from fastapi.templating import Jinja2Templates from .routers_v2 import ( current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, @@ -9,6 +12,8 @@ app = FastAPI( responses={404: {'description': 'Not found'}} ) +# templates = Jinja2Templates(directory=os.path.dirname(os.path.abspath(__file__))) + app.include_router(current.router) app.include_router(teams.router) app.include_router(rarity.router) diff --git a/app/routers_v2/battingcardratings.py b/app/routers_v2/battingcardratings.py index 987825f..d7f25ff 100644 --- a/app/routers_v2/battingcardratings.py +++ b/app/routers_v2/battingcardratings.py @@ -1,10 +1,10 @@ -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query, Response from typing import Literal, Optional, List import logging import pydantic from pydantic import validator, root_validator -from ..db_engine import db, BattingCardRatings, model_to_dict, chunked, BattingCard +from ..db_engine import db, BattingCardRatings, model_to_dict, chunked, BattingCard, Player, query_to_csv from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -74,7 +74,7 @@ class BattingCardRatingsModel(pydantic.BaseModel): values['flyout_bq'] + values['flyout_lf_b'] + values['flyout_rf_b'] + values['groundout_a'] + values['groundout_b'] + values['groundout_c']) - if total_chances != 108: + if round(total_chances) != 108: raise ValueError("Must have exactly 108 chances on the card") return values @@ -86,7 +86,8 @@ class RatingsList(pydantic.BaseModel): @router.get('') async def get_card_ratings( battingcard_id: list = Query(default=None), vs_hand: Literal['R', 'L', 'vR', 'vL'] = None, - short_output: bool = False, token: str = Depends(oauth2_scheme)): + short_output: bool = False, csv: bool = False, cardset_id: list = Query(default=None), + token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'Bad Token: {token}') db.close() @@ -100,13 +101,23 @@ async def get_card_ratings( if battingcard_id is not None: all_ratings = all_ratings.where(BattingCardRatings.battingcard_id << battingcard_id) if vs_hand is not None: - all_ratings = all_ratings.where(BattingCardRatings.vs_hand << vs_hand[-1]) + all_ratings = all_ratings.where(BattingCardRatings.vs_hand == vs_hand[-1]) + if cardset_id is not None: + set_players = Player.select(Player.player_id).where(Player.cardset_id << cardset_id) + set_cards = BattingCard.select(BattingCard.id).where(BattingCard.player << set_players) + all_ratings = all_ratings.where(BattingCardRatings.battingcard << set_cards) - return_val = {'count': all_ratings.count(), 'ratings': [ - model_to_dict(x, recurse=not short_output) for x in all_ratings - ]} - db.close() - return return_val + if csv: + return_val = query_to_csv(all_ratings) + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_ratings.count(), 'ratings': [ + model_to_dict(x, recurse=not short_output) for x in all_ratings + ]} + db.close() + return return_val @router.get('/{ratings_id}') diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py index d2f00ff..3f39c43 100644 --- a/app/routers_v2/players.py +++ b/app/routers_v2/players.py @@ -1,10 +1,17 @@ -from fastapi import APIRouter, Depends, HTTPException, Response, Query +import os.path + +from fastapi import APIRouter, Depends, HTTPException, Request, Response, Query +from fastapi.responses import FileResponse +from fastapi.templating import Jinja2Templates +from html2image import Html2Image from typing import Optional, List import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Player, model_to_dict, fn, chunked, Paperdex, Cardset, Rarity +from ..card_creation import get_batter_card_html, get_batter_card_data +from ..db_engine import db, Player, model_to_dict, fn, chunked, Paperdex, Cardset, Rarity, BattingCard, \ + BattingCardRatings from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -19,6 +26,9 @@ router = APIRouter( ) +templates = Jinja2Templates(directory="storage/templates") + + class PlayerPydantic(pydantic.BaseModel): player_id: int = None p_name: str @@ -60,7 +70,7 @@ async def get_players( has_vanity_card: Optional[bool] = None, strat_code: Optional[str] = None, bbref_id: Optional[str] = None, fangr_id: Optional[str] = None, inc_dex: Optional[bool] = True, in_desc: Optional[str] = None, flat: Optional[bool] = False, sort_by: Optional[str] = False, cardset_id_exclude: list = Query(default=None), - limit: Optional[int] = None, csv: Optional[bool] = None): + limit: Optional[int] = None, csv: Optional[bool] = None, short_output: Optional[bool] = False): all_players = Player.select() if all_players.count() == 0: db.close() @@ -158,7 +168,7 @@ async def get_players( return_val = {'count': len(final_players), 'players': []} for x in final_players: - this_record = model_to_dict(x, recurse=not flat) + this_record = model_to_dict(x, recurse=not (flat or short_output)) if inc_dex: this_dex = Paperdex.select().where(Paperdex.player == x) @@ -319,6 +329,67 @@ async def get_one_player(player_id, csv: Optional[bool] = False): return return_val +@router.get('/{player_id}/batting-card') +async def get_player_card( + request: Request, player_id: int, variant: int = 0, d: str = None, html: Optional[bool] = False): + if os.path.isfile(f'storage/cards/{player_id}-{d}-v{variant}.png') and html is False: + db.close() + return FileResponse( + path=f'storage/cards/{player_id}-{d}-v{variant}.png', + media_type='image/png' + ) + + try: + this_player = Player.get_by_id(player_id) + except Exception: + db.close() + raise HTTPException(status_code=404, detail=f'No player found with id {player_id}') + + this_bc = BattingCard.get_or_none(BattingCard.player == this_player, BattingCard.variant == variant) + if this_bc is None: + raise HTTPException(status_code=404, detail=f'Batting card not found for id {player_id}, variant {variant}') + + rating_vl = BattingCardRatings.get_or_none( + BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == 'L') + rating_vr = BattingCardRatings.get_or_none( + BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == 'R') + if None in [rating_vr, rating_vl]: + raise HTTPException(status_code=404, detail=f'Ratings not found for batting card {this_bc.id}') + + hti = Html2Image( + browser='chromium', + size=(1200, 600), + output_path=f'storage/cards', + custom_flags=['--no-sandbox', '--disable-remote-debugging', '--headless', '--disable-gpu', + '--disable-software-rasterizer', '--disable-dev-shm-usage'] + ) + card_data = { + 'player': this_player, + 'card_type': 'batter', + 'results_vl_one': 'Big Dongs', + 'results_vl_two': 'Lesser Dongs', + 'results_vl_three': 'Sad Dongs', + 'results_vr_one': 'Light Dongs', + 'results_vr_two': 'Hefty Dongs', + 'results_vr_three': 'Obese Dongs', + 'request': request + } + html_response = templates.TemplateResponse("player_card.html", card_data) + + if html: + db.close() + return html_response + + logging.debug(f'body:\n{html_response.body.decode("UTF-8")}') + x = hti.screenshot( + html_str=str(html_response.body.decode("UTF-8")), + save_as=f'{player_id}-{d}-v{variant}.png' + ) + + db.close() + return FileResponse(path=x[0], media_type='image/png') + + @router.patch('/{player_id}') async def v1_players_patch( player_id, name: Optional[str] = None, image: Optional[str] = None, image2: Optional[str] = None, diff --git a/requirements.txt b/requirements.txt index e6c901c..9d19235 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,5 @@ pygsheets pybaseball python-multipart requests +html2image +jinja2 From 23e1fd6f58d4e8496e7433226c7eadc8521e6f11 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 24 Sep 2023 19:00:21 -0500 Subject: [PATCH 17/40] Update player_scouting.py --- app/player_scouting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/player_scouting.py b/app/player_scouting.py index 2c33217..c6fcb40 100644 --- a/app/player_scouting.py +++ b/app/player_scouting.py @@ -1,6 +1,5 @@ from typing import Literal, Optional from pybaseball import playerid_reverse_lookup -from .card_creation import batter_calcs, pitcher_calcs, defense_calcs import pydantic From d4f26a6a2e9a1056765377fece0406498a475979 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 25 Sep 2023 00:19:26 -0500 Subject: [PATCH 18/40] Update dupe player check to use bbref_id --- app/routers_v2/players.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py index 3f39c43..9fc44d6 100644 --- a/app/routers_v2/players.py +++ b/app/routers_v2/players.py @@ -597,7 +597,7 @@ async def post_players(new_player: PlayerPydantic, token: str = Depends(oauth2_s ) dupe_query = Player.select().where( - (fn.Lower(Player.p_name) == new_player.p_name.lower()) & (Player.cardset_id == new_player.cardset_id) + (Player.bbref_id == new_player.bbref_id) & (Player.cardset_id == new_player.cardset_id) ) if dupe_query.count() != 0: db.close() From 561c7e99d434bcba76aad5354d5d378b3d37c98e Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 25 Sep 2023 00:19:41 -0500 Subject: [PATCH 19/40] Pitching card updates for card gen --- app/routers_v2/battingcards.py | 2 +- app/routers_v2/pitchingcardratings.py | 27 ++++++++++---- app/routers_v2/pitchingcards.py | 53 +++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/app/routers_v2/battingcards.py b/app/routers_v2/battingcards.py index 146680a..418b712 100644 --- a/app/routers_v2/battingcards.py +++ b/app/routers_v2/battingcards.py @@ -123,7 +123,7 @@ async def put_cards(cards: BattingCardList, token: str = Depends(oauth2_scheme)) else: logging.info(f'randomly setting offense_col for {this_player.p_name}') x.offense_col = random.randint(1, 3) - logging.info(f'x.dict(): {x.dict()}') + logging.debug(f'x.dict(): {x.dict()}') new_cards.append(x.dict()) with db.atomic(): diff --git a/app/routers_v2/pitchingcardratings.py b/app/routers_v2/pitchingcardratings.py index 2fb7125..f3eea46 100644 --- a/app/routers_v2/pitchingcardratings.py +++ b/app/routers_v2/pitchingcardratings.py @@ -1,10 +1,10 @@ -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query, Response from typing import Literal, Optional, List import logging import pydantic from pydantic import validator, root_validator -from ..db_engine import db, PitchingCardRatings, model_to_dict, chunked, PitchingCard +from ..db_engine import db, PitchingCardRatings, model_to_dict, chunked, PitchingCard, Player, query_to_csv from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -91,7 +91,8 @@ class RatingsList(pydantic.BaseModel): @router.get('') async def get_card_ratings( pitchingcard_id: list = Query(default=None), vs_hand: Literal['R', 'L', 'vR', 'vL'] = None, - short_output: bool = False, token: str = Depends(oauth2_scheme)): + short_output: bool = False, csv: bool = False, cardset_id: list = Query(default=None), + token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'Bad Token: {token}') db.close() @@ -106,12 +107,22 @@ async def get_card_ratings( all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard_id << pitchingcard_id) if vs_hand is not None: all_ratings = all_ratings.where(PitchingCardRatings.vs_hand << vs_hand[-1]) + if cardset_id is not None: + set_players = Player.select(Player.player_id).where(Player.cardset_id << cardset_id) + set_cards = PitchingCard.select(PitchingCard.id).where(PitchingCard.player << set_players) + all_ratings = all_ratings.where(PitchingCardRatings.battingcard << set_cards) - return_val = {'count': all_ratings.count(), 'ratings': [ - model_to_dict(x, recurse=not short_output) for x in all_ratings - ]} - db.close() - return return_val + if csv: + return_val = query_to_csv(all_ratings) + db.close() + return Response(content=return_val, media_type='text/csv') + + else: + return_val = {'count': all_ratings.count(), 'ratings': [ + model_to_dict(x, recurse=not short_output) for x in all_ratings + ]} + db.close() + return return_val @router.get('/{ratings_id}') diff --git a/app/routers_v2/pitchingcards.py b/app/routers_v2/pitchingcards.py index 7190011..77afd6c 100644 --- a/app/routers_v2/pitchingcards.py +++ b/app/routers_v2/pitchingcards.py @@ -1,9 +1,11 @@ +import random + from fastapi import APIRouter, Depends, HTTPException, Query from typing import Literal, Optional, List import logging import pydantic -from ..db_engine import db, PitchingCard, model_to_dict, chunked, Player +from ..db_engine import db, PitchingCard, model_to_dict, chunked, Player, fn, MlbPlayer from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -28,7 +30,7 @@ class PitchingCardModel(pydantic.BaseModel): relief_rating: int = 0 closer_rating: int = None batting: str = "#1WR-C" - offense_col: int + offense_col: int = None hand: Literal['R', 'L', 'S'] = 'R' @@ -38,13 +40,21 @@ class PitchingCardList(pydantic.BaseModel): @router.get('') async def get_pitching_cards( - player_id: list = Query(default=None), cardset_id: list = Query(default=None), short_output: bool = False): + player_id: list = Query(default=None), player_name: list = Query(default=None), + cardset_id: list = Query(default=None), short_output: bool = False, limit: Optional[int] = None): all_cards = PitchingCard.select() if player_id is not None: all_cards = all_cards.where(PitchingCard.player_id << player_id) if cardset_id is not None: all_players = Player.select().where(Player.cardset_id << cardset_id) all_cards = all_cards.where(PitchingCard.player << all_players) + if player_name is not None: + name_list = [x.lower() for x in player_name] + all_players = Player.select().where(fn.lower(Player.p_name) << name_list) + all_cards = all_cards.where(PitchingCard.player << all_players) + + if limit is not None: + all_cards = all_cards.limit(limit) return_val = {'count': all_cards.count(), 'cards': [ model_to_dict(x, recurse=not short_output) for x in all_cards @@ -85,7 +95,7 @@ async def put_cards(cards: PitchingCardList, token: str = Depends(oauth2_scheme) db.close() raise HTTPException( status_code=401, - detail='You are not authorized to post batting cards. This event has been logged.' + detail='You are not authorized to post pitching cards. This event has been logged.' ) new_cards = [] @@ -93,13 +103,26 @@ async def put_cards(cards: PitchingCardList, token: str = Depends(oauth2_scheme) for x in cards.cards: try: - PitchingCard.get( + old = PitchingCard.get( (PitchingCard.player_id == x.player_id) & (PitchingCard.variant == x.variant) ) + + if x.offense_col is None: + x.offense_col = old.offense_col updates += PitchingCard.update(x.dict()).where( (PitchingCard.player_id == x.player_id) & (PitchingCard.variant == x.variant) ).execute() except PitchingCard.DoesNotExist: + if x.offense_col is None: + this_player = Player.get_or_none(Player.player_id == x.player_id) + mlb_player = MlbPlayer.get_or_none(MlbPlayer.key_bbref == this_player.bbref_id) + if mlb_player is not None: + logging.info(f'setting offense_col to {mlb_player.offense_col} for {this_player.p_name}') + x.offense_col = mlb_player.offense_col + else: + logging.info(f'randomly setting offense_col for {this_player.p_name}') + x.offense_col = random.randint(1, 3) + logging.debug(f'x.dict(): {x.dict()}') new_cards.append(x.dict()) with db.atomic(): @@ -162,13 +185,13 @@ async def delete_card(card_id: int, token: str = Depends(oauth2_scheme)): db.close() raise HTTPException( status_code=401, - detail='You are not authorized to delete batting cards. This event has been logged.' + detail='You are not authorized to delete pitching cards. This event has been logged.' ) this_card = PitchingCard.get_or_none(PitchingCard.id == card_id) if this_card is None: db.close() - raise HTTPException(status_code=404, detail=f'BattingCard id {card_id} not found') + raise HTTPException(status_code=404, detail=f'Pitching id {card_id} not found') count = this_card.delete_instance() db.close() @@ -177,3 +200,19 @@ async def delete_card(card_id: int, token: str = Depends(oauth2_scheme)): return f'Card {this_card} has been deleted' else: raise HTTPException(status_code=500, detail=f'Card {this_card} could not be deleted') + + +@router.delete('') +async def delete_all_cards(token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to delete pitching cards. This event has been logged.' + ) + + d_query = PitchingCard.delete() + d_query.execute() + + return f'Deleted {d_query.count()} pitching cards' From 07c1b51d0f1d960130943777d8da580c1b5aec9a Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 3 Oct 2023 12:06:48 -0500 Subject: [PATCH 20/40] Bug fixes for unique indeces --- app/db_engine.py | 11 +- db_engine.py | 452 ++++++++++++++++++++++++++++------------------- 2 files changed, 277 insertions(+), 186 deletions(-) diff --git a/app/db_engine.py b/app/db_engine.py index dac9051..1683921 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -183,7 +183,7 @@ class Player(BaseModel): fangr_id = CharField(null=True) description = CharField() quantity = IntegerField(default=999) - mlb_player = ForeignKeyField(MlbPlayer, null=True) + mlbplayer = ForeignKeyField(MlbPlayer, null=True) def __str__(self): return f'{self.cardset} {self.p_name} ({self.rarity.name})' @@ -625,7 +625,7 @@ class PitchingCard(BaseModel): pc_index = ModelIndex(PitchingCard, (PitchingCard.player, PitchingCard.variant), unique=True) -PitchingCard.add_index(bc_index) +PitchingCard.add_index(pc_index) class PitchingCardRatings(BaseModel): @@ -644,8 +644,9 @@ class PitchingCardRatings(BaseModel): hbp = FloatField() walk = FloatField() strikeout = FloatField() - fo_slap = FloatField() - fo_center = FloatField() + flyout_lf_b = FloatField() + flyout_cf_b = FloatField() + flyout_rf_b = FloatField() groundout_a = FloatField() groundout_b = FloatField() xcheck_p = FloatField() @@ -683,7 +684,7 @@ class CardPosition(BaseModel): pos_index = ModelIndex( CardPosition, (CardPosition.player, CardPosition.variant, CardPosition.position), unique=True ) -PitchingCardRatings.add_index(pos_index) +CardPosition.add_index(pos_index) db.create_tables([BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition]) diff --git a/db_engine.py b/db_engine.py index 1b98dcd..52d1e7e 100644 --- a/db_engine.py +++ b/db_engine.py @@ -1,9 +1,13 @@ import math from datetime import datetime +from typing import List import logging import os +from pandas import DataFrame from peewee import * +from peewee import ModelSelect +from playhouse.shortcuts import model_to_dict db = SqliteDatabase( 'storage/pd_master.db', @@ -23,6 +27,62 @@ logging.basicConfig( ) +def model_csv_headers(this_obj, exclude=None) -> List: + data = model_to_dict(this_obj, recurse=False, exclude=exclude) + return [x for x in data.keys()] + + +def model_to_csv(this_obj, exclude=None) -> List: + data = model_to_dict(this_obj, recurse=False, exclude=exclude) + return [x for x in data.values()] + + +def query_to_csv(all_items: ModelSelect, exclude=None): + if all_items.count() == 0: + data_list = [['No data found']] + else: + data_list = [model_csv_headers(all_items[0], exclude=exclude)] + for x in all_items: + data_list.append(model_to_csv(x, exclude=exclude)) + + return DataFrame(data_list).to_csv(header=False, index=False) + + +def complex_data_to_csv(complex_data: List): + if len(complex_data) == 0: + data_list = [['No data found']] + else: + data_list = [[x for x in complex_data[0].keys()]] + for line in complex_data: + logging.info(f'line: {line}') + this_row = [] + for key in line: + logging.info(f'key: {key}') + if line[key] is None: + this_row.append('') + + elif isinstance(line[key], dict): + if 'name' in line[key]: + this_row.append(line[key]['name']) + elif 'abbrev' in line[key]: + this_row.append(line[key]['abbrev']) + else: + this_row.append(line[key]['id']) + + elif isinstance(line[key], int) and line[key] > 100000000: + this_row.append(f"'{line[key]}") + + elif isinstance(line[key], str) and ',' in line[key]: + this_row.append(line[key].replace(",", "-_-")) + + else: + this_row.append(line[key]) + + data_list.append(this_row) + + return DataFrame(data_list).to_csv(header=False, index=False) + + class BaseModel(Model): class Meta: database = db @@ -498,6 +558,7 @@ db.create_tables([ class BattingCard(BaseModel): player = ForeignKeyField(Player) + variant = IntegerField() steal_low = IntegerField() steal_high = IntegerField() steal_auto = BooleanField() @@ -509,9 +570,13 @@ class BattingCard(BaseModel): hand = CharField(default='R') +bc_index = ModelIndex(BattingCard, (BattingCard.player, BattingCard.variant), unique=True) +BattingCard.add_index(bc_index) + + class BattingCardRatings(BaseModel): battingcard = ForeignKeyField(BattingCard) - vs_hand = FloatField() + vs_hand = CharField(default='R') homerun = FloatField() bp_homerun = FloatField() triple = FloatField() @@ -539,20 +604,33 @@ class BattingCardRatings(BaseModel): slg = FloatField(null=True) +bcr_index = ModelIndex( + BattingCardRatings, (BattingCardRatings.battingcard, BattingCardRatings.vs_hand), unique=True +) +BattingCardRatings.add_index(bcr_index) + + class PitchingCard(BaseModel): player = ForeignKeyField(Player) + variant = IntegerField() balk = IntegerField() - wild_pitch = IntegerField(null=True) - hold = CharField() + wild_pitch = IntegerField() + hold = IntegerField() starter_rating = IntegerField() relief_rating = IntegerField() closer_rating = IntegerField(null=True) batting = CharField(null=True) + offense_col = IntegerField() + hand = CharField(default='R') + + +pc_index = ModelIndex(PitchingCard, (PitchingCard.player, PitchingCard.variant), unique=True) +PitchingCard.add_index(pc_index) class PitchingCardRatings(BaseModel): pitchingcard = ForeignKeyField(PitchingCard) - vs_hand = CharField() + vs_hand = CharField(default='R') homerun = FloatField() bp_homerun = FloatField() triple = FloatField() @@ -566,8 +644,9 @@ class PitchingCardRatings(BaseModel): hbp = FloatField() walk = FloatField() strikeout = FloatField() - fo_slap = FloatField() - fo_center = FloatField() + flyout_lf_b = FloatField() + flyout_cf_b = FloatField() + flyout_rf_b = FloatField() groundout_a = FloatField() groundout_b = FloatField() xcheck_p = FloatField() @@ -584,10 +663,15 @@ class PitchingCardRatings(BaseModel): slg = FloatField(null=True) +pcr_index = ModelIndex( + PitchingCardRatings, (PitchingCardRatings.pitchingcard, PitchingCardRatings.vs_hand), unique=True +) +PitchingCardRatings.add_index(pcr_index) + + class CardPosition(BaseModel): player = ForeignKeyField(Player) - batting = ForeignKeyField(BattingCard, null=True) - pitching = ForeignKeyField(PitchingCard, null=True) + variant = IntegerField() position = CharField() innings = IntegerField() range = IntegerField() @@ -597,182 +681,188 @@ class CardPosition(BaseModel): overthrow = IntegerField(null=True) +pos_index = ModelIndex( + CardPosition, (CardPosition.player, CardPosition.variant, CardPosition.position), unique=True +) +CardPosition.add_index(pos_index) + + db.create_tables([BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition]) db.close() -scout_db = SqliteDatabase( - 'storage/card_creation.db', - pragmas={ - 'journal_mode': 'wal', - 'cache_size': -1 * 64000, - 'synchronous': 0 - } -) - - -class BaseModelScout(Model): - class Meta: - database = scout_db - - -class ScoutCardset(BaseModelScout): - set_title = CharField() - set_subtitle = CharField(null=True) - - -class ScoutPlayer(BaseModelScout): - sba_id = IntegerField(primary_key=True) - name = CharField() - fg_id = IntegerField() - br_id = CharField() - offense_col = IntegerField() - hand = CharField(default='R') - - -scout_db.create_tables([ScoutCardset, ScoutPlayer]) - - -class BatterRatings(BaseModelScout): - id = CharField(unique=True, primary_key=True) - player = ForeignKeyField(ScoutPlayer) - cardset = ForeignKeyField(ScoutCardset) - vs_hand = FloatField() - is_prep = BooleanField() - homerun = FloatField() - bp_homerun = FloatField() - triple = FloatField() - double_three = FloatField() - double_two = FloatField() - double_pull = FloatField() - single_two = FloatField() - single_one = FloatField() - single_center = FloatField() - bp_single = FloatField() - hbp = FloatField() - walk = FloatField() - strikeout = FloatField() - lineout = FloatField() - popout = FloatField() - flyout_a = FloatField() - flyout_bq = FloatField() - flyout_lf_b = FloatField() - flyout_rf_b = FloatField() - groundout_a = FloatField() - groundout_b = FloatField() - groundout_c = FloatField() - avg = FloatField(null=True) - obp = FloatField(null=True) - slg = FloatField(null=True) - - -class PitcherRatings(BaseModelScout): - id = CharField(unique=True, primary_key=True) - player = ForeignKeyField(ScoutPlayer) - cardset = ForeignKeyField(ScoutCardset) - vs_hand = CharField() - is_prep = BooleanField() - homerun = FloatField() - bp_homerun = FloatField() - triple = FloatField() - double_three = FloatField() - double_two = FloatField() - double_cf = FloatField() - single_two = FloatField() - single_one = FloatField() - single_center = FloatField() - bp_single = FloatField() - hbp = FloatField() - walk = FloatField() - strikeout = FloatField() - fo_slap = FloatField() - fo_center = FloatField() - groundout_a = FloatField() - groundout_b = FloatField() - xcheck_p = FloatField() - xcheck_c = FloatField() - xcheck_1b = FloatField() - xcheck_2b = FloatField() - xcheck_3b = FloatField() - xcheck_ss = FloatField() - xcheck_lf = FloatField() - xcheck_cf = FloatField() - xcheck_rf = FloatField() - avg = FloatField(null=True) - obp = FloatField(null=True) - slg = FloatField(null=True) - - -# scout_db.create_tables([BatterRatings, PitcherRatings]) - - -class CardColumns(BaseModelScout): - id = CharField(unique=True, primary_key=True) - player = ForeignKeyField(ScoutPlayer) - hand = CharField() - b_ratings = ForeignKeyField(BatterRatings, null=True) - p_ratings = ForeignKeyField(PitcherRatings, null=True) - one_dice = CharField() - one_results = CharField() - one_splits = CharField() - two_dice = CharField() - two_results = CharField() - two_splits = CharField() - three_dice = CharField() - three_results = CharField() - three_splits = CharField() - - -class Position(BaseModelScout): - player = ForeignKeyField(ScoutPlayer) - cardset = ForeignKeyField(ScoutCardset) - position = CharField() - innings = IntegerField() - range = IntegerField() - error = IntegerField() - arm = CharField(null=True) - pb = IntegerField(null=True) - overthrow = IntegerField(null=True) - - -class BatterData(BaseModelScout): - player = ForeignKeyField(ScoutPlayer) - cardset = ForeignKeyField(ScoutCardset) - stealing = CharField() - st_low = IntegerField() - st_high = IntegerField() - st_auto = BooleanField() - st_jump = FloatField() - bunting = CharField(null=True) - hit_and_run = CharField(null=True) - running = CharField() - - -class PitcherData(BaseModelScout): - player = ForeignKeyField(ScoutPlayer) - cardset = ForeignKeyField(ScoutCardset) - balk = IntegerField(null=True) - wild_pitch = IntegerField(null=True) - hold = CharField() - starter_rating = IntegerField() - relief_rating = IntegerField() - closer_rating = IntegerField(null=True) - batting = CharField(null=True) - - -scout_db.create_tables([CardColumns, Position, BatterData, PitcherData]) - - -class CardOutput(BaseModelScout): - name = CharField() - hand = CharField() - positions = CharField() - stealing = CharField() - bunting = CharField() - hitandrun = CharField() - running = CharField() - - -scout_db.close() +# scout_db = SqliteDatabase( +# 'storage/card_creation.db', +# pragmas={ +# 'journal_mode': 'wal', +# 'cache_size': -1 * 64000, +# 'synchronous': 0 +# } +# ) +# +# +# class BaseModelScout(Model): +# class Meta: +# database = scout_db +# +# +# class ScoutCardset(BaseModelScout): +# set_title = CharField() +# set_subtitle = CharField(null=True) +# +# +# class ScoutPlayer(BaseModelScout): +# sba_id = IntegerField(primary_key=True) +# name = CharField() +# fg_id = IntegerField() +# br_id = CharField() +# offense_col = IntegerField() +# hand = CharField(default='R') +# +# +# scout_db.create_tables([ScoutCardset, ScoutPlayer]) +# +# +# class BatterRatings(BaseModelScout): +# id = CharField(unique=True, primary_key=True) +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# vs_hand = FloatField() +# is_prep = BooleanField() +# homerun = FloatField() +# bp_homerun = FloatField() +# triple = FloatField() +# double_three = FloatField() +# double_two = FloatField() +# double_pull = FloatField() +# single_two = FloatField() +# single_one = FloatField() +# single_center = FloatField() +# bp_single = FloatField() +# hbp = FloatField() +# walk = FloatField() +# strikeout = FloatField() +# lineout = FloatField() +# popout = FloatField() +# flyout_a = FloatField() +# flyout_bq = FloatField() +# flyout_lf_b = FloatField() +# flyout_rf_b = FloatField() +# groundout_a = FloatField() +# groundout_b = FloatField() +# groundout_c = FloatField() +# avg = FloatField(null=True) +# obp = FloatField(null=True) +# slg = FloatField(null=True) +# +# +# class PitcherRatings(BaseModelScout): +# id = CharField(unique=True, primary_key=True) +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# vs_hand = CharField() +# is_prep = BooleanField() +# homerun = FloatField() +# bp_homerun = FloatField() +# triple = FloatField() +# double_three = FloatField() +# double_two = FloatField() +# double_cf = FloatField() +# single_two = FloatField() +# single_one = FloatField() +# single_center = FloatField() +# bp_single = FloatField() +# hbp = FloatField() +# walk = FloatField() +# strikeout = FloatField() +# fo_slap = FloatField() +# fo_center = FloatField() +# groundout_a = FloatField() +# groundout_b = FloatField() +# xcheck_p = FloatField() +# xcheck_c = FloatField() +# xcheck_1b = FloatField() +# xcheck_2b = FloatField() +# xcheck_3b = FloatField() +# xcheck_ss = FloatField() +# xcheck_lf = FloatField() +# xcheck_cf = FloatField() +# xcheck_rf = FloatField() +# avg = FloatField(null=True) +# obp = FloatField(null=True) +# slg = FloatField(null=True) +# +# +# # scout_db.create_tables([BatterRatings, PitcherRatings]) +# +# +# class CardColumns(BaseModelScout): +# id = CharField(unique=True, primary_key=True) +# player = ForeignKeyField(ScoutPlayer) +# hand = CharField() +# b_ratings = ForeignKeyField(BatterRatings, null=True) +# p_ratings = ForeignKeyField(PitcherRatings, null=True) +# one_dice = CharField() +# one_results = CharField() +# one_splits = CharField() +# two_dice = CharField() +# two_results = CharField() +# two_splits = CharField() +# three_dice = CharField() +# three_results = CharField() +# three_splits = CharField() +# +# +# class Position(BaseModelScout): +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# position = CharField() +# innings = IntegerField() +# range = IntegerField() +# error = IntegerField() +# arm = CharField(null=True) +# pb = IntegerField(null=True) +# overthrow = IntegerField(null=True) +# +# +# class BatterData(BaseModelScout): +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# stealing = CharField() +# st_low = IntegerField() +# st_high = IntegerField() +# st_auto = BooleanField() +# st_jump = FloatField() +# bunting = CharField(null=True) +# hit_and_run = CharField(null=True) +# running = CharField() +# +# +# class PitcherData(BaseModelScout): +# player = ForeignKeyField(ScoutPlayer) +# cardset = ForeignKeyField(ScoutCardset) +# balk = IntegerField(null=True) +# wild_pitch = IntegerField(null=True) +# hold = CharField() +# starter_rating = IntegerField() +# relief_rating = IntegerField() +# closer_rating = IntegerField(null=True) +# batting = CharField(null=True) +# +# +# scout_db.create_tables([CardColumns, Position, BatterData, PitcherData]) +# +# +# class CardOutput(BaseModelScout): +# name = CharField() +# hand = CharField() +# positions = CharField() +# stealing = CharField() +# bunting = CharField() +# hitandrun = CharField() +# running = CharField() +# +# +# scout_db.close() From b5e3c40d62b2d6939d7f856c96da4ad7bff93a27 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 3 Oct 2023 12:07:19 -0500 Subject: [PATCH 21/40] Fixed typo --- db_migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db_migrations.py b/db_migrations.py index 418bc8b..8694c8b 100644 --- a/db_migrations.py +++ b/db_migrations.py @@ -25,7 +25,7 @@ mlb_player = ForeignKeyField(db_engine.MlbPlayer, field=db_engine.MlbPlayer.id, migrate( # migrator.add_column('current', 'active_theme_id', active_theme), # migrator.add_column('pack', 'pack_team_id', pack_team), - migrator.add_column('player', 'mlb_player_id', mlb_player), + migrator.add_column('player', 'mlbplayer_id', mlb_player), # migrator.rename_column('cardset', 'available', 'for_purchase') # migrator.add_column('player', 'offense_col', offense_col), # migrator.add_column('comment_tbl', 'comment', comment_field), From 1776df7173249a4ecf19ec2b9bdf6348e4c12b7a Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 3 Oct 2023 12:07:34 -0500 Subject: [PATCH 22/40] Final test updates --- app/routers_v2/pitchingcardratings.py | 19 ++++++++++--------- app/routers_v2/players.py | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/routers_v2/pitchingcardratings.py b/app/routers_v2/pitchingcardratings.py index f3eea46..17fed1a 100644 --- a/app/routers_v2/pitchingcardratings.py +++ b/app/routers_v2/pitchingcardratings.py @@ -35,8 +35,9 @@ class PitchingCardRatingsModel(pydantic.BaseModel): hbp: float = 0.0 walk: float = 0.0 strikeout: float = 0.0 - fo_slap: float = 0.0 - fo_center: float = 0.0 + flyout_lf_b: float = 0.0 + flyout_cf_b: float = 0.0 + flyout_rf_b: float = 0.0 groundout_a: float = 0.0 groundout_b: float = 0.0 xcheck_p: float = 0.0 @@ -74,12 +75,12 @@ class PitchingCardRatingsModel(pydantic.BaseModel): values['homerun'] + values['bp_homerun'] + values['triple'] + values['double_three'] + values['double_two'] + values['double_cf'] + values['single_two'] + values['single_one'] + values['single_center'] + values['bp_single'] + values['hbp'] + values['walk'] + - values['strikeout'] + values['fo_slap'] + values['fo_center'] + values['groundout_a'] + - values['groundout_b'] + values['xcheck_p'] + values['xcheck_c'] + values['xcheck_1b'] + - values['xcheck_2b'] + values['xcheck_3b'] + values['xcheck_ss'] + values['xcheck_lf'] + - values['xcheck_cf'] + values['xcheck_rf']) + values['strikeout'] + values['flyout_lf_b'] + values['flyout_cf_b'] + values['flyout_rf_b'] + + values['groundout_a'] + values['groundout_b'] + values['xcheck_p'] + values['xcheck_c'] + + values['xcheck_1b'] + values['xcheck_2b'] + values['xcheck_3b'] + values['xcheck_ss'] + + values['xcheck_lf'] + values['xcheck_cf'] + values['xcheck_rf']) - if total_chances != 108: + if round(total_chances) != 108: raise ValueError("Must have exactly 108 chances on the card") return values @@ -106,11 +107,11 @@ async def get_card_ratings( if pitchingcard_id is not None: all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard_id << pitchingcard_id) if vs_hand is not None: - all_ratings = all_ratings.where(PitchingCardRatings.vs_hand << vs_hand[-1]) + all_ratings = all_ratings.where(PitchingCardRatings.vs_hand == vs_hand[-1]) if cardset_id is not None: set_players = Player.select(Player.player_id).where(Player.cardset_id << cardset_id) set_cards = PitchingCard.select(PitchingCard.id).where(PitchingCard.player << set_players) - all_ratings = all_ratings.where(PitchingCardRatings.battingcard << set_cards) + all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard << set_cards) if csv: return_val = query_to_csv(all_ratings) diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py index 9fc44d6..e12231c 100644 --- a/app/routers_v2/players.py +++ b/app/routers_v2/players.py @@ -329,7 +329,7 @@ async def get_one_player(player_id, csv: Optional[bool] = False): return return_val -@router.get('/{player_id}/batting-card') +@router.get('/{player_id}/battingcard') async def get_player_card( request: Request, player_id: int, variant: int = 0, d: str = None, html: Optional[bool] = False): if os.path.isfile(f'storage/cards/{player_id}-{d}-v{variant}.png') and html is False: From 485a04685532ee434ea5b0af237503bd2ae91143 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 11 Oct 2023 00:58:30 -0500 Subject: [PATCH 23/40] First pass of real card gen --- app/card_creation.py | 731 +++++++++++++++++++++++++++++++- app/db_engine.py | 3 + app/main.py | 4 +- app/routers_v2/cardpositions.py | 22 +- app/routers_v2/players.py | 117 ++--- 5 files changed, 812 insertions(+), 65 deletions(-) diff --git a/app/card_creation.py b/app/card_creation.py index 7f0f8fe..57e996d 100644 --- a/app/card_creation.py +++ b/app/card_creation.py @@ -1,4 +1,528 @@ +import logging +import math + import pandas as pd +import pydantic + +from .db_engine import model_to_dict +from decimal import Decimal +from typing import Literal, Optional + + +class PlayResult(pydantic.BaseModel): + full_name: str + short_name: str + is_offense: bool = True + + +class BattingCardRatingsModel(pydantic.BaseModel): + battingcard: int + vs_hand: Literal['R', 'L'] + pull_rate: Decimal = Decimal(0.0) + center_rate: Decimal = Decimal(0.0) + slap_rate: Decimal = Decimal(0.0) + homerun: Decimal = Decimal(0.0) + bp_homerun: Decimal = Decimal(0.0) + triple: Decimal = Decimal(0.0) + double_three: Decimal = Decimal(0.0) + double_two: Decimal = Decimal(0.0) + double_pull: Decimal = Decimal(0.0) + single_two: Decimal = Decimal(0.0) + single_one: Decimal = Decimal(0.0) + single_center: Decimal = Decimal(0.0) + bp_single: Decimal = Decimal(0.0) + hbp: Decimal = Decimal(0.0) + walk: Decimal = Decimal(0.0) + strikeout: Decimal = Decimal(0.0) + lineout: Decimal = Decimal(0.0) + popout: Decimal = Decimal(0.0) + flyout_a: Decimal = Decimal(0.0) + flyout_bq: Decimal = Decimal(0.0) + flyout_lf_b: Decimal = Decimal(0.0) + flyout_rf_b: Decimal = Decimal(0.0) + groundout_a: Decimal = Decimal(0.0) + groundout_b: Decimal = Decimal(0.0) + groundout_c: Decimal = Decimal(0.0) + avg: Decimal = Decimal(0.0) + obp: Decimal = Decimal(0.0) + slg: Decimal = Decimal(0.0) + + +class PitchingCardRatingsModel(pydantic.BaseModel): + pitchingcard_id: int + vs_hand: Literal['R', 'L'] + pull_rate: Decimal = Decimal(0.0) + center_rate: Decimal = Decimal(0.0) + slap_rate: Decimal = Decimal(0.0) + homerun: Decimal = Decimal(0.0) + bp_homerun: Decimal = Decimal(0.0) + triple: Decimal = Decimal(0.0) + double_three: Decimal = Decimal(0.0) + double_two: Decimal = Decimal(0.0) + double_cf: Decimal = Decimal(0.0) + single_two: Decimal = Decimal(0.0) + single_one: Decimal = Decimal(0.0) + single_center: Decimal = Decimal(0.0) + bp_single: Decimal = Decimal(0.0) + hbp: Decimal = Decimal(0.0) + walk: Decimal = Decimal(0.0) + strikeout: Decimal = Decimal(0.0) + flyout_lf_b: Decimal = Decimal(0.0) + flyout_cf_b: Decimal = Decimal(0.0) + flyout_rf_b: Decimal = Decimal(0.0) + groundout_a: Decimal = Decimal(0.0) + groundout_b: Decimal = Decimal(0.0) + xcheck_p: Decimal = Decimal(1.0) + xcheck_c: Decimal = Decimal(3.0) + xcheck_1b: Decimal = Decimal(2.0) + xcheck_2b: Decimal = Decimal(6.0) + xcheck_3b: Decimal = Decimal(3.0) + xcheck_ss: Decimal = Decimal(7.0) + xcheck_lf: Decimal = Decimal(2.0) + xcheck_cf: Decimal = Decimal(3.0) + xcheck_rf: Decimal = Decimal(2.0) + avg: Decimal = Decimal(0.0) + obp: Decimal = Decimal(0.0) + slg: Decimal = Decimal(0.0) + + +class CardResult(pydantic.BaseModel): + result_one: str = None + result_two: str = None + d20_one: str = None + d20_two: str = None + bold_one: bool = False + bold_two: bool = False + + def __str__(self): + res_text = f'Empty' + if self.result_one is not None: + res_text = f'{self.result_one}' + if self.d20_one is not None: + res_text += f' | {self.d20_one}' + if self.result_two is not None: + res_text += f'\n{self.result_two} | {self.d20_two}' + return res_text + + def is_full(self): + return self.result_one is not None + + def assign_play(self, play: PlayResult, secondary_play: Optional[PlayResult] = None, d20_one: Optional[int] = None): + if secondary_play is None: + self.result_one = play.full_name + + if play.short_name[:2] in ['HR', 'TR', 'DO', 'SI', 'WA', 'HB']: + self.bold_one = True + else: + self.result_one = play.short_name + self.result_two = secondary_play.short_name + self.d20_one = f'1-{d20_one}' + self.d20_two = f'{d20_one + 1}-20' + + if play.short_name[:2] in ['HR', 'TR', 'DO', 'SI', 'WA', 'HB']: + self.bold_one = True + if secondary_play.short_name[:2] in ['HR', 'TR', 'DO', 'SI', 'WA', 'HB']: + self.bold_two = True + + +class CardColumn(pydantic.BaseModel): + two: CardResult = CardResult() # 1 chance + three: CardResult = CardResult() # 2 chances + four: CardResult = CardResult() # 3 chances + five: CardResult = CardResult() # 4 chances + six: CardResult = CardResult() # 5 chances + seven: CardResult = CardResult() # 6 chances + eight: CardResult = CardResult() # 5 chances + nine: CardResult = CardResult() # 4 chances + ten: CardResult = CardResult() # 3 chances + eleven: CardResult = CardResult() # 2 chances + twelve: CardResult = CardResult() # 1 chance + + def __str__(self): + return f'2-{self.two}\n' \ + f'3-{self.three}\n' \ + f'4-{self.four}\n' \ + f'5-{self.five}\n' \ + f'6-{self.six}\n' \ + f'7-{self.seven}\n' \ + f'8-{self.eight}\n' \ + f'9-{self.nine}\n' \ + f'10-{self.ten}\n' \ + f'11-{self.eleven}\n' \ + f'12-{self.twelve}' + + def get_text(self): + sixes = f'' + results = f'' + d20 = f'' + + def bold(text): + return f'{text}' + + def blank(): + return f' ' + + for count, x in enumerate( + [self.two, self.three, self.four, self.five, self.six, self.seven, self.eight, self.nine, + self.ten, self.eleven, self.twelve], start=2): + if x.bold_one: + this_six = bold(f'{count}-') + this_result = bold(x.result_one) + if x.d20_one is not None: + this_d20 = bold(x.d20_one) + else: + this_d20 = blank() + else: + this_six = f'{count}-' + this_result = f'{x.result_one}' + if x.d20_one is not None: + this_d20 = f'{x.d20_one}' + else: + this_d20 = blank() + + if x.result_two is not None: + if x.bold_two: + this_six += f'
{bold(blank())}' + this_result += f'
{bold(x.result_two)}' + this_d20 += f'
{bold(x.d20_one)}' + else: + this_six += f'
{blank()}' + this_result += f'
{x.result_two}' + this_d20 += f'
{x.d20_two}' + + sixes += f'{this_six}
' + results += f'{this_result}
' + d20 += f'{this_d20}
' + + return { + 'sixes': sixes, + 'results': results, + 'd20': d20 + } + + def is_full(self): + return self.two.is_full() and self.three.is_full() and self.four.is_full() and self.five.is_full() and \ + self.six.is_full() and self.seven.is_full() and self.eight.is_full() and self.nine.is_full() and \ + self.ten.is_full() and self.eleven.is_full() and self.twelve.is_full() + + def add_result( + self, play: PlayResult, alt_direction: int, chances: Decimal, secondary_play: Optional[PlayResult] = None): + if chances > Decimal(6.0): + logging.error(f'Cannot assign more than 6 chances per call\n' + f'Play: {play}\nAlt Direction: {alt_direction}\nChances: {chances}\n' + f'Secondary Play: {secondary_play}') + raise ValueError(f'Cannot assign more than 6 chances per call') + + # Chances is whole number + if math.floor(chances) == chances: + if chances == Decimal(6): + if not self.seven.is_full(): + self.seven.assign_play(play) + return chances + + # Plus one + if not self.six.is_full(): + if not self.two.is_full(): + self.six.assign_play(play) + self.two.assign_play(play) + return chances + elif not self.twelve.is_full(): + self.six.assign_play(play) + self.twelve.assign_play(play) + return chances + + # Plus one + if not self.eight.is_full(): + if not self.two.is_full(): + self.eight.assign_play(play) + self.two.assign_play(play) + return chances + elif not self.twelve.is_full(): + self.eight.assign_play(play) + self.twelve.assign_play(play) + return chances + + # Plus two + if not self.five.is_full(): + if not self.three.is_full(): + self.five.assign_play(play) + self.three.assign_play(play) + return chances + elif not self.eleven.is_full(): + self.five.assign_play(play) + self.eleven.assign_play(play) + return chances + + # Plus one + if not self.nine.is_full(): + if not self.three.is_full(): + self.nine.assign_play(play) + self.three.assign_play(play) + return chances + elif not self.eleven.is_full(): + self.nine.assign_play(play) + self.eleven.assign_play(play) + return chances + + if not self.four.is_full() and not self.nine.is_full(): + self.four.assign_play(play) + self.nine.assign_play(play) + return chances + + if chances == Decimal(5): + if not self.six.is_full(): + self.six.assign_play(play) + return chances + + if not self.eight.is_full(): + self.eight.assign_play(play) + return chances + + # Plus one + if not self.five.is_full(): + if not self.two.is_full(): + self.five.assign_play(play) + self.two.assign_play(play) + return chances + elif not self.twelve.is_full(): + self.five.result_one = play.full_name + self.twelve.result_one = play.full_name + return chances + + # Plus one + if not self.nine.is_full(): + if not self.two.is_full(): + self.nine.assign_play(play) + self.two.assign_play(play) + return chances + elif not self.twelve.is_full(): + self.nine.assign_play(play) + self.twelve.assign_play(play) + return chances + + # Plus two + if not self.four.is_full(): + if not self.three.is_full(): + self.four.assign_play(play) + self.three.assign_play(play) + return chances + elif not self.eleven.is_full(): + self.four.assign_play(play) + self.eleven.assign_play(play) + return chances + + # Plus two + if not self.ten.is_full(): + if not self.three.is_full(): + self.ten.assign_play(play) + self.three.assign_play(play) + return chances + elif not self.eleven.is_full(): + self.ten.assign_play(play) + self.eleven.assign_play(play) + return chances + + if chances == Decimal(4): + if not self.five.is_full(): + self.five.assign_play(play) + return chances + + if not self.nine.is_full(): + self.nine.assign_play(play) + return chances + + # Plus one + if not self.four.is_full(): + if not self.two.is_full(): + self.four.assign_play(play) + self.two.assign_play(play) + return chances + elif not self.twelve.is_full(): + self.four.assign_play(play) + self.twelve.assign_play(play) + return chances + + # Plus one + if not self.ten.is_full(): + if not self.two.is_full(): + self.ten.assign_play(play) + self.two.assign_play(play) + return chances + elif not self.twelve.is_full(): + self.ten.assign_play(play) + self.twelve.assign_play(play) + return chances + + if not self.three.is_full() and not self.eleven.is_full(): + self.three.assign_play(play) + self.eleven.assign_play(play) + return chances + + if chances == Decimal(3): + if not self.four.is_full(): + self.four.assign_play(play) + return chances + + if not self.ten.is_full(): + self.ten.assign_play(play) + return chances + + # Plus one + if not self.three.is_full(): + if not self.two.is_full(): + self.three.assign_play(play) + self.two.assign_play(play) + return chances + elif not self.twelve.is_full(): + self.three.result_one = play.full_name + self.twelve.result_one = play.full_name + return chances + + # Plus one + if not self.eleven.is_full(): + if not self.two.is_full(): + self.eleven.assign_play(play) + self.two.assign_play(play) + return chances + elif not self.twelve.is_full(): + self.eleven.assign_play(play) + self.twelve.assign_play(play) + return chances + + if chances == Decimal(2): + if not self.three.is_full(): + self.three.assign_play(play) + return chances + + if not self.eleven.is_full(): + self.eleven.assign_play(play) + return chances + + if not self.two.is_full() and not self.twelve.is_full(): + self.two.assign_play(play) + self.twelve.assign_play(play) + return chances + + if chances == Decimal(1): + if not self.two.is_full(): + self.two.assign_play(play) + return chances + + if not self.twelve.is_full(): + self.twelve.assign_play(play) + return chances + + return False + + else: + return False + + +class FullCard(pydantic.BaseModel): + col_one: CardColumn = CardColumn() + col_two: CardColumn = CardColumn() + col_three: CardColumn = CardColumn() + offense_col: int + alt_direction: int = 1 + + class Config: + arbitrary_types_allowed = True + + def is_complete(self): + return self.col_one.is_full() and self.col_two.is_full() and self.col_three.is_full() + + def sample_output(self): + return f'{"" if self.is_complete() else "NOT "}COMPLETE\n' \ + f'Column 1\n{self.col_one}\n\n' \ + f'Column 2\n{self.col_two}\n\n' \ + f'Column 3\n{self.col_three}' + + def add_result(self, play: PlayResult, chances: Decimal, secondary_play: Optional[PlayResult] = None): + if play.is_offense: + if self.offense_col == 1: + first = self.col_one + if self.alt_direction: + second = self.col_two + third = self.col_three + else: + second = self.col_three + third = self.col_two + elif self.offense_col == 2: + first = self.col_two + if self.alt_direction: + second = self.col_three + third = self.col_one + else: + second = self.col_one + third = self.col_three + else: + first = self.col_three + if self.alt_direction: + second = self.col_one + third = self.col_two + else: + second = self.col_two + third = self.col_one + else: + if self.offense_col == 1: + third = self.col_one + if self.alt_direction: + first = self.col_two + second = self.col_three + else: + first = self.col_three + second = self.col_two + elif self.offense_col == 2: + third = self.col_two + if self.alt_direction: + first = self.col_three + second = self.col_one + else: + first = self.col_one + second = self.col_three + else: + third = self.col_three + if self.alt_direction: + first = self.col_one + second = self.col_two + else: + first = self.col_two + second = self.col_one + + if first.add_result(play, self.alt_direction, chances, secondary_play): + return True + elif second.add_result(play, self.alt_direction, chances, secondary_play): + return True + elif third.add_result(play, self.alt_direction, chances, secondary_play): + return True + else: + return False + + def card_output(self): + c1_output = self.col_one.get_text() + c2_output = self.col_two.get_text() + c3_output = self.col_three.get_text() + + return { + 'one_2d6': c1_output['sixes'], + 'one_results': c1_output['results'], + 'one_d20': c1_output['d20'], + 'two_2d6': c2_output['sixes'], + 'two_results': c2_output['results'], + 'two_d20': c2_output['d20'], + 'three_2d6': c3_output['sixes'], + 'three_results': c3_output['results'], + 'three_d20': c3_output['d20'] + } + + +class FullBattingCard(FullCard): + ratings: BattingCardRatingsModel + + +class FullPitchingCard(FullCard): + ratings: BattingCardRatingsModel + chance_df = pd.DataFrame( { @@ -25,17 +549,206 @@ chance_df = pd.DataFrame( }, index=[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] ) +encoded_images = { + 'Hall of Fame': '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', + 'MVP': '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', + 'All-Star': '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', + 'Starter': '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', + 'Reserve': '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', + 'Replacement': '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' +} -def get_batter_card_html(request, player, batting_card, ratings_vl, ratings_vr): - """ - create header_data - create column data - return data to be templated - """ - return f'

{player.p_name}

' +def get_pos_string(all_pos) -> str: + final = '' + arm_added = False + for x in all_pos: + final += x.position.lower() + if x.position != 'DH': + final += f'-{x.range}' + if x.position in ['LF', 'CF', 'RF', 'C'] and not arm_added: + final += f'({"+" if x.arm >= 0 else ""}{x.arm})' + arm_added = True + + final += f'e{x.error}' + if x.position == 'C': + final += f' T-{x.overthrow}(pb {x.pb})' + final += ' ' + return final -def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr) -> dict: +def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions) -> dict: + steal_string = '-/- (---)' + if batting_card.steal_jump > 0: + jump_chances = round(batting_card.steal_jump * 36) - return {} + if jump_chances == 6: + good_jump = 7 + elif jump_chances == 5: + good_jump = 6 + elif jump_chances == 4: + good_jump = 5 + elif jump_chances == 3: + good_jump = 4 + elif jump_chances == 2: + good_jump = 3 + elif jump_chances == 1: + good_jump = 2 + elif jump_chances == 7: + good_jump = '4,5' + elif jump_chances == 8: + good_jump = '4,6' + elif jump_chances == 9: + good_jump = '3-5' + elif jump_chances == 10: + good_jump = '2-5' + elif jump_chances == 11: + good_jump = '6,7' + elif jump_chances == 12: + good_jump = '4-6' + elif jump_chances == 13: + good_jump = '2,4-6' + elif jump_chances == 14: + good_jump = '3-6' + elif jump_chances == 15: + good_jump = '2-6' + elif jump_chances == 16: + good_jump = '2,5-6' + elif jump_chances == 17: + good_jump = '3,5-6' + elif jump_chances == 18: + good_jump = '4-6' + elif jump_chances == 19: + good_jump = '2,4-7' + elif jump_chances == 20: + good_jump = '3-7' + elif jump_chances == 21: + good_jump = '2-7' + elif jump_chances == 22: + good_jump = '2-7,12' + elif jump_chances == 23: + good_jump = '2-7,11' + elif jump_chances == 24: + good_jump = '2,4-8' + elif jump_chances == 25: + good_jump = '3-8' + elif jump_chances == 26: + good_jump = '2-8' + elif jump_chances == 27: + good_jump = '2-8,12' + elif jump_chances == 28: + good_jump = '2-8,11' + elif jump_chances == 29: + good_jump = '3-9' + elif jump_chances == 30: + good_jump = '2-9' + elif jump_chances == 31: + good_jump = '2-9,12' + elif jump_chances == 32: + good_jump = '2-9,11' + elif jump_chances == 33: + good_jump = '2-10' + elif jump_chances == 34: + good_jump = '3-11' + elif jump_chances == 35: + good_jump = '2-11' + else: + good_jump = '2-12' + steal_string = f'{"*" if batting_card.steal_auto else ""}{good_jump}/- ({batting_card.steal_high}-' \ + f'{batting_card.steal_low})' + + rarity_file = encoded_images[player.rarity.name] + + vl_dict = model_to_dict(ratings_vl) + vl_dict['battingcard'] = ratings_vl.battingcard_id + vl = FullBattingCard( + ratings=BattingCardRatingsModel(**vl_dict), + offense_col=batting_card.offense_col, + alt_direction=batting_card.player_id % 2 + ) + vr_dict = model_to_dict(ratings_vr) + vr_dict['battingcard'] = ratings_vr.battingcard_id + vr = FullBattingCard( + ratings=BattingCardRatingsModel(**vr_dict), + offense_col=batting_card.offense_col, + alt_direction=batting_card.player_id % 2 + ) + + vl.add_result(PlayResult(full_name="SINGLE**", short_name='SI**', is_offense=True), Decimal(4)) + vl.add_result(PlayResult(full_name="HOMERUN", short_name='HR', is_offense=True), Decimal(4)) + vl.add_result(PlayResult(full_name="DOUBLE**", short_name='DO**', is_offense=True), Decimal(4)) + + vl_output = vl.card_output() + vr_output = vr.card_output() + + logging.info(f'vl: {vl.sample_output()}') + logging.info(f'vr: {vr.sample_output()}') + + # for x in [vl, vr]: + # while not x.is_complete(): + # pass + + return { + 'player': player, + 'card_type': 'batter', + # 'vl_one_2d6': '2-', + # 'vl_one_results': 'HOMERUN', + # 'vl_one_d20': ' ', + # 'vl_two_2d6': '2-', + # 'vl_two_results': 'fly (cf) B', + # 'vl_two_d20': '', + # 'vl_three_2d6': '2-
 ', + # 'vl_three_results': 'HR
fly (cf) B', + # 'vl_three_d20': '1-16
17-20', + # 'results_vr_one': 'Light Dongs', + # 'results_vr_two': 'Hefty Dongs', + # 'results_vr_three': 'Obese Dongs', + 'vl_one_2d6': vl_output['one_2d6'], + 'vl_one_results': vl_output['one_results'], + 'vl_one_d20': vl_output['one_d20'], + 'vl_two_2d6': vl_output['two_2d6'], + 'vl_two_results': vl_output['two_results'], + 'vl_two_d20': vl_output['two_d20'], + 'vl_three_2d6': vl_output['three_2d6'], + 'vl_three_results': vl_output['three_results'], + 'vl_three_d20': vl_output['three_d20'], + 'vr_one_2d6': vr_output['one_2d6'], + 'vr_one_results': vr_output['one_results'], + 'vr_one_d20': vr_output['one_d20'], + 'vr_two_2d6': vr_output['two_2d6'], + 'vr_two_results': vr_output['two_results'], + 'vr_two_d20': vr_output['two_d20'], + 'vr_three_2d6': vr_output['three_2d6'], + 'vr_three_results': vr_output['three_results'], + 'vr_three_d20': vr_output['three_d20'], + 'hand': batting_card.hand, + 'position_string': get_pos_string(positions), + 'bat_card': batting_card, + 'stealing_string': steal_string, + 'rarity_file': rarity_file + } + + +def get_pitcher_card_data(player, pitching_card, ratings_vl, ratings_vr, positions) -> dict: + rarity_file = encoded_images[player.rarity.name] + + return { + 'player': player, + 'card_type': 'pitcher', + 'vl_one_2d6': '2-', + 'vl_one_results': 'HOMERUN', + 'vl_one_d20': ' ', + 'vl_two_2d6': '2-', + 'vl_two_results': 'fly (cf) B', + 'vl_two_d20': '', + 'vl_three_2d6': '2-
 ', + 'vl_three_results': 'HR
fly (cf) B', + 'vl_three_d20': '1-16
17-20', + 'results_vr_one': 'Light Dongs', + 'results_vr_two': 'Hefty Dongs', + 'results_vr_three': 'Obese Dongs', + 'hand': pitching_card.hand, + 'position_string': get_pos_string(positions), + 'pit_card': pitching_card, + 'rarity_file': rarity_file + } diff --git a/app/db_engine.py b/app/db_engine.py index 1683921..9eb8921 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -577,6 +577,9 @@ BattingCard.add_index(bc_index) class BattingCardRatings(BaseModel): battingcard = ForeignKeyField(BattingCard) vs_hand = CharField(default='R') + pull_rate: FloatField() + center_rate: FloatField() + slap_rate: FloatField() homerun = FloatField() bp_homerun = FloatField() triple = FloatField() diff --git a/app/main.py b/app/main.py index 423c32a..28f4709 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,8 @@ import os from fastapi import FastAPI -from fastapi.templating import Jinja2Templates +# from fastapi.staticfiles import StaticFiles +# from fastapi.templating import Jinja2Templates from .routers_v2 import ( current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, @@ -12,6 +13,7 @@ app = FastAPI( responses={404: {'description': 'Not found'}} ) +# app.mount("/static", StaticFiles(directory="storage/static"), name="static") # templates = Jinja2Templates(directory=os.path.dirname(os.path.abspath(__file__))) app.include_router(current.router) diff --git a/app/routers_v2/cardpositions.py b/app/routers_v2/cardpositions.py index d9f927a..554ee30 100644 --- a/app/routers_v2/cardpositions.py +++ b/app/routers_v2/cardpositions.py @@ -4,7 +4,7 @@ import logging import pydantic from pydantic import root_validator -from ..db_engine import db, CardPosition, model_to_dict, chunked, Player +from ..db_engine import db, CardPosition, model_to_dict, chunked, Player, fn from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -22,7 +22,7 @@ router = APIRouter( class CardPositionModel(pydantic.BaseModel): player_id: int variant: int = 0 - position: Literal['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'] + position: Literal['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH'] innings: int = 1 range: int = 5 error: int = 0 @@ -45,10 +45,10 @@ class PositionList(pydantic.BaseModel): @router.get('') async def get_card_positions( - player_id: Optional[int] = None, position: list = Query(default=None), min_innings: Optional[int] = 1, + player_id: list = Query(default=None), position: list = Query(default=None), min_innings: Optional[int] = 1, r: list = Query(default=None), e: list = Query(default=None), arm: list = Query(default=None), pb: list = Query(default=None), overthrow: list = Query(default=None), cardset_id: list = Query(default=None), - short_output: Optional[bool] = False): + short_output: Optional[bool] = False, sort: Optional[str] = 'innings-desc'): all_pos = CardPosition.select().where(CardPosition.innings >= min_innings).order_by( CardPosition.player, CardPosition.position, CardPosition.variant ) @@ -56,7 +56,8 @@ async def get_card_positions( if player_id is not None: all_pos = all_pos.where(CardPosition.player_id << player_id) if position is not None: - all_pos = all_pos.where(CardPosition.position << position) + p_list = [x.lower() for x in position] + all_pos = all_pos.where(fn.Lower(CardPosition.position) << p_list) if r is not None: all_pos = all_pos.where(CardPosition.range << r) if e is not None: @@ -67,10 +68,19 @@ async def get_card_positions( all_pos = all_pos.where(CardPosition.pb << pb) if overthrow is not None: all_pos = all_pos.where(CardPosition.overthrow << overthrow) - if position is not None: + if cardset_id is not None: all_players = Player.select().where(Player.cardset_id << cardset_id) all_pos = all_pos.where(CardPosition.player << all_players) + if sort == 'innings-desc': + all_pos = all_pos.order_by(CardPosition.innings.desc()) + elif sort == 'innings-asc': + all_pos = all_pos.order_by(CardPosition.innings) + elif sort == 'range-desc': + all_pos = all_pos.order_by(CardPosition.range.desc()) + elif sort == 'range-asc': + all_pos = all_pos.order_by(CardPosition.range) + return_val = {'count': all_pos.count(), 'positions': [ model_to_dict(x, recurse=not short_output) for x in all_pos ]} diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py index e12231c..06e2a70 100644 --- a/app/routers_v2/players.py +++ b/app/routers_v2/players.py @@ -1,17 +1,18 @@ import os.path +import base64 from fastapi import APIRouter, Depends, HTTPException, Request, Response, Query from fastapi.responses import FileResponse from fastapi.templating import Jinja2Templates from html2image import Html2Image -from typing import Optional, List +from typing import Optional, List, Literal import logging import pydantic from pandas import DataFrame -from ..card_creation import get_batter_card_html, get_batter_card_data +from ..card_creation import get_batter_card_data, get_pitcher_card_data from ..db_engine import db, Player, model_to_dict, fn, chunked, Paperdex, Cardset, Rarity, BattingCard, \ - BattingCardRatings + BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -329,57 +330,70 @@ async def get_one_player(player_id, csv: Optional[bool] = False): return return_val -@router.get('/{player_id}/battingcard') -async def get_player_card( - request: Request, player_id: int, variant: int = 0, d: str = None, html: Optional[bool] = False): - if os.path.isfile(f'storage/cards/{player_id}-{d}-v{variant}.png') and html is False: - db.close() - return FileResponse( - path=f'storage/cards/{player_id}-{d}-v{variant}.png', - media_type='image/png' - ) - +@router.get('/{player_id}/{card_type}card') +async def get_batter_card( + request: Request, player_id: int, card_type: Literal['batting', 'pitching'], variant: int = 0, d: str = None, + html: Optional[bool] = False): try: this_player = Player.get_by_id(player_id) except Exception: db.close() raise HTTPException(status_code=404, detail=f'No player found with id {player_id}') - this_bc = BattingCard.get_or_none(BattingCard.player == this_player, BattingCard.variant == variant) - if this_bc is None: - raise HTTPException(status_code=404, detail=f'Batting card not found for id {player_id}, variant {variant}') + if os.path.isfile(f'storage/cards/cardset-{this_player.cardset.id}/{player_id}-{d}-v{variant}.png') and html is False: + db.close() + return FileResponse( + path=f'storage/cards/cardset-{this_player.cardset.id}/{player_id}-{d}-v{variant}.png', + media_type='image/png' + ) - rating_vl = BattingCardRatings.get_or_none( - BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == 'L') - rating_vr = BattingCardRatings.get_or_none( - BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == 'R') - if None in [rating_vr, rating_vl]: - raise HTTPException(status_code=404, detail=f'Ratings not found for batting card {this_bc.id}') + all_pos = CardPosition.select().where(CardPosition.player == this_player).order_by(CardPosition.innings.desc()) - hti = Html2Image( - browser='chromium', - size=(1200, 600), - output_path=f'storage/cards', - custom_flags=['--no-sandbox', '--disable-remote-debugging', '--headless', '--disable-gpu', - '--disable-software-rasterizer', '--disable-dev-shm-usage'] - ) - card_data = { - 'player': this_player, - 'card_type': 'batter', - 'results_vl_one': 'Big Dongs', - 'results_vl_two': 'Lesser Dongs', - 'results_vl_three': 'Sad Dongs', - 'results_vr_one': 'Light Dongs', - 'results_vr_two': 'Hefty Dongs', - 'results_vr_three': 'Obese Dongs', - 'request': request - } - html_response = templates.TemplateResponse("player_card.html", card_data) + if card_type == 'batting': + this_bc = BattingCard.get_or_none(BattingCard.player == this_player, BattingCard.variant == variant) + if this_bc is None: + raise HTTPException(status_code=404, detail=f'Batting card not found for id {player_id}, variant {variant}') + + rating_vl = BattingCardRatings.get_or_none( + BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == 'L') + rating_vr = BattingCardRatings.get_or_none( + BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == 'R') + if None in [rating_vr, rating_vl]: + raise HTTPException(status_code=404, detail=f'Ratings not found for batting card {this_bc.id}') + + card_data = get_batter_card_data(this_player, this_bc, rating_vl, rating_vr, all_pos) + card_data['request'] = request + html_response = templates.TemplateResponse("player_card.html", card_data) + + else: + this_pc = PitchingCard.get_or_none(PitchingCard.player == this_player, PitchingCard.variant == variant) + if this_pc is None: + raise HTTPException( + status_code=404, detail=f'Pitching card not found for id {player_id}, variant {variant}') + + rating_vl = PitchingCardRatings.get_or_none( + PitchingCardRatings.pitchingcard == this_pc, PitchingCardRatings.vs_hand == 'L') + rating_vr = PitchingCardRatings.get_or_none( + PitchingCardRatings.pitchingcard == this_pc, PitchingCardRatings.vs_hand == 'R') + if None in [rating_vr, rating_vl]: + raise HTTPException(status_code=404, detail=f'Ratings not found for pitching card {this_pc.id}') + + card_data = get_pitcher_card_data(this_player, this_pc, rating_vl, rating_vr, all_pos) + card_data['request'] = request + html_response = templates.TemplateResponse("player_card.html", card_data) if html: db.close() return html_response + hti = Html2Image( + browser='chromium', + size=(1200, 600), + output_path=f'storage/cards/cardset-{this_player.cardset.id}/', + custom_flags=['--no-sandbox', '--disable-remote-debugging', '--headless', '--disable-gpu', + '--disable-software-rasterizer', '--disable-dev-shm-usage'] + ) + logging.debug(f'body:\n{html_response.body.decode("UTF-8")}') x = hti.screenshot( html_str=str(html_response.body.decode("UTF-8")), @@ -390,6 +404,11 @@ async def get_player_card( return FileResponse(path=x[0], media_type='image/png') +# @router.get('/{player_id}/pitchingcard') +# async def get_pitcher_card( +# request: Request, player_id: int, variant: int = 0, d: str = None, html: Optional[bool] = False) + + @router.patch('/{player_id}') async def v1_players_patch( player_id, name: Optional[str] = None, image: Optional[str] = None, image2: Optional[str] = None, @@ -444,42 +463,42 @@ async def v1_players_patch( raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}') this_player.rarity = this_rarity if pos_1 is not None: - if pos_1 == 'False': + if pos_1 in ['None', 'False', '']: this_player.pos_1 = None else: this_player.pos_1 = pos_1 if pos_2 is not None: - if pos_2 == 'False': + if pos_2 in ['None', 'False', '']: this_player.pos_2 = None else: this_player.pos_2 = pos_2 if pos_3 is not None: - if pos_3 == 'False': + if pos_3 in ['None', 'False', '']: this_player.pos_3 = None else: this_player.pos_3 = pos_3 if pos_4 is not None: - if pos_4 == 'False': + if pos_4 in ['None', 'False', '']: this_player.pos_4 = None else: this_player.pos_4 = pos_4 if pos_5 is not None: - if pos_5 == 'False': + if pos_5 in ['None', 'False', '']: this_player.pos_5 = None else: this_player.pos_5 = pos_5 if pos_6 is not None: - if pos_6 == 'False': + if pos_6 in ['None', 'False', '']: this_player.pos_6 = None else: this_player.pos_6 = pos_6 if pos_7 is not None: - if pos_7 == 'False': + if pos_7 in ['None', 'False', '']: this_player.pos_7 = None else: this_player.pos_7 = pos_7 if pos_8 is not None: - if pos_8 == 'False': + if pos_8 in ['None', 'False', '']: this_player.pos_8 = None else: this_player.pos_8 = pos_8 From a44250803a9736bf30af412c621200baf44f22d0 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 14 Oct 2023 23:43:50 -0500 Subject: [PATCH 24/40] Batting Card Generation Complete --- app/card_creation.py | 1517 +++++++++++++++++++++++--- app/routers_v2/battingcardratings.py | 90 +- app/routers_v2/players.py | 18 + 3 files changed, 1486 insertions(+), 139 deletions(-) diff --git a/app/card_creation.py b/app/card_creation.py index 57e996d..4712211 100644 --- a/app/card_creation.py +++ b/app/card_creation.py @@ -1,3 +1,4 @@ +import copy import logging import math @@ -6,14 +7,82 @@ import pydantic from .db_engine import model_to_dict from decimal import Decimal +from pydantic import validator from typing import Literal, Optional +chance_df = pd.DataFrame( + { + 'd20-1': [0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.25, 0.2, 0.15, 0.1, 0.05], + 'd20-2': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1], + 'd20-3': [0.15, 0.3, 0.45, 0.6, 0.75, 0.9, 0.75, 0.6, 0.45, 0.3, 0.15], + 'd20-4': [0.2, 0.4, 0.6, 0.8, 1, 1.2, 1, 0.8, 0.6, 0.4, 0.2], + 'd20-5': [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.25, 1, 0.75, 0.5, 0.25], + 'd20-6': [0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 1.5, 1.2, 0.9, 0.6, 0.3], + 'd20-7': [0.35, 0.7, 1.05, 1.4, 1.75, 2.1, 1.75, 1.4, 1.05, 0.7, 0.35], + 'd20-8': [0.4, 0.8, 1.2, 1.6, 2, 2.4, 2, 1.6, 1.2, 0.8, 0.4], + 'd20-9': [0.45, 0.9, 1.35, 1.8, 2.25, 2.7, 2.25, 1.8, 1.35, 0.9, 0.45], + 'd20-10': [0.5, 1, 1.5, 2, 2.5, 3, 2.5, 2, 1.5, 1, 0.5], + 'd20-11': [0.55, 1.1, 1.65, 2.2, 2.75, 3.3, 2.75, 2.2, 1.65, 1.1, 0.55], + 'd20-12': [0.6, 1.2, 1.8, 2.4, 3, 3.6, 3, 2.4, 1.8, 1.2, 0.6], + 'd20-13': [0.65, 1.3, 1.95, 2.6, 3.25, 3.9, 3.25, 2.6, 1.95, 1.3, 0.65], + 'd20-14': [0.7, 1.4, 2.1, 2.8, 3.5, 4.2, 3.5, 2.8, 2.1, 1.4, 0.7], + 'd20-15': [0.75, 1.5, 2.25, 3, 3.75, 4.5, 3.75, 3, 2.25, 1.5, 0.75], + 'd20-16': [0.8, 1.6, 2.4, 3.2, 4, 4.8, 4, 3.2, 2.4, 1.6, 0.8], + 'd20-17': [0.85, 1.7, 2.55, 3.4, 4.25, 5.1, 4.25, 3.4, 2.55, 1.7, 0.85], + 'd20-18': [0.9, 1.8, 2.7, 3.6, 4.5, 5.4, 4.5, 3.6, 2.7, 1.8, 0.9], + 'd20-19': [0.95, 1.9, 2.85, 3.8, 4.75, 5.7, 4.75, 3.8, 2.85, 1.9, 0.95], + 'd20-20': [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1] + }, + index=[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] +) +encoded_images = { + 'Hall of Fame': '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', + 'MVP': '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', + 'All-Star': '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', + 'Starter': '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', + 'Reserve': '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', + 'Replacement': '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' +} +EXACT_CHANCES = [ + Decimal('5.7'), Decimal('5.4'), Decimal('5.1'), Decimal('4.8'), Decimal('4.75'), Decimal('4.5'), Decimal('4.25'), + Decimal('4.2'), Decimal('3.9'), Decimal('3.8'), Decimal('3.75'), Decimal('3.6'), Decimal('3.5'), Decimal('3.4'), + Decimal('3.3'), Decimal('3.25'), Decimal('3.2'), Decimal('2.85'), Decimal('2.8'), Decimal('2.75'), Decimal('2.7'), + Decimal('2.6'), Decimal('2.55'), Decimal('2.5'), Decimal('2.4'), Decimal('2.25'), Decimal('2.2'), Decimal('2.1'), + Decimal('1.95'), Decimal('1.9'), Decimal('1.8'), Decimal('1.75'), Decimal('1.7'), Decimal('1.65'), Decimal('1.6'), + Decimal('1.5'), Decimal('1.4'), Decimal('1.35'), Decimal('1.3'), Decimal('1.25'), Decimal('1.2'), Decimal('1.1'), + Decimal('1.05') +] + + class PlayResult(pydantic.BaseModel): full_name: str short_name: str is_offense: bool = True + @validator("is_offense", always=True) + def offense_validator(cls, v, values, **kwargs): + return values['short_name'][:2] in ['HR', 'TR', 'DO', 'SI', 'WA', 'HB', '◆B'] + + +PLAY_RESULTS = { + 'hr': PlayResult(full_name='HOMERUN', short_name='HR'), + 'bp-hr': PlayResult(full_name='◆BP-HR', short_name='◆BP-HR'), + 'tr': PlayResult(full_name='TRIPLE', short_name='TR'), + 'do-lf': PlayResult(full_name=f'DOUBLE (lf)', short_name=f'DO (lf)'), + 'do-rf': PlayResult(full_name=f'DOUBLE (rf)', short_name=f'DO (rf)'), + 'do***': PlayResult(full_name=f'DOUBLE***', short_name=f'DO***'), + 'do**': PlayResult(full_name=f'DOUBLE**', short_name=f'DO**'), + 'si**': PlayResult(full_name='SINGLE**', short_name='SI**'), + 'si*': PlayResult(full_name='SINGLE*', short_name='SI*'), + 'si-cf': PlayResult(full_name='SINGLE (cf)', short_name='SI (cf)'), + 'bp-si': PlayResult(full_name='▼BP-SI', short_name='◆BP-SI'), + 'walk': PlayResult(full_name='WALK', short_name='WALK'), + 'fly-rf': PlayResult(full_name=f'fly (rf) B', short_name=f'fly (rf) B'), + 'fly-lf': PlayResult(full_name=f'fly (lf) B', short_name=f'fly (lf) B'), + 'fly-bq': PlayResult(full_name=f'fly B?', short_name=f'fly B?') +} + class BattingCardRatingsModel(pydantic.BaseModel): battingcard: int @@ -47,6 +116,14 @@ class BattingCardRatingsModel(pydantic.BaseModel): obp: Decimal = Decimal(0.0) slg: Decimal = Decimal(0.0) + def total_chances(self): + return Decimal(sum([ + self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_pull, + self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk, self.strikeout, + self.lineout, self.popout, self.flyout_a, self.flyout_bq, self.flyout_lf_b, self.flyout_rf_b, + self.groundout_a, self.groundout_b, self.groundout_c + ])) + class PitchingCardRatingsModel(pydantic.BaseModel): pitchingcard_id: int @@ -107,23 +184,28 @@ class CardResult(pydantic.BaseModel): def is_full(self): return self.result_one is not None - def assign_play(self, play: PlayResult, secondary_play: Optional[PlayResult] = None, d20_one: Optional[int] = None): + def assign_play(self, play: PlayResult, secondary_play: Optional[PlayResult] = None, d20: Optional[int] = None): if secondary_play is None: self.result_one = play.full_name - if play.short_name[:2] in ['HR', 'TR', 'DO', 'SI', 'WA', 'HB']: + if play.is_offense: self.bold_one = True else: self.result_one = play.short_name self.result_two = secondary_play.short_name - self.d20_one = f'1-{d20_one}' - self.d20_two = f'{d20_one + 1}-20' + self.d20_one = f'1-{d20}' + if d20 == 19: + self.d20_two = f'20' + else: + self.d20_two = f'{d20 + 1}-20' - if play.short_name[:2] in ['HR', 'TR', 'DO', 'SI', 'WA', 'HB']: + if play.is_offense: self.bold_one = True - if secondary_play.short_name[:2] in ['HR', 'TR', 'DO', 'SI', 'WA', 'HB']: + if secondary_play.is_offense: self.bold_two = True + logging.debug(f'this result: {self}') + class CardColumn(pydantic.BaseModel): two: CardResult = CardResult() # 1 chance @@ -137,6 +219,9 @@ class CardColumn(pydantic.BaseModel): ten: CardResult = CardResult() # 3 chances eleven: CardResult = CardResult() # 2 chances twelve: CardResult = CardResult() # 1 chance + num_splits: int = 0 + num_lomax: int = 0 + num_plusgb: int = 0 def __str__(self): return f'2-{self.two}\n' \ @@ -184,7 +269,7 @@ class CardColumn(pydantic.BaseModel): if x.bold_two: this_six += f'
{bold(blank())}' this_result += f'
{bold(x.result_two)}' - this_d20 += f'
{bold(x.d20_one)}' + this_d20 += f'
{bold(x.d20_two)}' else: this_six += f'
{blank()}' this_result += f'
{x.result_two}' @@ -212,211 +297,638 @@ class CardColumn(pydantic.BaseModel): f'Play: {play}\nAlt Direction: {alt_direction}\nChances: {chances}\n' f'Secondary Play: {secondary_play}') raise ValueError(f'Cannot assign more than 6 chances per call') + elif math.floor(chances) != chances and secondary_play is None: + logging.error(f'Must have secondary play for fractional chances\n' + f'Play: {play}\nChances: {chances}\nSecondary Play: {secondary_play}') + raise ValueError(f'Cannot assign fractional chances without secondary play') # Chances is whole number if math.floor(chances) == chances: if chances == Decimal(6): if not self.seven.is_full(): self.seven.assign_play(play) - return chances + return chances, 0 # Plus one if not self.six.is_full(): if not self.two.is_full(): self.six.assign_play(play) self.two.assign_play(play) - return chances + return chances, 0 elif not self.twelve.is_full(): self.six.assign_play(play) self.twelve.assign_play(play) - return chances + return chances, 0 # Plus one if not self.eight.is_full(): if not self.two.is_full(): self.eight.assign_play(play) self.two.assign_play(play) - return chances + return chances, 0 elif not self.twelve.is_full(): self.eight.assign_play(play) self.twelve.assign_play(play) - return chances + return chances, 0 # Plus two if not self.five.is_full(): if not self.three.is_full(): self.five.assign_play(play) self.three.assign_play(play) - return chances + return chances, 0 elif not self.eleven.is_full(): self.five.assign_play(play) self.eleven.assign_play(play) - return chances + return chances, 0 + + # Bulk 2, 3, 4 and 10, 11, 12 + if not self.three.is_full() and not self.two.is_full() and not self.four.is_full(): + self.four.assign_play(play) + self.three.assign_play(play) + self.two.assign_play(play) + return chances, 0 + + if not self.ten.is_full() and not self.eleven.is_full() and not self.twelve.is_full(): + self.ten.assign_play(play) + self.eleven.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 - # Plus one if not self.nine.is_full(): if not self.three.is_full(): self.nine.assign_play(play) self.three.assign_play(play) - return chances + return chances, 0 elif not self.eleven.is_full(): self.nine.assign_play(play) self.eleven.assign_play(play) - return chances - - if not self.four.is_full() and not self.nine.is_full(): - self.four.assign_play(play) - self.nine.assign_play(play) - return chances + return chances, 0 if chances == Decimal(5): if not self.six.is_full(): self.six.assign_play(play) - return chances + return chances, 0 if not self.eight.is_full(): self.eight.assign_play(play) - return chances + return chances, 0 + + # Bulk 3, 4 and 10, 11 + if not self.three.is_full() and not self.four.is_full(): + self.four.assign_play(play) + self.three.assign_play(play) + return chances, 0 + + if not self.ten.is_full() and not self.eleven.is_full(): + self.ten.assign_play(play) + self.eleven.assign_play(play) + return chances, 0 # Plus one if not self.five.is_full(): if not self.two.is_full(): self.five.assign_play(play) self.two.assign_play(play) - return chances + return chances, 0 elif not self.twelve.is_full(): self.five.result_one = play.full_name self.twelve.result_one = play.full_name - return chances + return chances, 0 # Plus one if not self.nine.is_full(): if not self.two.is_full(): self.nine.assign_play(play) self.two.assign_play(play) - return chances + return chances, 0 elif not self.twelve.is_full(): self.nine.assign_play(play) self.twelve.assign_play(play) - return chances + return chances, 0 # Plus two if not self.four.is_full(): if not self.three.is_full(): self.four.assign_play(play) self.three.assign_play(play) - return chances + return chances, 0 elif not self.eleven.is_full(): self.four.assign_play(play) self.eleven.assign_play(play) - return chances + return chances, 0 # Plus two if not self.ten.is_full(): if not self.three.is_full(): self.ten.assign_play(play) self.three.assign_play(play) - return chances + return chances, 0 elif not self.eleven.is_full(): self.ten.assign_play(play) self.eleven.assign_play(play) - return chances + return chances, 0 if chances == Decimal(4): if not self.five.is_full(): self.five.assign_play(play) - return chances + return chances, 0 if not self.nine.is_full(): self.nine.assign_play(play) - return chances + return chances, 0 # Plus one if not self.four.is_full(): if not self.two.is_full(): self.four.assign_play(play) self.two.assign_play(play) - return chances + return chances, 0 elif not self.twelve.is_full(): self.four.assign_play(play) self.twelve.assign_play(play) - return chances + return chances, 0 # Plus one if not self.ten.is_full(): if not self.two.is_full(): self.ten.assign_play(play) self.two.assign_play(play) - return chances + return chances, 0 elif not self.twelve.is_full(): self.ten.assign_play(play) self.twelve.assign_play(play) - return chances + return chances, 0 if not self.three.is_full() and not self.eleven.is_full(): self.three.assign_play(play) self.eleven.assign_play(play) - return chances + return chances, 0 if chances == Decimal(3): if not self.four.is_full(): self.four.assign_play(play) - return chances + return chances, 0 if not self.ten.is_full(): self.ten.assign_play(play) - return chances + return chances, 0 # Plus one if not self.three.is_full(): if not self.two.is_full(): self.three.assign_play(play) self.two.assign_play(play) - return chances + return chances, 0 elif not self.twelve.is_full(): - self.three.result_one = play.full_name - self.twelve.result_one = play.full_name - return chances + self.three.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 # Plus one if not self.eleven.is_full(): + if not self.twelve.is_full(): + self.eleven.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 if not self.two.is_full(): self.eleven.assign_play(play) self.two.assign_play(play) - return chances - elif not self.twelve.is_full(): - self.eleven.assign_play(play) - self.twelve.assign_play(play) - return chances + return chances, 0 if chances == Decimal(2): if not self.three.is_full(): self.three.assign_play(play) - return chances + return chances, 0 if not self.eleven.is_full(): self.eleven.assign_play(play) - return chances + return chances, 0 if not self.two.is_full() and not self.twelve.is_full(): self.two.assign_play(play) self.twelve.assign_play(play) - return chances + return chances, 0 if chances == Decimal(1): if not self.two.is_full(): self.two.assign_play(play) - return chances + return chances, 0 if not self.twelve.is_full(): self.twelve.assign_play(play) - return chances + return chances, 0 return False + logging.info(f'Not a whole number | Chances: {chances}') + if chances in EXACT_CHANCES and self.num_splits < 4: + logging.info(f'In Exact Chances!') + if chances >= 3: + self.num_splits += 1 + logging.info(f'Chances is greater than 3') + if chances == Decimal('3.2'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 16) + return chances, Decimal('0.8') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 16) + return chances, Decimal('0.8') + elif chances == Decimal('3.25'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 13) + return chances, Decimal('1.75') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 13) + return chances, Decimal('1.75') + elif chances == Decimal('3.3') and not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 11) + return chances, Decimal('2.7') + elif chances == Decimal('3.4'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 17) + return chances, Decimal('0.6') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 17) + return chances, Decimal('0.6') + elif chances == Decimal('3.5'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 14) + return chances, Decimal('1.5') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 14) + return chances, Decimal('1.5') + elif chances == Decimal('3.6'): + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 18) + return chances, Decimal('0.4') + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 18) + return chances, Decimal('0.4') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 12) + return chances, Decimal('2.4') + elif chances == Decimal('3.75'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 15) + return chances, Decimal('1.25') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 15) + return chances, Decimal('1.25') + elif chances == Decimal('3.8'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 19) + return chances, Decimal('0.2') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 19) + return chances, Decimal('0.2') + elif chances == Decimal('3.9'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 13) + return chances, Decimal('2.1') + elif chances == Decimal('4.2'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 14) + return chances, Decimal('1.8') + elif chances == Decimal('4.25'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 17) + return chances, Decimal('0.75') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 17) + return chances, Decimal('0.75') + elif chances == Decimal('4.5'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 18) + return chances, Decimal('0.5') + if not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 18) + return chances, Decimal('0.5') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 15) + return chances, Decimal('1.5') + elif chances == Decimal('4.75'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 19) + return chances, Decimal('0.25') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 19) + return chances, Decimal('0.25') + elif chances == Decimal('4.8'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 16) + return chances, Decimal('1.2') + elif chances == Decimal('5.1'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 17) + return chances, Decimal('0.9') + elif chances == Decimal('5.4'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 18) + return chances, Decimal('0.6') + elif chances == Decimal('5.7'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 19) + return chances, Decimal('0.3') + elif chances >= 1: + self.num_splits += 1 + logging.info(f'Chances is greater than 1') + if chances == Decimal('1.05'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 7) + return chances, Decimal('1.95') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 7) + return chances, Decimal('1.95') + if chances == Decimal('1.1'): + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 11) + return chances, Decimal('0.9') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 11) + return chances, Decimal('0.9') + if chances == Decimal('1.2'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 6) + return chances, Decimal('2.8') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 6) + return chances, Decimal('2.8') + elif not self.four.is_full(): + self.four.assign_play(play, secondary_play, 8) + return chances, Decimal('1.8') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 8) + return chances, Decimal('1.8') + elif not self.three.is_full(): + self.three.assign_play(play, secondary_play, 12) + return chances, Decimal('0.8') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 12) + return chances, Decimal('0.8') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 4) + return chances, Decimal('4.8') + if chances == Decimal('1.25'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 5) + return chances, Decimal('3.75') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 5) + return chances, Decimal('3.75') + if chances == Decimal('1.3'): + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 13) + return chances, Decimal('0.7') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 13) + return chances, Decimal('0.7') + if chances == Decimal('1.35'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 9) + return chances, Decimal('1.65') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 9) + return chances, Decimal('1.65') + if chances == Decimal('1.4'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 7) + return chances, Decimal('2.6') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 7) + return chances, Decimal('2.6') + elif not self.three.is_full(): + self.three.assign_play(play, secondary_play, 14) + return chances, Decimal('0.6') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 14) + return chances, Decimal('0.6') + if chances == Decimal('1.5'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 6) + return chances, Decimal('3.5') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 6) + return chances, Decimal('3.5') + elif not self.four.is_full(): + self.four.assign_play(play, secondary_play, 10) + return chances, Decimal('1.5') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 10) + return chances, Decimal('1.5') + elif not self.three.is_full(): + self.three.assign_play(play, secondary_play, 15) + return chances, Decimal('0.5') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 15) + return chances, Decimal('0.5') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 5) + return chances, Decimal('4.5') + if chances == Decimal('1.6'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 8) + return chances, Decimal('2.4') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 8) + return chances, Decimal('2.4') + elif not self.three.is_full(): + self.three.assign_play(play, secondary_play, 16) + return chances, Decimal('0.4') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 16) + return chances, Decimal('0.4') + if chances == Decimal('1.65'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 11) + return chances, Decimal('1.35') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 11) + return chances, Decimal('1.35') + if chances == Decimal('1.7'): + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 17) + return chances, Decimal('0.3') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 17) + return chances, Decimal('0.3') + if chances == Decimal('1.75'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 7) + return chances, Decimal('3.25') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 7) + return chances, Decimal('3.25') + if chances == Decimal('1.8'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 9) + return chances, Decimal('2.2') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 9) + return chances, Decimal('2.2') + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 12) + return chances, Decimal('1.2') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 12) + return chances, Decimal('1.2') + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 18) + return chances, Decimal('0.2') + if not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 18) + return chances, Decimal('0.2') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 6) + return chances, Decimal('4.2') + if chances == Decimal('1.9'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 13) + return chances, Decimal('1.1') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 13) + return chances, Decimal('1.1') + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 19) + return chances, Decimal('0.1') + if not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 19) + return chances, Decimal('0.1') + if chances == Decimal('1.95'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 13) + return chances, Decimal('1.05') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 13) + return chances, Decimal('1.05') + if chances == Decimal('2.1'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 14) + return chances, Decimal('0.9') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 14) + return chances, Decimal('0.9') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 7) + return chances, Decimal('3.9') + if chances == Decimal('2.2'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 11) + return chances, Decimal('1.8') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 11) + return chances, Decimal('1.8') + if chances == Decimal('2.25'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 9) + return chances, Decimal('2.75') + if not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 9) + return chances, Decimal('2.75') + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 15) + return chances, Decimal('0.75') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 15) + return chances, Decimal('0.75') + if chances == Decimal('2.4'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 12) + return chances, Decimal('1.6') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 12) + return chances, Decimal('1.6') + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 16) + return chances, Decimal('0.6') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 16) + return chances, Decimal('0.6') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 8) + return chances, Decimal('3.6') + if chances == Decimal('2.5'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 10) + return chances, Decimal('2.5') + if not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 10) + return chances, Decimal('2.5') + if chances == Decimal('2.55'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 17) + return chances, Decimal('0.45') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 17) + return chances, Decimal('0.45') + if chances == Decimal('2.6'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 13) + return chances, Decimal('1.4') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 13) + return chances, Decimal('1.4') + if chances == Decimal('2.7'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 18) + return chances, Decimal('0.3') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 18) + return chances, Decimal('0.3') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 9) + return chances, Decimal('3.3') + if chances == Decimal('2.75'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 11) + return chances, Decimal('2.25') + if not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 11) + return chances, Decimal('2.25') + if chances == Decimal('2.8'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 14) + return chances, Decimal('1.2') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 14) + return chances, Decimal('1.2') + if chances == Decimal('2.85'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 19) + return chances, Decimal('0.15') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 19) + return chances, Decimal('0.15') + else: + logging.info(f'Chances is less than 1') + return False + + self.num_splits -= 1 + else: + logging.info(f'Not a whole number and not in Exact Chances! Trying to add a subset') + for x in EXACT_CHANCES: + if x < chances and ((chances - x) == round(chances - x)): + logging.info(f'Trying to add {x} chances') + return self.add_result(play, alt_direction, x, secondary_play) + logging.info(f'Could not find a valid match') return False + def total_chances(self): + total = 0 + total += 1 if self.two.is_full() else 0 + total += 2 if self.three.is_full() else 0 + total += 3 if self.four.is_full() else 0 + total += 4 if self.five.is_full() else 0 + total += 5 if self.six.is_full() else 0 + total += 6 if self.seven.is_full() else 0 + total += 5 if self.eight.is_full() else 0 + total += 4 if self.nine.is_full() else 0 + total += 3 if self.ten.is_full() else 0 + total += 2 if self.eleven.is_full() else 0 + total += 1 if self.twelve.is_full() else 0 + return total + class FullCard(pydantic.BaseModel): col_one: CardColumn = CardColumn() @@ -424,6 +936,8 @@ class FullCard(pydantic.BaseModel): col_three: CardColumn = CardColumn() offense_col: int alt_direction: int = 1 + num_plusgb: int = 0 + num_lomax: int = 0 class Config: arbitrary_types_allowed = True @@ -489,14 +1003,27 @@ class FullCard(pydantic.BaseModel): first = self.col_two second = self.col_one - if first.add_result(play, self.alt_direction, chances, secondary_play): - return True - elif second.add_result(play, self.alt_direction, chances, secondary_play): - return True - elif third.add_result(play, self.alt_direction, chances, secondary_play): - return True - else: - return False + if 'gb' in play.full_name and chances + self.num_plusgb <= 6: + play.full_name += '+' + + for x in [first, second, third]: + r_data = x.add_result(play, self.alt_direction, chances, secondary_play) + if r_data: + if '+' in play.full_name: + self.num_plusgb += r_data[0] + elif 'max' in play.full_name: + self.num_lomax += r_data[0] + return r_data + + return False + + def card_fill(self, play: PlayResult): + for x in range(6, 0, -1): + r_data = self.add_result(play, Decimal(x)) + if r_data: + return r_data + + return 0, 0 def card_output(self): c1_output = self.col_one.get_text() @@ -515,6 +1042,9 @@ class FullCard(pydantic.BaseModel): 'three_d20': c3_output['d20'] } + def total_chances(self): + return self.col_one.total_chances() + self.col_two.total_chances() + self.col_three.total_chances() + class FullBattingCard(FullCard): ratings: BattingCardRatingsModel @@ -524,60 +1054,28 @@ class FullPitchingCard(FullCard): ratings: BattingCardRatingsModel -chance_df = pd.DataFrame( - { - 'd20-1': [0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.25, 0.2, 0.15, 0.1, 0.05], - 'd20-2': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1], - 'd20-3': [0.15, 0.3, 0.45, 0.6, 0.75, 0.9, 0.75, 0.6, 0.45, 0.3, 0.15], - 'd20-4': [0.2, 0.4, 0.6, 0.8, 1, 1.2, 1, 0.8, 0.6, 0.4, 0.2], - 'd20-5': [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.25, 1, 0.75, 0.5, 0.25], - 'd20-6': [0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 1.5, 1.2, 0.9, 0.6, 0.3], - 'd20-7': [0.35, 0.7, 1.05, 1.4, 1.75, 2.1, 1.75, 1.4, 1.05, 0.7, 0.35], - 'd20-8': [0.4, 0.8, 1.2, 1.6, 2, 2.4, 2, 1.6, 1.2, 0.8, 0.4], - 'd20-9': [0.45, 0.9, 1.35, 1.8, 2.25, 2.7, 2.25, 1.8, 1.35, 0.9, 0.45], - 'd20-10': [0.5, 1, 1.5, 2, 2.5, 3, 2.5, 2, 1.5, 1, 0.5], - 'd20-11': [0.55, 1.1, 1.65, 2.2, 2.75, 3.3, 2.75, 2.2, 1.65, 1.1, 0.55], - 'd20-12': [0.6, 1.2, 1.8, 2.4, 3, 3.6, 3, 2.4, 1.8, 1.2, 0.6], - 'd20-13': [0.65, 1.3, 1.95, 2.6, 3.25, 3.9, 3.25, 2.6, 1.95, 1.3, 0.65], - 'd20-14': [0.7, 1.4, 2.1, 2.8, 3.5, 4.2, 3.5, 2.8, 2.1, 1.4, 0.7], - 'd20-15': [0.75, 1.5, 2.25, 3, 3.75, 4.5, 3.75, 3, 2.25, 1.5, 0.75], - 'd20-16': [0.8, 1.6, 2.4, 3.2, 4, 4.8, 4, 3.2, 2.4, 1.6, 0.8], - 'd20-17': [0.85, 1.7, 2.55, 3.4, 4.25, 5.1, 4.25, 3.4, 2.55, 1.7, 0.85], - 'd20-18': [0.9, 1.8, 2.7, 3.6, 4.5, 5.4, 4.5, 3.6, 2.7, 1.8, 0.9], - 'd20-19': [0.95, 1.9, 2.85, 3.8, 4.75, 5.7, 4.75, 3.8, 2.85, 1.9, 0.95], - 'd20-20': [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1] - }, - index=[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] -) -encoded_images = { - 'Hall of Fame': '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', - 'MVP': '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', - 'All-Star': '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', - 'Starter': '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', - 'Reserve': 'iVBORw0KGgoAAAANSUhEUgAAASwAAABQCAYAAACj6kh7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAASXVJREFUeNrsfWmTJddxXWa9tbeZwQwGGJDEYi1kyJIsm9/9wf7gH+Of4n8gf7UV4bAUsqywIizJkkIWFQrJkihCosAFC4nBLMBMT09vb6266Xtrze2+7gFIERQx5EN3v7WqXt1TJ0+ezEQA+Pp/+Fc3/tOXbk3+PSKkf5j+X4z3YDQaQbm5gKJ7oGh/YvO09BPTfwgIm3/tq9ufiM0N2ue1/7rf+9d3z4XhNf3jRb9R/Wd0jxdFAcMWs+2pH8P2vuH928+u36fo96V+ELrN696zebz7LBr2pX560fzU+xlvhbM/gMO21f9jn93vDw6f3X0O9MdPPI8dh/Y5w3s2+yb2CcX3wL+f5j/Uvof8F0K8n+IXm27pOfWf1D4WoPmV2p/xt3QftM9pn9u9lpoHhvuGW3oEu7/T6wLxz+3er3u+vw31trbPpcA+s30esNd275vuCKH5/MA+P/0Mzmc276m3Mf0ehmPQfz77LLbvgdh2DtvDjgGwz2DHOED/+foY2PfTx4T/jf1rh/2Ednth2Jb6+7TbWr8eaHisfV77Ec3zoDsY4kf93uks7M6l7iXYnYzUnIfFeA5lKIDKRVxL1OPAyZK++bcPw39MKwS3JT2OD1U95tTncRV3suoXU39udwjB//VowRYx9FvabhwBf2m32LqXMyQcnof87Qv+rgMAOmDV/GyPQ3/IeoBodo/0R0mgFLvHFzywxc0OTfMa/mk9Ig/AwQ4M6t0VIK6OMQ43VAefXwyQXxbalYn8ndrjMmwjed9mf1Krp/GvU/wuwaVdXPy13YLXx8Z+tPy0bhURiIXKt49vA+jPJLaw1Da16w8bQIFhu8VrutdJEJbnNA6fTwPADm9A9njIAzlsAwzbwZ/MF3l36IZjYQ9fs2/ofMskvi91yRnOLXUMujNNgU3zH0T3GxSvrbGIgVV3LndX4H57ulcEc3rEp5zXRCL9Z7kJT6tAW4Eo9TuO2NYJFOoXOVuKepXDgH7DtnTgAgxw+OIDcTeKoyCODfrnuAUdtEeTbZZc0WYX5GpFcPeTGG77IKDYDQMPZOxOInp3XNE5rhrJyIAS8jdm+9u/loYH+nU1rDH+hYl9BCDn7OSPUn82C4iiDNo5XyEHIGovOwxMKfceGrPkZ/PNkKtu2NL2QJG/q/y7IOdDqWXi4tz21jPtQml5RxaQxMGy722XB4cmeUJ1x5SuuFgBY1oCtAjyiwjs+TZ8pcO50x+yGGWgWPPdc3GZ7qgB6/iifFpWcCmOcWRYmPAM9WXdfgvYntCcXaFiAD1TM3A0hD39aYHiUYFk/QlBMIRl7XuQZl8s7ESBUBobUBxLEGxLoeRwdej3s0BJm8RJq3ETJSMg8kMy5J/lnPBmu/jFkbNXRPN4d8EYWEHLNsQVnGFMFwINv7IzGeXC4gyC7AKgHMh0bJgzI4fR11dqGq7iQ7g3oC5xkkkMOGkAdhkeypAOFKh14RMP8+pQiLM7Aj8UFowvi4WKqfqPi2uDx2bZwvfYaE+K7HUGJYNl3ztAv+/m+2ZsUDBRQzW65ymaCEquif8LVQV1DCxfn6L3sx6wzpbV83jHSr5Z+nKDw2howA+EPsTw8BV1dOThe39QSbJlzMcL3XokjsLuNuyOONABXnRDQntfBzQDqKCB8l7zsotOMkHyt7tfXFzr4hcItGGlCTNRMVRx3NhPkgtLhiSDXqEDhe5ERqFbELvAgAQN4FpLd+VnCx7kNnI9SDEvtYVggAY0GJF+PRqA5SyRMy8Ub2kDqhzBIB0eafZEmn2SPC/IfuoAzCTXEO1icpYd+lEIis8jtk7EFhKBgCbvsBOXkeR7mxe0OJI0LEgaMduZeNptNxU8Sb8mwKKzRQoJ4VJQsfoFob3s4vDBnS7D7kN+hYRGlLbHCRklpP69CLiWJTWZ5nGJygVnTyCZGyLuhivkjINTeAaGIvxRhEwhMOpAHxEsj8Or/+bMzPlMNPvA3wVNEoNvZx8isKuYYJwdnQIp0nIBNx+68GVDKpzPh1McqEIgIdYiqJNcARt5epXQQWToZ8kgGtADHQaT1cJIXDBVNMDZl9g+MieSYFECeJCdSyrMZomC7jig0ot6lkMO2xI8jIFxrw2GATT4sSFw2WbHKCSrzYSS6IAVY6PyapkuzpN4YazEORuxabPcwvMOsMJqG47j7YnRYhLwFCMrMKmwtbsidSyjPqBchNcaEKMGyAR4dDSXokZbdPQrVHoMD+mGlc7DUQQp+KPahu7xggOAAsJCZ9xUFtILCXm+AFFrfJopd+Euqh3i3w0a9iRCKLVOsOjEdhyu0EYcQaV1tQss8AWk4CoM4EI8fAqMNbOTPrif64MMmYWAMomjAAJVaKahBRRTA5EJ5KEQGUBDqcKzc77dJ3PtQY05KiSyVEjrfTxRgDy0yAjd3nG14SBJJGIQzFmwOHI+hgMnqL2ebTCL1IWExBUXZZDabEe1rokS3/5Im5aLLR33gBXv3j6/rB62afHhJA8bGI2mFq84HfTYjQckLKuAXE9AiYdYqJOsp3EdjJJlky1TCvV78oyeZRZ8YxCkbsW/NK39dABDSguRNoE8weYC+mCPaH8vJDMyTBFtMkFrz/r1yPS5Tpcp+pAQtSBm9KMh28QvUCjDGWyOV2B2gj5bqjJ7w3eMYkH1zw8Mcfttgv47txoKGX2NLxK+sobnggFRHVbmjq9c0M15RmCZIAfbPhxmNggfVNBhU+o91XZ3oOLptkAq7Cad+UPz3SjVZ9gORYzQYU3Ew2eyCTAproIfOxKP7Eicw3FbLs839EkHWIl/bReb8DhdMOQFPfQvJCOiF44dACVhViCGXD/XgnIfZtLgLRJITyYM5D4nL/7uhWWdJOw+q7sSOtqSzAqqgEaxH7H/jLUV18gY+YmAIZOZy/5zvU2EgT2oct8YshPKuUArsXU4HsRYln5etyBQbI+nrRFP7StfDyiFhovKKBYVSmuBtmtkMoS+yM8XP0jAI818vAXNQCirGZGvIal9EPsODkXW/gCdsdPJAE8v5HSIpDDhJ26d13PbDg0MMyeMGSKH6O1Qb8EaPi/wzE793BgSnq5LOO5E93Q6VqfL6n58YC1CofoAjuLfIyfTB5kN89YiGlYkQ6jhSmoWuAYmBXQ8yVpw1sL1MOuuYOCJIiw13wCifZ0Ck04LQ84ikH+ONIqCbzcDMDCN0ubgiXLsgkGAyhc2ZDwL4VvT/i+WdUWVeasfMOoQe38aWBCQUbYECyWuV+Ggp7QXkcDDPC6UB2Ia6fAZgWk7xN7LMVW4lhXs9RMUoR4xPdPPvqHjO/KycuRk+cjXhgKJyJt8xDLCP/neLlDODR94wGOTCmiZpikBFiU5Uq8friFoozLpyOn/LkZzqO2lQmcAWFf0NB6eyw6wEsOqTi7Kx6tteGasRxhqb4RnRhq0HFShnTTTSX2JOBYJ0R6lw2MIoiyGif8ORlHLekCE/gidw12EhohWDkfuch+0K7EFKA2rRmgTmhwOWTkeniL3VhF4aUpu35AAhsKwWiCIBa9No8I+0u5LryuhvzR4aCc8kZxtdSFgcBYkgrE3oKfqMAatL96AYEJCzDAb5OEHZYylAG5IpGzfjAlpMLK0BJUYKUJi5WtzwQJVxpOx0iwoGW0qk4V0QkVtTaGdJip0ZA4u6ZAhIt1xQ4X4RCTPZR5eFkm+2AySQruBq7LOEFaCYZ0tw/HZorqvo55a8k4My0Ys+jqvdBGQLnmh48jsoKIjEqh4+CbNU8OVHCGfYUN2OhFJPPDCuszvnCF1N89jhCo7Y7QKUXajTLeC7YHQubjGMgDYoLcVIjTDIRwVJxAIRsG3l1/tQ7DWhi6sRORJLJlZ5CJID2iiVIVlB8kKwAg8pPGc66rkRZq1xAKnTObTDa+6hUV60YHwbQnNxhO0lVDtsSuhQ3Fdiaz1wpiyzMXCsfN2pUPMupIJbA2TEu7+9nOxzSJzXY10EkDZWpAlh4Qxhlgkxb12nDC0+gNzaYUYDj5qcapnWGUVaHH/ZPP3aX/5G1BYw3iyJ7kPD2tQieMOu0HHOMkzbZx15f0EJBiNrjskq/VLoyRI8yjKtzZOFce7b0VKpXkVXNtCMN4pd+8Ys0Fr7xM6Q969b8MdK/6jSTXzMHnQD0EJnvKYWKM5mSycCEcQ3OwYAhfnHRDyjIzgi8SiTEXpWyRzbaLWjdcwipBWJB4seA9pfVuCBA7gKlUpq6mg2W6wyQShxVrAMSEhgazzU3YVLxGA7FgJ5qtOKk/Csr6zjEsTpbMNi3G8b9ZnCLuXlAE265IedipAHxIm4f2j48078QmlFoDrAyks8ywNrkRX74zqaSBab5HOHCFoVqYZHPrZOLSGOAJbaC0+SbvgUQILucwqY8RLDIQzo1zyIaencD+Wdvujp2/lxHsbng4F2NirnChsP8yCQB5FIJEJEmEM+eGNmxPPkA/wdBnws3U6zPcFb1uEawRrkJneXOiotbYBKZQ3CRwzqmaIuXobsiEykGdHMG8pL0K0+5gMj8vLu+eG9y7WO2B3x3XUMxXK6GB4z6ox37D7ykAXl9tacK80YFUxJHz/bFE+kqJw3MxqBUXyY6Gq/HezgWgpIiLzaoF7tdWgoTOA1mWgSnY6j5TwFDneelRfMNk0bdeNodBAJbpUgOjQgAK00WZwOWgI7xb22hrXl/TJJhJhmCtfIgnMAru46KpS8oiiwEKzJxOmmDw3CYsCsNIU3umAVMhJ5K8qZmCRYSkLLYPbAUFn0aT+pY2afakt2zd03WFqt1UshazMyRwzxTiIQKT/g+pooT2OoDxPoNzxojuGo7ORKgkCYzdBefzIgxqpf/a/kReB2GoIUEkWqbA2vyfBHbVsgSn0g08uNvS0pV4CsMr4tMXzy+pdbn4bvpQRaNkJdUcExWC4oOfyAvRT9TIuJpdYknpdH3ujIrZCsLYZwqsuEjqIcW0GqHUxUl9095kEmY8F77ruhoAoGSIHLcMOcxW1oK0C/NSxOoZ3yc7pz3Ld8nAKzTYgYpaBkXa7GwMimZCZhzHkaCUCyNTnCL+19ncwOiNr8cgK546twKM+Equ5TqQ8W6izfmRiQtJeK7IWC5c3O7WIpBMH4n+K1qEXpFvH/gDGqECfra5IiELtcJcn1vmGPqgCLDjDCh1gpbDw4cn2W/EJQRwgqmA0nsiSGzcQoTwLBGmAHNqt4I4FxVu7mB43RjCWdgYJMhJQHL1HWw6EcVFBMJrrTyZOVx8pmJESP8nRtogL8Lw3ltLl0Kbh+YIJZH04qvaiF+CDZlJIXgAsQCPf6kRqMub4kdd2RTEi5XWCzGJUVqdshktsM4vuyKTUUOpGKIGzZ2kmO4euWA6iNhCNluedlEbbA925huT7o2Xk5LBDRE+TU/oV+YkEAh0+O8XtKNuDDA0LyFpr+tKqss0Q8uMP4fE5fSvhEmdYgbOsD4/X/3C5rk4lY0omvirStqmMqVCHgcxy75R5APqWA9Xbwel2QHLhgmxwV3RAwEpaELzSoIHUEsiwbnAND7WOBSI4HXKGsLDgehux7KZMBoDqRiEBWb2PBiMnzZ+8KkRa/IfByClMnCiYKv/uhJjPwlDf5a1kZEJjAPVc4pzFdL6pEFSxOzgsBHx8JZflksj0IRfute6mUluBZBhsOjxkRG0Dfhn3l3tMmX2CPPHalDDZKgS55yTjSJRdNnoPXIvQpoIJtOGcW0x4SOlrWOKCK7LSmAEyHM63Wg4ZQ93pigLTfgk2JZyfrukfuWzVAVbZ3Rbr8PH9483fabk+oZ+usdM0ghjbQMeLREQ7QhuwrUB0GYdP4pSO42dgephiRlW/zlFrX07TQGVsAyG6S0uEZ3EQbBOlqZQI7WJkYA3omVdluU93whWiS6qCAR3ekbUXdMCCHplB6LtaassAMuYRlKCfbA7IU5CkzaYkLSIwFMAHHWoJz5etQ+SFwuS9JoS+Eoh4phrR1g2C720i5TQfNCJdT4l+2xgn1eaHbgC6tYtDp+V3iazfmWPS4pUSblhuLlKu+ix9DeT3S+vtEaQkgfj3aLoXf5Ydp+2X7cUG3o/R3jOGT1J0b+9cfXS8+dP1NlzqDx5PphENJ7KchcMTSnuDll4K1ezO7ewJaDsX1PV2hQmTERyGx0JCVMJ2T0N4XaDStoCckiJAP9xQ2c5CZD7tvlmPhzxpeccamYBQBdwoax3RiPQ23221pB3ZOna+C5EYlc2hOyYhk80ie2HgHicQmtFwvgeSbWU4aOnXdoI1OoI4cfAkMvYbfQKaUEsL7TQ47sk5pm7G1FNeVVNSUeysjLeiM4Nw+ut2OaSEffJjdA8M2X55qIqQ10JNMsZEUSh0XSNbxHVdjFKHhlKax+N/ztf0D/G3tcewKs6yHp9u37uMTMvsbrWG6fwwU37jeX9s6gVdKRCzYjSqbjyd+VTX7YmwDD2hHGQmLyd8K71Jdwq14SpasozQMiVVa6n3xnXIQ9vfXfqmcBCqWMcGlQRBv7+LrO+T4YPRiURdmkcEyLUQoDD06p5ZHAAlq/IkYaXwDXpTJg2PRobfaSKRAKa6SshEHAuHPJOf6Ys1/DQL080m2MfRKRCmLKvi2iH6Wp6oXUQZb5iLC5rmoeSUPeUWPo9AbImSfzwSAUrtZGrAoiFSi0S8fLbsw8FSi+48LNyuNuHZg5PN30bKX0nBbgvjovkQKQ7zcA0Fo0AVMsoyHhR+KP4w+u5H03EFlV8K0PFeocxYCoanOh5qsCTDdnjq3Xq3nGCzCZ3MfphqQTDapCgZkszVmGOd8gqRajchjtNXyTFJisZ8pIqGQdlWwMlocy5ETgGx0jF1TaLxAnk95ZXGE5wOoCL07RhcIFuhYdCBWNgIyoKApgBcamGeKx37oRKyflEaWMm0RvAyixzUg+rqoGtguZ/MKzcC41PjZTSkM8HZzhOqK2oPjuRUXaSGfVOoyvXA2NvHzlbww+cr+h4DK8GwOpa1bZ+weXSy+fuzZfVEy0GhXMEofki2WZsOnbKFvlc5z8iEXG5TYKfGj9v9e1qNNgtn+iUpcZ7A9qsnAcY65HOuOH3LHL8VCDncwity9np7i4k/akqRHDiBu6ITcxLJhecdoAGAZOuWHT4NKa+rnugohPfeUe+aOUl2qdU+IyNHUz7sQ17GMrwC+/YrSsMitR+6CyppG8WOLCqpEJq0xEhO4bHyeAkpQMTvWYE+G8qA3/PK+xoRLd3sQZJIkgRdUwiyE+54Mm96YMkLSxXB6lvx5wUjUpWXJUyAlXKLm4+ON99/8Gzz1zykaK5KW5hOplSzLNaqV68LyRx0EbPUZEg0ovfLZ2xmkb8fExC9tseiZrDQNhaRfkXEnbG/yIQh+h4qdANFN4umjamokhF2SIXqIw/55o7GRMpb1qLVE3j9H7D6QF6G2+ta/Ph1Ar0ukhZmRFuu1W1SM4oLIOTc6uDXzXHQE4mbvlmg3z1B1Aqi17iPTC8tbnwlJfQj6IaCkilnvVCmL5fuqEDqs3xGk9Nc1HQcp2uNnvqjJ4H4IXg2zNMtrXV7JWUbGY1nELbrlPwgHgVtK7iM4eBftHi01YBFSsOqAWtb0fNv31/8afy5AebPaeaSrXE623cXiaI/NshClL4ctG1+wQFBln8XRhM+yMLW3NkWIQQEVvxRJUCKuciPt8XShfd52pqvGJo3LUd3ZLDhhrZf6AyjwJmhGR7I0IPIGGqGRRFAF7WpzJcL0XUTQt2CB5xwy7QqZ+GKbtaHkA89RDjb7rss4xqOk6lxVLWCqC8mIr2PLoR2rB3RZ3fZxg5eKE2eLSpvYdZvJLPrAG4XdS8hwHQ3YeK9YqyPP+CCQMzAzAxJ4c9N7CpEdoXYtILs/i1L+PB8TR9yXV0DltCwWtBaP7so3334bPPtENp+kH06eAsjDHX7YmALyS5aEP2+beU4B5hMp8L2jYII6e1wVp61MgXHCJkQFp0soO4tLgetokVECXpodbyCFW0XmsYrcBZ2GtVfnr8PoExJo2OV0KESV9dEXZsofyLQg01DILcBSW9rwMHrY/xbbegTVNhkC4/zWohtQSPbEJPuqe6w/O48DOS0gGlEXub3JEcH9LK6lgHp5wfTzmUAm9CIWdlaU1tC0+4jOs32aLePDtTFQEa5KsnCz1HdSTVrEkZlr8i04W7fLMlK42kCrK3M6wXYnCzp7TLAmWJXBrAqBljrFrRW33mw+P3zVfikh5tuz8Kmzhh2dXd8OjIoqwPvFICOyJ5TuGRZi3A52BiNAYsXrvmlOLKHtRHvC2QObRXSIGZ9YHp7h24IXWZEg7vqL6/+1p0mTChY6IESlqh7jdnktGHpaRIj2wo/S4haj3F6Iak64R4cgDU/5INMu9i592+p4Z8cAG0ojCKMIZNsQBG+gcMBhkNItlkeyXPLs1IBZBptGpAk2RUhI4KjqPNEE0qa7LSpIXSmRFlPqkzq6GLua/7TTTWF1sURMZ5QkxihbZZn3XfVa0KXW3oYAevvWvzhIWHggMVZVs+w0u0HT9Zvf/x882199IkqmIwLGEekROau5s5OUTApuhCYyYQOu9otDBrwQHmFMK1VuuemrhNqQEUXiujSIdC9elQpkRmrladyogB6GNiBZobjTv+W6itmgk5nQIHQ9dATu0iaynjmytiqiBEy1YdKh4q5GXzktIXRs46JXSQImPFQHml/XovtH6UqujJsxm+lDJmQinKTWk1aXzJaRLAVGNYkATrZZTUvNLsLOUMpSK3Ygqyu8du16NDWeXIFhEh6Hvm2tlev8WRWN1MIbXawj9/jcyNYfetsTT9wAKtv4EfK2rBlLGtdBXr+9x9e/sHZonrK37t+WbmA2XQ69Hd3OjnIrAD5WQaUPiGvE8TgoFfthnk2D8F/DnOSmv4+4PRyV+DBB197TW4wkypFbvxki7VjRMi69Qkm1YWSySwrwj60jf0yWhwPa3WWS0890eECovEN9Iss14TOGDfR66BAIoWv0nbKEOozD+obAILbLcD4iEzPLVKhEKnRW7bonPRUiF2N/AywBTFgVIO00BQV+xU+MbVeSM8pJC+bacnN1d46XedKusWi6hyiyo90gzlHlJ7Oj6DaLGrI4ZHJuoKLD0/DH8Zw8NQT3DuGBU5YuGEsa/X0fPvD7z9e/EVVM/HuBMA6Bi/i06fJWg9+G9xdaW7e4jez5MEd0AByQrZoRoIAGebLyhEw2+TOtpqWrnsiL7xE0e3A9oYB2aLGcdPzFsboXF35qDBrrcwzdwJ9EXM8OGqIqBCeQWpNiHmtRYYjtPNCrb1W3OQo0/qypIWX3UitzGkkyD7LG7xBjvfI9C93OyBYY6ydnKxnYaEjepuG1HK7++OOonszuI56Uo75/NGnTG2PuWDkG2plXosqhWrXQprCRdW29l7xARnJKHq6om9vmu6ia8awDGBRhmX1YWFZ0dnfvHf+e88uto8BUVy5QtjCfDaqC6NNOMfZYIGZDpzSdtAX5uoD0S/0wmhlBKDG3XMA0L3nfac5qoZ+XNzmDK8QmUL53t2Vp5sDCN7IMYfd6C9e7F9hw+Zew2m/uYJ9htc6xaw4zQwQ2bklPWMaXDu20QvuxCZKk58t83pV8SZ6pApEycuIkWonQ3I6tBedUcZMRKznO+BVz9P2FD/b6GpZBKZpHy8XIsiL0sLzx8Z65WUS3Gm0lRiFglECc7jzFje8m4owo9KOhITRIwY/VuoqOt27CeXmopaU+P6sSnh6/5T+ILW4YoRpowALCrXfWZa1Kenx2z+4+N1tGcpmQ7uNDzEWXcL+3h4LDdsFXAxdE+S8MkecVtqWLu7l6W8ttuYmwgxlMju6fjIKSzqD5mybbKEvW6TokWeIYKcGuzoWijBSjAcnnb3kHSpU6pjylg4OTNoWEgKZQAxIhR/q5OfhODF/F+0wJjaN+0iI77ZdSY4XIPMKEfMqoczagecl0sXRujULinNlZwWeNyYMMKtlCY2O+7ycpt79AGJln5BJEmZaJc0OGTA5TQKMloh6jqPUC+iKbqSohu/6vqTh/klqtV5tmjIcmcyg4wX95fMlvc3Y1YY5FzoyRYU60pplrTvASj//8aPLb3z/0fJvQ6dGsCwThhXs7d8YGJDKCHb3Q2YenVhgNCSmSWc4QNUQ64JizqxM4z7HfOoBqZlJi6r1i/wiim6MlumKMFgZwLbBt7YMp6bSm5WoZyn2ry9Y6w7DDKTfCvpsGzij7Yf37Ayh2vjHjYaolH8KlPHsyHAOwfa+4pn/oBYoB22zwDKj/PQUZ2KA1793CLI5Hck0vt9X3YagfesWIjF4ll/Ydo0BIy2oe92yVCbY9hMTg8pMI0fbz33XbEiZ9SXyq6KJtECKqvso1AXORRHjvu25EvCJllt6+uic/iw1aGCY04WEHbsiLrrvYlk9aMVtOPvmB+e/dXy2/WHgDof6HQJMii1Mp3PngsR2GP2CZKH3FLl+WZr66ip5doCxO94kxXmWv0YeSqKcKVhPnmGthMVAV9F1wnO6o2t74M51VG1lUE+Z7domq95YOmRF3nyMWKYG7GSovgYO7dhyKcCSTSiQEvDZAkugFgKJpnhai+kvIZTRnSS/M1OVyZgk/cVOrHVw4G2b1fdldCRTfUM7s2RyUYLtxgm2NTNkwnYeHuqhFqJ7K7+Y01AzafvAo7pAUUZs11oeOoYfJxTlUbppYosASjNMZGU2P6y1qzoJwRIgVcSwGAr+7/NNnRlc79CviDOsq1hWYljLWoA/237w7qPFn6+3Yc3FuWZc+Qbms0mNpmZOYWYRi44L1jEiF422SvDHceC5aAYwgDFRIAuRUEluehCGUs99sxx6Wo8F2+Yxp8Xyjo4VxkKBoHpYaPREe/oRC3tpKK7lvavEOHd2RFwz5U6vEekkcMaCIEMYzyWHzIFEvEYsn8uRHcNJNsUjyr+QdHhlRbfd3gFHwfYiBDEGzWnlqTOAWbaVCz0NY7uOe8o7LJmRYHqk4HCVGrpCsPNwOjus6wVDDAc5a0jn9PkaPnxwFv6EmkGpOXbVARaMnK3DzG3Ual7Fx6ebx6lk5427818u2virE34LDHED96AuD6uFNTvuyrQl5gJ2W+vXM4tCdfFEMPZ/j3Hw1xWsTaseE1Yw20PR+rSaKdLYv7ZnYwX2Xi7+nvywdabQzu+F7PVFUShW1W1fITOBxD6Xhbv16wnYY2gKsanbdh5Zg2oDwZIYBF6IoxgZKwj2zuDGCe+AWWawge0izpr1qXYApsuwdsHzhnBu3yPH7m+yYU6tndKo5G6gZU9k8hoS/BFM2x4yBlBW9M0nYAO5gMbLtIzJkywpIFXcQHxGJUgPnuujU73zhz5lKP2P7OdoPI9R1wzK9blIHKb9XFd48c6T8OurEt6HRmy/bH+uHNACLbrnwkLNspZlRc/f+ejyzx6drN+Pv1fAQsN08ha0hr35vGmpfMWIK14PaEwGqmGdN+TKzihEe+AcM4AzjkFlMFFdQXg3StNfQbm6UeQZkGdgUCdRbRsJU2OI3rah8Yx1+0CG+ZCbxtdalBgibxpiWfOhYFA61U6KF5DKThKZBUBums0bjSU1FV1sbftZUT4eU9TOA7DBNS87msqxXrkKg3xGUQrn5E9wpgzTcQa17Gyx4MlgjDGK78kMk8gsIpGKsv+ShSFJRAms9DkTQ8HN4/Pwp2dr+i7DFh0OcnZV30YZfogZ1lV0twhUZQwPn9x7afbW4d74JRTWBYJxQTCb3YBtWTa9mvlU55yGA2CZWMdy+HMLaSXgry/a+YmWYQHTgIbMYnqeeX9U4n1RiBFe3X52YEId6yo0A+zMnyjMtXq/RR8vBcCDybTbTjU5mncfZb3d0XicyAwXDV22F4euoQCZOvwODILuUKnCHOXnCiYzNoiJpIw/lGkHA1eZrwVPRDNQA1XGi0QVtmMlADlVXICGOjiyvo4UYKNr3uRdL7wdM453rx887mI/PmheI4I1wadua95lMrUsI96kCSdgb/9mhJ0lVLV2JRnt8xW9873j8F+qAE8Zs1p2CT5tZ+hB8Mqg1oaGPWhdrqrTi1X1/PU781+eTkbzQg2JGBUVTKYHUKUsTDrTOVCYkI6NbheAVvRb1IVUIlxjc/1AFBvL5oD8/Tk4FDzMFOFlIbeXudALLtq3gGd9ZoVo3SxYIDqAxbe1YOAOEriKQg91Rac/EQ3skjhAeOY/ctoVO252NQcsV1BLplOB7v01sBSUl3rZdE9gg52ig6qpnGR/KLYPRQhnYkwW1pEFSe1c5w38mADq1905YG1AyWt4x5sykvDJoSjJUf3wmb5HhqA6zRcR/RDV9wipOaPylOC+qzSya//gFlC5iGC1FkXmaTvP1nD/gxP6zYsNfJ+BFQesLhSsQFWMXwewdgLX88vy5GxZLd98Ze+XxiMcd6unWWAEkyJpWvuCaflsStkSWDlNP8GmQOEKTxfHggFQ4Yywt1Obuc7EAAHBsEBgn1kogJEAxMCQhXRFb+fgzIyDZiHDvAIFk+vAyRt7jw1NFGwLhPnTb6LopOVEiMZPLlQWCJ6BRbf+TLmuWb9wIxqB9EVZjw9IP5AIweTAB+Ctn1URd15gJ6XdOKEq5UaNkXWU64JpkeFTRlpQg09R93gn3/jqDqOQE25cBytkRsCFIC946IwP4xeGnfDQvC7VCN546VUIMQxs3OzyOWXA9T9+HP7zsyV9swWr7rZk2lWpwGonw9oVGoIDWnByvv14W9Eyhoe/MBkVkyTED+PaqWZao9G8fnozLFExH09U156qPqzyHem+AD+wEjDA1DAlA6AM/ArnPQEc4BDho7IeFLJOUIamzvb34jua7QF9vBQb5eZiMmc52XYsrjAiR2YRa82LoIYf9N1c0WSltA/Hs4EjgCymNmyKMUABcqiFuHzYI9oQqwESjvtf5zLJS+W74KXwAj0dyzFFkfVUkLJHaxCR9Zhorz/OcA1Q7a/J+KxQfC8mZ0/GmWg1q/EEDo9uQ7WJYLVdKmaVSmbg7MFZ+KOH5/RH8c9zBlYLFgpq75U4aKNr5zt3ZxLh4+frR+MC9yJovTkZ10xLaCyjIrKtyTT1u6nDQ6vXsPDGARIZukltZ2gXrBa3G25anQnZFJ1RkWFpXUgmsoaFDDXrx4eMH99HUKBjNLNOg2Kf0zPAQraXQe3PQnTLotyBH2JiOO3WNEheU2U4RFkrg5nK7bQ6Mdk41zJBmcwkCaMEZUij2M9M90/TSFkB3E4NO9MsDzLuB78XugJJPpbMc1WIMB9MC538Qsas5iXkJ0S3azNcwa5SB+IksGNYQ7ldSbCiJLJT9fEl/dUHz8LvBqp1q4ViV2vlvaIXYVi7t84HL/roePVBKmK8d2v+8zE8nPAi4UbTCrA/jztVROCqKtn/mQFKjjlZ7QcG/YprWQAOYEk2UhQomvOJgazOa/Nhp8eapBGUJwq6E6JgYMdBmdshCmVbaB7zgA5EJ1dQEZJkIY5h0xgLtVHXtgcW/fNZelyvTFIMQmcZB/Hf09fAHxfDjJtIXjp/iGkDkWViuQnVelyy6daqjpl0opq5iGTGYXlGTlYb6UzC7jOUaL1dJvOqw1FwGBs5U4ycC8+gd1ojSj8qr/09NePb3zuIv6+aMBBk2VXyFT84o2+89yz8t22oi5s9sOI2hiwEj14AlAjyA7460ArPLrZP4roavXxj+kZkWpNh8XUsJMB4VNQ7Wffy1sZIxqDQCfG87CAvFMasqA9Wgyqk2F0ULZigp3GhcMgDE+25UK+Ffe3F0qCGGVDjTQQFG9M6n7BvyCk4aMILO7bc13rY4gpB9d4n92SnnR1j/XIWj21xRrS7rIQU4JJoApjNjimrB3hgsUPrs5Eu5Q5dXuzWj+WGqZo+QLIXlQ4bcy1qgKwdiK5hf3B9bMDPOYRJZFXTyQxCeQmhKlVnjQbHn1zSN99/Fv77pnLBSoeClGNX1wGs6zAt8dyyou2HT5bvJz/W6y/vfy2GWAWCZBtFvCzOJqkYMu4oFbXBlLOIvlslK57mFgMeMg3YUYjwkC96LpiTygSapn3acgH8fQqbqWu3Npf9TK8hZWotmC0i/c7tEDy5IHQ4LOy2qUnTXZtcPliBj7Xiq8FqsOQMKtCAhMIgiqr3fK8TBTVVB+SgEemGlkELWZ16qFdFm3XUeyC6arJiaXLBbUjVA6Kzz6wPPs8SgpNVZGx0V5gtQUqNwDMWDVCDIFAAnZ7Y7M7apKFA3DAtkJN3tOkXzQAJFA3DprMDmI6LuHwXiXwgqbxHZLfh+Yp+8O1Pwq9HsHrYgtWFsjGsle+KPgvDusLy6Yb59Xn38Hj9w8i2Tl67M39rPh3tmbq3WoyniM7juBWTiM6B6UWF79ty9CgNAjkXPDjZvaxA73mkeqCS5s2EI6Ou0Z4CWBAtYuy2CVuG3mZgTfy4edbLZDoubsjJo07zPS6uyp5mekgCme4TwFsKZ4yk6Oo3OvSw/ncw7nJoa9HAsiuHy+m6PCEiM+Zip08T7O7m5Q+I1KwP9LxI7j/DXD8yMCEzD4HJaF8eIGYKqP1mEq6WiObzBnBN0dHe3iFMsIJyu0jPQ9Ebvw0DP76g//e9J+E31hXcV8xKh4LlVaHgZwUsykuLwzn69Gzz8OGz1Q9m09HRncPJvXrJoWyzktbyNGLWfDaLC3vSh4no6FZC60HLhiADZMbe0KBFU+BcWI1MhG9FwdrG+O8HvRlVesY4OBbGwOqFh8zjhXzQRhtSF7YEgjPDnCjdA4ITB4iJMswQWhcz56wBpsMCKS0HZLPF3aq1TPUTqH5X4MZT2V5MqsUM7/GgS2cCc8tqNiX0Lt4sihSYg+P+J2ecvNutwVoe5PyDfM80PsGaTPiM0o0vGCWaekEvrCWyAF0UY5jPD2FvNgHaXkCZWsVQM4KET4aLAHX53nH4vQ9Owv+IzOqBAqvFFRaGnaD1IoCFV4CW+0EXy/Ls0bPVhzf3J7cP9sa3ku0BnCzZKK712SQu8pptVeJTMcOOQHU/0Nkz31HvlPHwyTla53IsFyJLyBgUIPhGUK5/GUYoPxMyOpcFZhvCmswaUSbLZb8q5Ho1ybmB6NFp1nnVnPDZ0eag3O/WK2S6fXYLEnddN0EMVCVVAkS6hZDnTHcyd9p7T2QLisn5fMBsF6qduQRQ2ULdqFC4ZD1NUpQiIQNPNI5V0UpGyApe2VoBxWgEe/P92qLUNOAjkwlM/1+VdPbRKf3+R6fhDyuCZ5kw8FOB1adhWHgNpmWI52oTFu8+vPzuYl1d3rs9fysyrpkeMV+rD5FYRraF+/Np3fGhkUKC0ZAsOIFhPkbkdoyexunehmIkNDMULEmyMZnZ5BlDbvos+h5haC0aOfc7qLASUWkKQzfKAlFeaWEYwSXEaJUAc3szoehgoub1KTMk2jYt1Lb07bejHWXlLfBdY9MD8BY5JE2cZL2engLGQ5pApPylmVFYJC0dfvdSFKGXm6VjJlxRg6o6Ynj1k5TL9IlCa5I6ojjIaPdPWfLMzEid9GjP1fFkD/b3b8AkIkXSqqpyE48l6vCvfuvzNT36zpPwXx+d0zfi36cqBNTmUNPr6jppgE8TEl4HtMxjVaDt42er+xG43r11OH3l9tH0DuIwNqJb6c2ipppt7SXgStNh2XQAGQ5qNlJkXfR8sfc6FXBzJ+uD1bnbVZbQhHOFFMW9cK/3qbBwsS/5EbWRkm31oZ8o+aHh/UEO1+iFcPS+LDUEgVgxN5Dvg+oWVfALnt2ZcwSuB4yDh3AtIeSZ2FU5H92ttGeWu53YOR8YP3496LKSJq93Hankw9DQUA719a0OPFQjYcxES6RUbejw5rLvGOz6AvxkBliza6NTTSJQ3azlGiova7tCaGc5MLtC97N6cBa+8e5x+M2zNbzTMiovDFyqjGB1XaD6rBrWdZmWDhlDZFnP3/nw7NuXq2p79+bsldlkNMcCh6IpLnJH4JpPIMbMs3o0UFWnTUnW3IHqeSVYkGwj42UGeWFzDQ6FtkIU/clSCCvDEHo2j3GwkU530D4uN9yzQIiqvYzQ/1hWCturHbcakB4eSMrFzNkSYzvoeZp4i2SEbKcHLqIH1TMe1MQacj1XcGWhLqpMHAHvEkqu1wuMcO8I/iT1HJFpZdlNkX01Jz2qMiUyRX2i9z7iThOnABtbjujbIRznvtA9SRV2C+mraWOcOgfPJ5MIVGc1o6razw9st0PzNy22dPzDE/qT956F39pW8JgBlBcG7jKH0o8TsF5E0yJG+0Kb9d48Ol598OR0/VEKXSLjujsaFWNbbtmHipFxQTyYIxjFUDEVV9ZDTlnqVepF3OA5gI7HysBkHbXXSdYyDu8zONqLIlOuo0XzDiwzpUKuN0v1qxcDWwHFsFnPaGPa/Ir6PZ5pkpkrbSZFMSTVhioAGSe7K5hbxsOn4Yj0P+j+WHQ9LYgVbZOTcCAVBqEScHY67sFpjuf52sC66GX20jP5etN48pN28qK8vQh4WNp8t0W9rmazPZhFRoVhGYFq1Uz9Jjm9OrQ2iU1JiyeX9PYHJ+G3P76gv4hvcwZDEfOuMPBTg9VnBawcaOWAStzSVMNn55vjdx9dvjeK63h/Nrp5sDc+gBqeQDUKb34mw+lsWtSzENOtaRIIvffK6Fhgy3qEBgZXid2yvIjrXHkjqJ2JWCiBn7vyNSMDxcIGsOLgOpQk6Q6cfiyE5hvLVoQpVuFqTxkf5nXMoW4P9l19zvV8hGx7X7oyDKTMqDndWC+X8rf7jZmBq+SGkSQnYqgOpyg1tB3Zwd3Hz/V42i6q7fk7ne7Xxs9JvP4WYV2Pjq9q6wj2/e06ltHqVrQs4ez+afjjd5+F/7XYwAfQ1AUursgG5jow0IsAzmcFrF2igQtUINueVvGArD94fPm9t98//ebFqrw42p/cOZgn4GqrLogZDZnxNImAe7MRJIF+Wg9zHfWNcxDyHSC4gdRoWbqYWdU69p4ouKJVjMg8DiFe/XLMZAcZawPlC+Mhrz7gorc82bmA3GIgipjJ80OBKx4DM4MCqmGq3DBJkkUg926R03IYvFl+/vBS0ailfbOgzLBuqEa+jaOHkKC7eYIZQuF17yG4KhGY6xWvNDPjddsBRKgZ6m5QzW1aqvmbjMcwj+tmnLAkJHtC2TIo7Gr/mEZVsyo6XdKjj07p/77zSfUbx0v663j3cwZOmlV5PqvPBFY/KsDKgRa9AHCFsqLVg6fLD588X32YmgNGMDqcjos54tB6dGjL2jKRulVx8nEl1jWKX8QExkXitIXYsiLXG8vYG9Ap4wHgLWMMUEGudTOKyc8ckIrCsz3kOz4MXfn8JnJSG/EYhS+SCL1LjPMiu0iAA1fefErqvzoEIqc+Tw5GJdsGGZRW5rT0zbXz9ZUmtINR0WFCnvG0PlZkt592lD0SZId4cHMpsauJ8I5p9stmOu5KMPCC5uQmmoynNUillk8jjCwqbAdgapO53TCRMAjrtNrC6fGC3nn/WfifTxf0N2WAJ5nwT5fb6LmCnwmsfpSAlQN3L0Ss2A4IthUP0PYkhonfvX/2nfcfXXz/9LJcvHZ77/W6+0P79QSQJQ4N62p6rE9GCPNZEVnXpAavukVz/aUHYfyUnRuGOkdtReDie2+fQDm4dehhxQT7gmX/CusXI1AhacFDPgesVHyGKsHAM0teKxPjyyI5G69jTwiyzGaXl0t3zCTXokCmEJpUD3HKGiRJzMxDxJ2nObEunkOjva44mvs12GJHEDV6xliUuwITKAMSiYEZhilxfuhk5sjJptoicV20g0DGG4YCpNK62JsfwTxd0Mfp3C3j8SjrkC811QysrXngY9Xa+zcVLR+dhW+99yz8TgwB/08MBe/H+3X4twBbanOVuE6fFlRGP0awAgesNMPqbqX++3xZnnzw+OL7b79/8vZiU52Nx3i4Ny32R1gUQ9pZf7m1ot2AV/yS4vPhYG8cAWyvYWPFqP6ieLjls6+MjYFl/gpla7D1i7z2T2lXeqpzzwidFjTgh4PoeAJllhQcLcgmykVnS97CF9A3cJLX18nqXHL70Db5y4CoDh9Rj27TlbXcaB/82XliViANviyesURP+DHZtkyloKhDRJVFZYDKhoH0BgbVdNAv+yHRTE8mHgdAT7fpdK+u0d2fTSJIRWZF65o+JT9c0nyrFpg4SDWsqjP1QrjY0LOHZ/RX33safufBGf3xKgIVDVYFDVYLh1XtahNDnwVY8McEWF7frBG7RbyHSQqn29tM3ebs9/o5EVxuvnXv8F/+u3/z6r99697RGwfzyd5kEmEo0d3U/SEBUvszifPjGKPX948aEOi0iLJKwlkB6+0GqiqI9iPc8iBE+8J2SUCvoZ5TcA2mwyrrPIroMj9hjyhkMt+dgwi2rq5jTdxAyh/rACodg2656ecKiwKb9ScHsrYnfroQQKd7qOJnAS7Ul/5YMCRlDiWhgYUuRoHuc0nsix2MCjWb4NN7TAPDesGG/gJotoUbQdttDqo7RPeYGL6q9lu+znlvVcIjjg+AKSRvFIJxe+FMetQYMKQxWmXPkKpmXmTaNByAiXqQ6mYbpjLebUWrZUnP7j8Pf/XkMnxzXdYlNR1TEgOV2U8+lotPav6RhYD63/jHyLC49RgdlsWn8mxhGNo6Yz/7WxLn3394fvrw6eLbb756+OaX7+6/9Wu/cPvrt2/M7tw8mB5EfKqpVyioZlHpyyp68BrV4JVYz3zWgMEh7NUnaXrutiTYbMt68VIbQnLxXli+GQ7L0h1gM1AxOy0I0byaPRVB+fd6fQNRzhME3XmEnJa6XrZMtf9NJ3xgA1BD0N0+SZgMu+4UGtQQbe8rM3mad4vQArc3C1D1lsdOaNeWC2CTCnhJDmeiQ79U5W/Sk8iV4uW1k1G7uCtL52VXiV9m3OyqvER13STGo2l933gUL8b1xbeC6RibFkC0ai8c2IJV6AAdOYsKPWABbauwXm7p5PmS3n96SW+fruiHq5KOWWaPA1IOpDRQ/UhDwH9KDetFrA/VFSFif18S54/P1k9+8PjiB9/98Oy9p6frTyI40Ww6OpxNGtGK1BUptFfchk0VvWA/Ho1i6DiC+Lr6CjWbTuoi7FG8PxV6IqfvXhF14bc89pv5KbuCU+dYZCwWDD3VwFXpeHYNhMAHPnjDNlXPTWWc1KZNff4huk5Hae0k2Y1BmBZJluDIMV1gxHf0EnFkB/GCAXHK1A7K1sCc2fkNZ6QQTx7DVSEeb22jC6PlitDHLVl5JjCdpPAu1fLNYFwkllHW8z9HzaR3DG1WL12k61vLGgfgYvaE+G+1pcXzVbj/4JT+8v1n4Q8fnoY/j2Hg+2WAExX2LRyNaqnAa7sDrOBHCVY/zpBw1+eY6TtOqMjDxakOD9Wtfv50UtyKoeJbX//ay7/65bsHb7157+i1eF8kVgWO2hFcPHRMYNWEjKM6xVs3FUy31L+qPd6hPQk22wCrzbbWx+oTIFSD+511AZWtk3UhtAQj4uGm8YzpDKHUqkjP8YIhjEC2ELrOF8KhDcO4LjHbLwQTlunwUIQ7ylXehWG95aBrA9OFbgw4gnpPUEK5Dr3Ec8jWFXZhalCfMfy0IZ6pTex/BmNc7cIz/dPsb7BNBU1YzLS2wC4yxWhWmzeTxWA8whqokss8dentS2KY5tSEe6HVoqjVqEhl+Zq/y0Dl5ZqenyzCeyfL8O4n5+G72wqe0DChpmTak2ZP+u+tE/r9WFnVTwKwPitwTTJgJUAr/T6fjY7u3tq7/bU3bn7tyy8fvP6VVw7fuH1j/tLRwWQvghKO6o6CRQ9eHWiN2tBxXIzaEHLUivJ9M7Lmy69SCJkaDo7iz23T6hmLlsWRK9prER4zQr9vXpUsSZeV6JS3NyOvO3HdgQ5CB2oBwDTgk1pSB3whyBFfDUhxbUcvWNa6hs8O1KDD9iNojSqQYCvEQUkBb06X8zU6Z789UINBtwPnMVez44J/mxRKxypdHCeTebx/W8sRs8k4bn/FtDgewlF7sez+Di2Laj6nave7fW5Yl7TalHR+uQ4fnyzpvWeL6r3zFT3aVLUjfa2kmK0CpI3zN2dSpZJ3fqys6icJWB5owTWAy2Nekwxodc8fxwU+O5iPb9y7s//KL75+81d+5edu/1JiXvt7k0liXkU7cr7Wt4pWoC9GVAv38TKX7k90vAayWsQf1UMquu9jm1zBEbwonnirdQmbBGSQgGzTiNBN4y3mli9Ur/dd7vqh1lHbC+orOdrwTzMfjyl5wMaza5KVkaq9axcFew4vkNY+Ls2iOkAjtQ19q2ySrIr4yPYQWP+nFjDAY1wMgFpzkb8vUtynIB/nQBc8RgbEthfygJ5Cg9lhPH/iaVktawacGFR7kSBsKzu6qo3EnKjVYKsEXnWI17C3QZeCgVG1xyc+FuLpVz5fhMfHl+H9pxfV311u6EkErufxaQvGikoHqPhPDmTl5wWofpKA9WmAKwdeHoBN1HPH3esj2Oy/dGP26ldfv/X6L0fwev3Vozfu3Ny7dbQ/ndXMq7UrFC3zqnWucSPWj9rM46QFr3G8GiYw655bDzyomnAxiaGJhdX1RxXBer2uJ+GmdtBVZGXpZBt8W2qgBp+5iEN3Vj4slJTzUA891Qwi/auqIZHQsxjFXnhrk45t6PfVTG3IaPmf3YdZwWF9ZEeGaZAYwKwLoVCGrzA8jzfeozZMAgGwkAVUGxq2ABzAuQhItgXFJLKl5rxJpS3pe5vNEnOK58F2C/NJ0Ya4UJdJVWEA5vS9DMwptJnr0JxLLBM6WBE6RhVZ1DasztfhaQz3PnlyUX0nhnwfXazDg/j40gGa7QvcSkdI94AK/inB6icNWC8CXN1t7NwmTvg41mxL3WoAiyfWwd58/NLXv3b3V/71L979uS+/cvhaDB9v7c8n08TBakNqAa2HawCyWv9Kv3fgFX9OJo04msqEJpOGjUG7aNIQ2TIBVZ2RDDWgVQHr7OR6s27nxGMvniM3mqYNUA7xEPwZfh1b6gTzPszqGBk4NoBAA8Pg4jQNE2dyoSCCNBvadL0CoWBDUZ3u73qgaR0uKJsGtEAoQ7MBKKsQhMYW2qGhGnQH3ap7Hth9rsO4UXv84sVmNK1be0/jdxzCpr4IJc8TqZGdHGRqYKqaMC59/40+2jCnLrQTYR4HqfhLvO4lgFqstnQew7sHj0+rdyOLei+GfR+3WpQGmVIBVemAkgdQOZD6iQLV5wmwrgNcHvMS4OOA1ETdP3YArH9thKf50cH0xpv3btx97eWDe//iSzfe+NLdw1ePDma3Dvcm+4f7073ItrAJJSUL64X8Vvuqs4+pTGicWFgDYA2wTerOjd2FOZL4CGRVrYXV+lgEs+V6VXdcTUCVAoYa7OJVu6s7qzM+yX5BQZg9STMmseiGJnqcDQ1+LRDakRdKGj8XF5CBbNjlAZfrb3JAVIRpLETL6EQhSFe8ZZqeaN71FmudPZENJXY8Gs360L8uXYnfxaS9KNUDRVow7QzHgW1DApuOMVX1RSlQ/Bvr+1sWlV6fdFBqGRUHp7SdkZHH0C6UMZRbrrdJg6qeni7Do8t1+CSGe/cXm3ARHzuLL9kFUqUK57YZAKteAKR+okD1eQSs3DbpSdOYCRlHDoCNd4DXOANe9XslVj8djw6+8urhaz/35Vv3fv4rt77yyu391yMLeyWC2HweHyxa8EqhXZOJbMEr/Z1O8KLLRjZMbDJpQGw2m9VDZefzeX3fqDaRxSty/OjkRC5rU+smrqEtrGI4uVqtYmixrgGsjKws3bfdbPpMI8VQMy2CNMByMEaTFcADCVMnz6xRICeslKxMakgyS8izgqINMsnXC3Nk53A0gJnJcPIQlGUrBRsjYOFma09IIVtqSVRTz6q+UDRANK2/i6IFLKoBa/DsQVv21RkcuixcB0jpYlOz5ar92d4fhjAv7joh9QyqDQNDaC8Ctfuckv4UmdNysQ5np8sqgdPj44vqwcmi+ni5CU+VBqVtP2Xmts3cn7MN6c4qkMn40ecJHH5agAszYeMoc9sFYDuBSzO6eOWd3Tqa33nj3o1Xfu2rd9+8eTS7/fJLB69+5ZWjlw8PpvPZZDwajwYgq/WvZHNIv6fMI9PIRskAOG5tFYmNRVY2mU6bdh+z/Xrm23h+WLeLLuJza4YVN6eMrKuKt+26bVm7XcIm/r5cLmBxed5nmtICSVraZlPBOjK3xNZGbf+uZDDsRN6aDbQha+1i40J5m/3sNaD2byFg96l0X/AX7IhlJIMHcArc6hC3GLeaX8HstomllDVQpG2aTua1mF20wIRQ9VrXLF4UUi+11NGj70zbDxYpehBpssBVy3rLWn/qwShdEKqSgVHDlDprQXdfD5at/6kDq6Q5VZE9LbfV8nxRnUSmdHK6qB5FUHrw7KJ8stjQ0/icywyYaH9ieQUYeeGdFs2rHUzKHT3yeQSFnwbwwmswLw5gxRXgNdoBWONdIBgX/jQyrb07t/ZuvP7q0a1bN+Z37t05uPelV47uvHxr/6UYRh4d1OHkbD5pso5YtNnJzoNVtMys6G0WDSOr9bHRuGdgKZxMKfDRZC/epvWopdF4XoNZ0lSgrg/HOqxpskhQhzQxuIDNahEBa1H/rIdd1kyhAb0qMrfE3tarCHqRtSVgS2pMV+9Yhy8tkPT1Z6HqQ6DQpeHrMp+KaWdg6we75EFXWK4a/zYyW+gF8xQi18M66wTHuHF4t6CPQ4fl/qKQXpeeU7MiZv/YbLYDu6kaTbEDorJswvJNB05lVWfnOpCqEsokcybTl+r3YjaD9uJAHatKId1qk7AprNZlOL9YVs8iOB1HkHoaQ7zj82U4WW5DDPnCZWgE8irDeCqHFeXAqdoBdkH5pYL6mZ3H8HlnMT9NwJUDr6sATLOmHECNM+Fm7ta/Z3JKRICa3zycHb5y5+Dmq3cObt+5ufelt7780hs///pL9+7c2j84mE8joYrLrPaF8Z5ZReeip6I2veouqsPzGu1s1C7kSQ1qqZX0OIJZYmjjaWRq0wPA6X4NdDjej+A2i1s6rX1kgJFo4rbObIWyrAfahlYcbkKktEDLWomO/CA+vEFKo8i3l0CpI2X6GUPXql7sTWIhAQA1z+9BqWtDXfe9b7c5MaFRYk5199hR/9U1LviiZUNFc0/8jG0E0sQg92bz5nO2ZZvMKGugXa038fFlA0LxsXW8r2ZJEawSU9q2oVtV2vCNi95VVfMj7JziXbaOmOcpbWPEMar1pjJUi025jCzp+Pnl9pPzGNLF0O7R6bI8juHcYrUNF/GlK7AdSirwu5dUGWZUZTSnXQClGVQOoD7XIPXTDFifBrxwR9ZxdEUoedVtfAV4CaBM08ziKpzsz8dHd28f3Prqm3deff3ezZdu39y7c+Nwduv2rf3bd1/aPzo6mM2m0xgkJlaWyru7dljG8t521wE2xALYpGtm04g3GhWp18WoBraOtY3Gs5qtpWEfDVubtSFoE4YOLanbcqU61J1A29GwTathDYCNy75qw6zQOh+rGnBq5hVD1wSO1DKzxPaqpNdFhpeGHJTxZ7lZ1RpeAqEENpv4e7nd1gxo297S72UdsrVMqWdMoc+01WDUhrsDG+RaUuiZklMYnNgSpZ+RBW2Xm2p1uarO48/T9SYypnUCp/L46dn2Wbz/+WJTncSXrXcAUnCYTrgCdHaVqoUd4MRd51eFevTTAFL/XADrRcFLZx2RgQnuALHPAmxFBsBc8ExdcSLAzPb2Jjdee/no7mt3j27/6ldf/fKXXrlx89bR/PDoYHowm4zn89l4FtlZqiaqCdpQkpzmEA2hUOvbIkQxIGc4SMimILPfRQcKUbvIp0yzej1uhwDWoykM7KTPhAUGKp3wzFhOXYDePd49RsNj3Wu7kGzIYrZeptZvNnxWfR8FJsS3FQupLrVab6t1BKV1fO91ZEaLk4vN0+Pzzcknp5vj08vt04tV9SQ+7zwDOFd21IV8D7gcAFU7nq/fnxwNapf94KeGSf1zB6zrCva7GJhmYbiDje0CseIFQWsnkKUi/Qgg4+l4NNvfm8xuHs3nkZHt3ziYzffnk6NkxTjcmx4cHkz3797avxH/3o9glnKY89GomCRpLIanMfwsxpNxkbxnNUlquRpS1wpAzwbUplQg2alUABU4Xi5pTdD2hx7IlIG0Yzr8d/mzZj4dSFGrGYXNtoqRIEWyFXEoxrHxP5GAhfVyXV4u1tXiYlleRoZ0uY63TRkuk36UbvH+dbx/E5+zisC0ia9dXRFSXQVa1TXB6roszPvs3OyEq9gT/XNb2D8r4PVpQCxnqyh2hIHXed4u0Brt+Hy0DA1TgnIyHhcpk5nAbH7zcL738q29/Vs3Zvsv39o/iki2F8FrP0aae7PpaB6DxElKDMyno9l8Noq/TGZNIhExsI4F7e+YAALBtDJGYkXVbb0gtYDYPbftLFD/E94ubmFojZa1ZLaNQLTelNuEa5fL7Wq1KVcRcFaRFS2rKkZrZViUISyWq/IiAs/ybLFdJgC6XJWrBFARmFYRyC5SslSl7it4gYEpGeCorgFU12FkIWMroCvA6WcCoH4WAevTAtguKwVeA8Suextd828Nnp4uh5kwGHawyFEf5NWSFyYmNk9sbD4dxwi0qL2yNyL4JRC6eTibJzTbm47GB3vTaQtE/bFMjx0dTKYJrM4uNwlUWpGL+m4sEXw2EXzKFMBeLLbrJGCfL7cJhEIEo4RTVXzdJjGe1AetXaTohD7XMTfmOt56AHAd0AovAERXgR/tYE30KfQn+llbvPAFgLkL/boghtcAlGJHFvM64eKuJIL+/F02kOuA3Ofh34uAz67nvwhoXYdh7QrXdn3GrkzddXxQP1MA9QVgfbpj8mlALAdoOevFLjC7KjT1AOg6YKXf7/N4TuiFHT4DaIVrakFeWHbVz08DTPAC4PQzCVBfANY/LYhdB8zwmmCGV7A4/fuubbgOyP00Miy6JsjlWM1VoSLs0JZ2gdMuZvQFOH0BWJ8bENt134sAGrxAGIpwvcqAHMh9Xs4NbwFfB3w8QMgB0VVhWe55V/0OL/D7F+D0BWB9LkHsRX6/KpMJ1wC13PvmQA4+Z6CVW9RXgc8uZvYiIvZ1S1Z2gc8XwPQj/vf/BRgAFK8ELPF74+wAAAAASUVORK5CYII=', - 'Replacement': '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' -} - - def get_pos_string(all_pos) -> str: final = '' arm_added = False for x in all_pos: - final += x.position.lower() - if x.position != 'DH': - final += f'-{x.range}' - if x.position in ['LF', 'CF', 'RF', 'C'] and not arm_added: - final += f'({"+" if x.arm >= 0 else ""}{x.arm})' - arm_added = True + if x.position != 'P': + final += x.position.lower() + if x.position != 'DH': + final += f'-{x.range}' + if x.position in ['LF', 'CF', 'RF', 'C'] and not arm_added: + final += f'({"+" if x.arm >= 0 else ""}{x.arm})' + arm_added = True - final += f'e{x.error}' - if x.position == 'C': - final += f' T-{x.overthrow}(pb {x.pb})' + final += f'e{x.error}' + if x.position == 'C': + final += f' T-{x.overthrow}(pb {x.pb})' final += ' ' return final def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions) -> dict: + player_binary = batting_card.player_id % 2 + logging.info(f'\n\nRunning Card for {player.p_name}') steal_string = '-/- (---)' if batting_card.steal_jump > 0: jump_chances = round(batting_card.steal_jump * 36) @@ -661,22 +1159,739 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions vl_dict = model_to_dict(ratings_vl) vl_dict['battingcard'] = ratings_vl.battingcard_id + vl_ratings = BattingCardRatingsModel(**vl_dict) vl = FullBattingCard( - ratings=BattingCardRatingsModel(**vl_dict), + ratings=vl_ratings, offense_col=batting_card.offense_col, - alt_direction=batting_card.player_id % 2 + alt_direction=player_binary ) vr_dict = model_to_dict(ratings_vr) vr_dict['battingcard'] = ratings_vr.battingcard_id + vr_ratings = BattingCardRatingsModel(**vr_dict) vr = FullBattingCard( - ratings=BattingCardRatingsModel(**vr_dict), + ratings=vr_ratings, offense_col=batting_card.offense_col, - alt_direction=batting_card.player_id % 2 + alt_direction=player_binary ) - vl.add_result(PlayResult(full_name="SINGLE**", short_name='SI**', is_offense=True), Decimal(4)) - vl.add_result(PlayResult(full_name="HOMERUN", short_name='HR', is_offense=True), Decimal(4)) - vl.add_result(PlayResult(full_name="DOUBLE**", short_name='DO**', is_offense=True), Decimal(4)) + def assign_bchances( + this_card: FullBattingCard, play: PlayResult, chances: Decimal, + secondary_play: Optional[PlayResult] = None): + logging.info(f'Assign batting chances\n{play}\nChances: {chances}\nBackup: {secondary_play}') + r_data = this_card.add_result(play, chances, secondary_play) + if r_data: + return r_data + else: + logging.warning(f'Could not find valid column trying new values') + for x in EXACT_CHANCES: + if x < math.floor(chances): + logging.warning(f'Using the whole number {math.floor(chances)}') + r_data = this_card.add_result(play, Decimal(math.floor(chances)), secondary_play) + if r_data: + return r_data + else: + logging.error(f'Whole number was no good') + break + + if x < chances: + logging.warning(f'Trying {x} chances now') + r_data = this_card.add_result(play, x, secondary_play) + if r_data: + return r_data + else: + logging.warning(f'No good; checking the next value') + + logging.error('Could not assign chances\n') + logging.debug(f'vl: {vl.sample_output()}') + logging.debug(f'vr: {vr.sample_output()}') + return 0, 0 + + def get_chances(total_chances, apply_limits=True): + if total_chances > 12.5 and apply_limits: + return 6 + elif total_chances > 10.5 and apply_limits: + return 5 + elif total_chances > 8.5 and apply_limits: + return 4 + elif total_chances > 5.5 and apply_limits: + return 3 + else: + return min(total_chances, 6) + + def get_pullside_of(): + if batting_card.hand == 'L': + return 'rf' + elif batting_card.hand == 'R': + return 'lf' + elif data.vs_hand == 'L': + return 'lf' + else: + return 'rf' + + def get_preferred_mif(ratings): + if batting_card.hand == 'L' and ratings.slap_rate > ratings.pull_rate: + return 'ss' + elif batting_card.hand == 'L' or (batting_card.hand == 'R' and ratings.slap_rate > ratings.pull_rate): + return '2b' + else: + return 'ss' + + def full_log(this_ratings, this_card): + logging.info( + f'Rating Chances: {this_ratings.total_chances()} / Card Chances: {this_card.total_chances()}\n' + f'{this_card.sample_output()}\n' + ) + + new_battingratings = [] + for card, data, new_ratings in [ + (vl, copy.deepcopy(vl_ratings), BattingCardRatingsModel(battingcard=vl_ratings.battingcard, vs_hand='L')), + (vr, copy.deepcopy(vr_ratings), BattingCardRatingsModel(battingcard=vr_ratings.battingcard, vs_hand='R'))]: + logging.info(f'\n\nBeginning v{data.vs_hand}') + pull_of = get_pullside_of() + pref_mif = get_preferred_mif(data) + + res_chances = data.bp_homerun + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_bchances(card, PLAY_RESULTS['bp-hr'], ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.bp_homerun += r_val[0] + + full_log(new_ratings, card) + res_chances = data.hbp + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_bchances(card, PlayResult(full_name='HBP', short_name='HBP'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.hbp += r_val[0] + + full_log(new_ratings, card) + res_chances = data.homerun + while res_chances > 0: + if res_chances < 1: + if data.double_pull > 0: + data.double_pull += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.triple > 0: + data.triple += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_center > 0: + data.single_center += res_chances + break + + ch = get_chances(res_chances) + if data.double_pull > (data.flyout_rf_b + data.flyout_lf_b) and data.double_pull > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-{pull_of}'] + elif data.flyout_lf_b > data.flyout_rf_b and data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_pull > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-{pull_of}'] + elif data.double_three > max(1 - ch, 0): + secondary = PLAY_RESULTS['do***'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + elif data.triple > max(1 - ch, 0): + secondary = PLAY_RESULTS['tr**'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['hr'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.homerun += r_val[0] + if r_val[1] > 0: + if secondary.short_name[:4] == 'DO (': + data.double_pull -= r_val[1] + new_ratings.double_pull += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + elif 'TR' in secondary.short_name: + data.triple -= r_val[1] + new_ratings.triple += r_val[1] + + full_log(new_ratings, card) + res_chances = data.triple + while res_chances > 0: + if res_chances < 1: + if data.double_pull > 0: + data.double_pull += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_pull > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-{pull_of}'] + elif data.double_three > max(1 - ch, 0): + secondary = PLAY_RESULTS['do***'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['tr'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.triple += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_pull -= r_val[1] + new_ratings.double_pull += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + full_log(new_ratings, card) + res_chances = data.double_three + while res_chances > 0: + if res_chances < 1: + if data.double_pull > 0: + data.double_pull += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_pull > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-{pull_of}'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['do***'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.double_three += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_pull -= r_val[1] + new_ratings.double_pull += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + full_log(new_ratings, card) + res_chances = data.double_pull + while res_chances > 0: + if res_chances < 1: + if data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.flyout_lf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B') + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (rf) B', short_name=f'fly b') + elif data.single_one > max(1 - ch, 0): + secondary = PLAY_RESULTS['si*'] + elif data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS[f'do-{pull_of}'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.double_pull += r_val[0] + if r_val[1] > 0: + if 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + full_log(new_ratings, card) + res_chances = data.double_two + while res_chances > 0: + if res_chances < 1: + if data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.single_center > max(1 - ch, 0): + secondary = PLAY_RESULTS['si-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['do**'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.double_two += r_val[0] + if r_val[1] > 0: + if 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + + full_log(new_ratings, card) + res_chances = data.single_two + while res_chances > 0: + if res_chances < 1: + if data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.groundout_c > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A') + elif data.groundout_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B') + elif data.groundout_a > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) C', short_name=f'gb ({pref_mif}) C') + elif data.lineout > max(1 - ch, 0): + secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})') + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['si**'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.single_two += r_val[0] + if r_val[1] > 0: + if 'C' in secondary.short_name: + data.groundout_c -= r_val[1] + new_ratings.groundout_c += r_val[1] + elif 'B' in secondary.short_name: + data.groundout_b -= r_val[1] + new_ratings.groundout_b += r_val[1] + elif 'A' in secondary.short_name: + data.groundout_a -= r_val[1] + new_ratings.groundout_a += r_val[1] + elif 'lo' in secondary.short_name: + data.lineout -= r_val[1] + new_ratings.lineout += r_val[1] + + full_log(new_ratings, card) + res_chances = data.single_center + while res_chances > 0: + if res_chances < 1: + if data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.flyout_bq > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly B?', short_name=f'fly B?') + elif data.flyout_lf_b > max(1 - ch, 0) and data.flyout_lf_b > data.flyout_rf_b: + secondary = PlayResult(full_name=f'fly (LF) B', short_name=f'fly B') + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (RF) B', short_name=f'fly B') + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (LF) B', short_name=f'fly B') + elif data.lineout > max(1 - ch, 0): + secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})') + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['si-cf'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.single_center += r_val[0] + if r_val[1] > 0: + if '?' in secondary.short_name: + data.flyout_bq -= r_val[1] + new_ratings.flyout_bq += r_val[1] + elif 'LF' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'RF' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'lo' in secondary.short_name: + data.lineout -= r_val[1] + new_ratings.lineout += r_val[1] + + full_log(new_ratings, card) + res_chances = data.single_one + while res_chances > 0: + if res_chances < 1: + if data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.groundout_c > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) C', short_name=f'gb ({pref_mif}) C') + elif data.groundout_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B') + elif data.groundout_a > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A') + elif data.lineout > max(1 - ch, 0): + secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})') + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['si*'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.single_one += r_val[0] + if r_val[1] > 0: + if 'C' in secondary.short_name: + data.groundout_c -= r_val[1] + new_ratings.groundout_c += r_val[1] + elif 'B' in secondary.short_name: + data.groundout_b -= r_val[1] + new_ratings.groundout_b += r_val[1] + elif 'A' in secondary.short_name: + data.groundout_a -= r_val[1] + new_ratings.groundout_a += r_val[1] + elif 'lo' in secondary.short_name: + data.lineout -= r_val[1] + new_ratings.lineout += r_val[1] + + full_log(new_ratings, card) + res_chances = data.walk + while res_chances >= 1: + ch = get_chances(res_chances) + if data.strikeout > max(1 - ch, 0): + secondary = PlayResult(full_name=f'strikeout', short_name=f'so') + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['walk'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.walk += r_val[0] + if r_val[1] > 0: + data.strikeout -= r_val[1] + new_ratings.strikeout += r_val[1] + + full_log(new_ratings, card) + res_chances = data.bp_single + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_bchances(card, PLAY_RESULTS['bp-si'], ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.bp_single += r_val[0] + + # Special lomax result + full_log(new_ratings, card) + r_val = assign_bchances( + card, PlayResult(full_name=f'lo ({pref_mif}) max', short_name=f'lo ({pref_mif}) max'), Decimal(1)) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + data.lineout -= r_val[0] + new_ratings.lineout += r_val[0] + + full_log(new_ratings, card) + res_chances = data.popout + while res_chances >= 1: + ch = get_chances(res_chances) + this_if = '2b' if pref_mif == 'ss' else 'ss' + r_val = assign_bchances( + card, + PlayResult(full_name=f'popout ({this_if})', short_name=f'popout ({this_if})'), + Decimal(math.floor(ch)) + ) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.lineout += res_chances + break + else: + res_chances -= r_val[0] + new_ratings.popout += r_val[0] + + full_log(new_ratings, card) + res_chances = data.flyout_a + while res_chances >= 1: + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'fly (cf) A', short_name=f'fly (cf) A'), Decimal(math.floor(ch))) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.strikeout += res_chances if data.strikeout > 2 else 0 + break + else: + res_chances -= r_val[0] + new_ratings.flyout_a += r_val[0] + + full_log(new_ratings, card) + res_chances = data.flyout_lf_b + while res_chances >= 1: + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'fly (lf) B', short_name=f'fly (lf) B'), Decimal(math.floor(ch))) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.strikeout += res_chances if data.strikeout > 2 else 0 + break + else: + res_chances -= r_val[0] + new_ratings.flyout_lf_b += r_val[0] + + full_log(new_ratings, card) + res_chances = data.flyout_rf_b + while res_chances >= 1: + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'fly (rf) B', short_name=f'fly (rf) B'), Decimal(math.floor(ch))) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.strikeout += res_chances if data.strikeout > 2 else 0 + break + else: + res_chances -= r_val[0] + new_ratings.flyout_rf_b += r_val[0] + + count_gb = 0 + + def get_gb_if(): + if count_gb % 4 == 1: + return pref_mif + elif count_gb % 4 == 2: + return '2b' if pref_mif == 'ss' else 'ss' + elif count_gb % 4 == 3: + return '1b' if pref_mif == '2b' else 'p' + else: + return '3b' if pref_mif == 'ss' else 'p' + + full_log(new_ratings, card) + res_chances = data.groundout_a + while res_chances >= 1: + count_gb += 1 + this_if = get_gb_if() + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'gb ({this_if}) A', short_name=f'gb ({this_if}) A'), + Decimal(math.floor(ch))) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.groundout_b += res_chances + break + else: + res_chances -= r_val[0] + new_ratings.groundout_a += r_val[0] + + full_log(new_ratings, card) + res_chances = data.groundout_b + while res_chances >= 1: + count_gb += 1 + this_if = get_gb_if() + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'gb ({this_if}) B', short_name=f'gb ({this_if}) B'), + Decimal(math.floor(ch))) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.groundout_c += res_chances + break + else: + res_chances -= r_val[0] + new_ratings.groundout_b += r_val[0] + + full_log(new_ratings, card) + res_chances = data.groundout_c + while res_chances >= 1: + count_gb += 1 + this_if = get_gb_if() + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'gb ({this_if}) C', short_name=f'gb ({this_if}) C'), + Decimal(math.floor(ch))) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.strikeout += res_chances + break + else: + res_chances -= r_val[0] + new_ratings.groundout_c += r_val[0] + + full_log(new_ratings, card) + res_chances = data.lineout + while res_chances >= 1: + ch = get_chances(res_chances) + this_if = '3b' if pref_mif == 'ss' else '1b' + r_val = assign_bchances( + card, + PlayResult(full_name=f'lineout ({this_if})', short_name=f'lineout ({this_if})'), + Decimal(math.floor(ch)) + ) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + break + else: + res_chances -= r_val[0] + new_ratings.lineout += r_val[0] + + full_log(new_ratings, card) + res_chances = data.strikeout + while res_chances >= 1: + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'strikeout', short_name=f'strikeout'), Decimal(math.floor(ch))) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + break + else: + res_chances -= r_val[0] + new_ratings.strikeout += r_val[0] + + log_data = vl.sample_output() if data.vs_hand == 'L' else vr.sample_output() + logging.info(f'Pre-filler total chances: {new_ratings.total_chances()}\n{log_data}') + plays = sorted( + [(data.strikeout, 'so'), (data.lineout, 'lo'), (data.groundout_c, 'gb'), (data.popout, 'po')], + key=lambda z: z[0], + reverse=True + ) + count_filler = -1 + while not card.is_complete(): + count_filler += 1 + this_play = plays[count_filler % 4] + if this_play[1] == 'so': + play_res = PlayResult(full_name=f'strikeout', short_name=f'strikeout') + elif this_play[1] == 'lo': + this_if = '3b' if pref_mif == 'ss' else '1b' + play_res = PlayResult(full_name=f'lineout ({this_if})', short_name=f'lineout ({this_if})') + elif this_play[1] == 'gb': + count_gb += 1 + this_if = get_gb_if() + play_res = PlayResult(full_name=f'gb ({this_if}) C', short_name=f'gb ({this_if}) C') + else: + play_res = PlayResult(full_name=f'popout (c)', short_name=f'popout (c)') + + logging.info(f'Send Card Fill\n{play_res}') + r_val = card.card_fill(play_res) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if this_play[1] == 'so': + new_ratings.strikeout += r_val[0] + elif this_play[1] == 'lo': + new_ratings.lineout += r_val[0] + elif this_play[1] == 'gb': + new_ratings.groundout_c += r_val[0] + else: + new_ratings.popout += r_val[0] + + full_log(new_ratings, card) + + new_battingratings.append(new_ratings) vl_output = vl.card_output() vr_output = vr.card_output() @@ -684,9 +1899,13 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions logging.info(f'vl: {vl.sample_output()}') logging.info(f'vr: {vr.sample_output()}') - # for x in [vl, vr]: - # while not x.is_complete(): - # pass + vl_total = new_battingratings[0].total_chances() + vr_total = new_battingratings[1].total_chances() + logging.info(f'New Ratings\nTotal Chances:\n{vl_total}\n{new_battingratings[0]}\n\n' + f'Total Chances: {vr_total}\n{new_battingratings[1]}') + + if vl_total + vr_total != Decimal(216): + raise ValueError(f'vl chances: {vl_total} / vr chances: {vr_total}') return { 'player': player, @@ -725,28 +1944,66 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions 'position_string': get_pos_string(positions), 'bat_card': batting_card, 'stealing_string': steal_string, - 'rarity_file': rarity_file + 'rarity_file': rarity_file, + 'new_ratings_vl': new_battingratings[0], + 'new_ratings_vr': new_battingratings[1] } def get_pitcher_card_data(player, pitching_card, ratings_vl, ratings_vr, positions) -> dict: rarity_file = encoded_images[player.rarity.name] + vl_dict = model_to_dict(ratings_vl) + vl_dict['pitchingcard'] = ratings_vl.pitchingcard_id + vl = FullPitchingCard( + ratings=PitchingCardRatingsModel(**vl_dict), + offense_col=pitching_card.offense_col, + alt_direction=pitching_card.player_id % 2 + ) + vr_dict = model_to_dict(ratings_vr) + vr_dict['pitchingcard'] = ratings_vr.pitchingcard_id + vr = FullPitchingCard( + ratings=PitchingCardRatingsModel(**vr_dict), + offense_col=pitching_card.offense_col, + alt_direction=pitching_card.player_id % 2 + ) + + vl_output = vl.card_output() + vr_output = vr.card_output() + return { 'player': player, 'card_type': 'pitcher', - 'vl_one_2d6': '2-', - 'vl_one_results': 'HOMERUN', - 'vl_one_d20': ' ', - 'vl_two_2d6': '2-', - 'vl_two_results': 'fly (cf) B', - 'vl_two_d20': '', - 'vl_three_2d6': '2-
 ', - 'vl_three_results': 'HR
fly (cf) B', - 'vl_three_d20': '1-16
17-20', - 'results_vr_one': 'Light Dongs', - 'results_vr_two': 'Hefty Dongs', - 'results_vr_three': 'Obese Dongs', + # 'vl_one_2d6': '2-', + # 'vl_one_results': 'HOMERUN', + # 'vl_one_d20': ' ', + # 'vl_two_2d6': '2-', + # 'vl_two_results': 'fly (cf) B', + # 'vl_two_d20': '', + # 'vl_three_2d6': '2-
 ', + # 'vl_three_results': 'HR
fly (cf) B', + # 'vl_three_d20': '1-16
17-20', + # 'results_vr_one': 'Light Dongs', + # 'results_vr_two': 'Hefty Dongs', + # 'results_vr_three': 'Obese Dongs', + 'vl_one_2d6': vl_output['one_2d6'], + 'vl_one_results': vl_output['one_results'], + 'vl_one_d20': vl_output['one_d20'], + 'vl_two_2d6': vl_output['two_2d6'], + 'vl_two_results': vl_output['two_results'], + 'vl_two_d20': vl_output['two_d20'], + 'vl_three_2d6': vl_output['three_2d6'], + 'vl_three_results': vl_output['three_results'], + 'vl_three_d20': vl_output['three_d20'], + 'vr_one_2d6': vr_output['one_2d6'], + 'vr_one_results': vr_output['one_results'], + 'vr_one_d20': vr_output['one_d20'], + 'vr_two_2d6': vr_output['two_2d6'], + 'vr_two_results': vr_output['two_results'], + 'vr_two_d20': vr_output['two_d20'], + 'vr_three_2d6': vr_output['three_2d6'], + 'vr_three_results': vr_output['three_results'], + 'vr_three_d20': vr_output['three_d20'], 'hand': pitching_card.hand, 'position_string': get_pos_string(positions), 'pit_card': pitching_card, diff --git a/app/routers_v2/battingcardratings.py b/app/routers_v2/battingcardratings.py index d7f25ff..41c3737 100644 --- a/app/routers_v2/battingcardratings.py +++ b/app/routers_v2/battingcardratings.py @@ -1,10 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Response from typing import Literal, Optional, List import logging +import pandas as pd import pydantic from pydantic import validator, root_validator -from ..db_engine import db, BattingCardRatings, model_to_dict, chunked, BattingCard, Player, query_to_csv +from ..db_engine import db, BattingCardRatings, model_to_dict, chunked, BattingCard, Player, query_to_csv, Team from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -85,16 +86,24 @@ class RatingsList(pydantic.BaseModel): @router.get('') async def get_card_ratings( - battingcard_id: list = Query(default=None), vs_hand: Literal['R', 'L', 'vR', 'vL'] = None, - short_output: bool = False, csv: bool = False, cardset_id: list = Query(default=None), - token: str = Depends(oauth2_scheme)): - if not valid_token(token): - logging.warning(f'Bad Token: {token}') + team_id: int, ts: str, battingcard_id: list = Query(default=None), cardset_id: list = Query(default=None), + vs_hand: Literal['R', 'L', 'vR', 'vL'] = None, short_output: bool = False, csv: bool = False): + this_team = Team.get_or_none(Team.id == team_id) + logging.debug(f'Team: {this_team} / has_guide: {this_team.has_guide}') + if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1: + logging.warning(f'Team_id {team_id} attempted to pull ratings') db.close() raise HTTPException( status_code=401, detail='You are not authorized to pull card ratings.' ) + # elif not valid_token(token): + # logging.warning(f'Bad Token: {token}') + # db.close() + # raise HTTPException( + # status_code=401, + # detail='You are not authorized to pull card ratings.' + # ) all_ratings = BattingCardRatings.select() @@ -108,9 +117,16 @@ async def get_card_ratings( all_ratings = all_ratings.where(BattingCardRatings.battingcard << set_cards) if csv: - return_val = query_to_csv(all_ratings) + # return_val = query_to_csv(all_ratings) + return_vals = [model_to_dict(x) for x in all_ratings] + for x in return_vals: + x.update(x['battingcard']) + x['player_id'] = x['battingcard']['player']['player_id'] + del x['battingcard'] + del x['player'] + db.close() - return Response(content=return_val, media_type='text/csv') + return Response(content=pd.DataFrame(return_vals).to_csv(index=False), media_type='text/csv') else: return_val = {'count': all_ratings.count(), 'ratings': [ @@ -120,6 +136,52 @@ async def get_card_ratings( return return_val +@router.get('/scouting') +async def get_card_scouting(team_id: int, ts: str, cardset_id: list = Query(default=None)): + this_team = Team.get_or_none(Team.id == team_id) + logging.debug(f'Team: {this_team} / has_guide: {this_team.has_guide}') + if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1: + logging.warning(f'Team_id {team_id} attempted to pull ratings') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to pull card ratings.' + ) + + all_ratings = BattingCardRatings.select() + if cardset_id is not None: + set_players = Player.select(Player.player_id).where(Player.cardset_id << cardset_id) + set_cards = BattingCard.select(BattingCard.id).where(BattingCard.player << set_players) + all_ratings = all_ratings.where(BattingCardRatings.battingcard << set_cards) + + vl_query = all_ratings.where(BattingCardRatings.vs_hand == 'L') + vr_query = all_ratings.where(BattingCardRatings.vs_hand == 'R') + + vl_vals = [model_to_dict(x) for x in vl_query] + for x in vl_vals: + x.update(x['battingcard']) + x['player_id'] = x['battingcard']['player']['player_id'] + x['player_name'] = x['battingcard']['player']['p_name'] + x['rarity'] = x['battingcard']['player']['rarity']['name'] + del x['battingcard'] + del x['player'] + + vr_vals = [model_to_dict(x) for x in vr_query] + for x in vr_vals: + x['player_id'] = x['battingcard']['player']['player_id'] + del x['battingcard'] + + vl = pd.DataFrame(vl_vals) + vr = pd.DataFrame(vr_vals) + + output = pd.merge(vl, vr, on='player_id', suffixes=('_vl', '_vr')) + first = ['player_id', 'player_name', 'rarity', 'hand', 'variant'] + exclude = first + ['id_vl', 'id_vr'] + output = output[first + [col for col in output.columns if col not in exclude]] + db.close() + return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv') + + @router.get('/{ratings_id}') async def get_one_rating(ratings_id: int, token: str = Depends(oauth2_scheme)): if not valid_token(token): @@ -141,7 +203,17 @@ async def get_one_rating(ratings_id: int, token: str = Depends(oauth2_scheme)): @router.get('/player/{player_id}') -async def get_player_ratings(player_id: int, variant: list = Query(default=None), short_output: bool = False): +async def get_player_ratings( + player_id: int, variant: list = Query(default=None), short_output: bool = False, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'Bad Token: {token}') + db.close() + raise HTTPException( + status_code=401, + detail='You are not authorized to pull card ratings.' + ) + all_cards = BattingCard.select().where(BattingCard.player_id == player_id).order_by(BattingCard.variant) if variant is not None: all_cards = all_cards.where(BattingCard.variant << variant) diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py index 06e2a70..d6ea69d 100644 --- a/app/routers_v2/players.py +++ b/app/routers_v2/players.py @@ -386,6 +386,24 @@ async def get_batter_card( db.close() return html_response + updates = 0 + if card_type == 'batting': + updates += BattingCardRatings.update(card_data['new_ratings_vl'].dict()).where( + (BattingCardRatings.id == rating_vl.id) + ).execute() + updates += BattingCardRatings.update(card_data['new_ratings_vr'].dict()).where( + (BattingCardRatings.id == rating_vr.id) + ).execute() + else: + updates += PitchingCardRatings.update(card_data['new_ratings_vl'].dict()).where( + (PitchingCardRatings.id == rating_vl.id) + ).execute() + updates += PitchingCardRatings.update(card_data['new_ratings_vr'].dict()).where( + (PitchingCardRatings.id == rating_vr.id) + ).execute() + + logging.info(f'Rating updates: {updates}') + hti = Html2Image( browser='chromium', size=(1200, 600), From 96be768ec900a3c45cb97028c47842c1295a0897 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Thu, 19 Oct 2023 23:16:47 -0500 Subject: [PATCH 25/40] Pre-Season 6 Updates --- app/card_creation.py | 991 ++++++++++++++++++++++++-- app/db_engine.py | 58 +- app/routers_v2/batstats.py | 10 +- app/routers_v2/battingcardratings.py | 21 +- app/routers_v2/pitchingcardratings.py | 51 +- app/routers_v2/pitstats.py | 10 +- app/routers_v2/players.py | 63 +- app/routers_v2/teams.py | 34 +- db_engine.py | 57 +- db_migrations.py | 7 +- 10 files changed, 1134 insertions(+), 168 deletions(-) diff --git a/app/card_creation.py b/app/card_creation.py index 4712211..f7da020 100644 --- a/app/card_creation.py +++ b/app/card_creation.py @@ -1,6 +1,7 @@ import copy import logging import math +import re import pandas as pd import pydantic @@ -70,6 +71,7 @@ PLAY_RESULTS = { 'bp-hr': PlayResult(full_name='◆BP-HR', short_name='◆BP-HR'), 'tr': PlayResult(full_name='TRIPLE', short_name='TR'), 'do-lf': PlayResult(full_name=f'DOUBLE (lf)', short_name=f'DO (lf)'), + 'do-cf': PlayResult(full_name=f'DOUBLE (cf)', short_name=f'DO (cf)'), 'do-rf': PlayResult(full_name=f'DOUBLE (rf)', short_name=f'DO (rf)'), 'do***': PlayResult(full_name=f'DOUBLE***', short_name=f'DO***'), 'do**': PlayResult(full_name=f'DOUBLE**', short_name=f'DO**'), @@ -80,6 +82,7 @@ PLAY_RESULTS = { 'walk': PlayResult(full_name='WALK', short_name='WALK'), 'fly-rf': PlayResult(full_name=f'fly (rf) B', short_name=f'fly (rf) B'), 'fly-lf': PlayResult(full_name=f'fly (lf) B', short_name=f'fly (lf) B'), + 'fly-cf': PlayResult(full_name=f'fly (cf) B', short_name=f'fly (cf) B'), 'fly-bq': PlayResult(full_name=f'fly B?', short_name=f'fly B?') } @@ -126,11 +129,8 @@ class BattingCardRatingsModel(pydantic.BaseModel): class PitchingCardRatingsModel(pydantic.BaseModel): - pitchingcard_id: int + pitchingcard: int vs_hand: Literal['R', 'L'] - pull_rate: Decimal = Decimal(0.0) - center_rate: Decimal = Decimal(0.0) - slap_rate: Decimal = Decimal(0.0) homerun: Decimal = Decimal(0.0) bp_homerun: Decimal = Decimal(0.0) triple: Decimal = Decimal(0.0) @@ -149,19 +149,28 @@ class PitchingCardRatingsModel(pydantic.BaseModel): flyout_rf_b: Decimal = Decimal(0.0) groundout_a: Decimal = Decimal(0.0) groundout_b: Decimal = Decimal(0.0) - xcheck_p: Decimal = Decimal(1.0) - xcheck_c: Decimal = Decimal(3.0) - xcheck_1b: Decimal = Decimal(2.0) - xcheck_2b: Decimal = Decimal(6.0) - xcheck_3b: Decimal = Decimal(3.0) - xcheck_ss: Decimal = Decimal(7.0) - xcheck_lf: Decimal = Decimal(2.0) - xcheck_cf: Decimal = Decimal(3.0) - xcheck_rf: Decimal = Decimal(2.0) + xcheck_p: Decimal = Decimal(0.0) + xcheck_c: Decimal = Decimal(0.0) + xcheck_1b: Decimal = Decimal(0.0) + xcheck_2b: Decimal = Decimal(0.0) + xcheck_3b: Decimal = Decimal(0.0) + xcheck_ss: Decimal = Decimal(0.0) + xcheck_lf: Decimal = Decimal(0.0) + xcheck_cf: Decimal = Decimal(0.0) + xcheck_rf: Decimal = Decimal(0.0) avg: Decimal = Decimal(0.0) obp: Decimal = Decimal(0.0) slg: Decimal = Decimal(0.0) + def total_chances(self): + return Decimal(sum([ + self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_cf, + self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk, self.strikeout, + self.flyout_lf_b, self.flyout_cf_b, self.flyout_rf_b, self.groundout_a, self.groundout_b, self.xcheck_p, + self.xcheck_c, self.xcheck_1b, self.xcheck_2b, self.xcheck_3b, self.xcheck_ss, self.xcheck_lf, + self.xcheck_cf, self.xcheck_rf + ])) + class CardResult(pydantic.BaseModel): result_one: str = None @@ -187,6 +196,9 @@ class CardResult(pydantic.BaseModel): def assign_play(self, play: PlayResult, secondary_play: Optional[PlayResult] = None, d20: Optional[int] = None): if secondary_play is None: self.result_one = play.full_name + if '++' in play.full_name: + logging.warning(f'Too many plus symbols: {play.full_name}') + self.result_one = re.sub(r'\++', '+', play.full_name) if play.is_offense: self.bold_one = True @@ -298,9 +310,13 @@ class CardColumn(pydantic.BaseModel): f'Secondary Play: {secondary_play}') raise ValueError(f'Cannot assign more than 6 chances per call') elif math.floor(chances) != chances and secondary_play is None: - logging.error(f'Must have secondary play for fractional chances\n' - f'Play: {play}\nChances: {chances}\nSecondary Play: {secondary_play}') - raise ValueError(f'Cannot assign fractional chances without secondary play') + if chances > Decimal(1.0): + chances = Decimal(math.floor(chances)) + else: + return False + logging.error(f'Must have secondary play for fractional chances; could not round down to an integer\n' + f'Play: {play}\nChances: {chances}\nSecondary Play: {secondary_play}') + raise ValueError(f'Cannot assign fractional chances without secondary play') # Chances is whole number if math.floor(chances) == chances: @@ -522,7 +538,7 @@ class CardColumn(pydantic.BaseModel): return False logging.info(f'Not a whole number | Chances: {chances}') - if chances in EXACT_CHANCES and self.num_splits < 4: + if chances in EXACT_CHANCES and self.num_splits < 4 and secondary_play is not None: logging.info(f'In Exact Chances!') if chances >= 3: self.num_splits += 1 @@ -938,6 +954,7 @@ class FullCard(pydantic.BaseModel): alt_direction: int = 1 num_plusgb: int = 0 num_lomax: int = 0 + is_batter: bool = False class Config: arbitrary_types_allowed = True @@ -1003,7 +1020,7 @@ class FullCard(pydantic.BaseModel): first = self.col_two second = self.col_one - if 'gb' in play.full_name and chances + self.num_plusgb <= 6: + if 'gb' in play.full_name and chances + self.num_plusgb <= 6 and self.is_batter: play.full_name += '+' for x in [first, second, third]: @@ -1048,29 +1065,85 @@ class FullCard(pydantic.BaseModel): class FullBattingCard(FullCard): ratings: BattingCardRatingsModel + is_batter: bool = True class FullPitchingCard(FullCard): - ratings: BattingCardRatingsModel + ratings: PitchingCardRatingsModel + is_batter: bool = False -def get_pos_string(all_pos) -> str: +def get_pos_data(all_pos, is_pitcher: bool = False) -> dict: final = '' arm_added = False + first = True for x in all_pos: - if x.position != 'P': - final += x.position.lower() - if x.position != 'DH': - final += f'-{x.range}' - if x.position in ['LF', 'CF', 'RF', 'C'] and not arm_added: - final += f'({"+" if x.arm >= 0 else ""}{x.arm})' - arm_added = True + if not first: + final += ', ' - final += f'e{x.error}' - if x.position == 'C': - final += f' T-{x.overthrow}(pb {x.pb})' - final += ' ' - return final + first = False + if is_pitcher: + if x.position == 'P': + final += f'p-{x.range}e{x.error}' + else: + if x.position != 'P': + final += x.position.lower() + if x.position != 'DH': + final += f'-{x.range}' + if x.position in ['LF', 'CF', 'RF', 'C'] and not arm_added: + final += f'({"+" if x.arm >= 0 else ""}{x.arm})' + arm_added = True + + final += f'e{x.error}' + if x.position == 'C': + final += f' T-{x.overthrow}(pb {x.pb})' + + if len(final) >= 50: + font = 16 + margin = 5 + elif len(final) >= 46: + font = 18 + margin = 3 + elif len(final) >= 40: + font = 20 + margin = 1 + elif len(final) >= 35: + font = 22 + margin = -1 + elif len(final) >= 30: + font = 24 + margin = -2 + else: + font = 26 + margin = -3 + + return {'string': final, 'font': font, 'margin': margin} + + +def full_log(this_ratings, this_card, info=False): + if info: + logging.info( + f'Rating Chances: {this_ratings.total_chances()} / Card Chances: {this_card.total_chances()}\n' + f'{this_card.sample_output()}\n' + ) + else: + logging.debug( + f'Rating Chances: {this_ratings.total_chances()} / Card Chances: {this_card.total_chances()}\n' + f'{this_card.sample_output()}\n' + ) + + +def get_chances(total_chances, apply_limits=True): + if total_chances > 12.5 and apply_limits: + return 6 + elif total_chances > 10.5 and apply_limits: + return 5 + elif total_chances > 8.5 and apply_limits: + return 4 + elif total_chances > 5.5 and apply_limits: + return 3 + else: + return min(total_chances, 6) def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions) -> dict: @@ -1206,18 +1279,6 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions logging.debug(f'vr: {vr.sample_output()}') return 0, 0 - def get_chances(total_chances, apply_limits=True): - if total_chances > 12.5 and apply_limits: - return 6 - elif total_chances > 10.5 and apply_limits: - return 5 - elif total_chances > 8.5 and apply_limits: - return 4 - elif total_chances > 5.5 and apply_limits: - return 3 - else: - return min(total_chances, 6) - def get_pullside_of(): if batting_card.hand == 'L': return 'rf' @@ -1229,23 +1290,21 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions return 'rf' def get_preferred_mif(ratings): - if batting_card.hand == 'L' and ratings.slap_rate > ratings.pull_rate: + if batting_card.hand == 'L' and ratings.slap_rate > .24: return 'ss' - elif batting_card.hand == 'L' or (batting_card.hand == 'R' and ratings.slap_rate > ratings.pull_rate): + elif batting_card.hand == 'L' or (batting_card.hand == 'R' and ratings.slap_rate > .24): return '2b' else: return 'ss' - def full_log(this_ratings, this_card): - logging.info( - f'Rating Chances: {this_ratings.total_chances()} / Card Chances: {this_card.total_chances()}\n' - f'{this_card.sample_output()}\n' - ) - new_battingratings = [] for card, data, new_ratings in [ - (vl, copy.deepcopy(vl_ratings), BattingCardRatingsModel(battingcard=vl_ratings.battingcard, vs_hand='L')), - (vr, copy.deepcopy(vr_ratings), BattingCardRatingsModel(battingcard=vr_ratings.battingcard, vs_hand='R'))]: + (vl, copy.deepcopy(vl_ratings), BattingCardRatingsModel( + battingcard=vl_ratings.battingcard, pull_rate=vl_ratings.pull_rate, center_rate=vl_ratings.center_rate, + slap_rate=vl_ratings.slap_rate, vs_hand='L')), + (vr, copy.deepcopy(vr_ratings), BattingCardRatingsModel( + battingcard=vr_ratings.battingcard, pull_rate=vr_ratings.pull_rate, center_rate=vr_ratings.center_rate, + slap_rate=vr_ratings.slap_rate, vs_hand='R'))]: logging.info(f'\n\nBeginning v{data.vs_hand}') pull_of = get_pullside_of() pref_mif = get_preferred_mif(data) @@ -1301,7 +1360,7 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions elif data.double_two > max(1 - ch, 0): secondary = PLAY_RESULTS['do**'] elif data.triple > max(1 - ch, 0): - secondary = PLAY_RESULTS['tr**'] + secondary = PLAY_RESULTS['tr'] else: secondary = None @@ -1542,11 +1601,11 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions break ch = get_chances(res_chances) - if data.groundout_c > max(1 - ch, 0): + if data.groundout_a > max(1 - ch, 0): secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A') elif data.groundout_b > max(1 - ch, 0): secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B') - elif data.groundout_a > max(1 - ch, 0): + elif data.groundout_c > max(1 - ch, 0): secondary = PlayResult(full_name=f'gb ({pref_mif}) C', short_name=f'gb ({pref_mif}) C') elif data.lineout > max(1 - ch, 0): secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})') @@ -1907,6 +1966,7 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions if vl_total + vr_total != Decimal(216): raise ValueError(f'vl chances: {vl_total} / vr chances: {vr_total}') + pos_data = get_pos_data(positions) return { 'player': player, 'card_type': 'batter', @@ -1941,36 +2001,833 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions 'vr_three_results': vr_output['three_results'], 'vr_three_d20': vr_output['three_d20'], 'hand': batting_card.hand, - 'position_string': get_pos_string(positions), 'bat_card': batting_card, 'stealing_string': steal_string, 'rarity_file': rarity_file, 'new_ratings_vl': new_battingratings[0], - 'new_ratings_vr': new_battingratings[1] + 'new_ratings_vr': new_battingratings[1], + 'position_string': pos_data['string'], + 'position_font': pos_data['font'], + 'position_margin': pos_data['margin'] } def get_pitcher_card_data(player, pitching_card, ratings_vl, ratings_vr, positions) -> dict: + player_binary = pitching_card.player_id % 2 + logging.info(f'\n\nRunning Card for {player.p_name}') rarity_file = encoded_images[player.rarity.name] vl_dict = model_to_dict(ratings_vl) vl_dict['pitchingcard'] = ratings_vl.pitchingcard_id + vl_ratings = PitchingCardRatingsModel(**vl_dict) vl = FullPitchingCard( - ratings=PitchingCardRatingsModel(**vl_dict), + ratings=vl_ratings, offense_col=pitching_card.offense_col, - alt_direction=pitching_card.player_id % 2 + alt_direction=player_binary ) vr_dict = model_to_dict(ratings_vr) vr_dict['pitchingcard'] = ratings_vr.pitchingcard_id + vr_ratings = PitchingCardRatingsModel(**vr_dict) vr = FullPitchingCard( - ratings=PitchingCardRatingsModel(**vr_dict), + ratings=vr_ratings, offense_col=pitching_card.offense_col, - alt_direction=pitching_card.player_id % 2 + alt_direction=player_binary ) + def assign_pchances( + this_card: FullPitchingCard, play: PlayResult, chances: Decimal, + secondary_play: Optional[PlayResult] = None): + logging.info(f'Assign pitching chances\n{play}\nChances: {chances}\nBackup: {secondary_play}') + r_data = this_card.add_result(play, chances, secondary_play) + if r_data: + return r_data + else: + logging.warning(f'Could not find valid column trying new values') + for x in EXACT_CHANCES + [Decimal('0.95')]: + if x < math.floor(chances - Decimal('0.05')): + logging.warning(f'Using the whole number {math.floor(chances)}') + r_data = this_card.add_result(play, Decimal(math.floor(chances)), secondary_play) + if r_data: + return r_data + else: + logging.error(f'Whole number was no good') + break + + if x < chances and secondary_play is not None: + logging.warning(f'Trying {x} chances now') + r_data = this_card.add_result(play, x, secondary_play) + if r_data: + return r_data + else: + logging.warning(f'No good; checking the next value') + + logging.error('Could not assign chances\n') + logging.debug(f'vl: {vl.sample_output()}') + logging.debug(f'vr: {vr.sample_output()}') + return 0, 0 + + def get_preferred_mif(ratings): + if pitching_card.hand == 'L' and ratings.vs_hand == 'L': + return 'ss' + elif pitching_card.hand == 'L' or (pitching_card.hand == 'R' and ratings.vs_hand == 'R'): + return '2b' + else: + return 'ss' + + new_pitchingratings = [] + for card, data, new_ratings in [ + (vl, copy.deepcopy(vl_ratings), PitchingCardRatingsModel( + pitchingcard=vl_ratings.pitchingcard, vs_hand='L')), + (vr, copy.deepcopy(vr_ratings), PitchingCardRatingsModel( + pitchingcard=vr_ratings.pitchingcard, vs_hand='R'))]: + logging.info(f'\n\nBeginning v{data.vs_hand}') + + res_chances = data.bp_homerun + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['bp-hr'], ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.bp_homerun += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.hbp + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='HBP', short_name='HBP'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.hbp += r_val[0] + + if r_val[0] == 0: + break + + full_log(new_ratings, card, info=True) + res_chances = data.xcheck_p + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'gb (p) X', short_name=f'gb (p) X'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.xcheck_p += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.xcheck_c + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'catch X', short_name=f'catch X'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.xcheck_c += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.xcheck_1b + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'gb (1b) X', short_name=f'gb (1b) X'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.xcheck_1b += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.xcheck_3b + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'gb (3b) X', short_name=f'gb (3b) X'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.xcheck_3b += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.xcheck_rf + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'fly (rf) X', short_name=f'fly (rf) X'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.xcheck_rf += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.xcheck_lf + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'fly (lf) X', short_name=f'fly (lf) X'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.xcheck_lf += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.xcheck_2b + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'gb (2b) X', short_name=f'gb (2b) X'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.xcheck_2b += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.xcheck_cf + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'fly (cf) X', short_name=f'fly (cf) X'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.xcheck_cf += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.xcheck_ss + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'gb (ss) X', short_name=f'gb (ss) X'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.xcheck_ss += r_val[0] + + full_log(new_ratings, card, info=True) + res_chances = data.walk + while res_chances >= 1: + ch = get_chances(res_chances) + if data.strikeout > max(1 - ch, 0): + secondary = PlayResult(full_name=f'strikeout', short_name=f'so') + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['walk'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.walk += r_val[0] + if r_val[1] > 0: + data.strikeout -= r_val[1] + new_ratings.strikeout += r_val[1] + + if r_val[0] == 0: + break + + full_log(new_ratings, card, info=True) + res_chances = data.homerun + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_cf > 0: + data.double_cf += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.triple > 0: + data.triple += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_center > 0: + data.single_center += res_chances + break + + ch = get_chances(res_chances) + if data.double_cf > (data.flyout_rf_b + data.flyout_lf_b) and data.double_cf > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-cf'] + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > data.flyout_rf_b and data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_cf > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-cf'] + elif data.double_three > max(1 - ch, 0): + secondary = PLAY_RESULTS['do***'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + elif data.triple > max(1 - ch, 0): + secondary = PLAY_RESULTS['tr'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['hr'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.homerun += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_cf -= r_val[1] + new_ratings.double_cf += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'cf' in secondary.short_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + elif 'TR' in secondary.short_name: + data.triple -= r_val[1] + new_ratings.triple += r_val[1] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.triple + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_cf > 0: + data.double_cf += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_cf > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-cf'] + elif data.double_three > max(1 - ch, 0): + secondary = PLAY_RESULTS['do***'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['tr'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.triple += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_cf -= r_val[1] + new_ratings.double_cf += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'cf' in secondary.short_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.double_three + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_cf > 0: + data.double_cf += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_cf > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-cf'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['do***'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.double_three += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_cf -= r_val[1] + new_ratings.double_cf += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'cf' in secondary.short_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.double_cf + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.flyout_cf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (cf) B', short_name=f'fly B') + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B') + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (rf) B', short_name=f'fly b') + elif data.single_one > max(1 - ch, 0): + secondary = PLAY_RESULTS['si*'] + elif data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS[f'do-cf'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.double_cf += r_val[0] + if r_val[1] > 0: + if 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'cf' in secondary.full_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.double_two + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.single_center > max(1 - ch, 0): + secondary = PLAY_RESULTS['si-cf'] + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['do**'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.double_two += r_val[0] + if r_val[1] > 0: + if 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'cf' in secondary.full_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.single_two + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + pref_mif = get_preferred_mif(new_ratings) + ch = get_chances(res_chances) + if data.groundout_a > max(1 - ch, 0): + temp_mif = get_preferred_mif(new_ratings) + pref_mif = 'ss' if temp_mif == '2b' else '2b' + secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A') + elif data.groundout_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B') + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['si**'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.single_two += r_val[0] + if r_val[1] > 0: + if 'B' in secondary.short_name: + data.groundout_b -= r_val[1] + new_ratings.groundout_b += r_val[1] + elif 'A' in secondary.short_name: + data.groundout_a -= r_val[1] + new_ratings.groundout_a += r_val[1] + elif 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'cf' in secondary.full_name: + data.flyout_cf_b -= r_val[1] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.single_center + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.flyout_cf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (cf) B', short_name=f'fly B') + elif data.flyout_lf_b > max(1 - ch, 0) and data.flyout_lf_b > data.flyout_rf_b: + secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B') + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (rf) B', short_name=f'fly B') + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B') + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['si-cf'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.single_center += r_val[0] + if r_val[1] > 0: + if 'CF' in secondary.short_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'LF' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'RF' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.single_one + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.walk > 0: + data.walk += res_chances + break + + pref_mif = get_preferred_mif(new_ratings) + ch = get_chances(res_chances) + if data.groundout_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B') + elif data.groundout_a > max(1 - ch, 0): + temp_mif = get_preferred_mif(new_ratings) + pref_mif = 'ss' if temp_mif == '2b' else '2b' + secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A') + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['si*'], ch, secondary) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.single_one += r_val[0] + if r_val[1] > 0: + if 'B' in secondary.short_name: + data.groundout_b -= r_val[1] + new_ratings.groundout_b += r_val[1] + elif 'A' in secondary.short_name: + data.groundout_a -= r_val[1] + new_ratings.groundout_a += r_val[1] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.bp_single + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['bp-si'], ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.bp_single += r_val[0] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.strikeout + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name=f'strikeout', short_name=f'so'), ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.strikeout += r_val[0] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.flyout_cf_b + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['fly-cf'], ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.flyout_cf_b += r_val[0] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.flyout_lf_b + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['fly-lf'], ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.flyout_lf_b += r_val[0] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.flyout_rf_b + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['fly-rf'], ch) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.flyout_rf_b += r_val[0] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.groundout_a + retries = 0 + while res_chances > 0: + if retries > 0: + break + + temp_mif = get_preferred_mif(new_ratings) + pref_mif = 'ss' if temp_mif == '2b' else '2b' + ch = get_chances(res_chances) + r_val = assign_pchances( + card, + PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A'), + ch + ) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.groundout_a += r_val[0] + + if r_val[0] == 0: + retries += 1 + + full_log(new_ratings, card, info=True) + res_chances = data.groundout_b + retries = 0 + while res_chances > 0: + if retries > 0: + break + + pref_mif = get_preferred_mif(new_ratings) + ch = get_chances(res_chances) + r_val = assign_pchances( + card, + PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B'), + ch + ) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + res_chances -= r_val[0] + new_ratings.groundout_b += r_val[0] + + if r_val[0] == 0: + retries += 1 + + log_data = vl.sample_output() if data.vs_hand == 'L' else vr.sample_output() + logging.info(f'Pre-filler total chances: {new_ratings.total_chances()}\n{log_data}') + plays = sorted( + [(data.strikeout, 'so'), (data.groundout_a, 'gb'), (data.flyout_lf_b, 'lf'), (data.flyout_rf_b, 'rf')], + key=lambda z: z[0], + reverse=True + ) + count_filler = -1 + pref_mif = get_preferred_mif(new_ratings) + while not card.is_complete(): + count_filler += 1 + this_play = plays[count_filler % 4] + if this_play[1] == 'so': + play_res = PlayResult(full_name=f'strikeout', short_name=f'strikeout') + elif this_play[1] == 'gb': + this_if = '3b' if pref_mif == 'ss' else '1b' + play_res = PlayResult(full_name=f'gb ({this_if}) A', short_name=f'gb ({this_if}) A') + elif this_play[1] == 'lf': + play_res = PLAY_RESULTS['fly-lf'] + else: + play_res = PLAY_RESULTS['fly-rf'] + + logging.info(f'Send Card Fill\n{play_res}') + r_val = card.card_fill(play_res) + logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if this_play[1] == 'so': + new_ratings.strikeout += r_val[0] + elif this_play[1] == 'gb': + new_ratings.groundout_a += r_val[0] + elif this_play[1] == 'lf': + new_ratings.flyout_lf_b += r_val[0] + else: + new_ratings.flyout_rf_b += r_val[0] + + full_log(new_ratings, card) + + new_pitchingratings.append(new_ratings) + vl_output = vl.card_output() vr_output = vr.card_output() + logging.info(f'vl: {vl.sample_output()}') + logging.info(f'vr: {vr.sample_output()}') + + vl_total = new_pitchingratings[0].total_chances() + vr_total = new_pitchingratings[1].total_chances() + logging.info(f'New Ratings\nTotal Chances:\n{vl_total}\n{new_pitchingratings[0]}\n\n' + f'Total Chances: {vr_total}\n{new_pitchingratings[1]}') + + pos_data = get_pos_data(positions, is_pitcher=True) return { 'player': player, 'card_type': 'pitcher', @@ -2005,7 +2862,11 @@ def get_pitcher_card_data(player, pitching_card, ratings_vl, ratings_vr, positio 'vr_three_results': vr_output['three_results'], 'vr_three_d20': vr_output['three_d20'], 'hand': pitching_card.hand, - 'position_string': get_pos_string(positions), 'pit_card': pitching_card, - 'rarity_file': rarity_file + 'rarity_file': rarity_file, + 'position_string': pos_data['string'], + 'position_font': pos_data['font'], + 'position_margin': pos_data['margin'], + 'new_ratings_vl': new_pitchingratings[0], + 'new_ratings_vr': new_pitchingratings[1], } diff --git a/app/db_engine.py b/app/db_engine.py index 9eb8921..a22de92 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -54,10 +54,10 @@ def complex_data_to_csv(complex_data: List): else: data_list = [[x for x in complex_data[0].keys()]] for line in complex_data: - logging.info(f'line: {line}') + logging.debug(f'line: {line}') this_row = [] for key in line: - logging.info(f'key: {key}') + logging.debug(f'key: {key}') if line[key] is None: this_row.append('') @@ -395,11 +395,35 @@ class Roster(BaseModel): # this_roster.card26] +class Result(BaseModel): + away_team = ForeignKeyField(Team) + home_team = ForeignKeyField(Team) + away_score = IntegerField() + home_score = IntegerField() + away_team_value = IntegerField(null=True) + home_team_value = IntegerField(null=True) + away_team_ranking = IntegerField(null=True) + home_team_ranking = IntegerField(null=True) + scorecard = CharField() + week = IntegerField() + season = IntegerField() + ranked = BooleanField() + short_game = BooleanField() + game_type = CharField(null=True) + + @staticmethod + def select_season(season=None): + if not season: + season = Current.get().season + return Result.select().where(Result.season == season) + + class BattingStat(BaseModel): card = ForeignKeyField(Card) team = ForeignKeyField(Team) roster_num = IntegerField() vs_team = ForeignKeyField(Team) + result = ForeignKeyField(Result, null=True) pos = CharField() pa = IntegerField() ab = IntegerField() @@ -438,6 +462,7 @@ class PitchingStat(BaseModel): team = ForeignKeyField(Team) roster_num = IntegerField() vs_team = ForeignKeyField(Team) + result = ForeignKeyField(Result, null=True) ip = FloatField() hit = IntegerField() run = IntegerField() @@ -462,29 +487,6 @@ class PitchingStat(BaseModel): game_id = IntegerField() -class Result(BaseModel): - away_team = ForeignKeyField(Team) - home_team = ForeignKeyField(Team) - away_score = IntegerField() - home_score = IntegerField() - away_team_value = IntegerField(null=True) - home_team_value = IntegerField(null=True) - away_team_ranking = IntegerField(null=True) - home_team_ranking = IntegerField(null=True) - scorecard = CharField() - week = IntegerField() - season = IntegerField() - ranked = BooleanField() - short_game = BooleanField() - game_type = CharField(null=True) - - @staticmethod - def select_season(season=None): - if not season: - season = Current.get().season - return Result.select().where(Result.season == season) - - class Award(BaseModel): name = CharField() season = IntegerField() @@ -577,9 +579,9 @@ BattingCard.add_index(bc_index) class BattingCardRatings(BaseModel): battingcard = ForeignKeyField(BattingCard) vs_hand = CharField(default='R') - pull_rate: FloatField() - center_rate: FloatField() - slap_rate: FloatField() + pull_rate = FloatField() + center_rate = FloatField() + slap_rate = FloatField() homerun = FloatField() bp_homerun = FloatField() triple = FloatField() diff --git a/app/routers_v2/batstats.py b/app/routers_v2/batstats.py index 580ceff..8618520 100644 --- a/app/routers_v2/batstats.py +++ b/app/routers_v2/batstats.py @@ -6,7 +6,7 @@ import logging import pydantic from pandas import DataFrame -from ..db_engine import db, BattingStat, model_to_dict, fn, Card +from ..db_engine import db, BattingStat, model_to_dict, fn, Card, Player, Current from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -69,6 +69,12 @@ async def get_batstats( season: int = None, week_start: int = None, week_end: int = None, created: int = None, csv: bool = None): all_stats = BattingStat.select().join(Card).join(Player) + if season is not None: + all_stats = all_stats.where(BattingStat.season == season) + else: + curr = Current.latest() + all_stats = all_stats.where(BattingStat.season == curr.season) + if card_id is not None: all_stats = all_stats.where(BattingStat.card_id == card_id) if player_id is not None: @@ -79,8 +85,6 @@ async def get_batstats( all_stats = all_stats.where(BattingStat.vs_team_id == vs_team_id) if week is not None: all_stats = all_stats.where(BattingStat.week == week) - if season is not None: - all_stats = all_stats.where(BattingStat.season == season) if week_start is not None: all_stats = all_stats.where(BattingStat.week >= week_start) if week_end is not None: diff --git a/app/routers_v2/battingcardratings.py b/app/routers_v2/battingcardratings.py index 41c3737..734a665 100644 --- a/app/routers_v2/battingcardratings.py +++ b/app/routers_v2/battingcardratings.py @@ -48,6 +48,9 @@ class BattingCardRatingsModel(pydantic.BaseModel): avg: float = 0.0 obp: float = 0.0 slg: float = 0.0 + pull_rate: float = 0.0 + center_rate: float = 0.0 + slap_rate: float = 0.0 @validator("avg", always=True) def avg_validator(cls, v, values, **kwargs): @@ -143,10 +146,9 @@ async def get_card_scouting(team_id: int, ts: str, cardset_id: list = Query(defa if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1: logging.warning(f'Team_id {team_id} attempted to pull ratings') db.close() - raise HTTPException( - status_code=401, - detail='You are not authorized to pull card ratings.' - ) + return 'Your team does not have the ratings guide enabled. If you have purchased a copy, ping Cal to ' \ + 'make sure it is enabled on your team. If you are interested, you can pick it up here: ' \ + 'https://ko-fi.com/manticorum/shop' all_ratings = BattingCardRatings.select() if cardset_id is not None: @@ -163,6 +165,8 @@ async def get_card_scouting(team_id: int, ts: str, cardset_id: list = Query(defa x['player_id'] = x['battingcard']['player']['player_id'] x['player_name'] = x['battingcard']['player']['p_name'] x['rarity'] = x['battingcard']['player']['rarity']['name'] + x['cardset_id'] = x['battingcard']['player']['cardset']['id'] + x['cardset_name'] = x['battingcard']['player']['cardset']['name'] del x['battingcard'] del x['player'] @@ -173,12 +177,13 @@ async def get_card_scouting(team_id: int, ts: str, cardset_id: list = Query(defa vl = pd.DataFrame(vl_vals) vr = pd.DataFrame(vr_vals) + db.close() output = pd.merge(vl, vr, on='player_id', suffixes=('_vl', '_vr')) - first = ['player_id', 'player_name', 'rarity', 'hand', 'variant'] - exclude = first + ['id_vl', 'id_vr'] - output = output[first + [col for col in output.columns if col not in exclude]] - db.close() + first = ['player_id', 'player_name', 'cardset_name', 'rarity', 'hand', 'variant'] + exclude = first + ['id_vl', 'id_vr', 'vs_hand_vl', 'vs_hand_vr'] + output = output[first + [col for col in output.columns if col not in exclude]].sort_values(by=['player_id']) + # output = output.sort_values(by=['player_id']) return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv') diff --git a/app/routers_v2/pitchingcardratings.py b/app/routers_v2/pitchingcardratings.py index 17fed1a..d7ae4df 100644 --- a/app/routers_v2/pitchingcardratings.py +++ b/app/routers_v2/pitchingcardratings.py @@ -1,10 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Response from typing import Literal, Optional, List import logging +import pandas as pd import pydantic from pydantic import validator, root_validator -from ..db_engine import db, PitchingCardRatings, model_to_dict, chunked, PitchingCard, Player, query_to_csv +from ..db_engine import db, PitchingCardRatings, model_to_dict, chunked, PitchingCard, Player, query_to_csv, Team from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -126,6 +127,54 @@ async def get_card_ratings( return return_val +@router.get('/scouting') +async def get_card_scouting(team_id: int, ts: str, cardset_id: list = Query(default=None)): + this_team = Team.get_or_none(Team.id == team_id) + logging.debug(f'Team: {this_team} / has_guide: {this_team.has_guide}') + if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1: + logging.warning(f'Team_id {team_id} attempted to pull ratings') + db.close() + return 'Your team does not have the ratings guide enabled. If you have purchased a copy, ping Cal to ' \ + 'make sure it is enabled on your team. If you are interested, you can pick it up here: ' \ + 'https://ko-fi.com/manticorum/shop' + + all_ratings = PitchingCardRatings.select() + if cardset_id is not None: + set_players = Player.select(Player.player_id).where(Player.cardset_id << cardset_id) + set_cards = PitchingCard.select(PitchingCard.id).where(PitchingCard.player << set_players) + all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard << set_cards) + + vl_query = all_ratings.where(PitchingCardRatings.vs_hand == 'L') + vr_query = all_ratings.where(PitchingCardRatings.vs_hand == 'R') + + vl_vals = [model_to_dict(x) for x in vl_query] + for x in vl_vals: + x.update(x['pitchingcard']) + x['player_id'] = x['pitchingcard']['player']['player_id'] + x['player_name'] = x['pitchingcard']['player']['p_name'] + x['rarity'] = x['pitchingcard']['player']['rarity']['name'] + x['cardset_id'] = x['pitchingcard']['player']['cardset']['id'] + x['cardset_name'] = x['pitchingcard']['player']['cardset']['name'] + del x['pitchingcard'] + del x['player'] + + vr_vals = [model_to_dict(x) for x in vr_query] + for x in vr_vals: + x['player_id'] = x['pitchingcard']['player']['player_id'] + del x['pitchingcard'] + + vl = pd.DataFrame(vl_vals) + vr = pd.DataFrame(vr_vals) + db.close() + + output = pd.merge(vl, vr, on='player_id', suffixes=('_vl', '_vr')) + first = ['player_id', 'player_name', 'cardset_name', 'rarity', 'hand', 'variant'] + exclude = first + ['id_vl', 'id_vr', 'vs_hand_vl', 'vs_hand_vr'] + output = output[first + [col for col in output.columns if col not in exclude]].sort_values(by=['player_id']) + # output = output.sort_values(by=['player_id']) + return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv') + + @router.get('/{ratings_id}') async def get_one_rating(ratings_id: int, token: str = Depends(oauth2_scheme)): if not valid_token(token): diff --git a/app/routers_v2/pitstats.py b/app/routers_v2/pitstats.py index 79e5e88..29c4d3e 100644 --- a/app/routers_v2/pitstats.py +++ b/app/routers_v2/pitstats.py @@ -5,7 +5,7 @@ import logging import pydantic from pandas import DataFrame -from ..db_engine import db, PitchingStat, model_to_dict, Card, Player +from ..db_engine import db, PitchingStat, model_to_dict, Card, Player, Current from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -61,6 +61,12 @@ async def get_pit_stats( all_stats = PitchingStat.select().join(Card).join(Player) logging.debug(f'pit query:\n\n{all_stats}') + if season is not None: + all_stats = all_stats.where(PitchingStat.season == season) + else: + curr = Current.latest() + all_stats = all_stats.where(PitchingStat.season == curr.season) + if card_id is not None: all_stats = all_stats.where(PitchingStat.card_id == card_id) if player_id is not None: @@ -71,8 +77,6 @@ async def get_pit_stats( all_stats = all_stats.where(PitchingStat.vs_team_id == vs_team_id) if week is not None: all_stats = all_stats.where(PitchingStat.week == week) - if season is not None: - all_stats = all_stats.where(PitchingStat.season == season) if week_start is not None: all_stats = all_stats.where(PitchingStat.week >= week_start) if week_end is not None: diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py index d6ea69d..9bddd15 100644 --- a/app/routers_v2/players.py +++ b/app/routers_v2/players.py @@ -1,6 +1,7 @@ import os.path import base64 +import pandas as pd from fastapi import APIRouter, Depends, HTTPException, Request, Response, Query from fastapi.responses import FileResponse from fastapi.templating import Jinja2Templates @@ -146,24 +147,44 @@ async def get_players( # raise HTTPException(status_code=404, detail=f'No players found') if csv: - all_players.order_by(-Player.rarity.value, Player.p_name) - data_list = [['id', 'name', 'value', 'image', 'image2', 'mlbclub', 'franchise', 'cardset', 'rarity', 'pos_1', - 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card', - 'strat_code', 'bbref_id', 'description', 'for_purchase', 'ranked_legal']] - for line in final_players: - data_list.append( - [ - line.player_id, line.p_name, line.cost, line.image, line.image2, line.mlbclub, line.franchise, - line.cardset, line.rarity, 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.headshot, line.vanity_card, line.strat_code, line.bbref_id, - line.description, line.cardset.for_purchase, line.cardset.ranked_legal - # line.description, line.cardset.in_packs, line.quantity - ] - ) - return_val = DataFrame(data_list).to_csv(header=False, index=False) - + card_vals = [model_to_dict(x) for x in all_players] db.close() - return Response(content=return_val, media_type='text/csv') + + for x in card_vals: + x['player_name'] = x['p_name'] + x['cardset_name'] = x['cardset']['name'] + x['rarity'] = x['rarity']['name'] + x['for_purchase'] = x['cardset']['for_purchase'] + x['ranked_legal'] = x['cardset']['ranked_legal'] + if x['player_name'] not in x['description']: + x['description'] = f'{x["description"]} {x["player_name"]}' + + card_df = pd.DataFrame(card_vals) + output = card_df[[ + 'player_id', 'player_name', 'cost', 'image', 'image2', 'mlbclub', 'franchise', 'cardset_name', 'rarity', + 'pos_1', 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card', + 'fangr_id', 'bbref_id', 'description', 'for_purchase', 'ranked_legal' + ]] + return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv') + + # all_players.order_by(-Player.rarity.value, Player.p_name) + # data_list = [['id', 'name', 'value', 'image', 'image2', 'mlbclub', 'franchise', 'cardset', 'rarity', 'pos_1', + # 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card', + # 'strat_code', 'bbref_id', 'description', 'for_purchase', 'ranked_legal']] + # for line in final_players: + # data_list.append( + # [ + # line.player_id, line.p_name, line.cost, line.image, line.image2, line.mlbclub, line.franchise, + # line.cardset, line.rarity, 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.headshot, line.vanity_card, line.strat_code, line.bbref_id, + # line.description, line.cardset.for_purchase, line.cardset.ranked_legal + # # line.description, line.cardset.in_packs, line.quantity + # ] + # ) + # return_val = DataFrame(data_list).to_csv(header=False, index=False) + # + # db.close() + # return Response(content=return_val, media_type='text/csv') else: return_val = {'count': len(final_players), 'players': []} @@ -362,6 +383,10 @@ async def get_batter_card( raise HTTPException(status_code=404, detail=f'Ratings not found for batting card {this_bc.id}') card_data = get_batter_card_data(this_player, this_bc, rating_vl, rating_vr, all_pos) + if this_player.description in this_player.cardset.name: + card_data['cardset_name'] = this_player.cardset.name + else: + card_data['cardset_name'] = this_player.description card_data['request'] = request html_response = templates.TemplateResponse("player_card.html", card_data) @@ -379,6 +404,10 @@ async def get_batter_card( raise HTTPException(status_code=404, detail=f'Ratings not found for pitching card {this_pc.id}') card_data = get_pitcher_card_data(this_player, this_pc, rating_vl, rating_vr, all_pos) + if this_player.description in this_player.cardset.name: + card_data['cardset_name'] = this_player.cardset.name + else: + card_data['cardset_name'] = this_player.description card_data['request'] = request html_response = templates.TemplateResponse("player_card.html", card_data) diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index 7c83881..9635c1e 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -1,4 +1,6 @@ from datetime import datetime + +import pandas as pd from fastapi import APIRouter, Depends, HTTPException, Response from typing import Optional import logging @@ -392,23 +394,23 @@ async def get_team_cards(team_id, csv: Optional[bool] = True): db.close() raise HTTPException(status_code=404, detail=f'No cards found') - data_list = [[ - 'cardset', 'player', 'rarity', 'image', 'image2', 'pos_1', 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', - 'pos_7', 'pos_8', 'cost', 'mlbclub', 'franchise', 'set_num', 'bbref_id', 'player_id', 'card_id' - ]] - for line in all_cards: - data_list.append( - [ - line.player.cardset, line.player.p_name, line.player.rarity, line.player.image, line.player.image2, - line.player.pos_1, line.player.pos_2, line.player.pos_3, line.player.pos_4, line.player.pos_5, - line.player.pos_6, line.player.pos_7, line.player.pos_8, line.player.cost, line.player.mlbclub, - line.player.franchise, line.player.set_num, line.player.bbref_id, line.player.player_id, line.id - ] - ) - return_val = DataFrame(data_list).to_csv(header=False, index=False) - + card_vals = [model_to_dict(x) for x in all_cards] db.close() - return Response(content=return_val, media_type='text/csv') + + for x in card_vals: + x.update(x['player']) + x['player_id'] = x['player']['player_id'] + x['player_name'] = x['player']['p_name'] + x['cardset_id'] = x['player']['cardset']['id'] + x['cardset_name'] = x['player']['cardset']['name'] + x['rarity'] = x['player']['rarity']['name'] + x['card_id'] = x['id'] + + card_df = pd.DataFrame(card_vals) + output = card_df[[ + 'cardset_name', 'player_name', 'rarity', 'image', 'image2', 'pos_1', 'pos_2', 'pos_3', 'pos_4', 'pos_5', + 'pos_6', 'pos_7', 'pos_8', 'cost', 'mlbclub', 'franchise', 'fangr_id', 'bbref_id', 'player_id', 'card_id']] + return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv') @router.post('') diff --git a/db_engine.py b/db_engine.py index 52d1e7e..7eb3cc2 100644 --- a/db_engine.py +++ b/db_engine.py @@ -54,10 +54,10 @@ def complex_data_to_csv(complex_data: List): else: data_list = [[x for x in complex_data[0].keys()]] for line in complex_data: - logging.info(f'line: {line}') + logging.debug(f'line: {line}') this_row = [] for key in line: - logging.info(f'key: {key}') + logging.debug(f'key: {key}') if line[key] is None: this_row.append('') @@ -183,7 +183,7 @@ class Player(BaseModel): fangr_id = CharField(null=True) description = CharField() quantity = IntegerField(default=999) - mlb_player = ForeignKeyField(MlbPlayer, null=True) + mlbplayer = ForeignKeyField(MlbPlayer, null=True) def __str__(self): return f'{self.cardset} {self.p_name} ({self.rarity.name})' @@ -395,11 +395,35 @@ class Roster(BaseModel): # this_roster.card26] +class Result(BaseModel): + away_team = ForeignKeyField(Team) + home_team = ForeignKeyField(Team) + away_score = IntegerField() + home_score = IntegerField() + away_team_value = IntegerField(null=True) + home_team_value = IntegerField(null=True) + away_team_ranking = IntegerField(null=True) + home_team_ranking = IntegerField(null=True) + scorecard = CharField() + week = IntegerField() + season = IntegerField() + ranked = BooleanField() + short_game = BooleanField() + game_type = CharField(null=True) + + @staticmethod + def select_season(season=None): + if not season: + season = Current.get().season + return Result.select().where(Result.season == season) + + class BattingStat(BaseModel): card = ForeignKeyField(Card) team = ForeignKeyField(Team) roster_num = IntegerField() vs_team = ForeignKeyField(Team) + result = ForeignKeyField(Result, null=True) pos = CharField() pa = IntegerField() ab = IntegerField() @@ -438,6 +462,7 @@ class PitchingStat(BaseModel): team = ForeignKeyField(Team) roster_num = IntegerField() vs_team = ForeignKeyField(Team) + result = ForeignKeyField(Result, null=True) ip = FloatField() hit = IntegerField() run = IntegerField() @@ -462,29 +487,6 @@ class PitchingStat(BaseModel): game_id = IntegerField() -class Result(BaseModel): - away_team = ForeignKeyField(Team) - home_team = ForeignKeyField(Team) - away_score = IntegerField() - home_score = IntegerField() - away_team_value = IntegerField(null=True) - home_team_value = IntegerField(null=True) - away_team_ranking = IntegerField(null=True) - home_team_ranking = IntegerField(null=True) - scorecard = CharField() - week = IntegerField() - season = IntegerField() - ranked = BooleanField() - short_game = BooleanField() - game_type = CharField(null=True) - - @staticmethod - def select_season(season=None): - if not season: - season = Current.get().season - return Result.select().where(Result.season == season) - - class Award(BaseModel): name = CharField() season = IntegerField() @@ -577,6 +579,9 @@ BattingCard.add_index(bc_index) class BattingCardRatings(BaseModel): battingcard = ForeignKeyField(BattingCard) vs_hand = CharField(default='R') + pull_rate = FloatField() + center_rate = FloatField() + slap_rate = FloatField() homerun = FloatField() bp_homerun = FloatField() triple = FloatField() diff --git a/db_migrations.py b/db_migrations.py index 8694c8b..07958b1 100644 --- a/db_migrations.py +++ b/db_migrations.py @@ -16,16 +16,21 @@ migrator = SqliteMigrator(db_engine.db) # last_game = CharField(null=True) # game_type = CharField(null=True) mlb_player = ForeignKeyField(db_engine.MlbPlayer, field=db_engine.MlbPlayer.id, null=True) +result = ForeignKeyField(db_engine.Result, field=db_engine.Result.id, null=True) # active_theme = ForeignKeyField(PackTheme, to_field='id', field_type=int, null=True) # active_theme = ForeignKeyField(db_engine.PackTheme, field=db_engine.PackTheme.id, null=True) # for careers # game_type = CharField(null=True) # pack_team = ForeignKeyField(db_engine.Team, field=db_engine.Team.id, null=True) # pack_cardset = ForeignKeyField(db_engine.Cardset, field=db_engine.Cardset.id, null=True) +pull_rate = FloatField(default=0.333) migrate( # migrator.add_column('current', 'active_theme_id', active_theme), # migrator.add_column('pack', 'pack_team_id', pack_team), - migrator.add_column('player', 'mlbplayer_id', mlb_player), + # migrator.add_column('player', 'mlbplayer_id', mlb_player), + migrator.add_column('battingstat', 'result_id', result), + migrator.add_column('pitchingstat', 'result_id', result), + # migrator.add_column('battingcardratings', 'pull_rate', pull_rate), # migrator.rename_column('cardset', 'available', 'for_purchase') # migrator.add_column('player', 'offense_col', offense_col), # migrator.add_column('comment_tbl', 'comment', comment_field), From ed6e73528fd6cd56bf3d9e4c15d27ef26d1fec22 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 20 Oct 2023 16:02:43 -0500 Subject: [PATCH 26/40] Added /scouting error messages --- app/routers_v2/battingcardratings.py | 4 ++-- app/routers_v2/pitchingcardratings.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/routers_v2/battingcardratings.py b/app/routers_v2/battingcardratings.py index 734a665..b0146cc 100644 --- a/app/routers_v2/battingcardratings.py +++ b/app/routers_v2/battingcardratings.py @@ -146,8 +146,8 @@ async def get_card_scouting(team_id: int, ts: str, cardset_id: list = Query(defa if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1: logging.warning(f'Team_id {team_id} attempted to pull ratings') db.close() - return 'Your team does not have the ratings guide enabled. If you have purchased a copy, ping Cal to ' \ - 'make sure it is enabled on your team. If you are interested, you can pick it up here: ' \ + return 'Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to ' \ + 'make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): ' \ 'https://ko-fi.com/manticorum/shop' all_ratings = BattingCardRatings.select() diff --git a/app/routers_v2/pitchingcardratings.py b/app/routers_v2/pitchingcardratings.py index d7ae4df..344cad7 100644 --- a/app/routers_v2/pitchingcardratings.py +++ b/app/routers_v2/pitchingcardratings.py @@ -134,8 +134,8 @@ async def get_card_scouting(team_id: int, ts: str, cardset_id: list = Query(defa if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1: logging.warning(f'Team_id {team_id} attempted to pull ratings') db.close() - return 'Your team does not have the ratings guide enabled. If you have purchased a copy, ping Cal to ' \ - 'make sure it is enabled on your team. If you are interested, you can pick it up here: ' \ + return 'Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to ' \ + 'make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): ' \ 'https://ko-fi.com/manticorum/shop' all_ratings = PitchingCardRatings.select() From 47d7a7cad4986451b2ec329f360d3a2134d72136 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 20 Oct 2023 17:03:01 -0500 Subject: [PATCH 27/40] Add fatigue to pitcher cards --- app/card_creation.py | 113 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/app/card_creation.py b/app/card_creation.py index f7da020..42860ad 100644 --- a/app/card_creation.py +++ b/app/card_creation.py @@ -313,10 +313,9 @@ class CardColumn(pydantic.BaseModel): if chances > Decimal(1.0): chances = Decimal(math.floor(chances)) else: - return False logging.error(f'Must have secondary play for fractional chances; could not round down to an integer\n' f'Play: {play}\nChances: {chances}\nSecondary Play: {secondary_play}') - raise ValueError(f'Cannot assign fractional chances without secondary play') + return False # Chances is whole number if math.floor(chances) == chances: @@ -945,6 +944,40 @@ class CardColumn(pydantic.BaseModel): total += 1 if self.twelve.is_full() else 0 return total + def add_fatigue(self, num_chances: int, k_only: bool = False): + def is_valid_result(this_result: CardResult): + if k_only: + if this_result.result_one == 'strikeout': + return True + else: + return False + else: + if this_result.result_two is None and not this_result.bold_one and 'X' not in this_result.result_one: + return True + return False + + if num_chances == 6: + if is_valid_result(self.seven): + self.seven.result_one += ' •' + return 6 + elif num_chances == 5: + if is_valid_result(self.six): + self.six.result_one += ' •' + return 5 + if is_valid_result(self.eight): + self.eight.result_one += ' •' + return 5 + + elif num_chances == 4: + if is_valid_result(self.five): + self.five.result_one += ' •' + return 4 + if is_valid_result(self.nine): + self.nine.result_one += ' •' + return 4 + + return 0 + class FullCard(pydantic.BaseModel): col_one: CardColumn = CardColumn() @@ -959,17 +992,8 @@ class FullCard(pydantic.BaseModel): class Config: arbitrary_types_allowed = True - def is_complete(self): - return self.col_one.is_full() and self.col_two.is_full() and self.col_three.is_full() - - def sample_output(self): - return f'{"" if self.is_complete() else "NOT "}COMPLETE\n' \ - f'Column 1\n{self.col_one}\n\n' \ - f'Column 2\n{self.col_two}\n\n' \ - f'Column 3\n{self.col_three}' - - def add_result(self, play: PlayResult, chances: Decimal, secondary_play: Optional[PlayResult] = None): - if play.is_offense: + def get_columns(self, is_offense: bool): + if is_offense: if self.offense_col == 1: first = self.col_one if self.alt_direction: @@ -1020,6 +1044,20 @@ class FullCard(pydantic.BaseModel): first = self.col_two second = self.col_one + return first, second, third + + def is_complete(self): + return self.col_one.is_full() and self.col_two.is_full() and self.col_three.is_full() + + def sample_output(self): + return f'{"" if self.is_complete() else "NOT "}COMPLETE\n' \ + f'Column 1\n{self.col_one}\n\n' \ + f'Column 2\n{self.col_two}\n\n' \ + f'Column 3\n{self.col_three}' + + def add_result(self, play: PlayResult, chances: Decimal, secondary_play: Optional[PlayResult] = None): + first, second, third = self.get_columns(is_offense=play.is_offense) + if 'gb' in play.full_name and chances + self.num_plusgb <= 6 and self.is_batter: play.full_name += '+' @@ -1062,6 +1100,54 @@ class FullCard(pydantic.BaseModel): def total_chances(self): return self.col_one.total_chances() + self.col_two.total_chances() + self.col_three.total_chances() + def add_fatigue(self): + first, second, third = self.get_columns(is_offense=False) + + total_added = 0 + for x in [first, second, third]: + resp = x.add_fatigue(6, k_only=True) + if resp: + total_added += resp + break + + if total_added == 0: + for x in [first, second, third]: + resp = x.add_fatigue(6, k_only=False) + if resp: + total_added += resp + break + + if total_added == 0: + for x in [first, second, third]: + resp = x.add_fatigue(5, k_only=True) + if resp: + total_added += resp + break + + if total_added == 0: + for x in [first, second, third]: + resp = x.add_fatigue(5, k_only=False) + if resp: + total_added += resp + break + + if total_added != 10: + for x in [first, second, third]: + resp = x.add_fatigue(10 - total_added, k_only=True) + if resp: + total_added += resp + break + + if total_added != 10: + for x in [first, second, third]: + resp = x.add_fatigue(10 - total_added, k_only=False) + if resp: + total_added += resp + break + + if total_added != 10: + logging.error(f'FullCard add_fatigue - Could not add all fatigue results / total_added: {total_added}') + class FullBattingCard(FullCard): ratings: BattingCardRatingsModel @@ -2814,6 +2900,7 @@ def get_pitcher_card_data(player, pitching_card, ratings_vl, ratings_vr, positio full_log(new_ratings, card) + card.add_fatigue() new_pitchingratings.append(new_ratings) vl_output = vl.card_output() From b40409e844fd3608c7be30b021c84a16f6ba646d Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 21 Oct 2023 15:30:46 -0500 Subject: [PATCH 28/40] Update card_creation.py Calculate slash line once card creation is complete --- app/card_creation.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/card_creation.py b/app/card_creation.py index 42860ad..e0e98c2 100644 --- a/app/card_creation.py +++ b/app/card_creation.py @@ -127,6 +127,21 @@ class BattingCardRatingsModel(pydantic.BaseModel): self.groundout_a, self.groundout_b, self.groundout_c ])) + def update_slash_lines(self): + self.avg = Decimal( + (self.homerun + self.bp_homerun / 2 + self.triple + self.double_three + + self.double_two + self.double_pull + self.single_two + self.single_one + + self.single_center + self.bp_single / 2) / Decimal(108) + ) + + self.obp = Decimal(((self.hbp + self.walk) / 108) + self.avg) + + self.slg = Decimal( + (self.homerun * 4 + self.bp_homerun * 2 + self.triple * 3 + self.double_three * 2 + + self.double_two * 2 + self.double_pull * 2 + self.single_two + self.single_one + + self.single_center + self.bp_single / 2) / 108 + ) + class PitchingCardRatingsModel(pydantic.BaseModel): pitchingcard: int @@ -171,6 +186,20 @@ class PitchingCardRatingsModel(pydantic.BaseModel): self.xcheck_cf, self.xcheck_rf ])) + def update_slash_lines(self): + self.avg = Decimal( + (self.homerun + self.bp_homerun / 2 + self.triple + self.double_three + + self.double_two + self.double_cf + self.single_two + self.single_one + + self.single_center + self.bp_single / 2) / Decimal(108) + ) + + self.obp = Decimal(((self.hbp + self.walk) / 108) + self.avg) + + self.slg = Decimal( + (self.homerun * 4 + self.bp_homerun * 2 + self.triple * 3 + self.double_three * 2 + + self.double_two * 2 + self.double_cf * 2 + self.single_two + self.single_one + + self.single_center + self.bp_single / 2) / 108) + class CardResult(pydantic.BaseModel): result_one: str = None @@ -2036,6 +2065,7 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions full_log(new_ratings, card) + new_ratings.update_slash_lines() new_battingratings.append(new_ratings) vl_output = vl.card_output() @@ -2901,6 +2931,7 @@ def get_pitcher_card_data(player, pitching_card, ratings_vl, ratings_vr, positio full_log(new_ratings, card) card.add_fatigue() + new_ratings.update_slash_lines() new_pitchingratings.append(new_ratings) vl_output = vl.card_output() From 0a59d3e98d5ce6f1e9d63e62d2ac9566f94f1ecf Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 21 Oct 2023 15:31:36 -0500 Subject: [PATCH 29/40] Moved AI lineup creation to db --- app/db_engine.py | 109 +++++++++++ app/routers_v2/battingcardratings.py | 3 +- app/routers_v2/cards.py | 11 +- app/routers_v2/current.py | 3 +- app/routers_v2/teams.py | 271 ++++++++++++++++++++++++++- 5 files changed, 389 insertions(+), 8 deletions(-) diff --git a/app/db_engine.py b/app/db_engine.py index a22de92..fd22cab 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -26,6 +26,27 @@ logging.basicConfig( level=log_level ) +CARDSETS = { + 'ranked': { + 'primary': [9, 10, 13] # 2023, 23 Promos, 2018 + }, + 'minor-league': { + 'primary': [13, 8], # 2018, Mario + 'secondary': [9, 3] # 2023, 2022 + }, + 'major-league': { + 'primary': [13, 9, 8, 6], # 2018, 2023, Mario, 2013 + 'secondary': [3, 12] # 2022, 2008 + }, + 'hall-of-fame': { + 'primary': [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] + }, + 'tens': { + 'primary': [11, 7, 6, 12], # 2016, 2012, 2013, 2008, Mario + 'secondary': [13, 5] # 2018, 2019 + } +} + def model_csv_headers(this_obj, exclude=None) -> List: data = model_to_dict(this_obj, recurse=False, exclude=exclude) @@ -695,6 +716,94 @@ CardPosition.add_index(pos_index) db.create_tables([BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition]) +class StratGame(BaseModel): + season = IntegerField() + game_type = CharField() + away_team = ForeignKeyField(Team) + home_team = ForeignKeyField(Team) + + week = IntegerField(default=1) + away_score = IntegerField(default=0) + home_score = IntegerField(default=0) + away_team_value = IntegerField(null=True) + home_team_value = IntegerField(null=True) + away_team_ranking = IntegerField(null=True) + home_team_ranking = IntegerField(null=True) + ranked = BooleanField(default=False) + short_game = BooleanField(default=False) + + +class StratPlay(BaseModel): + game = ForeignKeyField(StratGame) + play_num = IntegerField() + batter = ForeignKeyField(Player, null=True) + batter_team = ForeignKeyField(Team, null=True) + pitcher = ForeignKeyField(Player) + pitcher_team = ForeignKeyField(Team) + on_base_code = CharField() + inning_half = CharField() + inning_num = IntegerField() + batting_order = IntegerField() + starting_outs = IntegerField() + away_score = IntegerField() + home_score = IntegerField() + batter_pos = CharField(null=True) + + # These _final fields track the base this runner advances to post-play (None) if out + on_first = ForeignKeyField(Player, null=True) + 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) + e_run = 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) + catcher_team = ForeignKeyField(Team, null=True) + defender = ForeignKeyField(Player, null=True) + defender_team = ForeignKeyField(Team, null=True) + runner = ForeignKeyField(Player, null=True) + runner_team = ForeignKeyField(Team, 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) + + +db.create_tables([StratGame, StratPlay]) + + db.close() # scout_db = SqliteDatabase( diff --git a/app/routers_v2/battingcardratings.py b/app/routers_v2/battingcardratings.py index b0146cc..3f16fc1 100644 --- a/app/routers_v2/battingcardratings.py +++ b/app/routers_v2/battingcardratings.py @@ -125,8 +125,7 @@ async def get_card_ratings( for x in return_vals: x.update(x['battingcard']) x['player_id'] = x['battingcard']['player']['player_id'] - del x['battingcard'] - del x['player'] + del x['battingcard'], x['player'] db.close() return Response(content=pd.DataFrame(return_vals).to_csv(index=False), media_type='text/csv') diff --git a/app/routers_v2/cards.py b/app/routers_v2/cards.py index ca2d64d..73d937e 100644 --- a/app/routers_v2/cards.py +++ b/app/routers_v2/cards.py @@ -4,7 +4,7 @@ import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Card, model_to_dict, Team, Player, Pack, Paperdex +from ..db_engine import db, Card, model_to_dict, Team, Player, Pack, Paperdex, CARDSETS from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -218,15 +218,18 @@ async def v1_cards_legal_check( status_code=401, detail='Unauthorized' ) - if rarity_name not in ['ranked']: + if rarity_name not in CARDSETS.keys(): return f'Rarity name {rarity_name} not a valid check' bad_cards = [] all_cards = Card.select().where(Card.id << card_id) for x in all_cards: - if x.player.cardset_id not in [3, 4, 9, 10]: - bad_cards.append(x.player.description) + if x.player.cardset_id not in CARDSETS[rarity_name]: + if x.player.p_name in x.player.description: + bad_cards.append(x.player.description) + else: + bad_cards.append(f'{x.player.description} {x.player.p_name}') return {'count': len(bad_cards), 'bad_cards': bad_cards} diff --git a/app/routers_v2/current.py b/app/routers_v2/current.py index f3d1dbf..e472712 100644 --- a/app/routers_v2/current.py +++ b/app/routers_v2/current.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Response +from pandas import DataFrame from typing import Optional import logging import pydantic diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index 9635c1e..b6b0539 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -8,7 +8,8 @@ import pydantic from pandas import DataFrame from ..db_engine import db, Team, model_to_dict, fn, Pack, Card, Player, Paperdex, Notification, PackType, \ - Rarity, Current, query_to_csv, complex_data_to_csv + Rarity, Current, query_to_csv, complex_data_to_csv, CARDSETS, CardPosition, BattingCardRatings, BattingCard, \ + PitchingCard, PitchingCardRatings from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp logging.basicConfig( @@ -153,6 +154,274 @@ async def get_one_team(team_id, csv: Optional[bool] = False): return return_val +@router.get('/{team_id}/lineup/{difficulty_name}') +async def get_team_lineup(team_id: int, difficulty_name: str, pitcher_name: str, d_rank: int = 5, o_rank: int = 5): + """ + d_rank: int - 10: best overall, 9: prioritize range, 8: prioritize error + """ + this_team = Team.get_or_none(Team.id == team_id) + if this_team is None: + db.close() + raise HTTPException(status_code=404, detail=f'Team id {team_id} not found') + + if difficulty_name not in CARDSETS.keys(): + db.close() + raise HTTPException(status_code=400, detail=f'Difficulty name {difficulty_name} not a valid check') + + # all_players = Player.select().where( + # (fn.Lower(Player.p_name) != pitcher_name.lower()) & (Player.mlbclub == this_team.lname) + # ) + all_players = Player.select().where(Player.mlbclub == this_team.lname) + + legal_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['primary']) + if 'secondary' in CARDSETS[difficulty_name]: + backup_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['secondary']) + else: + backup_players = None + + logging.info(f'legal_players: {legal_players.count()}') + if backup_players is not None: + logging.info(f'backup_players: {backup_players.count()}') + player_names = [] + starting_nine = { + 'C': {'player': None, 'vl': None, 'vr': None, 'ops': 0}, + '1B': {'player': None, 'vl': None, 'vr': None, 'ops': 0}, + '2B': {'player': None, 'vl': None, 'vr': None, 'ops': 0}, + '3B': {'player': None, 'vl': None, 'vr': None, 'ops': 0}, + 'SS': {'player': None, 'vl': None, 'vr': None, 'ops': 0}, + 'LF': {'player': None, 'vl': None, 'vr': None, 'ops': 0}, + 'CF': {'player': None, 'vl': None, 'vr': None, 'ops': 0}, + 'RF': {'player': None, 'vl': None, 'vr': None, 'ops': 0}, + 'DH': {'player': None, 'vl': None, 'vr': None, 'ops': 0} + } + + def get_bratings(player_id): + this_bcard = BattingCard.get_or_none(BattingCard.player_id == player_id) + vl_ratings = BattingCardRatings.get_or_none( + BattingCardRatings.battingcard == this_bcard, BattingCardRatings.vs_hand == 'L' + ) + vl_ops = vl_ratings.obp + vl_ratings.slg + vr_ratings = BattingCardRatings.get_or_none( + BattingCardRatings.battingcard == this_bcard, BattingCardRatings.vs_hand == 'R' + ) + vr_ops = vr_ratings.obp + vr_ratings.slg + return model_to_dict(vl_ratings), model_to_dict(vr_ratings), (vl_ops + vr_ops + min(vl_ops, vr_ops)) / 3 + + for position in starting_nine.keys(): + if position == 'DH': + # all_bcards = BattingCard.select().where(BattingCard.player << legal_players) + # all_batters = BattingCardRatings.select().where( + # BattingCardRatings.battingcard << all_bcards + # ).order_by(BattingCardRatings.obp + BattingCardRatings.sl) + # + # for x in all_batters: + # if x.battingcard.player.p_name not in player_names: + # starting_nine['DH'] = x.battingcard.player + # break + logging.info(f'Searching for a DH!') + dh_query = legal_players.order_by(Player.cost.desc()) + for x in dh_query: + logging.info(f'checking {x.p_name} for {position}') + if x.p_name not in player_names and 'P' not in x.pos_1: + logging.info(f'adding!') + starting_nine['DH']['player'] = model_to_dict(x) + try: + vl, vr, total_ops = get_bratings(x.player_id) + except AttributeError as e: + logging.info(f'Could not find batting lines') + else: + starting_nine['DH']['vl'] = vl + starting_nine['DH']['vr'] = vr + starting_nine['DH']['ops'] = total_ops + player_names.append(x.p_name) + break + + if starting_nine['DH']['player'] is None: + dh_query = backup_players.order_by(Player.cost.desc()) + for x in dh_query: + logging.info(f'checking {x.p_name} for {position}') + if x.p_name not in player_names: + logging.info(f'adding!') + starting_nine['DH']['player'] = model_to_dict(x) + try: + vl, vr, total_ops = get_bratings(x.player_id) + except AttributeError as e: + logging.info(f'Could not find batting lines') + else: + vl, vr, total_ops = get_bratings(x.player_id) + starting_nine['DH']['vl'] = vl + starting_nine['DH']['vr'] = vr + starting_nine['DH']['ops'] = total_ops + player_names.append(x.p_name) + break + + else: + pos_group = CardPosition.select().where( + (CardPosition.position == position) & (CardPosition.player << legal_players) + ) + backup_group = CardPosition.select().where( + (CardPosition.position == position) & (CardPosition.player << backup_players) + ) + if difficulty_name == 'minor-league': + pos_group = pos_group.order_by(CardPosition.innings.desc()) + elif d_rank == 10: + pos_group = pos_group.order_by((CardPosition.range * 5) + CardPosition.error) + elif d_rank == 9: + pos_group = pos_group.order_by(CardPosition.range) + elif d_rank == 8: + pos_group = pos_group.order_by(CardPosition.error.desc()) + + logging.info(f'pos_group: {pos_group}\n{starting_nine}\n{player_names}\n\n') + if difficulty_name == 'minor-league': + for x in pos_group: + logging.info(f'checking {x.player.p_name} for {position}') + if x.player.p_name not in player_names and x.player.p_name.lower() != pitcher_name: + logging.info(f'adding!') + starting_nine[position]['player'] = model_to_dict(x.player) + vl, vr, total_ops = get_bratings(x.player.player_id) + starting_nine[position]['vl'] = vl + starting_nine[position]['vr'] = vr + starting_nine[position]['ops'] = total_ops + player_names.append(x.player.p_name) + break + + if starting_nine[position]['player'] is None: + for x in backup_group: + logging.info(f'checking {x.player.p_name} for {position}') + if x.player.p_name not in player_names and x.player.p_name.lower() != pitcher_name: + logging.info(f'adding!') + starting_nine[position]['player'] = model_to_dict(x.player) + vl, vr, total_ops = get_bratings(x.player.player_id) + starting_nine[position]['vl'] = vl + starting_nine[position]['vr'] = vr + starting_nine[position]['ops'] = total_ops + player_names.append(x.player.p_name) + break + + # all_bcards = BattingCard.select().where(BattingCard.player << starting_nine.values()) + # all_ratings = BattingCardRatings.select().where(BattingCardRatings.battingcard << all_bcards) + # + # vl_query = all_ratings.where(BattingCardRatings.vs_hand == 'L') + # vr_query = all_ratings.where(BattingCardRatings.vs_hand == 'R') + # + # vl_vals = [model_to_dict(x) for x in vl_query] + # for x in vl_vals: + # x.update(x['battingcard']) + # x['player_id'] = x['battingcard']['player']['player_id'] + # x['player_name'] = x['battingcard']['player']['p_name'] + # x['rarity'] = x['battingcard']['player']['rarity']['name'] + # x['cardset_id'] = x['battingcard']['player']['cardset']['id'] + # x['cardset_name'] = x['battingcard']['player']['cardset']['name'] + # del x['player'] + # + # vr_vals = [model_to_dict(x) for x in vr_query] + # for x in vr_vals: + # x['player_id'] = x['battingcard']['player']['player_id'] + # del x['battingcard'] + # + # vl = pd.DataFrame(vl_vals) + # vr = pd.DataFrame(vr_vals) + # db.close() + # + # output = pd.merge(vl, vr, on='player_id', suffixes=('_vl', '_vr')) + # + # def get_total_ops(df_data): + # ops_vl = df_data['obp_vL'] + df_data['slg_vL'] + # ops_vr = df_data['obp_vR'] + df_data['slg_vR'] + # return (ops_vr + ops_vl + min(ops_vl, ops_vr)) / 3 + # output['total_OPS'] = output.apply(get_total_ops, axis=1) + # output = output.sort_values(by=['total_OPS'], ascending=False) + + sorted_nine = sorted(starting_nine.items(), key=lambda item: item[1]['ops'], reverse=True) + return { + 'json': dict(sorted_nine), + 'array': sorted_nine + } + + +@router.get('/{team_id}/sp/{difficulty_name}') +async def get_team_sp(team_id: int, difficulty_name: str, sp_rank: int): + logging.info(f'get_team_sp - team_id: {team_id} / difficulty_name: {difficulty_name} / sp_rank: {sp_rank}') + this_team = Team.get_or_none(Team.id == team_id) + if this_team is None: + db.close() + raise HTTPException(status_code=404, detail=f'Team id {team_id} not found') + + if difficulty_name not in CARDSETS.keys(): + db.close() + raise HTTPException(status_code=400, detail=f'Difficulty name {difficulty_name} not a valid check') + + all_players = Player.select().where(Player.mlbclub == this_team.lname) + + legal_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['primary']) + if 'secondary' in CARDSETS[difficulty_name]: + backup_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['secondary']) + else: + backup_players = None + + def sort_starters(starter_query) -> DataFrame | None: + all_s = [model_to_dict(x, recurse=False) for x in starter_query] + if len(all_s) == 0: + logging.error(f'Empty starter_query: {starter_query}') + return None + + starter_df = pd.DataFrame(all_s).set_index('player', drop=False) + logging.debug(f'starter_df: {starter_df}') + + def get_total_ops(df_data): + vlval = PitchingCardRatings.get_or_none( + PitchingCardRatings.pitchingcard_id == df_data['id'], PitchingCardRatings.vs_hand == 'L') + vrval = PitchingCardRatings.get_or_none( + PitchingCardRatings.pitchingcard_id == df_data['id'], PitchingCardRatings.vs_hand == 'R') + + ops_vl = vlval.obp + vlval.slg + ops_vr = vrval.obp + vrval.slg + return (ops_vr + ops_vl + min(ops_vl, ops_vr)) / 3 + + starter_df['total_ops'] = starter_df.apply(get_total_ops, axis=1) + return starter_df.sort_values(by='total_ops') + + # Find SP in primary cardsets + s_query = PitchingCard.select().join(Player).where( + (PitchingCard.player << legal_players) & (PitchingCard.starter_rating >= 4) + ) + all_starters = sort_starters(s_query) + logging.debug(f'sorted: {all_starters}') + + if all_starters is not None and len(all_starters.index) >= sp_rank: + this_player_id = all_starters.iloc[sp_rank - 1].player + this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False) + db.close() + return this_player + + if all_starters is not None and len(all_starters.index) > 0: + this_player_id = all_starters.iloc[len(all_starters.index) - 1].player + this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False) + db.close() + return this_player + + # Include backup cardsets + s_query = PitchingCard.select().where( + (PitchingCard.player << backup_players) & (PitchingCard.starter_rating >= 4) + ) + all_starters = sort_starters(s_query) + logging.debug(f'sorted: {all_starters}') + + if all_starters is not None and len(all_starters.index) >= sp_rank: + this_player_id = all_starters.iloc[sp_rank - 1].player + this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False) + db.close() + return this_player + + if all_starters is not None and len(all_starters.index) > 0: + this_player_id = all_starters.iloc[len(all_starters.index) - 1].player + this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False) + db.close() + return this_player + + raise HTTPException(status_code=500, detail=f'No SP #{sp_rank} found for Team {team_id}') + + @router.get('/{team_id}/buy/players') async def team_buy_players(team_id: int, ids: str, ts: str): try: From 0b3c4b2f55d3ad0f4069fb15c33731bc0cb3a3e2 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 21 Oct 2023 15:31:49 -0500 Subject: [PATCH 30/40] Added StratGame to db --- app/main.py | 3 +- app/routers_v2/stratgame.py | 173 ++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 app/routers_v2/stratgame.py diff --git a/app/main.py b/app/main.py index 28f4709..85afd0e 100644 --- a/app/main.py +++ b/app/main.py @@ -7,7 +7,7 @@ from fastapi import FastAPI from .routers_v2 import ( current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards, - battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers) + battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers, stratgame) app = FastAPI( responses={404: {'description': 'Not found'}} @@ -41,3 +41,4 @@ app.include_router(pitchingcardratings.router) app.include_router(cardpositions.router) app.include_router(scouting.router) app.include_router(mlbplayers.router) +app.include_router(stratgame.router) diff --git a/app/routers_v2/stratgame.py b/app/routers_v2/stratgame.py new file mode 100644 index 0000000..3147c7a --- /dev/null +++ b/app/routers_v2/stratgame.py @@ -0,0 +1,173 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Response +from typing import Literal, Optional, List +import logging +import pandas as pd +import pydantic +from pydantic import validator, root_validator + +from ..db_engine import db, StratGame, model_to_dict, chunked, PitchingCard, Player, query_to_csv, Team, 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/v2/stratgames', + tags=['stratgames'] +) + + +class GameModel(pydantic.BaseModel): + season: int + game_type: str + away_team_id: int + home_team_id: int + week: int = 1 + away_score: int = 0 + home_score: int = 0 + away_team_value: int = None + home_team_value: int = None + away_team_ranking: int = None + home_team_ranking: int = None + ranked: bool = False + short_game: bool = False + + +class GameList(pydantic.BaseModel): + games: List[GameModel] + + +@router.get('') +async def get_games( + season: list = Query(default=None), away_team_id: list = Query(default=None), + home_team_id: list = Query(default=None), team1_id: list = Query(default=None), + team2_id: list = Query(default=None), game_type: list = Query(default=None), ranked: Optional[bool] = None, + short_game: Optional[bool] = None, csv: Optional[bool] = False, short_output: bool = False): + all_games = StratGame.select() + + if season is not None: + all_games = all_games.where(StratGame.season << season) + if away_team_id is not None: + all_games = all_games.where(StratGame.away_team_id << away_team_id) + if home_team_id is not None: + all_games = all_games.where(StratGame.home_team_id << home_team_id) + if team1_id is not None: + all_games = all_games.where( + (StratGame.away_team_id << team1_id) | (StratGame.home_team_id << team1_id) + ) + if team2_id is not None: + all_games = all_games.where( + (StratGame.away_team_id << team2_id) | (StratGame.home_team_id << team2_id) + ) + if game_type is not None: + g_list = [x.lower() for x in game_type] + all_games = all_games.where(fn.Lower(StratGame.game_type) << g_list) + if ranked is not None: + all_games = all_games.where(StratGame.ranked == ranked) + if short_game is not None: + all_games = all_games.where(StratGame.short_game == short_game) + + if csv: + return_vals = [model_to_dict(x) for x in all_games] + for x in return_vals: + x['away_team_name'] = x['away_team']['lname'] + x['home_team_name'] = x['home_team']['lname'] + del x['away_team'], x['home_team'] + + db.close() + return Response(content=pd.DataFrame(return_vals).to_csv(index=False), media_type='text/csv') + + return_val = {'count': all_games.count(), 'games': [ + model_to_dict(x, recurse=not short_output) for x in all_games + ]} + db.close() + return return_val + + +@router.get('/{game_id}') +async def get_one_game(game_id: int): + this_game = StratGame.get_or_none(StratGame.id == game_id) + if not this_game: + db.close() + raise HTTPException(status_code=404, detail=f'StratGame ID {game_id} not found') + + g_result = model_to_dict(this_game) + db.close() + return g_result + + +@router.patch('/{game_id}') +async def patch_game( + game_id: int, game_type: Optional[str] = None, away_score: Optional[int] = None, + home_score: Optional[int] = None, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_game - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_game = StratGame.get_or_none(StratGame.id == game_id) + if not this_game: + db.close() + raise HTTPException(status_code=404, detail=f'StratGame ID {game_id} not found') + + if away_score is not None: + this_game.away_score = away_score + if home_score is not None: + this_game.home_score = home_score + if game_type is not None: + this_game.game_type = game_type + + if this_game.save() == 1: + g_result = model_to_dict(this_game) + db.close() + return g_result + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to patch game {game_id}') + + +@router.post('') +async def post_games(game_list: GameList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_games - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_games = [] + for x in game_list.games: + if Team.get_or_none(Team.id == x.away_team_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.away_team_id} not found') + if Team.get_or_none(Team.id == x.home_team_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.home_team_id} not found') + + new_games.append(x.dict()) + + with db.atomic(): + for batch in chunked(new_games, 16): + StratGame.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_games)} games' + + +@router.delete('/{game_id}') +async def delete_game(game_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_game - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_game = StratGame.get_or_none(StratGame.id == game_id) + if not this_game: + db.close() + raise HTTPException(status_code=404, detail=f'StratGame ID {game_id} not found') + + count = this_game.delete_instance() + db.close() + + if count == 1: + return f'StratGame {game_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'StratGame {game_id} could not be deleted') + + From edde7a1b82439baa0f54f7c83426f3db04760c5c Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 21 Oct 2023 19:54:31 -0500 Subject: [PATCH 31/40] StratPlays added --- app/db_engine.py | 5 +- app/main.py | 3 +- app/routers_v2/stratgame.py | 36 ++-- app/routers_v2/stratplays.py | 369 +++++++++++++++++++++++++++++++++++ app/routers_v2/teams.py | 5 +- 5 files changed, 395 insertions(+), 23 deletions(-) create mode 100644 app/routers_v2/stratplays.py diff --git a/app/db_engine.py b/app/db_engine.py index fd22cab..ac3dda2 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -721,7 +721,6 @@ class StratGame(BaseModel): game_type = CharField() away_team = ForeignKeyField(Team) home_team = ForeignKeyField(Team) - week = IntegerField(default=1) away_score = IntegerField(default=0) home_score = IntegerField(default=0) @@ -731,6 +730,7 @@ class StratGame(BaseModel): home_team_ranking = IntegerField(null=True) ranked = BooleanField(default=False) short_game = BooleanField(default=False) + forfeit = BooleanField(default=False) class StratPlay(BaseModel): @@ -780,7 +780,8 @@ class StratPlay(BaseModel): sb = IntegerField(default=0) cs = IntegerField(default=0) outs = IntegerField(default=0) - wpa = FloatField(default=0) + wpa = FloatField(default=0.0) + re24 = FloatField(default=0.0) # These fields are only required if the play is an x-check or baserunning play catcher = ForeignKeyField(Player, null=True) diff --git a/app/main.py b/app/main.py index 85afd0e..295ab12 100644 --- a/app/main.py +++ b/app/main.py @@ -7,7 +7,7 @@ from fastapi import FastAPI from .routers_v2 import ( current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards, - battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers, stratgame) + battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers, stratgame, stratplays) app = FastAPI( responses={404: {'description': 'Not found'}} @@ -42,3 +42,4 @@ app.include_router(cardpositions.router) app.include_router(scouting.router) app.include_router(mlbplayers.router) app.include_router(stratgame.router) +app.include_router(stratplays.router) diff --git a/app/routers_v2/stratgame.py b/app/routers_v2/stratgame.py index 3147c7a..24fa89d 100644 --- a/app/routers_v2/stratgame.py +++ b/app/routers_v2/stratgame.py @@ -15,8 +15,8 @@ logging.basicConfig( ) router = APIRouter( - prefix='/api/v2/stratgames', - tags=['stratgames'] + prefix='/api/v2/games', + tags=['games'] ) @@ -34,6 +34,7 @@ class GameModel(pydantic.BaseModel): home_team_ranking: int = None ranked: bool = False short_game: bool = False + forfeit: bool = False class GameList(pydantic.BaseModel): @@ -42,7 +43,7 @@ class GameList(pydantic.BaseModel): @router.get('') async def get_games( - season: list = Query(default=None), away_team_id: list = Query(default=None), + season: list = Query(default=None), forfeit: Optional[bool] = None, away_team_id: list = Query(default=None), home_team_id: list = Query(default=None), team1_id: list = Query(default=None), team2_id: list = Query(default=None), game_type: list = Query(default=None), ranked: Optional[bool] = None, short_game: Optional[bool] = None, csv: Optional[bool] = False, short_output: bool = False): @@ -50,6 +51,8 @@ async def get_games( if season is not None: all_games = all_games.where(StratGame.season << season) + if forfeit is not None: + all_games = all_games.where(StratGame.forfeit == forfeit) if away_team_id is not None: all_games = all_games.where(StratGame.away_team_id << away_team_id) if home_team_id is not None: @@ -129,26 +132,23 @@ async def patch_game( @router.post('') -async def post_games(game_list: GameList, token: str = Depends(oauth2_scheme)): +async def post_game(this_game: GameModel, token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'post_games - Bad Token: {token}') raise HTTPException(status_code=401, detail='Unauthorized') - new_games = [] - for x in game_list.games: - if Team.get_or_none(Team.id == x.away_team_id) is None: - raise HTTPException(status_code=404, detail=f'Team ID {x.away_team_id} not found') - if Team.get_or_none(Team.id == x.home_team_id) is None: - raise HTTPException(status_code=404, detail=f'Team ID {x.home_team_id} not found') + this_game = StratGame(**this_game.dict()) - new_games.append(x.dict()) - - with db.atomic(): - for batch in chunked(new_games, 16): - StratGame.insert_many(batch).on_conflict_replace().execute() - db.close() - - return f'Inserted {len(new_games)} games' + saved = this_game.save() + if saved == 1: + return_val = model_to_dict(this_game) + db.close() + return return_val + else: + raise HTTPException( + status_code=418, + detail='Well slap my ass and call me a teapot; I could not save that game' + ) @router.delete('/{game_id}') diff --git a/app/routers_v2/stratplays.py b/app/routers_v2/stratplays.py new file mode 100644 index 0000000..43d024a --- /dev/null +++ b/app/routers_v2/stratplays.py @@ -0,0 +1,369 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Response +from typing import List, Optional, Literal +import logging +from pydantic import BaseModel, validator + +from ..db_engine import db, StratPlay, StratGame, Team, Player, model_to_dict, chunked, fn, SQL, \ + complex_data_to_csv +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/plays', + tags=['plays'] +) + +POS_LIST = Literal['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'P', 'DH', 'PH', 'PR', 'GHOST'] + + +class PlayModel(BaseModel): + game_id: int + play_num: int + batter_id: int = None + batter_team_id: int = None + pitcher_id: int + pitcher_team_id: int = None + on_base_code: str + inning_half: Literal['top', 'bot', 'Top', 'Bot'] + inning_num: int + batting_order: int + starting_outs: int + away_score: int + home_score: int + batter_pos: POS_LIST = None + + on_first_id: int = None + on_first_final: int = None + on_second_id: int = None + on_second_final: int = None + on_third_id: int = None + on_third_final: int = None + batter_final: int = None + + pa: int = 0 + ab: int = 0 + e_run: int = 0 + run: int = 0 + hit: int = 0 + rbi: int = 0 + double: int = 0 + triple: int = 0 + homerun: int = 0 + bb: int = 0 + so: int = 0 + hbp: int = 0 + sac: int = 0 + ibb: int = 0 + gidp: int = 0 + bphr: int = 0 + bpfo: int = 0 + bp1b: int = 0 + bplo: int = 0 + sb: int = 0 + cs: int = 0 + outs: int = 0 + wpa: float = 0.0 + re24: float = 0.0 + + catcher_id: int = None + catcher_team_id: int = None + defender_id: int = None + defender_team_id: int = None + runner_id: int = None + runner_team_id: int = None + + check_pos: POS_LIST = None + error: int = 0 + wild_pitch: int = 0 + passed_ball: int = 0 + pick_off: int = 0 + balk: int = 0 + is_go_ahead: bool = False + is_tied: bool = False + is_new_inning: bool = False + + @validator('on_first_final') + def no_final_if_no_runner_one(cls, v, values): + if values['on_first_id'] is None: + return None + return v + + @validator('on_second_final') + def no_final_if_no_runner_two(cls, v, values): + if values['on_second_id'] is None: + return None + return v + + @validator('on_third_final') + def no_final_if_no_runner_three(cls, v, values): + if values['on_third_id'] is None: + return None + return v + + @validator('batter_final') + def no_final_if_no_batter(cls, v, values): + if values['batter_id'] is None: + return None + return v + + +class PlayList(BaseModel): + plays: List[PlayModel] + + +@router.get('') +async def get_plays( + game_id: list = Query(default=None), batter_id: list = Query(default=None), season: list = Query(default=None), + week: list = Query(default=None), has_defender: Optional[bool] = None, has_catcher: Optional[bool] = None, + has_defender_or_catcher: Optional[bool] = None, is_scoring_play: Optional[bool] = None, + pitcher_id: list = Query(default=None), obc: list = Query(default=None), inning: list = Query(default=None), + batting_order: list = Query(default=None), starting_outs: list = Query(default=None), + batter_pos: list = Query(default=None), catcher_id: list = Query(default=None), + defender_id: list = Query(default=None), runner_id: list = Query(default=None), + offense_team_id: list = Query(default=None), defense_team_id: list = Query(default=None), + hit: Optional[int] = None, double: Optional[int] = None, triple: Optional[int] = None, + homerun: Optional[int] = None, play_num: list = Query(default=None), game_type: list = Query(default=None), + sb: Optional[int] = None, cs: Optional[int] = None, manager_id: list = Query(default=None), + run: Optional[int] = None, e_run: Optional[int] = None, rbi: list = Query(default=None), + outs: list = Query(default=None), wild_pitch: Optional[int] = None, is_final_out: Optional[bool] = None, + is_go_ahead: Optional[bool] = None, is_tied: Optional[bool] = None, is_new_inning: Optional[bool] = None, + min_wpa: Optional[float] = None, max_wpa: Optional[float] = None, sort: Optional[str] = None, + short_output: Optional[bool] = False, limit: Optional[int] = 200, page_num: Optional[int] = 1): + all_plays = StratPlay.select() + + if season is not None: + s_games = StratGame.select().where(StratGame.season << season) + all_plays = all_plays.where(StratPlay.game << s_games) + if week is not None: + w_games = StratGame.select().where(StratGame.week << week) + all_plays = all_plays.where(StratPlay.game << w_games) + if has_defender is not None: + all_plays = all_plays.where(StratPlay.defender.is_null(False)) + if has_catcher is not None: + all_plays = all_plays.where(StratPlay.catcher.is_null(False)) + if has_defender_or_catcher is not None: + all_plays = all_plays.where( + (StratPlay.catcher.is_null(False)) | (StratPlay.defender.is_null(False)) + ) + if game_id is not None: + all_plays = all_plays.where(StratPlay.game_id << game_id) + if batter_id is not None: + all_plays = all_plays.where(StratPlay.batter_id << batter_id) + if pitcher_id is not None: + all_plays = all_plays.where(StratPlay.pitcher_id << pitcher_id) + if obc is not None: + all_plays = all_plays.where(StratPlay.on_base_code << obc) + if inning is not None: + all_plays = all_plays.where(StratPlay.inning_num << inning) + if batting_order is not None: + all_plays = all_plays.where(StratPlay.batting_order << batting_order) + if starting_outs is not None: + all_plays = all_plays.where(StratPlay.starting_outs << starting_outs) + if batter_pos is not None: + all_plays = all_plays.where(StratPlay.batter_pos << batter_pos) + if catcher_id is not None: + all_plays = all_plays.where(StratPlay.catcher_id << catcher_id) + if defender_id is not None: + all_plays = all_plays.where(StratPlay.defender_id << defender_id) + if runner_id is not None: + all_plays = all_plays.where(StratPlay.runner_id << runner_id) + if offense_team_id is not None: + all_teams = Team.select().where(Team.id << offense_team_id) + all_plays = all_plays.where( + (StratPlay.batter_team << all_teams) | (StratPlay.runner_team << all_teams) + ) + if defense_team_id is not None: + all_teams = Team.select().where(Team.id << defense_team_id) + all_plays = all_plays.where( + (StratPlay.catcher_team << all_teams) | (StratPlay.defender_team << all_teams) + ) + if hit is not None: + all_plays = all_plays.where(StratPlay.hit == hit) + if double is not None: + all_plays = all_plays.where(StratPlay.double == double) + if triple is not None: + all_plays = all_plays.where(StratPlay.triple == triple) + if homerun is not None: + all_plays = all_plays.where(StratPlay.homerun == homerun) + if sb is not None: + all_plays = all_plays.where(StratPlay.sb == sb) + if cs is not None: + all_plays = all_plays.where(StratPlay.cs == cs) + if wild_pitch is not None: + all_plays = all_plays.where(StratPlay.wild_pitch == wild_pitch) + if run is not None: + all_plays = all_plays.where(StratPlay.run == run) + if e_run is not None: + all_plays = all_plays.where(StratPlay.e_run == e_run) + if rbi is not None: + all_plays = all_plays.where(StratPlay.rbi << rbi) + if outs is not None: + all_plays = all_plays.where(StratPlay.outs << outs) + if manager_id is not None: + all_games = StratGame.select().where( + (StratGame.away_manager_id << manager_id) | (StratGame.home_manager_id << manager_id) + ) + all_plays = all_plays.where(StratPlay.game << all_games) + if is_final_out is not None: + all_plays = all_plays.where(StratPlay.starting_outs + StratPlay.outs == 3) + if is_go_ahead is not None: + all_plays = all_plays.where(StratPlay.is_go_ahead == is_go_ahead) + if is_tied is not None: + all_plays = all_plays.where(StratPlay.is_tied == is_tied) + if is_new_inning is not None: + all_plays = all_plays.where(StratPlay.is_new_inning == is_new_inning) + if is_scoring_play is not None: + all_plays = all_plays.where( + (StratPlay.on_first_final == 4) | (StratPlay.on_second_final == 4) | (StratPlay.on_third_final == 4) | + (StratPlay.batter_final == 4) + ) + if min_wpa is not None: + all_plays = all_plays.where(StratPlay.wpa >= min_wpa) + if max_wpa is not None: + all_plays = all_plays.where(StratPlay.wpa <= max_wpa) + if play_num is not None: + all_plays = all_plays.where(StratPlay.play_num << play_num) + if game_type is not None: + all_types = [x.lower() for x in game_type] + all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types) + all_plays = all_plays.where(StratPlay.game << all_games) + + if limit > 5000: + limit = 5000 + elif limit < 1: + limit = 1 + if page_num < 1: + page_num = 1 + + if sort == 'wpa-desc': + all_plays = all_plays.order_by(-fn.ABS(StratPlay.wpa)) + elif sort == 'wpa-asc': + all_plays = all_plays.order_by(fn.ABS(StratPlay.wpa)) + elif sort == 're24-desc': + all_plays = all_plays.order_by(-fn.ABS(StratPlay.re24)) + elif sort == 're24-asc': + all_plays = all_plays.order_by(fn.ABS(StratPlay.re24)) + elif sort == 'newest': + all_plays = all_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc()) + elif sort == 'oldest': + all_plays = all_plays.order_by(StratPlay.game_id, StratPlay.play_num) + + all_plays = all_plays.paginate(page_num, limit) + + return_plays = { + 'count': all_plays.count(), + 'plays': [model_to_dict(x, recurse=not short_output) for x in all_plays] + } + db.close() + return return_plays + + +@router.get('/{play_id}') +async def get_one_play(play_id: int): + if StratPlay.get_or_none(StratPlay.id == play_id) is None: + db.close() + raise HTTPException(status_code=404, detail=f'Play ID {play_id} not found') + r_play = model_to_dict(StratPlay.get_by_id(play_id)) + db.close() + return r_play + + +@router.patch('/{play_id}') +async def patch_play(play_id: int, new_play: PlayModel, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_play - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + if StratPlay.get_or_none(StratPlay.id == play_id) is None: + db.close() + raise HTTPException(status_code=404, detail=f'Play ID {play_id} not found') + + StratPlay.update(**new_play.dict()).where(StratPlay.id == play_id).execute() + r_play = model_to_dict(StratPlay.get_by_id(play_id)) + db.close() + return r_play + + +@router.post('') +async def post_plays(p_list: PlayList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_plays - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_plays = [] + this_game = StratGame.get_or_none(StratGame.id == p_list.plays[0].game_id) + if this_game is None: + raise HTTPException(status_code=404, detail=f'Game ID {p_list.plays[0].game_id} not found') + + for play in p_list.plays: + this_play = play + this_play.inning_half = this_play.inning_half.lower() + top_half = this_play.inning_half == 'top' + + if this_play.batter_team_id is None and this_play.batter_id is not None: + this_play.batter_team_id = this_game.away_team.id if top_half else this_game.home_team.id + if this_play.pitcher_team_id is None: + this_play.pitcher_team_id = this_game.home_team.id if top_half else this_game.away_team.id + if this_play.catcher_id is not None: + this_play.catcher_team_id = this_game.home_team.id if top_half else this_game.away_team.id + if this_play.defender_id is not None: + this_play.defender_team_id = this_game.home_team.id if top_half else this_game.away_team.id + if this_play.runner_id is not None: + this_play.runner_team_id = this_game.away_team.id if top_half else this_game.home_team.id + if this_play.pa == 0: + this_play.batter_final = None + + new_plays.append(this_play.dict()) + + with db.atomic(): + for batch in chunked(new_plays, 20): + StratPlay.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_plays)} plays' + + +@router.delete('/{play_id}') +async def delete_play(play_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_play - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_play = StratPlay.get_or_none(StratPlay.id == play_id) + if not this_play: + db.close() + raise HTTPException(status_code=404, detail=f'Play ID {play_id} not found') + + count = this_play.delete_instance() + db.close() + + if count == 1: + return f'Play {play_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Play {play_id} could not be deleted') + + +@router.delete('/game/{game_id}') +async def delete_plays_game(game_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_plays_game - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_game = StratGame.get_or_none(StratGame.id == game_id) + if not this_game: + db.close() + raise HTTPException(status_code=404, detail=f'Game ID {game_id} not found') + + count = StratPlay.delete().where(StratPlay.game == this_game).execute() + db.close() + + if count > 0: + return f'Deleted {count} plays matching Game ID {game_id}' + else: + raise HTTPException(status_code=500, detail=f'No plays matching Game ID {game_id} were deleted') + diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index b6b0539..da5b3fe 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -134,7 +134,7 @@ async def get_teams( @router.get('/{team_id}') -async def get_one_team(team_id, csv: Optional[bool] = False): +async def get_one_team(team_id, inc_packs: bool = True, csv: Optional[bool] = False): try: this_team = Team.get_by_id(team_id) except Exception: @@ -148,7 +148,8 @@ async def get_one_team(team_id, csv: Optional[bool] = False): return_val = complex_data_to_csv([data]) else: return_val = model_to_dict(this_team) - return_val['sealed_packs'] = [model_to_dict(x) for x in p_query] + if inc_packs: + return_val['sealed_packs'] = [model_to_dict(x) for x in p_query] db.close() return return_val From 25be95090a3562f05f10bdd0d04bff379698afd2 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 21 Oct 2023 23:32:05 -0500 Subject: [PATCH 32/40] Plays/pitching is functional --- app/routers_v2/stratplays.py | 572 ++++++++++++++++++++++++++++++++++- app/routers_v2/teams.py | 1 + 2 files changed, 566 insertions(+), 7 deletions(-) diff --git a/app/routers_v2/stratplays.py b/app/routers_v2/stratplays.py index 43d024a..a952090 100644 --- a/app/routers_v2/stratplays.py +++ b/app/routers_v2/stratplays.py @@ -1,10 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Response from typing import List, Optional, Literal import logging +import pandas as pd from pydantic import BaseModel, validator from ..db_engine import db, StratPlay, StratGame, Team, Player, model_to_dict, chunked, fn, SQL, \ - complex_data_to_csv + complex_data_to_csv, Decision from ..dependencies import oauth2_scheme, valid_token, LOG_DATA logging.basicConfig( @@ -128,7 +129,7 @@ async def get_plays( offense_team_id: list = Query(default=None), defense_team_id: list = Query(default=None), hit: Optional[int] = None, double: Optional[int] = None, triple: Optional[int] = None, homerun: Optional[int] = None, play_num: list = Query(default=None), game_type: list = Query(default=None), - sb: Optional[int] = None, cs: Optional[int] = None, manager_id: list = Query(default=None), + sb: Optional[int] = None, cs: Optional[int] = None, csv: Optional[bool] = False, run: Optional[int] = None, e_run: Optional[int] = None, rbi: list = Query(default=None), outs: list = Query(default=None), wild_pitch: Optional[int] = None, is_final_out: Optional[bool] = None, is_go_ahead: Optional[bool] = None, is_tied: Optional[bool] = None, is_new_inning: Optional[bool] = None, @@ -204,11 +205,6 @@ async def get_plays( all_plays = all_plays.where(StratPlay.rbi << rbi) if outs is not None: all_plays = all_plays.where(StratPlay.outs << outs) - if manager_id is not None: - all_games = StratGame.select().where( - (StratGame.away_manager_id << manager_id) | (StratGame.home_manager_id << manager_id) - ) - all_plays = all_plays.where(StratPlay.game << all_games) if is_final_out is not None: all_plays = all_plays.where(StratPlay.starting_outs + StratPlay.outs == 3) if is_go_ahead is not None: @@ -255,6 +251,67 @@ async def get_plays( all_plays = all_plays.paginate(page_num, limit) + if csv: + return_vals = [model_to_dict(x) for x in all_plays] + for x in return_vals: + x['game_id'] = x['game']['id'] + x['game_type'] = x['game']['game_type'] + x['batter_id'] = x['batter']['player_id'] + x['batter_name'] = x['batter']['p_name'] + x['batter_cardset'] = x['batter']['cardset']['name'] + x['batter_team_id'] = x['batter_team']['id'] + x['batter_team_abbrev'] = x['batter_team']['abbrev'] + x['pitcher_id'] = x['pitcher']['player_id'] + x['pitcher_name'] = x['pitcher']['p_name'] + x['pitcher_cardset'] = x['pitcher']['cardset']['name'] + x['pitcher_team_id'] = x['pitcher_team']['id'] + x['pitcher_team_abbrev'] = x['pitcher_team']['abbrev'] + + if x['catcher'] is not None: + x['catcher_id'] = x['catcher']['player_id'] + x['catcher_name'] = x['catcher']['p_name'] + x['catcher_cardset'] = x['catcher']['cardset']['name'] + x['catcher_team_id'] = x['catcher_team']['id'] + x['catcher_team_abbrev'] = x['catcher_team']['abbrev'] + else: + x['catcher_id'] = None + x['catcher_name'] = None + x['catcher_cardset'] = None + x['catcher_team_id'] = None + x['catcher_team_abbrev'] = None + + if x['defender'] is not None: + x['defender_id'] = x['defender']['player_id'] + x['defender_name'] = x['defender']['p_name'] + x['defender_cardset'] = x['defender']['cardset']['name'] + x['defender_team_id'] = x['defender_team']['id'] + x['defender_team_abbrev'] = x['defender_team']['abbrev'] + else: + x['defender_id'] = None + x['defender_name'] = None + x['defender_cardset'] = None + x['defender_team_id'] = None + x['defender_team_abbrev'] = None + + if x['runner'] is not None: + x['runner_id'] = x['runner']['player_id'] + x['runner_name'] = x['runner']['p_name'] + x['runner_cardset'] = x['runner']['cardset']['name'] + x['runner_team_id'] = x['runner_team']['id'] + x['runner_team_abbrev'] = x['runner_team']['abbrev'] + else: + x['runner_id'] = None + x['runner_name'] = None + x['runner_cardset'] = None + x['runner_team_id'] = None + x['runner_team_abbrev'] = None + + del x['game'], x['batter'], x['batter_team'], x['pitcher'], x['pitcher_team'], x['catcher'], \ + x['catcher_team'], x['defender'], x['defender_team'], x['runner'], x['runner_team'] + + db.close() + return Response(content=pd.DataFrame(return_vals).to_csv(index=False), media_type='text/csv') + return_plays = { 'count': all_plays.count(), 'plays': [model_to_dict(x, recurse=not short_output) for x in all_plays] @@ -263,6 +320,507 @@ async def get_plays( return return_plays +@router.get('/batting') +async def get_batting_totals( + season: list = Query(default=None), week: list = Query(default=None), position: list = Query(default=None), + player_id: list = Query(default=None), min_wpa: Optional[float] = -999, max_wpa: Optional[float] = 999, + group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league', 'gametype'] = 'player', + min_pa: Optional[int] = 1, team_id: list = Query(default=None), inning: list = Query(default=None), + obc: list = Query(default=None), risp: Optional[bool] = None, game_type: list = Query(default=None), + page_num: Optional[int] = 1, sort: Optional[str] = None, limit: Optional[int] = 500, + short_output: Optional[bool] = False, csv: Optional[bool] = False): + season_games = StratGame.select() + if season is not None: + season_games = season_games.where(StratGame.season << season) + if week is not None: + season_games = season_games.where(StratGame.week << week) + + bat_plays = ( + StratPlay + .select(StratPlay.batter, StratPlay.game, fn.SUM(StratPlay.pa).alias('sum_pa'), + fn.SUM(StratPlay.ab).alias('sum_ab'), fn.SUM(StratPlay.run).alias('sum_run'), + fn.SUM(StratPlay.hit).alias('sum_hit'), fn.SUM(StratPlay.rbi).alias('sum_rbi'), + fn.SUM(StratPlay.double).alias('sum_double'), fn.SUM(StratPlay.triple).alias('sum_triple'), + fn.SUM(StratPlay.homerun).alias('sum_hr'), fn.SUM(StratPlay.bb).alias('sum_bb'), + fn.SUM(StratPlay.so).alias('sum_so'), StratPlay.batter_team, + fn.SUM(StratPlay.hbp).alias('sum_hbp'), fn.SUM(StratPlay.sac).alias('sum_sac'), + fn.SUM(StratPlay.ibb).alias('sum_ibb'), fn.SUM(StratPlay.gidp).alias('sum_gidp'), + fn.SUM(StratPlay.sb).alias('sum_sb'), fn.SUM(StratPlay.cs).alias('sum_cs'), + fn.SUM(StratPlay.bphr).alias('sum_bphr'), fn.SUM(StratPlay.bpfo).alias('sum_bpfo'), + fn.SUM(StratPlay.bp1b).alias('sum_bp1b'), fn.SUM(StratPlay.bplo).alias('sum_bplo'), + fn.SUM(StratPlay.wpa).alias('sum_wpa'), + fn.COUNT(StratPlay.on_first_final).filter( + StratPlay.on_first_final.is_null(False) & (StratPlay.on_first_final != 4)).alias('count_lo1'), + fn.COUNT(StratPlay.on_second_final).filter( + StratPlay.on_second_final.is_null(False) & (StratPlay.on_second_final != 4)).alias('count_lo2'), + fn.COUNT(StratPlay.on_third_final).filter( + StratPlay.on_third_final.is_null(False) & (StratPlay.on_third_final != 4)).alias('count_lo3'), + fn.COUNT(StratPlay.on_first).filter(StratPlay.on_first.is_null(False)).alias('count_runner1'), + fn.COUNT(StratPlay.on_second).filter(StratPlay.on_second.is_null(False)).alias('count_runner2'), + fn.COUNT(StratPlay.on_third).filter(StratPlay.on_third.is_null(False)).alias('count_runner3'), + fn.COUNT(StratPlay.on_first_final).filter( + StratPlay.on_first_final.is_null(False) & (StratPlay.on_first_final != 4) & + (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo1_3out'), + fn.COUNT(StratPlay.on_second_final).filter( + StratPlay.on_second_final.is_null(False) & (StratPlay.on_second_final != 4) & + (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo2_3out'), + fn.COUNT(StratPlay.on_third_final).filter( + StratPlay.on_third_final.is_null(False) & (StratPlay.on_third_final != 4) & + (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo3_3out') + # fn.COUNT(StratPlay.on_first).filter(StratPlay.on_first.is_null(False) & + # (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_runner1_3out'), + # fn.COUNT(StratPlay.on_second).filter(StratPlay.on_second.is_null(False) & + # (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_runner2_3out'), + # fn.COUNT(StratPlay.on_third).filter(StratPlay.on_third.is_null(False) & + # (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_runner3_3out') + ) + .where((StratPlay.game << season_games) & (StratPlay.batter.is_null(False))) + .having(fn.SUM(StratPlay.pa) >= min_pa) + ) + run_plays = ( + StratPlay + .select(StratPlay.runner, StratPlay.runner_team, fn.SUM(StratPlay.sb).alias('sum_sb'), + fn.SUM(StratPlay.cs).alias('sum_cs'), fn.SUM(StratPlay.pick_off).alias('sum_pick'), + fn.SUM(StratPlay.wpa).alias('sum_wpa')) + .where((StratPlay.game << season_games) & (StratPlay.runner.is_null(False))) + ) + + if player_id is not None: + all_players = Player.select().where(Player.id << player_id) + bat_plays = bat_plays.where(StratPlay.batter << all_players) + run_plays = run_plays.where(StratPlay.runner << all_players) + if team_id is not None: + all_teams = Team.select().where(Team.id << team_id) + bat_plays = bat_plays.where(StratPlay.batter_team << all_teams) + run_plays = run_plays.where(StratPlay.runner_team << all_teams) + if position is not None: + bat_plays = bat_plays.where(StratPlay.batter_pos << position) + + if obc is not None: + bat_plays = bat_plays.where(StratPlay.on_base_code << obc) + if risp is not None: + bat_plays = bat_plays.where(StratPlay.on_base_code << ['100', '101', '110', '111', '010', '011']) + if inning is not None: + bat_plays = bat_plays.where(StratPlay.inning_num << inning) + if game_type is not None: + all_types = [x.lower() for x in game_type] + all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types) + bat_plays = bat_plays.where(StratPlay.game << all_games) + run_plays = run_plays.where(StratPlay.game << all_games) + + if group_by is not None: + if group_by == 'player': + bat_plays = bat_plays.group_by(StratPlay.batter) + run_plays = run_plays.group_by(StratPlay.runner) + elif group_by == 'team': + bat_plays = bat_plays.group_by(StratPlay.batter_team) + run_plays = run_plays.group_by(StratPlay.runner_team) + elif group_by == 'playerteam': + bat_plays = bat_plays.group_by(StratPlay.batter, StratPlay.batter_team) + run_plays = run_plays.group_by(StratPlay.runner, StratPlay.runner_team) + elif group_by == 'playergame': + bat_plays = bat_plays.group_by(StratPlay.batter, StratPlay.game) + run_plays = run_plays.group_by(StratPlay.runner, StratPlay.game) + elif group_by == 'teamgame': + bat_plays = bat_plays.group_by(StratPlay.batter_team, StratPlay.game) + run_plays = run_plays.group_by(StratPlay.runner_team, StratPlay.game) + elif group_by == 'league': + bat_plays = bat_plays.join(StratGame) + bat_plays = bat_plays.group_by(StratPlay.game.season) + run_plays = run_plays.join(StratGame) + run_plays = run_plays.group_by(StratPlay.game.season) + elif group_by == 'gametype': + bat_plays = bat_plays.join(StratGame) + bat_plays = bat_plays.group_by(StratPlay.game.game_type) + run_plays = run_plays.join(StratGame) + run_plays = run_plays.group_by(StratPlay.game.game_type) + elif group_by == 'playerteamgametype': + bat_plays = bat_plays.join(StratGame) + bat_plays = bat_plays.group_by(StratPlay.batter, StratPlay.batter_team, StratPlay.game.game_type) + run_plays = run_plays.join(StratGame) + run_plays = run_plays.group_by(StratPlay.runner, StratPlay.runner_team, StratPlay.game.game_type) + + if sort is not None: + if sort == 'player': + bat_plays = bat_plays.order_by(StratPlay.batter) + run_plays = run_plays.order_by(StratPlay.runner) + elif sort == 'team': + bat_plays = bat_plays.order_by(StratPlay.batter_team) + run_plays = run_plays.order_by(StratPlay.runner_team) + elif sort == 'wpa-desc': + bat_plays = bat_plays.order_by(SQL('sum_wpa').desc()) + elif sort == 'wpa-asc': + bat_plays = bat_plays.order_by(SQL('sum_wpa').asc()) + elif sort == 'pa-desc': + bat_plays = bat_plays.order_by(SQL('sum_pa').desc()) + elif sort == 'pa-asc': + bat_plays = bat_plays.order_by(SQL('sum_pa').asc()) + elif sort == 'newest': + bat_plays = bat_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc()) + run_plays = run_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc()) + elif sort == 'oldest': + bat_plays = bat_plays.order_by(StratPlay.game_id, StratPlay.play_num) + run_plays = run_plays.order_by(StratPlay.game_id, StratPlay.play_num) + + if limit < 1: + limit = 1 + elif limit > 500: + limit = 500 + bat_plays = bat_plays.paginate(page_num, limit) + + logging.info(f'bat_plays query: {bat_plays}') + logging.info(f'run_plays query: {run_plays}') + + return_stats = { + 'count': bat_plays.count(), + 'stats': [] + } + + for x in bat_plays: + this_run = run_plays.order_by(StratPlay.id) + if 'player' in group_by: + this_run = this_run.where(StratPlay.runner == x.batter) + if 'game' in group_by: + this_run = this_run.where(StratPlay.game == x.game) + + if this_run.count() > 0: + sum_sb = this_run[0].sum_sb + sum_cs = this_run[0].sum_cs + run_wpa = this_run[0].sum_wpa + else: + sum_sb = 0 + sum_cs = 0 + run_wpa = 0 + this_wpa = bat_plays.where( + (StratPlay.wpa >= min_wpa) & (StratPlay.wpa <= max_wpa) & (StratPlay.batter == x.batter) + ) + if this_wpa.count() > 0: + sum_wpa = this_wpa[0].sum_wpa + else: + sum_wpa = 0 + + tot_ab = x.sum_ab if x.sum_ab > 0 else 1 + obp = (x.sum_hit + x.sum_bb + x.sum_hbp + x.sum_ibb) / x.sum_pa + slg = (x.sum_hr * 4 + x.sum_triple * 3 + x.sum_double * 2 + + (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr)) / tot_ab + + this_game = 'TOT' + if group_by in ['playergame', 'teamgame']: + this_game = x.game_id if short_output else model_to_dict(x.game, recurse=False) + + lob_all_rate, lob_2outs_rate, rbi_rate = 0, 0, 0 + if x.count_runner1 + x.count_runner2 + x.count_runner3 > 0: + lob_all_rate = (x.count_lo1 + x.count_lo2 + x.count_lo3) / \ + (x.count_runner1 + x.count_runner2 + x.count_runner3) + rbi_rate = (x.sum_rbi - x.sum_hr) / (x.count_runner1 + x.count_runner2 + x.count_runner3) + + return_stats['stats'].append({ + 'player': x.batter_id if short_output else model_to_dict(x.batter, recurse=True, max_depth=1), + 'team': x.batter_team_id if short_output else model_to_dict(x.batter_team, recurse=True, max_depth=1), + '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': sum_sb, + 'cs': sum_cs, + 'bphr': x.sum_bphr, + 'bpfo': x.sum_bpfo, + 'bp1b': x.sum_bp1b, + 'bplo': x.sum_bplo, + 'wpa': sum_wpa + run_wpa, + 'avg': x.sum_hit / tot_ab, + 'obp': obp, + 'slg': slg, + 'ops': obp + slg, + 'woba': (.69 * x.sum_bb + .72 * x.sum_hbp + .89 * (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr) + + 1.27 * x.sum_double + 1.62 * x.sum_triple + 2.1 * x.sum_hr) / max(x.sum_pa - x.sum_ibb, 1), + 'game': this_game, + 'lob_all': x.count_lo1 + x.count_lo2 + x.count_lo3, + 'lob_all_rate': lob_all_rate, + 'lob_2outs': x.count_lo1_3out + x.count_lo2_3out + x.count_lo3_3out, + 'rbi%': rbi_rate + }) + + if csv: + return_vals = return_stats['stats'] + if len(return_vals) == 0: + return Response(content=pd.DataFrame().to_csv(index=False), media_type='text/csv') + + for x in return_vals: + x['player_id'] = x['player']['player_id'] + x['player_name'] = x['player']['p_name'] + x['player_cardset'] = x['player']['cardset']['name'] + x['team_id'] = x['team']['id'] + x['team_abbrev'] = x['team']['abbrev'] + del x['player'], x['team'] + + output = pd.DataFrame(return_vals) + first = ['player_id', 'player_name', 'player_cardset', 'team_id', 'team_abbrev'] + exclude = first + ['lob_all', 'lob_all_rate', 'lob_2outs', 'rbi%'] + output = output[first + [col for col in output.columns if col not in exclude]] + + db.close() + return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv') + + db.close() + return return_stats + + +@router.get('/pitching') +async def get_pitching_totals( + season: list = Query(default=None), week: list = Query(default=None), + s_type: Literal['regular', 'post', 'total', None] = None, player_id: list = Query(default=None), + group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league', 'gametype'] = 'player', + min_pa: Optional[int] = 1, team_id: list = Query(default=None), manager_id: list = Query(default=None), + obc: list = Query(default=None), risp: Optional[bool] = None, inning: list = Query(default=None), + page_num: Optional[int] = 1, game_type: list = Query(default=None), sort: Optional[str] = None, + limit: Optional[int] = 500, short_output: Optional[bool] = False, csv: Optional[bool] = False): + season_games = StratGame.select() + if season is not None: + season_games = season_games.where(StratGame.season << season) + if week is not None and s_type is not None: + raise HTTPException(status_code=400, detail=f'Week and s_type parameters cannot be used in the same query') + if week is not None: + season_games = season_games.where(StratGame.week << week) + if s_type is not None: + if s_type == 'regular': + season_games = season_games.where(StratGame.week <= 18) + elif s_type == 'post': + season_games = season_games.where(StratGame.week > 18) + if manager_id is not None: + season_games = season_games.where( + (StratGame.away_manager_id << manager_id) | (StratGame.home_manager_id << manager_id) + ) + + pit_plays = ( + StratPlay + .select(StratPlay.pitcher, StratPlay.pitcher_team, StratPlay.game, fn.SUM(StratPlay.pa).alias('sum_pa'), + fn.SUM(StratPlay.ab).alias('sum_ab'), fn.SUM(StratPlay.run).alias('sum_run'), + fn.SUM(StratPlay.hit).alias('sum_hit'), fn.SUM(StratPlay.rbi).alias('sum_rbi'), + fn.SUM(StratPlay.double).alias('sum_double'), fn.SUM(StratPlay.triple).alias('sum_triple'), + fn.SUM(StratPlay.homerun).alias('sum_hr'), fn.SUM(StratPlay.bb).alias('sum_bb'), + fn.SUM(StratPlay.so).alias('sum_so'), fn.SUM(StratPlay.wpa).alias('sum_wpa'), + fn.SUM(StratPlay.hbp).alias('sum_hbp'), fn.SUM(StratPlay.sac).alias('sum_sac'), + fn.SUM(StratPlay.ibb).alias('sum_ibb'), fn.SUM(StratPlay.gidp).alias('sum_gidp'), + fn.SUM(StratPlay.sb).alias('sum_sb'), fn.SUM(StratPlay.cs).alias('sum_cs'), + fn.SUM(StratPlay.bphr).alias('sum_bphr'), fn.SUM(StratPlay.bpfo).alias('sum_bpfo'), + fn.SUM(StratPlay.bp1b).alias('sum_bp1b'), fn.SUM(StratPlay.bplo).alias('sum_bplo'), + fn.SUM(StratPlay.wild_pitch).alias('sum_wp'), fn.SUM(StratPlay.balk).alias('sum_balk'), + fn.SUM(StratPlay.outs).alias('sum_outs'), fn.SUM(StratPlay.e_run).alias('sum_erun'), + fn.SUM(StratPlay.re24).alias('sum_re24'), + fn.COUNT(StratPlay.on_first_final).filter( + StratPlay.on_first_final.is_null(False) & (StratPlay.on_first_final != 4)).alias('count_lo1'), + fn.COUNT(StratPlay.on_second_final).filter( + StratPlay.on_second_final.is_null(False) & (StratPlay.on_second_final != 4)).alias('count_lo2'), + fn.COUNT(StratPlay.on_third_final).filter( + StratPlay.on_third_final.is_null(False) & (StratPlay.on_third_final != 4)).alias('count_lo3'), + fn.COUNT(StratPlay.on_first).filter(StratPlay.on_first.is_null(False)).alias('count_runner1'), + fn.COUNT(StratPlay.on_second).filter(StratPlay.on_second.is_null(False)).alias('count_runner2'), + fn.COUNT(StratPlay.on_third).filter(StratPlay.on_third.is_null(False)).alias('count_runner3'), + fn.COUNT(StratPlay.on_first_final).filter( + StratPlay.on_first_final.is_null(False) & (StratPlay.on_first_final != 4) & + (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo1_3out'), + fn.COUNT(StratPlay.on_second_final).filter( + StratPlay.on_second_final.is_null(False) & (StratPlay.on_second_final != 4) & + (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo2_3out'), + fn.COUNT(StratPlay.on_third_final).filter( + StratPlay.on_third_final.is_null(False) & (StratPlay.on_third_final != 4) & + (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo3_3out')) + .where((StratPlay.game << season_games) & (StratPlay.pitcher.is_null(False))) + .having(fn.SUM(StratPlay.pa) >= min_pa) + ) + all_dec = ( + Decision + .select(Decision.pitcher, fn.SUM(Decision.win).alias('sum_win'), fn.SUM(Decision.loss).alias('sum_loss'), + fn.SUM(Decision.hold).alias('sum_hold'), fn.SUM(Decision.is_save).alias('sum_save'), + fn.SUM(Decision.b_save).alias('sum_b_save'), fn.SUM(Decision.irunners).alias('sum_irunners'), + fn.SUM(Decision.irunners_scored).alias('sum_irun_scored'), + fn.SUM(Decision.is_start).alias('sum_gs'), fn.COUNT(Decision.game).alias('sum_game')) + .where(Decision.game << season_games) + ) + + if player_id is not None: + all_players = Player.select().where(Player.id << player_id) + pit_plays = pit_plays.where(StratPlay.pitcher << all_players) + if team_id is not None: + all_teams = Team.select().where(Team.id << team_id) + pit_plays = pit_plays.where(StratPlay.pitcher_team << all_teams) + + if obc is not None: + pit_plays = pit_plays.where(StratPlay.on_base_code << obc) + if risp is not None: + pit_plays = pit_plays.where(StratPlay.on_base_code << ['100', '101', '110', '111', '010', '011']) + if inning is not None: + pit_plays = pit_plays.where(StratPlay.inning_num << inning) + if game_type is not None: + all_types = [x.lower() for x in game_type] + all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types) + pit_plays = pit_plays.where(StratPlay.game << all_games) + + if group_by is not None: + if group_by == 'player': + pit_plays = pit_plays.group_by(StratPlay.pitcher) + elif group_by == 'team': + pit_plays = pit_plays.group_by(StratPlay.pitcher_team) + elif group_by == 'playerteam': + pit_plays = pit_plays.group_by(StratPlay.pitcher, StratPlay.pitcher_team) + elif group_by == 'playergame': + pit_plays = pit_plays.group_by(StratPlay.pitcher, StratPlay.game) + elif group_by == 'teamgame': + pit_plays = pit_plays.group_by(StratPlay.pitcher_team, StratPlay.game) + elif group_by == 'league': + pit_plays = pit_plays.join(StratGame) + pit_plays = pit_plays.group_by(StratPlay.game.season) + elif group_by == 'gametype': + pit_plays = pit_plays.join(StratGame) + pit_plays = pit_plays.group_by(StratPlay.game.game_type) + elif group_by == 'playerteamgametype': + pit_plays = pit_plays.join(StratGame) + pit_plays = pit_plays.group_by(StratPlay.pitcher, StratPlay.pitcher_team, StratPlay.game.game_type) + if sort is not None: + if sort == 'player': + pit_plays = pit_plays.order_by(StratPlay.pitcher) + elif sort == 'team': + pit_plays = pit_plays.order_by(StratPlay.pitcher_team) + elif sort == 'wpa-desc': + pit_plays = pit_plays.order_by(SQL('sum_wpa').asc()) # functions seem reversed since pitcher plays negative + elif sort == 'wpa-asc': + pit_plays = pit_plays.order_by(SQL('sum_wpa').desc()) + elif sort == 're24-desc': + pit_plays = pit_plays.order_by(SQL('sum_re24').asc()) # functions seem reversed since pitcher plays negative + elif sort == 're24-asc': + pit_plays = pit_plays.order_by(SQL('sum_re24').desc()) + elif sort == 'ip-desc': + pit_plays = pit_plays.order_by(SQL('sum_outs').desc()) + elif sort == 'ip-asc': + pit_plays = pit_plays.order_by(SQL('sum_outs').asc()) + elif sort == 'game-desc': + pit_plays = pit_plays.order_by(SQL('sum_game').desc()) + elif sort == 'game-asc': + pit_plays = pit_plays.order_by(SQL('sum_game').asc()) + elif sort == 'newest': + pit_plays = pit_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc()) + elif sort == 'oldest': + pit_plays = pit_plays.order_by(StratPlay.game_id, StratPlay.play_num) + + if limit < 1: + limit = 1 + elif limit > 500: + limit = 500 + pit_plays = pit_plays.paginate(page_num, limit) + + return_stats = { + 'count': pit_plays.count(), + 'stats': [] + } + + for x in pit_plays: + this_dec = all_dec.where(Decision.pitcher == x.pitcher) + tot_outs = x.sum_outs if x.sum_outs > 0 else 1 + obp = (x.sum_hit + x.sum_bb + x.sum_hbp + x.sum_ibb) / x.sum_pa + slg = (x.sum_hr * 4 + x.sum_triple * 3 + x.sum_double * 2 + + (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr)) / max(x.sum_ab, 1) + tot_bb = 0.1 if x.sum_bb == 0 else x.sum_bb + + this_game = 'TOT' + if group_by in ['playergame', 'teamgame']: + this_game = x.game_id if short_output else model_to_dict(x.game, recurse=False) + this_dec = all_dec.where((Decision.pitcher == x.pitcher) & (Decision.game == x.game)) + + lob_all_rate, lob_2outs_rate, rbi_rate = 0, 0, 0 + if x.count_runner1 + x.count_runner2 + x.count_runner3 > 0: + lob_all_rate = (x.count_lo1 + x.count_lo2 + x.count_lo3) / \ + (x.count_runner1 + x.count_runner2 + x.count_runner3) + rbi_rate = (x.sum_rbi - x.sum_hr) / (x.count_runner1 + x.count_runner2 + x.count_runner3) + + return_stats['stats'].append({ + 'player': x.pitcher_id if short_output else model_to_dict(x.pitcher), + 'team': x.pitcher_team_id if short_output else model_to_dict(x.pitcher_team), + 'tbf': x.sum_pa, + 'outs': x.sum_outs, + 'games': this_dec[0].sum_game, + 'gs': this_dec[0].sum_gs, + 'win': this_dec[0].sum_win, + 'loss': this_dec[0].sum_loss, + 'hold': this_dec[0].sum_hold, + 'save': this_dec[0].sum_save, + 'bsave': this_dec[0].sum_b_save, + 'ir': this_dec[0].sum_irunners, + 'ir_sc': this_dec[0].sum_irun_scored, + 'ab': x.sum_ab, + 'run': x.sum_run, + 'e_run': x.sum_erun, + 'hits': x.sum_hit, + '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, + 'wp': x.sum_wp, + 'balk': x.sum_balk, + 'wpa': x.sum_wpa * -1, + 're24': x.sum_re24 * -1, + 'era': (x.sum_erun * 27) / tot_outs, + 'whip': ((x.sum_bb + x.sum_hit + x.sum_ibb) * 3) / tot_outs, + 'avg': x.sum_hit / max(x.sum_ab, 1), + 'obp': obp, + 'slg': slg, + 'ops': obp + slg, + 'woba': (.69 * x.sum_bb + .72 * x.sum_hbp + .89 * (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr) + + 1.27 * x.sum_double + 1.62 * x.sum_triple + 2.1 * x.sum_hr) / max(x.sum_pa - x.sum_ibb, 1), + 'k/9': x.sum_so * 9 / (tot_outs / 3), + 'bb/9': x.sum_bb * 9 / (tot_outs / 3), + 'k/bb': x.sum_so / tot_bb, + 'game': this_game, + 'lob_2outs': x.count_lo1_3out + x.count_lo2_3out + x.count_lo3_3out, + 'rbi%': rbi_rate + }) + db.close() + + if csv: + return_vals = return_stats['stats'] + if len(return_vals) == 0: + return Response(content=pd.DataFrame().to_csv(index=False), media_type='text/csv') + + for x in return_vals: + x['player_id'] = x['player']['player_id'] + x['player_name'] = x['player']['p_name'] + x['player_cardset'] = x['player']['cardset']['name'] + x['team_id'] = x['team']['id'] + x['team_abbrev'] = x['team']['abbrev'] + del x['player'], x['team'] + + output = pd.DataFrame(return_vals) + first = ['player_id', 'player_name', 'player_cardset', 'team_id', 'team_abbrev'] + exclude = first + ['lob_2outs', 'rbi%'] + output = output[first + [col for col in output.columns if col not in exclude]] + + db.close() + return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv') + + return return_stats + + @router.get('/{play_id}') async def get_one_play(play_id: int): if StratPlay.get_or_none(StratPlay.id == play_id) is None: diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index da5b3fe..e7bade4 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -208,6 +208,7 @@ async def get_team_lineup(team_id: int, difficulty_name: str, pitcher_name: str, vr_ops = vr_ratings.obp + vr_ratings.slg return model_to_dict(vl_ratings), model_to_dict(vr_ratings), (vl_ops + vr_ops + min(vl_ops, vr_ops)) / 3 + # IDEA: Rank guys by their bat per-position and take the best one that meets a threshold of defensive ability for position in starting_nine.keys(): if position == 'DH': # all_bcards = BattingCard.select().where(BattingCard.player << legal_players) From 714345c58983f70b3148dd33f60d9be7c33c285f Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 22 Oct 2023 00:08:11 -0500 Subject: [PATCH 33/40] Decision endpoint functional --- app/db_engine.py | 20 +++- app/main.py | 3 +- app/routers_v2/decisions.py | 230 ++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 app/routers_v2/decisions.py diff --git a/app/db_engine.py b/app/db_engine.py index ac3dda2..9b13bed 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -802,7 +802,25 @@ class StratPlay(BaseModel): is_new_inning = BooleanField(default=False) -db.create_tables([StratGame, StratPlay]) +class Decision(BaseModel): + season = IntegerField() + game = ForeignKeyField(StratGame) + pitcher = ForeignKeyField(Player) + pitcher_team = ForeignKeyField(Team) + week = IntegerField(default=1) + win = IntegerField(default=0) + loss = IntegerField(default=0) + hold = IntegerField(default=0) + is_save = IntegerField(default=0) + b_save = IntegerField(default=0) + irunners = IntegerField(default=0) + irunners_scored = IntegerField(default=0) + rest_ip = FloatField(default=0.0) + rest_required = IntegerField(default=0) + is_start = BooleanField(default=False) + + +db.create_tables([StratGame, StratPlay, Decision]) db.close() diff --git a/app/main.py b/app/main.py index 295ab12..05dd9b9 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from fastapi import FastAPI # from fastapi.templating import Jinja2Templates from .routers_v2 import ( - current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, + current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, decisions, batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards, battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers, stratgame, stratplays) @@ -43,3 +43,4 @@ app.include_router(scouting.router) app.include_router(mlbplayers.router) app.include_router(stratgame.router) app.include_router(stratplays.router) +app.include_router(decisions.router) diff --git a/app/routers_v2/decisions.py b/app/routers_v2/decisions.py new file mode 100644 index 0000000..3dc2295 --- /dev/null +++ b/app/routers_v2/decisions.py @@ -0,0 +1,230 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Response +from typing import List, Optional, Literal +import copy +import logging +import pandas as pd +import pydantic + +from ..db_engine import db, Decision, StratGame, Player, model_to_dict, chunked, fn, Team +from ..dependencies import oauth2_scheme, valid_token, LOG_DATA + +logging.basicConfig( + filename=LOG_DATA['filename'], + format=LOG_DATA['format'], + level=LOG_DATA['log_level'] +) + +router = APIRouter( + prefix='/api/v2/decisions', + tags=['decisions'] +) + + +class DecisionModel(pydantic.BaseModel): + game_id: int + season: int + week: int + pitcher_id: int + pitcher_team_id: int + win: int = 0 + loss: int = 0 + hold: int = 0 + is_save: int = 0 + is_start: bool = False + b_save: int = 0 + irunners: int = 0 + irunners_scored: int = 0 + rest_ip: float = 0 + rest_required: int = 0 + + +class DecisionList(pydantic.BaseModel): + decisions: List[DecisionModel] + + +@router.get('') +async def get_decisions( + season: list = Query(default=None), week: list = Query(default=None), team_id: list = Query(default=None), + win: Optional[int] = None, loss: Optional[int] = None, hold: Optional[int] = None, save: Optional[int] = None, + b_save: Optional[int] = None, irunners: list = Query(default=None), irunners_scored: list = Query(default=None), + game_type: list = Query(default=None), + game_id: list = Query(default=None), player_id: list = Query(default=None), csv: Optional[bool] = False, + limit: Optional[int] = 100, page_num: Optional[int] = 1, short_output: Optional[bool] = False): + all_dec = Decision.select().order_by(-Decision.season, -Decision.week, -Decision.id) + + if season is not None: + all_dec = all_dec.where(Decision.season << season) + if week is not None: + all_dec = all_dec.where(Decision.week << week) + if game_id is not None: + all_dec = all_dec.where(Decision.game_id << game_id) + if player_id is not None: + all_dec = all_dec.where(Decision.pitcher_id << player_id) + if team_id is not None: + all_dec = all_dec.where(Decision.pitcher_team_id << team_id) + if win is not None: + all_dec = all_dec.where(Decision.win == win) + if loss is not None: + all_dec = all_dec.where(Decision.loss == loss) + if hold is not None: + all_dec = all_dec.where(Decision.hold == hold) + if save is not None: + all_dec = all_dec.where(Decision.save == save) + if b_save is not None: + all_dec = all_dec.where(Decision.b_save == b_save) + if irunners is not None: + all_dec = all_dec.where(Decision.irunners << irunners) + if irunners_scored is not None: + all_dec = all_dec.where(Decision.irunners_scored << irunners_scored) + + if game_type is not None: + all_types = [x.lower() for x in game_type] + all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types) + all_dec = all_dec.where(Decision.game << all_games) + if limit < 1: + limit = 1 + if limit > 100: + limit = 100 + all_dec = all_dec.paginate(page_num, limit) + + return_dec = { + 'count': all_dec.count(), + 'decisions': [model_to_dict(x, recurse=not short_output) for x in all_dec] + } + db.close() + + if csv: + return_vals = return_dec['decisions'] + if len(return_vals) == 0: + return Response(content=pd.DataFrame().to_csv(index=False), media_type='text/csv') + + for x in return_vals: + x['game_id'] = x['game']['id'] + x['game_type'] = x['game']['game_type'] + x['player_id'] = x['pitcher']['player_id'] + x['player_name'] = x['pitcher']['p_name'] + x['player_cardset'] = x['pitcher']['cardset']['name'] + x['team_id'] = x['pitcher_team']['id'] + x['team_abbrev'] = x['pitcher_team']['abbrev'] + del x['pitcher'], x['pitcher_team'], x['game'] + + output = pd.DataFrame(return_vals) + first = ['player_id', 'player_name', 'player_cardset', 'team_id', 'team_abbrev'] + exclude = first + ['lob_all', 'lob_all_rate', 'lob_2outs', 'rbi%'] + output = output[first + [col for col in output.columns if col not in exclude]] + + db.close() + return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv') + + return return_dec + + +@router.patch('/{decision_id}') +async def patch_decision( + decision_id: int, win: Optional[int] = None, loss: Optional[int] = None, hold: Optional[int] = None, + save: Optional[int] = None, b_save: Optional[int] = None, irunners: Optional[int] = None, + irunners_scored: Optional[int] = None, rest_ip: Optional[int] = None, rest_required: Optional[int] = None, + token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'patch_decision - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_dec = Decision.get_or_none(Decision.id == decision_id) + if this_dec is None: + db.close() + raise HTTPException(status_code=404, detail=f'Decision ID {decision_id} not found') + + if win is not None: + this_dec.win = win + if loss is not None: + this_dec.loss = loss + if hold is not None: + this_dec.hold = hold + if save is not None: + this_dec.is_save = save + if b_save is not None: + this_dec.b_save = b_save + if irunners is not None: + this_dec.irunners = irunners + if irunners_scored is not None: + this_dec.irunners_scored = irunners_scored + if rest_ip is not None: + this_dec.rest_ip = rest_ip + if rest_required is not None: + this_dec.rest_required = rest_required + + if this_dec.save() == 1: + d_result = model_to_dict(this_dec) + db.close() + return d_result + else: + db.close() + raise HTTPException(status_code=500, detail=f'Unable to patch decision {decision_id}') + + +@router.post('') +async def post_decisions(dec_list: DecisionList, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'post_decisions - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + new_dec = [] + for x in dec_list.decisions: + if StratGame.get_or_none(StratGame.id == x.game_id) is None: + raise HTTPException(status_code=404, detail=f'Game ID {x.game_id} not found') + if Player.get_or_none(Player.player_id == x.pitcher_id) is None: + raise HTTPException(status_code=404, detail=f'Player ID {x.pitcher_id} not found') + if Team.get_or_none(Team.id == x.pitcher_team_id) is None: + raise HTTPException(status_code=404, detail=f'Team ID {x.pitcher_team_id} not found') + + new_dec.append(x.dict()) + + with db.atomic(): + for batch in chunked(new_dec, 10): + Decision.insert_many(batch).on_conflict_replace().execute() + db.close() + + return f'Inserted {len(new_dec)} decisions' + + +@router.delete('/{decision_id}') +async def delete_decision(decision_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_decision - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_dec = Decision.get_or_none(Decision.id == decision_id) + if this_dec is None: + db.close() + raise HTTPException(status_code=404, detail=f'Decision ID {decision_id} not found') + + count = this_dec.delete_instance() + db.close() + + if count == 1: + return f'Decision {decision_id} has been deleted' + else: + raise HTTPException(status_code=500, detail=f'Decision {decision_id} could not be deleted') + + +@router.delete('/game/{game_id}') +async def delete_decisions_game(game_id: int, token: str = Depends(oauth2_scheme)): + if not valid_token(token): + logging.warning(f'delete_decisions_game - Bad Token: {token}') + raise HTTPException(status_code=401, detail='Unauthorized') + + this_game = StratGame.get_or_none(StratGame.id == game_id) + if not this_game: + db.close() + raise HTTPException(status_code=404, detail=f'Game ID {game_id} not found') + + count = Decision.delete().where(Decision.game == this_game).execute() + db.close() + + if count > 0: + return f'Deleted {count} decisions matching Game ID {game_id}' + else: + raise HTTPException(status_code=500, detail=f'No decisions matching Game ID {game_id} were deleted') + + + From 22bbb3e3bbdfc6949c74478a9166d2ffae9ba0fb Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 22 Oct 2023 13:53:27 -0500 Subject: [PATCH 34/40] Fix loop on batter cards --- app/card_creation.py | 105 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/app/card_creation.py b/app/card_creation.py index e0e98c2..c5e3a6b 100644 --- a/app/card_creation.py +++ b/app/card_creation.py @@ -1434,8 +1434,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions new_ratings.bp_homerun += r_val[0] full_log(new_ratings, card) + retries = 0 res_chances = data.hbp while res_chances > 0: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) r_val = assign_bchances(card, PlayResult(full_name='HBP', short_name='HBP'), ch) logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') @@ -1443,10 +1447,14 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions res_chances -= r_val[0] new_ratings.hbp += r_val[0] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.homerun while res_chances > 0: - if res_chances < 1: + if res_chances < 1 or retries > 0: if data.double_pull > 0: data.double_pull += res_chances elif data.double_two > 0: @@ -1504,10 +1512,14 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions data.triple -= r_val[1] new_ratings.triple += r_val[1] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.triple while res_chances > 0: - if res_chances < 1: + if res_chances < 1 or retries > 0: if data.double_pull > 0: data.double_pull += res_chances elif data.double_two > 0: @@ -1561,10 +1573,14 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions data.double_two -= r_val[1] new_ratings.double_two += r_val[1] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.double_three while res_chances > 0: - if res_chances < 1: + if res_chances < 1 or retries > 0: if data.double_pull > 0: data.double_pull += res_chances elif data.double_two > 0: @@ -1613,10 +1629,14 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions data.double_two -= r_val[1] new_ratings.double_two += r_val[1] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.double_pull while res_chances > 0: - if res_chances < 1: + if res_chances < 1 or retries > 0: if data.double_two > 0: data.double_two += res_chances elif data.single_two > 0: @@ -1661,10 +1681,14 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions data.double_two -= r_val[1] new_ratings.double_two += r_val[1] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.double_two while res_chances > 0: - if res_chances < 1: + if res_chances < 1 or retries > 0: if data.single_two > 0: data.single_two += res_chances elif data.single_center > 0: @@ -1703,10 +1727,14 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions data.single_two -= r_val[1] new_ratings.single_two += r_val[1] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.single_two while res_chances > 0: - if res_chances < 1: + if res_chances < 1 or retries > 0: if data.single_center > 0: data.single_center += res_chances elif data.single_one > 0: @@ -1746,10 +1774,14 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions data.lineout -= r_val[1] new_ratings.lineout += r_val[1] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.single_center while res_chances > 0: - if res_chances < 1: + if res_chances < 1 or retries > 0: if data.single_one > 0: data.single_one += res_chances elif data.walk > 0: @@ -1789,10 +1821,14 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions data.lineout -= r_val[1] new_ratings.lineout += r_val[1] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.single_one while res_chances > 0: - if res_chances < 1: + if res_chances < 1 or retries > 0: if data.walk > 0: data.walk += res_chances break @@ -1828,9 +1864,16 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions data.lineout -= r_val[1] new_ratings.lineout += r_val[1] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.walk while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) if data.strikeout > max(1 - ch, 0): secondary = PlayResult(full_name=f'strikeout', short_name=f'so') @@ -1846,9 +1889,16 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions data.strikeout -= r_val[1] new_ratings.strikeout += r_val[1] + if r_val[0] == 0: + retries += 1 + full_log(new_ratings, card) + retries = 0 res_chances = data.bp_single while res_chances > 0: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) r_val = assign_bchances(card, PLAY_RESULTS['bp-si'], ch) logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') @@ -1856,6 +1906,9 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions res_chances -= r_val[0] new_ratings.bp_single += r_val[0] + if r_val[0] == 0: + retries += 1 + # Special lomax result full_log(new_ratings, card) r_val = assign_bchances( @@ -1866,8 +1919,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions new_ratings.lineout += r_val[0] full_log(new_ratings, card) + retries = 0 res_chances = data.popout while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) this_if = '2b' if pref_mif == 'ss' else 'ss' r_val = assign_bchances( @@ -1885,8 +1942,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions new_ratings.popout += r_val[0] full_log(new_ratings, card) + retries = 0 res_chances = data.flyout_a while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) r_val = assign_bchances( card, PlayResult(full_name=f'fly (cf) A', short_name=f'fly (cf) A'), Decimal(math.floor(ch))) @@ -1900,8 +1961,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions new_ratings.flyout_a += r_val[0] full_log(new_ratings, card) + retries = 0 res_chances = data.flyout_lf_b while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) r_val = assign_bchances( card, PlayResult(full_name=f'fly (lf) B', short_name=f'fly (lf) B'), Decimal(math.floor(ch))) @@ -1915,8 +1980,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions new_ratings.flyout_lf_b += r_val[0] full_log(new_ratings, card) + retries = 0 res_chances = data.flyout_rf_b while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) r_val = assign_bchances( card, PlayResult(full_name=f'fly (rf) B', short_name=f'fly (rf) B'), Decimal(math.floor(ch))) @@ -1942,8 +2011,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions return '3b' if pref_mif == 'ss' else 'p' full_log(new_ratings, card) + retries = 0 res_chances = data.groundout_a while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + count_gb += 1 this_if = get_gb_if() ch = get_chances(res_chances) @@ -1960,8 +2033,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions new_ratings.groundout_a += r_val[0] full_log(new_ratings, card) + retries = 0 res_chances = data.groundout_b while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + count_gb += 1 this_if = get_gb_if() ch = get_chances(res_chances) @@ -1978,8 +2055,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions new_ratings.groundout_b += r_val[0] full_log(new_ratings, card) + retries = 0 res_chances = data.groundout_c while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + count_gb += 1 this_if = get_gb_if() ch = get_chances(res_chances) @@ -1996,8 +2077,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions new_ratings.groundout_c += r_val[0] full_log(new_ratings, card) + retries = 0 res_chances = data.lineout while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) this_if = '3b' if pref_mif == 'ss' else '1b' r_val = assign_bchances( @@ -2014,8 +2099,12 @@ def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions new_ratings.lineout += r_val[0] full_log(new_ratings, card) + retries = 0 res_chances = data.strikeout while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) r_val = assign_bchances( card, PlayResult(full_name=f'strikeout', short_name=f'strikeout'), Decimal(math.floor(ch))) From 23181d55e35039e38c29d2cd3434a801afb65001 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 22 Oct 2023 13:53:39 -0500 Subject: [PATCH 35/40] Add RP lookup --- app/routers_v2/teams.py | 134 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 2 deletions(-) diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index e7bade4..33aee77 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -1,8 +1,8 @@ from datetime import datetime import pandas as pd -from fastapi import APIRouter, Depends, HTTPException, Response -from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Response, Query +from typing import Optional, Literal import logging import pydantic from pandas import DataFrame @@ -341,6 +341,29 @@ async def get_team_lineup(team_id: int, difficulty_name: str, pitcher_name: str, } +def sort_pitchers(pitching_card_query) -> DataFrame | None: + all_s = [model_to_dict(x, recurse=False) for x in pitching_card_query] + if len(all_s) == 0: + logging.error(f'Empty pitching_card_query: {pitching_card_query}') + return None + + pitcher_df = pd.DataFrame(all_s).set_index('player', drop=False) + logging.info(f'pitcher_df: {pitcher_df}') + + def get_total_ops(df_data): + vlval = PitchingCardRatings.get_or_none( + PitchingCardRatings.pitchingcard_id == df_data['id'], PitchingCardRatings.vs_hand == 'L') + vrval = PitchingCardRatings.get_or_none( + PitchingCardRatings.pitchingcard_id == df_data['id'], PitchingCardRatings.vs_hand == 'R') + + ops_vl = vlval.obp + vlval.slg + ops_vr = vrval.obp + vrval.slg + return (ops_vr + ops_vl + min(ops_vl, ops_vr)) / 3 + + pitcher_df['total_ops'] = pitcher_df.apply(get_total_ops, axis=1) + return pitcher_df.sort_values(by='total_ops') + + @router.get('/{team_id}/sp/{difficulty_name}') async def get_team_sp(team_id: int, difficulty_name: str, sp_rank: int): logging.info(f'get_team_sp - team_id: {team_id} / difficulty_name: {difficulty_name} / sp_rank: {sp_rank}') @@ -424,6 +447,113 @@ async def get_team_sp(team_id: int, difficulty_name: str, sp_rank: int): raise HTTPException(status_code=500, detail=f'No SP #{sp_rank} found for Team {team_id}') +@router.get('/{team_id}/rp/{difficulty_name}') +async def get_team_rp( + team_id: int, difficulty_name: str, need: Literal['length', 'setup', 'closer', 'middle'], + used_pitcher_ids: list = Query(default=[])): + logging.info(f'get_team_rp - team_id: {team_id} / difficulty_name: {difficulty_name} / need: {need} ' + f'/ used_pitcher_ids: {used_pitcher_ids}') + this_team = Team.get_or_none(Team.id == team_id) + if this_team is None: + db.close() + raise HTTPException(status_code=404, detail=f'Team id {team_id} not found') + + if difficulty_name not in CARDSETS.keys(): + db.close() + raise HTTPException(status_code=400, detail=f'Difficulty name {difficulty_name} not a valid check') + + all_players = Player.select().where( + (Player.mlbclub == this_team.lname) & (Player.player_id.not_in(used_pitcher_ids)) + ) + + legal_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['primary']) + if 'secondary' in CARDSETS[difficulty_name]: + backup_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['secondary']) + else: + backup_players = None + + if need == 'closer': + for query in [PitchingCard.select().join(Player).where( + (PitchingCard.player << legal_players) & (PitchingCard.closer_rating >= 3) & + (PitchingCard.starter_rating == 1)), + PitchingCard.select().join(Player).where( + (PitchingCard.player << legal_players) & (PitchingCard.closer_rating >= 1) & + (PitchingCard.starter_rating == 1)), + PitchingCard.select().join(Player).where( + (PitchingCard.player << backup_players) & (PitchingCard.closer_rating >= 3) & + (PitchingCard.starter_rating == 1)), + PitchingCard.select().join(Player).where( + (PitchingCard.player << backup_players) & (PitchingCard.closer_rating >= 1) & + (PitchingCard.starter_rating == 1)), + PitchingCard.select().join(Player).where( + (PitchingCard.player << backup_players) & (PitchingCard.starter_rating < 4)) + ]: + all_relievers = sort_pitchers(query) + + if all_relievers is not None: + this_player_id = all_relievers.iloc[0].player + this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False) + db.close() + return this_player + + elif need == 'setup': + for query in [PitchingCard.select().join(Player).where( + (PitchingCard.player << legal_players) & (PitchingCard.starter_rating == 1)), + PitchingCard.select().join(Player).where( + (PitchingCard.player << backup_players) & (PitchingCard.starter_rating < 4)) + ]: + all_relievers = sort_pitchers(query) + + if all_relievers is not None and len(all_relievers.index) >= 2: + this_player_id = all_relievers.iloc[1].player + this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False) + db.close() + return this_player + + elif need == 'length' or len(used_pitcher_ids) > 4: + for query in [PitchingCard.select().join(Player).where( + (PitchingCard.player << legal_players) & (PitchingCard.relief_rating >= 3) & + (PitchingCard.starter_rating < 4)), + PitchingCard.select().join(Player).where( + (PitchingCard.player << legal_players) & (PitchingCard.relief_rating >= 2) & + (PitchingCard.starter_rating < 4)), + PitchingCard.select().join(Player).where( + (PitchingCard.player << backup_players) & (PitchingCard.relief_rating >= 2) & + (PitchingCard.starter_rating < 4)) + ]: + all_relievers = sort_pitchers(query) + + if all_relievers is not None: + this_player_id = all_relievers.iloc[0].player + this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False) + db.close() + return this_player + + elif need == 'middle': + for query in [PitchingCard.select().join(Player).where( + (PitchingCard.player << legal_players) & (PitchingCard.starter_rating == 1)), + PitchingCard.select().join(Player).where( + (PitchingCard.player << backup_players) & (PitchingCard.starter_rating < 4)) + ]: + all_relievers = sort_pitchers(query) + + if all_relievers is not None and len(all_relievers.index) >= 3: + this_player_id = all_relievers.iloc[2].player + this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False) + db.close() + return this_player + + all_relievers = sort_pitchers( + PitchingCard.select().join(Player).where((PitchingCard.player << backup_players)) + ) + + if all_relievers is not None: + this_player_id = all_relievers.iloc[len(all_relievers.index) - 1].player + this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False) + db.close() + return this_player + + @router.get('/{team_id}/buy/players') async def team_buy_players(team_id: int, ids: str, ts: str): try: From 74a037b3fc6c0f2c2e803839caeb08e5b6826ead Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 22 Oct 2023 16:41:32 -0500 Subject: [PATCH 36/40] Update /games and /plays for sheets import --- app/routers_v2/stratgame.py | 11 ++++++++--- app/routers_v2/stratplays.py | 8 ++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/routers_v2/stratgame.py b/app/routers_v2/stratgame.py index 24fa89d..847f671 100644 --- a/app/routers_v2/stratgame.py +++ b/app/routers_v2/stratgame.py @@ -76,12 +76,17 @@ async def get_games( if csv: return_vals = [model_to_dict(x) for x in all_games] for x in return_vals: - x['away_team_name'] = x['away_team']['lname'] - x['home_team_name'] = x['home_team']['lname'] + x['away_abbrev'] = x['away_team']['abbrev'] + x['home_abbrev'] = x['home_team']['abbrev'] del x['away_team'], x['home_team'] db.close() - return Response(content=pd.DataFrame(return_vals).to_csv(index=False), media_type='text/csv') + output = pd.DataFrame(return_vals)[[ + 'id', 'away_abbrev', 'home_abbrev', 'away_score', 'home_score', 'away_team_value', 'home_team_value', + 'game_type', 'season', 'week', 'short_game', 'ranked' + ]] + + return Response(content=output.to_csv(index=False), media_type='text/csv') return_val = {'count': all_games.count(), 'games': [ model_to_dict(x, recurse=not short_output) for x in all_games diff --git a/app/routers_v2/stratplays.py b/app/routers_v2/stratplays.py index a952090..9a353d5 100644 --- a/app/routers_v2/stratplays.py +++ b/app/routers_v2/stratplays.py @@ -327,7 +327,7 @@ async def get_batting_totals( group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league', 'gametype'] = 'player', min_pa: Optional[int] = 1, team_id: list = Query(default=None), inning: list = Query(default=None), obc: list = Query(default=None), risp: Optional[bool] = None, game_type: list = Query(default=None), - page_num: Optional[int] = 1, sort: Optional[str] = None, limit: Optional[int] = 500, + page_num: Optional[int] = 1, sort: Optional[str] = 'pa-desc', limit: Optional[int] = 500, short_output: Optional[bool] = False, csv: Optional[bool] = False): season_games = StratGame.select() if season is not None: @@ -386,7 +386,7 @@ async def get_batting_totals( ) if player_id is not None: - all_players = Player.select().where(Player.id << player_id) + all_players = Player.select().where(Player.player_id << player_id) bat_plays = bat_plays.where(StratPlay.batter << all_players) run_plays = run_plays.where(StratPlay.runner << all_players) if team_id is not None: @@ -583,7 +583,7 @@ async def get_pitching_totals( group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league', 'gametype'] = 'player', min_pa: Optional[int] = 1, team_id: list = Query(default=None), manager_id: list = Query(default=None), obc: list = Query(default=None), risp: Optional[bool] = None, inning: list = Query(default=None), - page_num: Optional[int] = 1, game_type: list = Query(default=None), sort: Optional[str] = None, + page_num: Optional[int] = 1, game_type: list = Query(default=None), sort: Optional[str] = 'ip-desc', limit: Optional[int] = 500, short_output: Optional[bool] = False, csv: Optional[bool] = False): season_games = StratGame.select() if season is not None: @@ -650,7 +650,7 @@ async def get_pitching_totals( ) if player_id is not None: - all_players = Player.select().where(Player.id << player_id) + all_players = Player.select().where(Player.player_id << player_id) pit_plays = pit_plays.where(StratPlay.pitcher << all_players) if team_id is not None: all_teams = Team.select().where(Team.id << team_id) From a49861bf31fb101acb872e58cf6dac82b68dd429 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 22 Oct 2023 22:41:06 -0500 Subject: [PATCH 37/40] Added /games/game-summary --- app/routers_v2/stratplays.py | 126 ++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 2 deletions(-) diff --git a/app/routers_v2/stratplays.py b/app/routers_v2/stratplays.py index 9a353d5..93b8495 100644 --- a/app/routers_v2/stratplays.py +++ b/app/routers_v2/stratplays.py @@ -1,3 +1,5 @@ +import math + from fastapi import APIRouter, Depends, HTTPException, Query, Response from typing import List, Optional, Literal import logging @@ -348,7 +350,7 @@ async def get_batting_totals( fn.SUM(StratPlay.sb).alias('sum_sb'), fn.SUM(StratPlay.cs).alias('sum_cs'), fn.SUM(StratPlay.bphr).alias('sum_bphr'), fn.SUM(StratPlay.bpfo).alias('sum_bpfo'), fn.SUM(StratPlay.bp1b).alias('sum_bp1b'), fn.SUM(StratPlay.bplo).alias('sum_bplo'), - fn.SUM(StratPlay.wpa).alias('sum_wpa'), + fn.SUM(StratPlay.wpa).alias('sum_wpa'), fn.SUM(StratPlay.re24).alias('sum_re24'), fn.COUNT(StratPlay.on_first_final).filter( StratPlay.on_first_final.is_null(False) & (StratPlay.on_first_final != 4)).alias('count_lo1'), fn.COUNT(StratPlay.on_second_final).filter( @@ -381,7 +383,7 @@ async def get_batting_totals( StratPlay .select(StratPlay.runner, StratPlay.runner_team, fn.SUM(StratPlay.sb).alias('sum_sb'), fn.SUM(StratPlay.cs).alias('sum_cs'), fn.SUM(StratPlay.pick_off).alias('sum_pick'), - fn.SUM(StratPlay.wpa).alias('sum_wpa')) + fn.SUM(StratPlay.wpa).alias('sum_wpa'), fn.SUM(StratPlay.re24).alias('sum_re24')) .where((StratPlay.game << season_games) & (StratPlay.runner.is_null(False))) ) @@ -487,10 +489,12 @@ async def get_batting_totals( sum_sb = this_run[0].sum_sb sum_cs = this_run[0].sum_cs run_wpa = this_run[0].sum_wpa + run_re24 = this_run[0].sum_re24 else: sum_sb = 0 sum_cs = 0 run_wpa = 0 + run_re24 = 0 this_wpa = bat_plays.where( (StratPlay.wpa >= min_wpa) & (StratPlay.wpa <= max_wpa) & (StratPlay.batter == x.batter) ) @@ -538,6 +542,7 @@ async def get_batting_totals( 'bp1b': x.sum_bp1b, 'bplo': x.sum_bplo, 'wpa': sum_wpa + run_wpa, + 're24': x.sum_re24 + run_re24, 'avg': x.sum_hit / tot_ab, 'obp': obp, 'slg': slg, @@ -821,6 +826,123 @@ async def get_pitching_totals( return return_stats +@router.get('/game-summary/{game_id}') +async def get_game_summary( + game_id: int, csv: Optional[bool] = False, short_output: Optional[bool] = False, tp_max: Optional[int] = 1): + this_game = StratGame.get_or_none(StratGame.id == game_id) + if this_game is None: + db.close() + raise HTTPException(status_code=404, detail=f'Game {game_id} not found') + + game_plays = StratPlay.select().where(StratPlay.game_id == game_id) + all_hits = game_plays.where(StratPlay.hit == 1) + all_errors = game_plays.where(StratPlay.error == 1) + all_runs = game_plays.where(StratPlay.run == 1) + + all_dec = Decision.select().where(Decision.game_id == game_id) + winner = all_dec.where(Decision.win == 1) + loser = all_dec.where(Decision.loss == 1) + save_p = all_dec.where(Decision.is_save == 1) + save_pitcher = None + if save_p.count() > 0: + save_pitcher = model_to_dict(save_p.get().pitcher, recurse=not short_output) + + all_holds = all_dec.where(Decision.hold == 1) + all_bsaves = all_dec.where(Decision.b_save == 1) + + doubles = all_hits.where(StratPlay.double == 1) + triples = all_hits.where(StratPlay.triple == 1) + homeruns = all_hits.where((StratPlay.homerun == 1) | (StratPlay.bphr == 1)) + + steal_att = game_plays.where(StratPlay.runner.is_null(False)) + all_sb = steal_att.where(StratPlay.sb == 1) + all_cs = steal_att.where(StratPlay.cs == 1) + + top_batters = ( + StratPlay + .select(StratPlay.batter, fn.SUM(StratPlay.re24).alias('sum_re24'), fn.SUM(StratPlay.ab).alias('sum_ab'), + fn.SUM(StratPlay.run).alias('sum_run'), + fn.SUM(StratPlay.hit).alias('sum_hit'), fn.SUM(StratPlay.rbi).alias('sum_rbi'), + fn.SUM(StratPlay.double).alias('sum_double'), fn.SUM(StratPlay.triple).alias('sum_triple'), + fn.SUM(StratPlay.homerun).alias('sum_hr'), fn.SUM(StratPlay.bphr).alias('sum_bphr')) + .where(StratPlay.game_id == game_id) + .group_by(StratPlay.batter) + .order_by(SQL('sum_re24').desc()) + .limit(tp_max) + ) + top_pitchers = ( + StratPlay + .select(StratPlay.pitcher, fn.SUM(StratPlay.re24).alias('sum_re24'), fn.SUM(StratPlay.pa).alias('sum_pa'), + fn.SUM(StratPlay.outs).alias('sum_outs'), fn.SUM(StratPlay.e_run).alias('sum_erun'), + fn.SUM(StratPlay.run).alias('sum_run'), fn.SUM(StratPlay.so).alias('sum_so'), + fn.SUM(StratPlay.hit).alias('sum_hit')) + .where(StratPlay.game_id == game_id) + .group_by(StratPlay.pitcher) + .order_by(SQL('sum_re24').asc()) + .limit(tp_max) + ) + top_b = [{ + 'player': model_to_dict(x.batter, recurse=not short_output), + '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 + x.sum_bphr, + 're24': x.sum_re24 + } for x in top_batters] + top_p = [{ + 'player': model_to_dict(x.pitcher, recurse=not short_output), + 'tbf': x.sum_pa, + 'ip': math.floor(x.sum_outs / 3) + ((x.sum_outs % 3) * .1), + 'run': x.sum_run, + 'e_run': x.sum_erun, + 'hit': x.sum_hit, + 'so': x.sum_so, + 're24': x.sum_re24 * -1 + } for x in top_pitchers] + top_players = [*top_b, *top_p] + logging.info(f'top_players: {top_players}') + + return { + 'game': model_to_dict(this_game, recurse=not short_output), + 'teams': { + 'away': model_to_dict(this_game.away_team, recurse=not short_output), + 'home': model_to_dict(this_game.home_team, recurse=not short_output), + }, + 'runs': { + 'away': all_runs.where(StratPlay.batter_team == this_game.away_team).count(), + 'home': all_runs.where(StratPlay.batter_team == this_game.home_team).count() + }, + 'hits': { + 'away': all_hits.where(StratPlay.batter_team == this_game.away_team).count(), + 'home': all_hits.where(StratPlay.batter_team == this_game.home_team).count() + }, + 'errors': { + 'away': all_errors.where(StratPlay.defender_team == this_game.away_team).count(), + 'home': all_errors.where(StratPlay.defender_team == this_game.home_team).count() + }, + 'top-players': sorted(top_players, key=lambda x: x['re24'], reverse=True)[:tp_max], + 'pitchers': { + 'win': model_to_dict(winner.get().pitcher, recurse=not short_output), + 'loss': model_to_dict(loser.get().pitcher, recurse=not short_output), + 'holds': [model_to_dict(x.pitcher, recurse=not short_output) for x in all_holds], + 'save': save_pitcher, + 'b_saves': [model_to_dict(x.pitcher, recurse=not short_output) for x in all_bsaves] + }, + 'xbh': { + '2b': [model_to_dict(x.batter, recurse=not short_output) for x in doubles], + '3b': [model_to_dict(x.batter, recurse=not short_output) for x in triples], + 'hr': [model_to_dict(x.batter, recurse=not short_output) for x in homeruns] + }, + 'running': { + 'sb': [model_to_dict(x.runner, recurse=not short_output) for x in all_sb], + 'csc': [model_to_dict(x.catcher, recurse=not short_output) for x in all_cs] + } + } + + @router.get('/{play_id}') async def get_one_play(play_id: int): if StratPlay.get_or_none(StratPlay.id == play_id) is None: From e835b6e2f21d5c348ff3a2d6588dac7526d143eb Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 22 Oct 2023 23:21:02 -0500 Subject: [PATCH 38/40] Update gauntletrewards.py Fixed endpoint prefixes --- app/routers_v2/gauntletrewards.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/routers_v2/gauntletrewards.py b/app/routers_v2/gauntletrewards.py index ce80750..dcbaa72 100644 --- a/app/routers_v2/gauntletrewards.py +++ b/app/routers_v2/gauntletrewards.py @@ -30,7 +30,7 @@ class GauntletRewardList(pydantic.BaseModel): rewards: List[GauntletRewardModel] -@router.get('/api/v1/gauntletrewards') +@router.get('') async def v1_gauntletreward_get( name: Optional[str] = None, gauntlet_id: Optional[int] = None, reward_id: list = Query(default=None), win_num: Optional[int] = None, loss_max: Optional[int] = None): @@ -57,7 +57,7 @@ async def v1_gauntletreward_get( return return_val -@router.get('/api/v1/gauntletrewards/{gauntletreward_id}') +@router.get('/{gauntletreward_id}') async def v1_gauntletreward_get_one(gauntletreward_id): try: this_reward = GauntletReward.get_by_id(gauntletreward_id) @@ -70,7 +70,7 @@ async def v1_gauntletreward_get_one(gauntletreward_id): return return_val -@router.patch('/api/v1/gauntletrewards/{gauntletreward_id}') +@router.patch('/{gauntletreward_id}') async def v1_gauntletreward_patch( gauntletreward_id, name: Optional[str] = None, gauntlet_id: Optional[int] = None, reward_id: Optional[int] = None, win_num: Optional[int] = None, loss_max: Optional[int] = None, @@ -108,7 +108,7 @@ async def v1_gauntletreward_patch( raise DatabaseError(f'Unable to patch gauntlet reward {gauntletreward_id}') -@router.post('/api/v1/gauntletrewards') +@router.post('') async def v1_gauntletreward_post(gauntletreward: GauntletRewardList, token: str = Depends(oauth2_scheme)): if not valid_token(token): logging.warning(f'Bad Token: {token}') @@ -130,7 +130,7 @@ async def v1_gauntletreward_post(gauntletreward: GauntletRewardList, token: str return f'Inserted {len(all_rewards)} gauntlet rewards' -@router.delete('/api/v1/gauntletrewards/{gauntletreward_id}') +@router.delete('/{gauntletreward_id}') async def v1_gauntletreward_delete(gauntletreward_id): if GauntletReward.delete_by_id(gauntletreward_id) == 1: return f'Deleted gauntlet reward ID {gauntletreward_id}' From e8a5c6f5030ca757807a23279ead7abbc96461aa Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 23 Oct 2023 00:58:47 -0500 Subject: [PATCH 39/40] Added gauntlet cardset data --- app/db_engine.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/db_engine.py b/app/db_engine.py index 9b13bed..c5c283e 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -44,6 +44,10 @@ CARDSETS = { 'tens': { 'primary': [11, 7, 6, 12], # 2016, 2012, 2013, 2008, Mario 'secondary': [13, 5] # 2018, 2019 + }, + 'gauntlet-3': { + 'primary': [13, 9], # 2018, 2023 + 'secondary': [5, 11] # 2019, 2016 } } From 67555d3b5588a2fa6736456b6e0402d4b7ebd1ec Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 24 Oct 2023 10:18:11 -0500 Subject: [PATCH 40/40] Add gauntlet to player replacement check --- app/routers_v2/stratplays.py | 5 +++++ app/routers_v2/teams.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/routers_v2/stratplays.py b/app/routers_v2/stratplays.py index 93b8495..359cf98 100644 --- a/app/routers_v2/stratplays.py +++ b/app/routers_v2/stratplays.py @@ -457,6 +457,10 @@ async def get_batting_totals( bat_plays = bat_plays.order_by(SQL('sum_pa').desc()) elif sort == 'pa-asc': bat_plays = bat_plays.order_by(SQL('sum_pa').asc()) + elif sort == 're24-desc': + bat_plays = bat_plays.order_by(SQL('sum_re24').desc()) + elif sort == 're24-asc': + bat_plays = bat_plays.order_by(SQL('sum_re24').asc()) elif sort == 'newest': bat_plays = bat_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc()) run_plays = run_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc()) @@ -660,6 +664,7 @@ async def get_pitching_totals( if team_id is not None: all_teams = Team.select().where(Team.id << team_id) pit_plays = pit_plays.where(StratPlay.pitcher_team << all_teams) + all_dec = all_dec.where(Decision.pitcher_team << all_teams) if obc is not None: pit_plays = pit_plays.where(StratPlay.on_base_code << obc) diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index 33aee77..434960f 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -264,7 +264,7 @@ async def get_team_lineup(team_id: int, difficulty_name: str, pitcher_name: str, backup_group = CardPosition.select().where( (CardPosition.position == position) & (CardPosition.player << backup_players) ) - if difficulty_name == 'minor-league': + if difficulty_name in ['minor-league', 'gauntlet-3']: pos_group = pos_group.order_by(CardPosition.innings.desc()) elif d_rank == 10: pos_group = pos_group.order_by((CardPosition.range * 5) + CardPosition.error) @@ -274,7 +274,7 @@ async def get_team_lineup(team_id: int, difficulty_name: str, pitcher_name: str, pos_group = pos_group.order_by(CardPosition.error.desc()) logging.info(f'pos_group: {pos_group}\n{starting_nine}\n{player_names}\n\n') - if difficulty_name == 'minor-league': + if difficulty_name in ['minor-league', 'gauntlet-3']: for x in pos_group: logging.info(f'checking {x.player.p_name} for {position}') if x.player.p_name not in player_names and x.player.p_name.lower() != pitcher_name: @@ -543,8 +543,11 @@ async def get_team_rp( db.close() return this_player + logging.info(f'Falling to last chance pitcher') all_relievers = sort_pitchers( - PitchingCard.select().join(Player).where((PitchingCard.player << backup_players)) + PitchingCard.select().join(Player).where( + (PitchingCard.player << backup_players) | (PitchingCard.player << legal_players) + ) ) if all_relievers is not None: @@ -620,7 +623,8 @@ async def team_buy_players(team_id: int, ids: str, ts: str): created=int_timestamp(datetime.now()), title=f'Price Change', desc='Modified by buying and selling', - field_name=f'{this_player.description}', + field_name=f'{this_player.description} ' + f'{this_player.p_name if this_player.p_name not in this_player.description else ""}', message=f'From {buy_price}₼ 📈 to **{this_player.cost}**₼', about=f'Player-{this_player.player_id}' ) @@ -747,7 +751,8 @@ async def team_sell_cards(team_id: int, ids: str, ts: str): created=int_timestamp(datetime.now()), title=f'Price Change', desc='Modified by buying and selling', - field_name=f'{this_player.description}', + field_name=f'{this_player.description} ' + f'{this_player.p_name if this_player.p_name not in this_player.description else ""}', message=f'From {orig_price}₼ 📉 to **{this_player.cost}**₼', about=f'Player-{this_player.id}' )