From f8aad38739cdf9dcb5148020211cd1e8dd040b23 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 23 Feb 2025 22:50:58 -0600 Subject: [PATCH] Update cache refresh logic to replace vs delete --- exceptions.py | 16 ++++ helpers.py | 2 +- in_game/gameplay_models.py | 3 +- in_game/gameplay_queries.py | 165 +++++++++++++++++++++++++++--------- migrations/env.py | 5 +- utilities/dropdown.py | 120 +++++++++++++++++--------- 6 files changed, 225 insertions(+), 86 deletions(-) diff --git a/exceptions.py b/exceptions.py index 2b6ae51..eb9bb5a 100644 --- a/exceptions.py +++ b/exceptions.py @@ -3,6 +3,22 @@ from typing import Literal logger = logging.getLogger('discord_app') + +def log_errors(func): + """ + This wrapper function will force all exceptions to be logged with executiona and stack info. + """ + + def wrap(*args, **kwargs): + try: + result = func(*args, **kwargs) + except Exception as e: + log_exception(e) + logger.info(func.__name__) + return result + + return wrap + def log_exception(e: Exception, msg: str = '', level: Literal['debug', 'error', 'info', 'warn'] = 'error'): if level == 'debug': logger.debug(msg, exc_info=True, stack_info=True) diff --git a/helpers.py b/helpers.py index 18ae284..e390e20 100644 --- a/helpers.py +++ b/helpers.py @@ -2438,7 +2438,7 @@ async def give_packs(team: dict, num_packs: int, pack_type: dict = None) -> dict def get_sheets(bot): try: - return bot.get_cog('Players').sheets + return bot.get_cog('Gameplay').sheets except Exception as e: logger.error(f'Could not grab sheets auth: {e}') raise ConnectionError(f'Bot has not authenticated with discord; please try again in 1 minute.') diff --git a/in_game/gameplay_models.py b/in_game/gameplay_models.py index d209843..d5e9d39 100644 --- a/in_game/gameplay_models.py +++ b/in_game/gameplay_models.py @@ -19,7 +19,8 @@ logger = logging.getLogger('discord_app') # sqlite_url = 'sqlite:///storage/gameplay.db' # connect_args = {"check_same_thread": False} # engine = create_engine(sqlite_url, echo=False, connect_args=connect_args) -engine = create_engine(f'postgresql://{os.getenv('DB_USERNAME')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_URL')}/{os.getenv('DB_NAME')}') +postgres_url = f'postgresql://{os.getenv('DB_USERNAME')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_URL')}/{os.getenv('DB_NAME')}' +engine = create_engine(postgres_url) CACHE_LIMIT = 1209600 # in seconds SBA_COLOR = 'a6ce39' SBA_LOGO = 'https://sombaseball.ddns.net/static/images/sba-logo.png' diff --git a/in_game/gameplay_queries.py b/in_game/gameplay_queries.py index 8edefe1..4bfe931 100644 --- a/in_game/gameplay_queries.py +++ b/in_game/gameplay_queries.py @@ -8,7 +8,7 @@ from sqlalchemy import func from api_calls import db_get, db_post from sqlmodel import col from in_game.gameplay_models import CACHE_LIMIT, BatterScouting, BatterScoutingBase, BattingCard, BattingCardBase, BattingRatings, BattingRatingsBase, Card, CardBase, Cardset, CardsetBase, GameCardsetLink, Lineup, PitcherScouting, PitchingCard, PitchingCardBase, PitchingRatings, PitchingRatingsBase, Player, PlayerBase, PositionRating, PositionRatingBase, RosterLink, Session, Team, TeamBase, select, or_, Game, Play -from exceptions import DatabaseError, PositionNotFoundException, log_exception, PlayNotFoundException +from exceptions import DatabaseError, PositionNotFoundException, log_errors, log_exception, PlayNotFoundException logger = logging.getLogger('discord_app') @@ -93,14 +93,14 @@ async def get_team_or_none( this_team = session.exec(select(Team).where(func.lower(Team.abbrev) == team_abbrev.lower())).one_or_none() if this_team is not None: - logger.debug(f'we found a team: {this_team} / created: {this_team.created}') + logger.info(f'we found a team: {this_team} / created: {this_team.created}') tdelta = datetime.datetime.now() - this_team.created - logger.debug(f'tdelta: {tdelta}') + logger.info(f'tdelta: {tdelta}') if tdelta.total_seconds() < CACHE_LIMIT: return this_team - else: - session.delete(this_team) - session.commit() + # else: + # session.delete(this_team) + # session.commit() def cache_team(json_data: dict) -> Team: logger.info(f'gameplay_queries - cache_team - writing a team to cache: {json_data}') @@ -108,10 +108,28 @@ async def get_team_or_none( logger.info(f'gameplay_queries - cache_team - valid_team: {valid_team}') db_team = Team.model_validate(valid_team) logger.info(f'gameplay_queries - cache_team - db_team: {db_team}') - session.add(db_team) - session.commit() - session.refresh(db_team) - return db_team + logger.info(f'Checking for existing team ID: {db_team.id}') + + try: + this_team = session.exec(select(Team).where(Team.id == db_team.id)).one() + logger.info(f'Found team: {this_team}\nUpdating with db team: {db_team}') + + for key, value in db_team.model_dump(exclude_unset=True).items(): + logger.info(f'Setting key ({key}) to value ({value})') + setattr(this_team, key, value) + + logger.info(f'Set this_team to db_team') + session.add(this_team) + session.commit() + logger.info(f'Refreshing this_team') + session.refresh(this_team) + return this_team + except: + logger.info(f'Team not found, adding to db') + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team if team_id is not None: t_query = await db_get('teams', object_id=team_id, params=[('inc_packs', include_packs)]) @@ -193,19 +211,35 @@ async def get_player_or_none(session: Session, player_id: int, skip_cache: bool if tdelta.total_seconds() < CACHE_LIMIT: logger.info(f'returning this player') return this_player - else: - logger.warning('Deleting old player record') - session.delete(this_player) - session.commit() + # else: + # logger.warning('Deleting old player record') + # session.delete(this_player) + # session.commit() def cache_player(json_data: dict) -> Player: logger.info(f'gameplay_models - get_player_or_none - cache_player - caching player data: {json_data}') valid_player = PlayerBase.model_validate(json_data, from_attributes=True) db_player = Player.model_validate(valid_player) - session.add(db_player) - session.commit() - session.refresh(db_player) - return db_player + + try: + this_player = session.get(Player, player_id) + logger.info(f'Found player: {this_player}\nUpdating with db_player: {db_player}') + + for key, value in db_player.model_dump(exclude_unset=True).items(): + logger.info(f'Setting key ({key}) to value ({value})') + setattr(this_player, key, value) + + logger.info(f'Set this_player to db_player') + session.add(this_player) + session.commit() + logger.info(f'Refreshing this_player') + session.refresh(this_player) + return this_player + except: + session.add(db_player) + session.commit() + session.refresh(db_player) + return db_player p_query = await db_get('players', object_id=player_id, params=[('inc_dex', False)]) if p_query is not None: @@ -426,10 +460,10 @@ async def get_or_create_ai_card(session: Session, player: Player, team: Team, sk if tdelta.total_seconds() < CACHE_LIMIT and (this_card.pitcherscouting is not None or this_card.batterscouting is not None): logger.info(f'returning this_card') return this_card - else: - logger.info(f'deleting card record') - session.delete(this_card) - session.commit() + # else: + # logger.info(f'deleting card record') + # session.delete(this_card) + # session.commit() async def pull_card(p: Player, t: Team): c_query = await db_get('cards', params=[('team_id', t.id), ('player_id', p.id)]) @@ -440,10 +474,29 @@ async def get_or_create_ai_card(session: Session, player: Player, team: Team, sk json_data['player_id'] = get_player_id_from_dict(json_data['player']) valid_card = CardBase.model_validate(c_query['cards'][0], from_attributes=True) db_card = Card.model_validate(valid_card) - session.add(db_card) - session.commit() - session.refresh(db_card) - return db_card + logger.info(f'gameplay_queries - cache_team - db_card: {db_card}') + logger.info(f'Checking for existing team ID: {db_card.id}') + + try: + this_card = session.exec(select(Card).where(Card.id == db_card.id)).one() + logger.info(f'Found card: {this_card}\nUpdating with db card: {db_card}') + + for key, value in db_card.model_dump(exclude_unset=True).items(): + logger.info(f'Setting key ({key}) to value ({value})') + setattr(this_card, key, value) + + logger.info(f'Set this_card to db_card') + session.add(this_card) + session.commit() + logger.info(f'Refreshing this_card') + session.refresh(this_card) + return this_card + except: + logger.info(f'Card not found, adding to db') + session.add(db_card) + session.commit() + session.refresh(db_card) + return db_card else: return None @@ -494,6 +547,7 @@ async def get_or_create_ai_card(session: Session, player: Player, team: Team, sk raise LookupError(err) +@log_errors async def get_card_or_none(session: Session, card_id: int, skip_cache: bool = False) -> Card | None: logger.info(f'Getting card {card_id} / skip_cache: {skip_cache}') if not skip_cache: @@ -506,28 +560,48 @@ async def get_card_or_none(session: Session, card_id: int, skip_cache: bool = Fa if tdelta.total_seconds() < CACHE_LIMIT and (this_card.pitcherscouting is not None or this_card.batterscouting is not None): logger.info(f'returning this_card') return this_card - else: - logger.info(f'deleting this_card') - try: - session.delete(this_card.batterscouting) - except Exception as e: - logger.error(f'Could not delete batter scouting: {e}') + # else: + # logger.info(f'deleting this_card') + # try: + # session.delete(this_card.batterscouting) + # except Exception as e: + # logger.error(f'Could not delete batter scouting: {e}') - try: - session.delete(this_card.pitcherscouting) - except Exception as e: - logger.error(f'Could not delete batter scouting: {e}') + # try: + # session.delete(this_card.pitcherscouting) + # except Exception as e: + # logger.error(f'Could not delete batter scouting: {e}') - session.delete(this_card) - session.commit() + # session.delete(this_card) + # session.commit() def cache_card(json_data: dict) -> Card: valid_card = CardBase.model_validate(json_data, from_attributes=True) db_card = Card.model_validate(valid_card) - session.add(db_card) - session.commit() - session.refresh(db_card) - return db_card + logger.info(f'gameplay_queries - cache_team - db_card: {db_card}') + logger.info(f'Checking for existing team ID: {db_card.id}') + + try: + this_card = session.exec(select(Card).where(Card.id == db_card.id)).one() + logger.info(f'Found card: {this_card}\nUpdating with db card: {db_card}') + + # this_team = db_team + for key, value in db_card.model_dump(exclude_unset=True).items(): + logger.info(f'Setting key ({key}) to value ({value})') + setattr(this_card, key, value) + + logger.info(f'Set this_card to db_card') + session.add(this_card) + session.commit() + logger.info(f'Refreshing this_card') + session.refresh(this_card) + return this_card + except: + logger.info(f'Card not found, adding to db') + session.add(db_card) + session.commit() + session.refresh(db_card) + return db_card c_query = await db_get('cards', object_id=card_id) if c_query is not None: @@ -545,15 +619,22 @@ async def get_card_or_none(session: Session, card_id: int, skip_cache: bool = Fa logger.info(f'Caching card ID {card_id} now') this_card = cache_card(c_query) + logger.info(f'Card is cached, checking for scouting') all_pos = [x for x in [this_player.pos_1, this_player.pos_2, this_player.pos_3, this_player.pos_3, this_player.pos_4, this_player.pos_5, this_player.pos_6, this_player.pos_7, this_player.pos_8] if x is not None] + logger.info(f'All positions: {all_pos}') if 'SP' in all_pos or 'RP' in all_pos: + logger.info(f'Pulling pitcher scouting') this_card.pitcherscouting = await shared_get_scouting(session, this_card, 'pitcher') if any(item in all_pos for item in ['DH', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']): + logger.info(f'Pulling batter scouting') this_card.batterscouting = await shared_get_scouting(session, this_card, 'batter') + logger.info(f'Updating this_card') session.add(this_card) session.commit() + logger.info(f'Refreshing this_card') session.refresh(this_card) + logger.info(f'this_card: {this_card}') return this_card diff --git a/migrations/env.py b/migrations/env.py index 3f64547..6bf223e 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -5,13 +5,14 @@ from sqlalchemy import pool from alembic import context -from in_game.gameplay_models import sqlite_url, Game +from in_game.gameplay_models import sqlite_url, postgres_url from sqlmodel import SQLModel # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -config.set_main_option('sqlalchemy.url', sqlite_url) +# config.set_main_option('sqlalchemy.url', sqlite_url) +config.set_main_option('sqlalchemy.url', postgres_url) # Interpret the config file for Python logging. # This line sets up loggers basically. diff --git a/utilities/dropdown.py b/utilities/dropdown.py index 15f517e..497b962 100644 --- a/utilities/dropdown.py +++ b/utilities/dropdown.py @@ -9,7 +9,7 @@ from discord.utils import MISSING from sqlmodel import Session from api_calls import db_delete, db_get, db_post -from exceptions import CardNotFoundException, LegalityCheckNotRequired, PlayNotFoundException, log_exception +from exceptions import CardNotFoundException, LegalityCheckNotRequired, PlayNotFoundException, PositionNotFoundException, log_exception, log_errors from helpers import get_card_embeds, random_insult from in_game.game_helpers import legal_check from in_game.gameplay_models import Game, Lineup, Play, Team @@ -262,14 +262,15 @@ class SelectReliefPitcher(discord.ui.Select): log_exception(e, 'Couldn\'t clean up after selecting rp') - +@log_errors class SelectSubPosition(discord.ui.Select): - def __init__(self, session: Session, this_lineup: Lineup, custom_id = ..., placeholder = None, options: List[SelectOption] = ..., responders: list[discord.User] = None): + def __init__(self, session: Session, this_lineup: Lineup, custom_id = MISSING, placeholder = None, options: List[SelectOption] = ..., responders: list[discord.User] = None): self.session = session self.this_lineup = this_lineup self.responders = responders super().__init__(custom_id=custom_id, placeholder=placeholder, min_values=1, max_values=1, options=options, disabled=False) + @log_errors async def callback(self, interaction: discord.Interaction): if self.responders is not None and interaction.user not in self.responders: await interaction.response.send_message( @@ -310,6 +311,7 @@ class SelectBatterSub(discord.ui.Select): self.responders = responders super().__init__(custom_id=custom_id, placeholder=placeholder, min_values=1, max_values=1, options=options) + @log_errors async def callback(self, interaction: discord.Interaction): if self.responders is not None and interaction.user not in self.responders: await interaction.response.send_message( @@ -322,16 +324,20 @@ class SelectBatterSub(discord.ui.Select): logger.info(f'Setting batter sub to Card ID: {self.values[0]}') # Get Human batter card + logger.info(f'Looking up card substitution') human_batter_card = await get_card_or_none(self.session, card_id=self.values[0]) if human_batter_card is None: log_exception(CardNotFoundException, f'Card ID {self.values[0]} not found') + logger.info(f'human_batter_card: {human_batter_card}') + logger.info(f'Checking team ownership of batter card') if human_batter_card.team_id != self.team.id: logger.error(f'Card_id {self.values[0]} does not belong to {self.team.abbrev} in Game {self.game.id}') await interaction.channel.send( f'Uh oh. Card ID {self.values[0]} is {human_batter_card.player.name} and belongs to {human_batter_card.team.sname}. Will you double check that before we get started?' ) return + logger.info(f'Ownership is good') # legal_data = await legal_check([self.values[0]], difficulty_name=self.league_name) # if not legal_data['legal']: @@ -340,6 +346,7 @@ class SelectBatterSub(discord.ui.Select): # ) # return + logger.info(f'Pulling current play to log subs') this_play = self.game.current_play_or_none(self.session) if this_play is None: log_exception(PlayNotFoundException, 'Play not found during substitution') @@ -360,10 +367,56 @@ class SelectBatterSub(discord.ui.Select): ) if same_position: + logger.info(f'same_position is True') position = last_lineup.position pos_text = '' view = None else: + logger.info(f'same_position is False') + position = 'PH' + pos_text = 'What position will they play?' + + logger.info(f'Deactivating last_lineup') + last_lineup.active = False + self.session.add(last_lineup) + logger.info(f'Set {last_lineup.card.player.name_with_desc} as inactive') + + logger.info(f'new position: {position}') + if position not in ['DH', 'PR', 'PH']: + logger.info(f'go get position rating') + try: + pos_rating = await get_position(self.session, human_batter_card, position) + except PositionNotFoundException as e: + await interaction.edit_original_response( + content=f'Uh oh, I cannot find {position} ratings for {human_batter_card.player.name_with_desc}. Please go double-check this sub and run again.', + view=None + ) + return + + logger.info(f'Creating new lineup record') + human_bat_lineup = Lineup( + team=self.team, + player=human_batter_card.player, + card=human_batter_card, + position=position, + batting_order=self.batting_order, + game=self.game, + after_play=max(this_play.play_num - 1, 0), + replacing_id=last_lineup.id + ) + logger.info(f'adding lineup to session: {human_bat_lineup}') + self.session.add(human_bat_lineup) + # self.session.commit() + + logger.info(f'Inserted {human_bat_lineup.card.player.name_with_desc} in the {self.batting_order} spot') + this_play.batter = human_bat_lineup + this_play.batter_pos = position + + logger.info(f'Adding play to session: {this_play}') + self.session.add(this_play) + self.session.commit() + + if not same_position: pos_dict_list = { 'Pinch Hitter': 'PH', 'Catcher': 'C', @@ -378,48 +431,35 @@ class SelectBatterSub(discord.ui.Select): 'Pitcher': 'P' } - position = 'PH' - pos_text = 'What position will they play?' - options=[SelectSubPosition(label=f'{x}', value=pos_dict_list[x], default=x=='Pinch Hitter', responders=[interaction.user]) for x in pos_dict_list] + logger.info(f'Prepping sub position') + pos_text = f'What position will {human_batter_card.player.name} play?' + options=[ + SelectOption( + label=f'{pos_name}', + value=pos_dict_list[pos_name], + default=pos_name=='Pinch Hitter' + ) + for pos_name in pos_dict_list.keys() + ] + logger.info(f'options: {options}') + view = SelectSubPosition( + self.session, + this_lineup=human_bat_lineup, + placeholder='Select Position', + options=options, + responders=[interaction.user] + ) - view = DropdownView(dropdown_objects=options) - - last_lineup.active = False - self.session.add(last_lineup) - logger.info(f'Set {last_lineup.card.player.name_with_desc} as inactive') - - if position not in ['DH', 'PR', 'PH']: - pos_rating = await get_position(self.session, human_batter_card, position) - - human_bat_lineup = Lineup( - team=self.team, - player=human_batter_card.player, - card=human_batter_card, - position=position, - batting_order=self.batting_order, - game=self.game, - after_play=max(this_play.play_num - 1, 0), - replacing_id=last_lineup.id - ) - logger.info(f'new lineup: {human_bat_lineup}') - self.session.add(human_bat_lineup) - # self.session.commit() - - try: - logger.info(f'Inserted {human_bat_lineup.card.player.name_with_desc} in the {self.batting_order} spot') - this_play.batter = human_bat_lineup - this_play.batter_pos = position - except Exception as e: - logger.error(e, exc_info=True, stack_info=True) - - self.session.add(this_play) - - self.session.commit() + logger.info(f'view: {view}') + logger.info(f'SelectSubPosition view is ready') + logger.info(f'Posting final sub message') + this_content = f'{human_batter_card.player.name_with_desc} has entered in the {self.batting_order} spot. {pos_text}' await interaction.edit_original_response( - content=f'{human_batter_card.player.name_with_desc} has entered in the {self.batting_order} spot. {pos_text}', + content=this_content, view=view ) + await interaction.channel.send(content='If you have additional subs to make, run `/substitution` again or run `/gamestate` to see the current plate appearance.') class SelectPokemonEvolution(discord.ui.Select):