diff --git a/commands/league/submit_scorecard.py b/commands/league/submit_scorecard.py index ef2aefb..e70c895 100644 --- a/commands/league/submit_scorecard.py +++ b/commands/league/submit_scorecard.py @@ -4,6 +4,7 @@ Scorecard Submission Commands Implements the /submit-scorecard command for submitting Google Sheets scorecards with play-by-play data, pitching decisions, and game results. """ + from typing import Optional, List import discord @@ -36,24 +37,18 @@ class SubmitScorecardCommands(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.logger = get_contextual_logger(f'{__name__}.SubmitScorecardCommands') + self.logger = get_contextual_logger(f"{__name__}.SubmitScorecardCommands") self.sheets_service = SheetsService() # Will use config automatically self.logger.info("SubmitScorecardCommands cog initialized") @app_commands.command( name="submit-scorecard", - description="Submit a Google Sheets scorecard with game results and play data" - ) - @app_commands.describe( - sheet_url="Full URL to the Google Sheets scorecard" + description="Submit a Google Sheets scorecard with game results and play data", ) + @app_commands.describe(sheet_url="Full URL to the Google Sheets scorecard") @app_commands.checks.has_any_role(get_config().sba_players_role_name) @logged_command("/submit-scorecard") - async def submit_scorecard( - self, - interaction: discord.Interaction, - sheet_url: str - ): + async def submit_scorecard(self, interaction: discord.Interaction, sheet_url: str): """ Submit scorecard with full transaction rollback support. @@ -97,7 +92,7 @@ class SubmitScorecardCommands(commands.Cog): setup_data = await self.sheets_service.read_setup_data(scorecard) # Validate scorecard version - if setup_data['version'] != current.bet_week: + if setup_data["version"] != current.bet_week: await interaction.edit_original_response( content=( f"❌ This scorecard appears out of date (version {setup_data['version']}, " @@ -113,12 +108,10 @@ class SubmitScorecardCommands(commands.Cog): ) away_team = await team_service.get_team_by_abbrev( - setup_data['away_team_abbrev'], - current.season + setup_data["away_team_abbrev"], current.season ) home_team = await team_service.get_team_by_abbrev( - setup_data['home_team_abbrev'], - current.season + setup_data["home_team_abbrev"], current.season ) if not away_team or not home_team: @@ -129,18 +122,15 @@ class SubmitScorecardCommands(commands.Cog): # Match managers away_manager = self._match_manager( - away_team, - setup_data['away_manager_name'] + away_team, setup_data["away_manager_name"] ) home_manager = self._match_manager( - home_team, - setup_data['home_manager_name'] + home_team, setup_data["home_manager_name"] ) # Phase 3: Permission Check user_team = await get_user_major_league_team( - interaction.user.id, - current.season + interaction.user.id, current.season ) if user_team is None: @@ -160,30 +150,26 @@ class SubmitScorecardCommands(commands.Cog): # Phase 4: Duplicate Game Check duplicate_game = await game_service.find_duplicate_game( current.season, - setup_data['week'], - setup_data['game_num'], + setup_data["week"], + setup_data["game_num"], away_team.id, - home_team.id + home_team.id, ) if duplicate_game: - view = ConfirmationView( - responders=[interaction.user], - timeout=30.0 - ) + view = ConfirmationView(responders=[interaction.user], timeout=30.0) await interaction.edit_original_response( content=( f"⚠️ This game has already been played!\n" f"Would you like me to wipe the old one and re-submit?" ), - view=view + view=view, ) await view.wait() if view.confirmed: await interaction.edit_original_response( - content="🗑️ Wiping old game data...", - view=None + content="🗑️ Wiping old game data...", view=None ) # Delete old data @@ -193,7 +179,9 @@ class SubmitScorecardCommands(commands.Cog): pass # May not exist try: - await decision_service.delete_decisions_for_game(duplicate_game.id) + await decision_service.delete_decisions_for_game( + duplicate_game.id + ) except: pass # May not exist @@ -202,16 +190,13 @@ class SubmitScorecardCommands(commands.Cog): else: await interaction.edit_original_response( content="❌ You think on it some more and get back to me later.", - view=None + view=None, ) return # Phase 5: Find Scheduled Game scheduled_game = await game_service.find_scheduled_game( - current.season, - setup_data['week'], - away_team.id, - home_team.id + current.season, setup_data["week"], away_team.id, home_team.id ) if not scheduled_game: @@ -234,7 +219,7 @@ class SubmitScorecardCommands(commands.Cog): # Add game_id to each play for play in plays_data: - play['game_id'] = game_id + play["game_id"] = game_id # Phase 7: POST Plays await interaction.edit_original_response( @@ -244,7 +229,9 @@ class SubmitScorecardCommands(commands.Cog): try: if not DRY_RUN: await play_service.create_plays_batch(plays_data) - self.logger.info(f'Posting plays_data (1 and 2): {plays_data[0]} / {plays_data[1]}') + self.logger.info( + f"Posting plays_data (1 and 2): {plays_data[0]} / {plays_data[1]}" + ) rollback_state = "PLAYS_POSTED" except APIException as e: await interaction.edit_original_response( @@ -269,14 +256,16 @@ class SubmitScorecardCommands(commands.Cog): if not DRY_RUN: await game_service.update_game_result( game_id, - box_score['away'][0], # Runs - box_score['home'][0], # Runs + box_score["away"][0], # Runs + box_score["home"][0], # Runs away_manager.id, home_manager.id, - setup_data['game_num'], - sheet_url + setup_data["game_num"], + sheet_url, ) - self.logger.info(f'Updating game ID {game_id}, {box_score['away'][0]} @ {box_score['home'][0]}, {away_manager.id} vs {home_manager.id}') + self.logger.info( + f"Updating game ID {game_id}, {box_score['away'][0]} @ {box_score['home'][0]}, {away_manager.id} vs {home_manager.id}" + ) rollback_state = "GAME_PATCHED" except APIException as e: # Rollback plays @@ -287,18 +276,21 @@ class SubmitScorecardCommands(commands.Cog): return # Phase 10: Read Pitching Decisions - decisions_data = await self.sheets_service.read_pitching_decisions(scorecard) + decisions_data = await self.sheets_service.read_pitching_decisions( + scorecard + ) # Add game metadata to each decision for decision in decisions_data: - decision['game_id'] = game_id - decision['season'] = current.season - decision['week'] = setup_data['week'] - decision['game_num'] = setup_data['game_num'] + decision["game_id"] = game_id + decision["season"] = current.season + decision["week"] = setup_data["week"] + decision["game_num"] = setup_data["game_num"] # Validate WP and LP exist and fetch Player objects - wp, lp, sv, holders, _blown_saves = \ + wp, lp, sv, holders, _blown_saves = ( await decision_service.find_winning_losing_pitchers(decisions_data) + ) if wp is None or lp is None: # Rollback @@ -333,9 +325,7 @@ class SubmitScorecardCommands(commands.Cog): return # Phase 12: Create Results Embed - await interaction.edit_original_response( - content="📰 Posting results..." - ) + await interaction.edit_original_response(content="📰 Posting results...") results_embed = await self._create_results_embed( away_team, @@ -348,7 +338,7 @@ class SubmitScorecardCommands(commands.Cog): lp, sv, holders, - game_id + game_id, ) # Phase 13: Post to News Channel @@ -356,13 +346,11 @@ class SubmitScorecardCommands(commands.Cog): self.bot, get_config().sba_network_news_channel, content=None, - embed=results_embed + embed=results_embed, ) # Phase 14: Recalculate Standings - await interaction.edit_original_response( - content="📊 Tallying standings..." - ) + await interaction.edit_original_response(content="📊 Tallying standings...") try: await standings_service.recalculate_standings(current.season) @@ -371,13 +359,11 @@ class SubmitScorecardCommands(commands.Cog): self.logger.error("Failed to recalculate standings") # Success! - await interaction.edit_original_response( - content="✅ You are all set!" - ) + await interaction.edit_original_response(content="✅ You are all set!") except Exception as e: # Unexpected error - attempt rollback - self.logger.error(f"Unexpected error in scorecard submission: {e}", exc_info=True) + self.logger.error(f"Unexpected error in scorecard submission: {e}", error=e) if rollback_state and game_id: try: @@ -421,7 +407,7 @@ class SubmitScorecardCommands(commands.Cog): lp: Optional[Player], sv: Optional[Player], holders: List[Player], - game_id: int + game_id: int, ): """ Create rich embed with game results. @@ -444,8 +430,8 @@ class SubmitScorecardCommands(commands.Cog): """ # Determine winner and loser - away_score = box_score['away'][0] - home_score = box_score['home'][0] + away_score = box_score["away"][0] + home_score = box_score["home"][0] if away_score > home_score: winning_team = away_team @@ -465,7 +451,7 @@ class SubmitScorecardCommands(commands.Cog): # Create embed embed = EmbedTemplate.create_base_embed( title=f"{winner_abbrev} defeats {loser_abbrev} {winner_score}-{loser_score}", - description=f"Season {current.season}, Week {setup_data['week']}, Game {setup_data['game_num']}" + description=f"Season {current.season}, Week {setup_data['week']}, Game {setup_data['game_num']}", ) embed.color = winning_team.get_color_int() if winning_team.thumbnail: @@ -498,13 +484,13 @@ class SubmitScorecardCommands(commands.Cog): decisions_text += f"**SV:** {sv.display_name}\n" if decisions_text: - embed.add_field(name="Pitching Decisions", value=decisions_text, inline=True) + embed.add_field( + name="Pitching Decisions", value=decisions_text, inline=True + ) # Add scorecard link embed.add_field( - name="Scorecard", - value=f"[View Full Scorecard]({sheet_url})", - inline=True + name="Scorecard", value=f"[View Full Scorecard]({sheet_url})", inline=True ) # Try to get key plays (non-critical) @@ -513,7 +499,9 @@ class SubmitScorecardCommands(commands.Cog): if key_plays: key_plays_text = format_key_plays(key_plays, away_team, home_team) if key_plays_text: - embed.add_field(name="Key Plays", value=key_plays_text, inline=False) + embed.add_field( + name="Key Plays", value=key_plays_text, inline=False + ) except Exception as e: self.logger.warning(f"Failed to get key plays: {e}") diff --git a/utils/logging.py b/utils/logging.py index ecb575c..2b16f26 100644 --- a/utils/logging.py +++ b/utils/logging.py @@ -4,6 +4,7 @@ Enhanced Logging Utilities Provides structured logging with contextual information for Discord bot debugging. Implements hybrid approach: human-readable console + structured JSON files. """ + import contextvars import json import logging @@ -13,66 +14,82 @@ from datetime import datetime from typing import Dict, Any, Optional, Union # Context variable for request tracking across async calls -log_context: contextvars.ContextVar[Dict[str, Any]] = contextvars.ContextVar('log_context', default={}) +log_context: contextvars.ContextVar[Dict[str, Any]] = contextvars.ContextVar( + "log_context", default={} +) -logger = logging.getLogger(f'{__name__}.logging_utils') +logger = logging.getLogger(f"{__name__}.logging_utils") JSONValue = Union[ - str, - int, - float, - bool, - None, - dict[str, Any], # nested object - list[Any] # arrays + str, int, float, bool, None, dict[str, Any], list[Any] # nested object # arrays ] class JSONFormatter(logging.Formatter): """Custom JSON formatter for structured file logging.""" - + def format(self, record) -> str: """Format log record as JSON with context information.""" # Base log object log_obj: dict[str, JSONValue] = { - 'timestamp': datetime.now().isoformat() + 'Z', - 'level': record.levelname, - 'logger': record.name, - 'message': record.getMessage() + "timestamp": datetime.now().isoformat() + "Z", + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), } - + # Add function/line info if available - if hasattr(record, 'funcName') and record.funcName: - log_obj['function'] = record.funcName - if hasattr(record, 'lineno') and record.lineno: - log_obj['line'] = record.lineno - + if hasattr(record, "funcName") and record.funcName: + log_obj["function"] = record.funcName + if hasattr(record, "lineno") and record.lineno: + log_obj["line"] = record.lineno + # Add exception info if present if record.exc_info: - log_obj['exception'] = { - 'type': record.exc_info[0].__name__ if record.exc_info[0] else 'Unknown', - 'message': str(record.exc_info[1]) if record.exc_info[1] else 'No message', - 'traceback': self.formatException(record.exc_info) + log_obj["exception"] = { + "type": ( + record.exc_info[0].__name__ if record.exc_info[0] else "Unknown" + ), + "message": ( + str(record.exc_info[1]) if record.exc_info[1] else "No message" + ), + "traceback": self.formatException(record.exc_info), } - + # Add context from contextvars context = log_context.get({}) if context: - log_obj['context'] = context.copy() - + log_obj["context"] = context.copy() + # Promote trace_id to standard key if available in context - if 'trace_id' in context: - log_obj['trace_id'] = context['trace_id'] - + if "trace_id" in context: + log_obj["trace_id"] = context["trace_id"] + # Add custom fields from extra parameter excluded_keys = { - 'name', 'msg', 'args', 'levelname', 'levelno', 'pathname', - 'filename', 'module', 'lineno', 'funcName', 'created', - 'msecs', 'relativeCreated', 'thread', 'threadName', - 'processName', 'process', 'getMessage', 'exc_info', - 'exc_text', 'stack_info' + "name", + "msg", + "args", + "levelname", + "levelno", + "pathname", + "filename", + "module", + "lineno", + "funcName", + "created", + "msecs", + "relativeCreated", + "thread", + "threadName", + "processName", + "process", + "getMessage", + "exc_info", + "exc_text", + "stack_info", } - + extra_data = {} for key, value in record.__dict__.items(): if key not in excluded_keys: @@ -82,56 +99,56 @@ class JSONFormatter(logging.Formatter): extra_data[key] = value except (TypeError, ValueError): extra_data[key] = str(value) - + if extra_data: - log_obj['extra'] = extra_data - - return json.dumps(log_obj, ensure_ascii=False) + '\n' + log_obj["extra"] = extra_data + + return json.dumps(log_obj, ensure_ascii=False) + "\n" class ContextualLogger: """ Logger wrapper that provides contextual information and structured logging. - + Automatically includes Discord context (user, guild, command) in all log messages. """ - + def __init__(self, logger_name: str): """ Initialize contextual logger. - + Args: logger_name: Name for the underlying logger """ self.logger = logging.getLogger(logger_name) self._start_time: Optional[float] = None - + def start_operation(self, operation_name: Optional[str] = None) -> str: """ Start timing an operation and generate a trace ID. - + Args: operation_name: Optional name for the operation being tracked - + Returns: Generated trace ID for this operation """ self._start_time = time.time() trace_id = str(uuid.uuid4())[:8] - + # Add trace_id to context current_context = log_context.get({}) - current_context['trace_id'] = trace_id + current_context["trace_id"] = trace_id if operation_name: - current_context['operation'] = operation_name + current_context["operation"] = operation_name log_context.set(current_context) - + return trace_id - + def end_operation(self, trace_id: str, operation_result: str = "completed") -> None: """ End an operation and log the final duration. - + Args: trace_id: The trace ID returned by start_operation operation_result: Result status (e.g., "completed", "failed", "cancelled") @@ -139,83 +156,99 @@ class ContextualLogger: if self._start_time is None: self.warning("end_operation called without corresponding start_operation") return - + duration_ms = int((time.time() - self._start_time) * 1000) - + # Get current context current_context = log_context.get({}) - + # Log operation completion - self.info(f"Operation {operation_result}", - trace_id=trace_id, - final_duration_ms=duration_ms, - operation_result=operation_result) - + self.info( + f"Operation {operation_result}", + trace_id=trace_id, + final_duration_ms=duration_ms, + operation_result=operation_result, + ) + # Clear operation-specific context - if 'operation' in current_context: - current_context.pop('operation', None) - if 'trace_id' in current_context and current_context['trace_id'] == trace_id: - current_context.pop('trace_id', None) + if "operation" in current_context: + current_context.pop("operation", None) + if "trace_id" in current_context and current_context["trace_id"] == trace_id: + current_context.pop("trace_id", None) log_context.set(current_context) - + # Reset start time self._start_time = None - + def _get_duration_ms(self) -> Optional[int]: """Get operation duration in milliseconds if start_operation was called.""" if self._start_time: return int((time.time() - self._start_time) * 1000) return None - + + @staticmethod + def _extract_logging_params(kwargs: dict) -> dict: + """Extract standard logging parameters from kwargs to prevent LogRecord conflicts. + + Python's LogRecord raises KeyError if reserved attributes like 'exc_info' or + 'stack_info' are passed via the 'extra' dict. This extracts them so they can + be passed as direct parameters to the underlying logger instead. + """ + return { + key: kwargs.pop(key) for key in ("exc_info", "stack_info") if key in kwargs + } + def debug(self, message: str, **kwargs): """Log debug message with context.""" duration = self._get_duration_ms() if duration is not None: - kwargs['duration_ms'] = duration - self.logger.debug(message, extra=kwargs) - + kwargs["duration_ms"] = duration + log_params = self._extract_logging_params(kwargs) + self.logger.debug(message, extra=kwargs, **log_params) + def info(self, message: str, **kwargs): """Log info message with context.""" duration = self._get_duration_ms() if duration is not None: - kwargs['duration_ms'] = duration - self.logger.info(message, extra=kwargs) - + kwargs["duration_ms"] = duration + log_params = self._extract_logging_params(kwargs) + self.logger.info(message, extra=kwargs, **log_params) + def warning(self, message: str, **kwargs): """Log warning message with context.""" duration = self._get_duration_ms() if duration is not None: - kwargs['duration_ms'] = duration - self.logger.warning(message, extra=kwargs) - + kwargs["duration_ms"] = duration + log_params = self._extract_logging_params(kwargs) + self.logger.warning(message, extra=kwargs, **log_params) + def error(self, message: str, error: Optional[Exception] = None, **kwargs): """ Log error message with context and exception information. - + Args: message: Error message error: Optional exception object - **kwargs: Additional context + **kwargs: Additional context (exc_info=True is supported) """ duration = self._get_duration_ms() if duration is not None: - kwargs['duration_ms'] = duration - + kwargs["duration_ms"] = duration + log_params = self._extract_logging_params(kwargs) + if error: - kwargs['error'] = { - 'type': type(error).__name__, - 'message': str(error) - } + kwargs["error"] = {"type": type(error).__name__, "message": str(error)} self.logger.error(message, exc_info=True, extra=kwargs) else: - self.logger.error(message, extra=kwargs) - + self.logger.error(message, extra=kwargs, **log_params) + def exception(self, message: str, **kwargs): """Log exception with full traceback and context.""" duration = self._get_duration_ms() if duration is not None: - kwargs['duration_ms'] = duration - self.logger.exception(message, extra=kwargs) + kwargs["duration_ms"] = duration + log_params = self._extract_logging_params(kwargs) + self.logger.exception(message, extra=kwargs, **log_params) def set_discord_context( @@ -224,45 +257,45 @@ def set_discord_context( guild_id: Optional[Union[str, int]] = None, channel_id: Optional[Union[str, int]] = None, command: Optional[str] = None, - **additional_context + **additional_context, ): """ Set Discord-specific context for logging. - + Args: interaction: Discord interaction object (will extract user/guild/channel) user_id: Discord user ID - guild_id: Discord guild ID + guild_id: Discord guild ID channel_id: Discord channel ID command: Command name (e.g., '/player') **additional_context: Any additional context to include """ context = log_context.get({}).copy() - + # Extract from interaction if provided if interaction: - context['user_id'] = str(interaction.user.id) + context["user_id"] = str(interaction.user.id) if interaction.guild: - context['guild_id'] = str(interaction.guild.id) - context['guild_name'] = interaction.guild.name + context["guild_id"] = str(interaction.guild.id) + context["guild_name"] = interaction.guild.name if interaction.channel: - context['channel_id'] = str(interaction.channel.id) - if hasattr(interaction, 'command') and interaction.command: - context['command'] = f"/{interaction.command.name}" - + context["channel_id"] = str(interaction.channel.id) + if hasattr(interaction, "command") and interaction.command: + context["command"] = f"/{interaction.command.name}" + # Override with explicit parameters if user_id: - context['user_id'] = str(user_id) + context["user_id"] = str(user_id) if guild_id: - context['guild_id'] = str(guild_id) + context["guild_id"] = str(guild_id) if channel_id: - context['channel_id'] = str(channel_id) + context["channel_id"] = str(channel_id) if command: - context['command'] = command - + context["command"] = command + # Add any additional context context.update(additional_context) - + log_context.set(context) @@ -274,11 +307,11 @@ def clear_context(): def get_contextual_logger(logger_name: str) -> ContextualLogger: """ Get a contextual logger instance. - + Args: logger_name: Name for the logger (typically __name__) - + Returns: ContextualLogger instance """ - return ContextualLogger(logger_name) \ No newline at end of file + return ContextualLogger(logger_name) diff --git a/views/players.py b/views/players.py index 1c0eafe..b5bed53 100644 --- a/views/players.py +++ b/views/players.py @@ -3,6 +3,7 @@ Player View Components Interactive Discord UI components for player information display with toggleable statistics. """ + from typing import Optional, TYPE_CHECKING import discord @@ -34,11 +35,11 @@ class PlayerStatsView(BaseView): def __init__( self, - player: 'Player', + player: "Player", season: int, - batting_stats: Optional['BattingStats'] = None, - pitching_stats: Optional['PitchingStats'] = None, - user_id: Optional[int] = None + batting_stats: Optional["BattingStats"] = None, + pitching_stats: Optional["PitchingStats"] = None, + user_id: Optional[int] = None, ): """ Initialize the player stats view. @@ -50,7 +51,9 @@ class PlayerStatsView(BaseView): pitching_stats: Pitching statistics (if available) user_id: Discord user ID who can interact with this view """ - super().__init__(timeout=300.0, user_id=user_id, logger_name=f'{__name__}.PlayerStatsView') + super().__init__( + timeout=300.0, user_id=user_id, logger_name=f"{__name__}.PlayerStatsView" + ) self.player = player self.season = season @@ -69,36 +72,37 @@ class PlayerStatsView(BaseView): self.remove_item(self.toggle_pitching_button) self.logger.debug("No pitching stats available, pitching button hidden") - self.logger.info("PlayerStatsView initialized", - player_id=player.id, - player_name=player.name, - season=season, - has_batting=bool(batting_stats), - has_pitching=bool(pitching_stats), - user_id=user_id) + self.logger.info( + "PlayerStatsView initialized", + player_id=player.id, + player_name=player.name, + season=season, + has_batting=bool(batting_stats), + has_pitching=bool(pitching_stats), + user_id=user_id, + ) @discord.ui.button( - label="Show Batting Stats", - style=discord.ButtonStyle.primary, - emoji="💥", - row=0 + label="Show Batting Stats", style=discord.ButtonStyle.primary, emoji="💥", row=0 ) async def toggle_batting_button( - self, - interaction: discord.Interaction, - button: discord.ui.Button + self, interaction: discord.Interaction, button: discord.ui.Button ): """Toggle batting statistics visibility.""" self.increment_interaction_count() self.show_batting = not self.show_batting # Update button label - button.label = "Hide Batting Stats" if self.show_batting else "Show Batting Stats" + button.label = ( + "Hide Batting Stats" if self.show_batting else "Show Batting Stats" + ) - self.logger.info("Batting stats toggled", - player_id=self.player.id, - show_batting=self.show_batting, - user_id=interaction.user.id) + self.logger.info( + "Batting stats toggled", + player_id=self.player.id, + show_batting=self.show_batting, + user_id=interaction.user.id, + ) # Rebuild and update embed await self._update_embed(interaction) @@ -107,24 +111,26 @@ class PlayerStatsView(BaseView): label="Show Pitching Stats", style=discord.ButtonStyle.primary, emoji="⚾", - row=0 + row=0, ) async def toggle_pitching_button( - self, - interaction: discord.Interaction, - button: discord.ui.Button + self, interaction: discord.Interaction, button: discord.ui.Button ): """Toggle pitching statistics visibility.""" self.increment_interaction_count() self.show_pitching = not self.show_pitching # Update button label - button.label = "Hide Pitching Stats" if self.show_pitching else "Show Pitching Stats" + button.label = ( + "Hide Pitching Stats" if self.show_pitching else "Show Pitching Stats" + ) - self.logger.info("Pitching stats toggled", - player_id=self.player.id, - show_pitching=self.show_pitching, - user_id=interaction.user.id) + self.logger.info( + "Pitching stats toggled", + player_id=self.player.id, + show_pitching=self.show_pitching, + user_id=interaction.user.id, + ) # Rebuild and update embed await self._update_embed(interaction) @@ -143,20 +149,24 @@ class PlayerStatsView(BaseView): # Update the message with new embed await interaction.response.edit_message(embed=embed, view=self) - self.logger.debug("Embed updated successfully", - show_batting=self.show_batting, - show_pitching=self.show_pitching) + self.logger.debug( + "Embed updated successfully", + show_batting=self.show_batting, + show_pitching=self.show_pitching, + ) except Exception as e: - self.logger.error("Failed to update embed", error=e, exc_info=True) + self.logger.error("Failed to update embed", error=e) # Try to send error message try: error_embed = EmbedTemplate.error( title="Update Failed", - description="Failed to update player statistics. Please try again." + description="Failed to update player statistics. Please try again.", + ) + await interaction.response.send_message( + embed=error_embed, ephemeral=True ) - await interaction.response.send_message(embed=error_embed, ephemeral=True) except Exception: self.logger.error("Failed to send error message", exc_info=True) @@ -181,23 +191,16 @@ class PlayerStatsView(BaseView): # Add injury indicator emoji if player is injured title = f"🤕 {player.name}" if player.il_return is not None else player.name - embed = EmbedTemplate.create_base_embed( - title=title, - color=embed_color - ) + embed = EmbedTemplate.create_base_embed(title=title, color=embed_color) # Basic info section (always visible) - embed.add_field( - name="Position", - value=player.primary_position, - inline=True - ) + embed.add_field(name="Position", value=player.primary_position, inline=True) - if hasattr(player, 'team') and player.team: + if hasattr(player, "team") and player.team: embed.add_field( name="Team", value=f"{player.team.abbrev} - {player.team.sname}", - inline=True + inline=True, ) # Add Major League affiliate if this is a Minor League team @@ -205,55 +208,33 @@ class PlayerStatsView(BaseView): major_affiliate = await player.team.major_league_affiliate() if major_affiliate: embed.add_field( - name="Major Affiliate", - value=major_affiliate, - inline=True + name="Major Affiliate", value=major_affiliate, inline=True ) - embed.add_field( - name="sWAR", - value=f"{player.wara:.2f}", - inline=True - ) + embed.add_field(name="sWAR", value=f"{player.wara:.2f}", inline=True) - embed.add_field( - name="Player ID", - value=str(player.id), - inline=True - ) + embed.add_field(name="Player ID", value=str(player.id), inline=True) # All positions if multiple if len(player.positions) > 1: embed.add_field( - name="Positions", - value=", ".join(player.positions), - inline=True + name="Positions", value=", ".join(player.positions), inline=True ) - embed.add_field( - name="Season", - value=str(season), - inline=True - ) + embed.add_field(name="Season", value=str(season), inline=True) # Always show injury rating embed.add_field( - name="Injury Rating", - value=player.injury_rating or "N/A", - inline=True + name="Injury Rating", value=player.injury_rating or "N/A", inline=True ) # Show injury return date only if player is currently injured if player.il_return: - embed.add_field( - name="Injury Return", - value=player.il_return, - inline=True - ) + embed.add_field(name="Injury Return", value=player.il_return, inline=True) # Add batting stats if visible and available if self.show_batting and self.batting_stats: - embed.add_field(name='', value='', inline=False) + embed.add_field(name="", value="", inline=False) self.logger.debug("Adding batting statistics to embed") batting_stats = self.batting_stats @@ -269,11 +250,7 @@ class PlayerStatsView(BaseView): "╰─────────────╯\n" "```" ) - embed.add_field( - name="Rate Stats", - value=rate_stats, - inline=True - ) + embed.add_field(name="Rate Stats", value=rate_stats, inline=True) count_stats = ( "```\n" @@ -288,15 +265,11 @@ class PlayerStatsView(BaseView): "╰───────────╯\n" "```" ) - embed.add_field( - name='Counting Stats', - value=count_stats, - inline=True - ) + embed.add_field(name="Counting Stats", value=count_stats, inline=True) # Add pitching stats if visible and available if self.show_pitching and self.pitching_stats: - embed.add_field(name='', value='', inline=False) + embed.add_field(name="", value="", inline=False) self.logger.debug("Adding pitching statistics to embed") pitching_stats = self.pitching_stats @@ -313,11 +286,7 @@ class PlayerStatsView(BaseView): "╰─────────────╯\n" "```" ) - embed.add_field( - name="Record Stats", - value=record_stats, - inline=True - ) + embed.add_field(name="Record Stats", value=record_stats, inline=True) strikeout_stats = ( "```\n" @@ -329,11 +298,7 @@ class PlayerStatsView(BaseView): "╰──────────╯\n" "```" ) - embed.add_field( - name='Counting Stats', - value=strikeout_stats, - inline=True - ) + embed.add_field(name="Counting Stats", value=strikeout_stats, inline=True) # Add a note if no stats are visible if not self.show_batting and not self.show_pitching: @@ -341,37 +306,46 @@ class PlayerStatsView(BaseView): embed.add_field( name="📊 Statistics", value="Click the buttons below to show statistics.", - inline=False + inline=False, ) else: embed.add_field( name="📊 Statistics", value="No statistics available for this season.", - inline=False + inline=False, ) # Set player card as main image if player.image: embed.set_image(url=player.image) - self.logger.debug("Player card image added to embed", image_url=player.image) + self.logger.debug( + "Player card image added to embed", image_url=player.image + ) # Set thumbnail with priority: fancycard → headshot → team logo thumbnail_url = None thumbnail_source = None - if hasattr(player, 'vanity_card') and player.vanity_card: + if hasattr(player, "vanity_card") and player.vanity_card: thumbnail_url = player.vanity_card thumbnail_source = "fancycard" - elif hasattr(player, 'headshot') and player.headshot: + elif hasattr(player, "headshot") and player.headshot: thumbnail_url = player.headshot thumbnail_source = "headshot" - elif hasattr(player, 'team') and player.team and hasattr(player.team, 'thumbnail') and player.team.thumbnail: + elif ( + hasattr(player, "team") + and player.team + and hasattr(player.team, "thumbnail") + and player.team.thumbnail + ): thumbnail_url = player.team.thumbnail thumbnail_source = "team logo" if thumbnail_url: embed.set_thumbnail(url=thumbnail_url) - self.logger.debug(f"Thumbnail set from {thumbnail_source}", thumbnail_url=thumbnail_url) + self.logger.debug( + f"Thumbnail set from {thumbnail_source}", thumbnail_url=thumbnail_url + ) # Footer with player ID footer_text = f"Player ID: {player.id}"