major-domo-v2/utils/decorators.py
Cal Corum 7b41520054 CLAUDE: Major bot enhancements - Admin commands, player stats, standings, schedules
Major Features Added:
• Admin Management System: Complete admin command suite with user moderation, system control, and bot maintenance tools
• Enhanced Player Commands: Added batting/pitching statistics with concurrent API calls and improved embed design
• League Standings: Full standings system with division grouping, playoff picture, and wild card visualization
• Game Schedules: Comprehensive schedule system with team filtering, series organization, and proper home/away indicators

New Admin Commands (12 total):
• /admin-status, /admin-help, /admin-reload, /admin-sync, /admin-clear
• /admin-announce, /admin-maintenance
• /admin-timeout, /admin-untimeout, /admin-kick, /admin-ban, /admin-unban, /admin-userinfo

Enhanced Player Display:
• Team logo positioned beside player name using embed author
• Smart thumbnail priority: fancycard → headshot → team logo fallback
• Concurrent batting/pitching stats fetching for performance
• Rich statistics display with team colors and comprehensive metrics

New Models & Services:
• BattingStats, PitchingStats, TeamStandings, Division, Game models
• StatsService, StandingsService, ScheduleService for data management
• CustomCommand system with CRUD operations and cleanup tasks

Bot Architecture Improvements:
• Admin commands integrated into bot.py with proper loading
• Permission checks and safety guards for moderation commands
• Enhanced error handling and comprehensive audit logging
• All 227 tests passing with new functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 15:32:38 -05:00

264 lines
11 KiB
Python

"""
Decorators for Discord bot v2.0
This module provides decorators to reduce boilerplate code in Discord commands,
particularly for logging, error handling, and caching.
"""
import inspect
import logging
from functools import wraps
from typing import List, Optional, Callable, Any
from utils.logging import set_discord_context, get_contextual_logger
cache_logger = logging.getLogger(f'{__name__}.CacheDecorators')
def logged_command(
command_name: Optional[str] = None,
log_params: bool = True,
exclude_params: Optional[List[str]] = None
):
"""
Decorator for Discord commands that adds comprehensive logging.
This decorator automatically handles:
- Setting Discord context with interaction details
- Starting/ending operation timing
- Logging command start/completion/failure
- Preserving function metadata and signature
Args:
command_name: Override command name (defaults to function name with slashes)
log_params: Whether to log command parameters (default: True)
exclude_params: List of parameter names to exclude from logging
Example:
@logged_command("/roster", exclude_params=["sensitive_data"])
async def team_roster(self, interaction, team_name: str, season: int = None):
# Clean business logic only - no logging boilerplate needed
team = await team_service.find_team(team_name)
players = await team_service.get_roster(team.id, season)
embed = create_roster_embed(team, players)
await interaction.followup.send(embed=embed)
Side Effects:
- Automatically sets Discord context for all subsequent log entries
- Creates trace_id for request correlation
- Logs command execution timing and results
- Re-raises all exceptions after logging (preserves original behavior)
Requirements:
- The decorated class must have a 'logger' attribute, or one will be created
- Function must be an async method with (self, interaction, ...) signature
- Preserves Discord.py command registration compatibility
"""
def decorator(func):
@wraps(func)
async def wrapper(self, interaction, *args, **kwargs):
# Auto-detect command name if not provided
cmd_name = command_name or f"/{func.__name__.replace('_', '-')}"
# Build context with safe parameter logging
context = {"command": cmd_name}
if log_params:
sig = inspect.signature(func)
param_names = list(sig.parameters.keys())[2:] # Skip self, interaction
exclude_set = set(exclude_params or [])
for i, (name, value) in enumerate(zip(param_names, args)):
if name not in exclude_set:
context[f"param_{name}"] = value
set_discord_context(interaction=interaction, **context)
# Get logger from the class instance or create one
logger = getattr(self, 'logger', get_contextual_logger(f'{self.__class__.__module__}.{self.__class__.__name__}'))
trace_id = logger.start_operation(f"{func.__name__}_command")
try:
logger.info(f"{cmd_name} command started")
result = await func(self, interaction, *args, **kwargs)
logger.info(f"{cmd_name} command completed successfully")
logger.end_operation(trace_id, "completed")
return result
except Exception as e:
logger.error(f"{cmd_name} command failed", error=e)
logger.end_operation(trace_id, "failed")
# Re-raise to maintain original exception handling behavior
raise
# Preserve signature for Discord.py command registration
wrapper.__signature__ = inspect.signature(func) # type: ignore
return wrapper
return decorator
def cached_api_call(ttl: Optional[int] = None, cache_key_suffix: str = ""):
"""
Decorator to add Redis caching to service methods that return List[T].
This decorator will:
1. Check cache for existing data using generated key
2. Return cached data if found
3. Execute original method if cache miss
4. Cache the result for future calls
Args:
ttl: Time-to-live override in seconds (uses service default if None)
cache_key_suffix: Additional suffix for cache key differentiation
Usage:
@cached_api_call(ttl=600, cache_key_suffix="by_season")
async def get_teams_by_season(self, season: int) -> List[Team]:
# Original method implementation
Requirements:
- Method must be async
- Method must return List[T] where T is a model
- Class must have self.cache (CacheManager instance)
- Class must have self._generate_cache_key, self._get_cached_items, self._cache_items methods
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(self, *args, **kwargs) -> List[Any]:
# Check if caching is available (service has cache manager)
if not hasattr(self, 'cache') or not hasattr(self, '_generate_cache_key'):
# No caching available, execute original method
return await func(self, *args, **kwargs)
# Generate cache key from method name, args, and kwargs
method_name = f"{func.__name__}{cache_key_suffix}"
# Convert args and kwargs to params list for consistent cache key
sig = inspect.signature(func)
bound_args = sig.bind(self, *args, **kwargs)
bound_args.apply_defaults()
# Skip 'self' and convert to params format
params = []
for param_name, param_value in bound_args.arguments.items():
if param_name != 'self' and param_value is not None:
params.append((param_name, param_value))
cache_key = self._generate_cache_key(method_name, params)
# Try to get from cache
if hasattr(self, '_get_cached_items'):
cached_result = await self._get_cached_items(cache_key)
if cached_result is not None:
cache_logger.debug(f"Cache hit: {method_name}")
return cached_result
# Cache miss - execute original method
cache_logger.debug(f"Cache miss: {method_name}")
result = await func(self, *args, **kwargs)
# Cache the result if we have items and caching methods
if result and hasattr(self, '_cache_items'):
await self._cache_items(cache_key, result, ttl)
cache_logger.debug(f"Cached {len(result)} items for {method_name}")
return result
return wrapper
return decorator
def cached_single_item(ttl: Optional[int] = None, cache_key_suffix: str = ""):
"""
Decorator to add Redis caching to service methods that return Optional[T].
Similar to cached_api_call but for methods returning a single model instance.
Args:
ttl: Time-to-live override in seconds
cache_key_suffix: Additional suffix for cache key differentiation
Usage:
@cached_single_item(ttl=300, cache_key_suffix="by_id")
async def get_player(self, player_id: int) -> Optional[Player]:
# Original method implementation
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(self, *args, **kwargs) -> Optional[Any]:
# Check if caching is available
if not hasattr(self, 'cache') or not hasattr(self, '_generate_cache_key'):
return await func(self, *args, **kwargs)
# Generate cache key
method_name = f"{func.__name__}{cache_key_suffix}"
sig = inspect.signature(func)
bound_args = sig.bind(self, *args, **kwargs)
bound_args.apply_defaults()
params = []
for param_name, param_value in bound_args.arguments.items():
if param_name != 'self' and param_value is not None:
params.append((param_name, param_value))
cache_key = self._generate_cache_key(method_name, params)
# Try cache first
try:
cached_data = await self.cache.get(cache_key)
if cached_data:
cache_logger.debug(f"Cache hit: {method_name}")
return self.model_class.from_api_data(cached_data)
except Exception as e:
cache_logger.warning(f"Error reading single item cache for {cache_key}: {e}")
# Cache miss - execute original method
cache_logger.debug(f"Cache miss: {method_name}")
result = await func(self, *args, **kwargs)
# Cache the single result
if result:
try:
cache_data = result.model_dump()
await self.cache.set(cache_key, cache_data, ttl)
cache_logger.debug(f"Cached single item for {method_name}")
except Exception as e:
cache_logger.warning(f"Error caching single item for {cache_key}: {e}")
return result
return wrapper
return decorator
def cache_invalidate(*cache_patterns: str):
"""
Decorator to invalidate cache entries when data is modified.
Args:
cache_patterns: Cache key patterns to invalidate (supports prefix matching)
Usage:
@cache_invalidate("players_by_team", "teams_by_season")
async def update_player(self, player_id: int, updates: dict) -> Optional[Player]:
# Original method implementation
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(self, *args, **kwargs):
# Execute original method first
result = await func(self, *args, **kwargs)
# Invalidate specified cache patterns
if hasattr(self, 'cache'):
for pattern in cache_patterns:
try:
cleared = await self.cache.clear_prefix(f"sba:{self.endpoint}_{pattern}")
if cleared > 0:
cache_logger.info(f"Invalidated {cleared} cache entries for pattern: {pattern}")
except Exception as e:
cache_logger.warning(f"Error invalidating cache pattern {pattern}: {e}")
return result
return wrapper
return decorator