major-domo-v2/services/schedule_service.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

257 lines
9.0 KiB
Python

"""
Schedule service for Discord Bot v2.0
Handles game schedule and results retrieval and processing.
"""
import logging
from typing import Optional, List, Dict, Tuple
from services.base_service import BaseService
from models.game import Game
from exceptions import APIException
logger = logging.getLogger(f'{__name__}.ScheduleService')
class ScheduleService:
"""
Service for schedule and game operations.
Features:
- Weekly schedule retrieval
- Team-specific schedules
- Game results and upcoming games
- Series organization
"""
def __init__(self):
"""Initialize schedule service."""
from api.client import get_global_client
self._get_client = get_global_client
logger.debug("ScheduleService initialized")
async def get_client(self):
"""Get the API client."""
return await self._get_client()
async def get_week_schedule(self, season: int, week: int) -> List[Game]:
"""
Get all games for a specific week.
Args:
season: Season number
week: Week number
Returns:
List of Game instances for the week
"""
try:
client = await self.get_client()
params = [
('season', str(season)),
('week', str(week))
]
response = await client.get('games', params=params)
if not response or 'games' not in response:
logger.warning(f"No games data found for season {season}, week {week}")
return []
games_list = response['games']
if not games_list:
logger.warning(f"Empty games list for season {season}, week {week}")
return []
# Convert to Game objects
games = []
for game_data in games_list:
try:
game = Game.from_api_data(game_data)
games.append(game)
except Exception as e:
logger.error(f"Error parsing game data: {e}")
continue
logger.info(f"Retrieved {len(games)} games for season {season}, week {week}")
return games
except Exception as e:
logger.error(f"Error getting week schedule for season {season}, week {week}: {e}")
return []
async def get_team_schedule(self, season: int, team_abbrev: str, weeks: Optional[int] = None) -> List[Game]:
"""
Get schedule for a specific team.
Args:
season: Season number
team_abbrev: Team abbreviation (e.g., 'NYY')
weeks: Number of weeks to retrieve (None for all weeks)
Returns:
List of Game instances for the team
"""
try:
team_games = []
team_abbrev_upper = team_abbrev.upper()
# If weeks not specified, try a reasonable range (18 weeks typical)
week_range = range(1, (weeks + 1) if weeks else 19)
for week in week_range:
week_games = await self.get_week_schedule(season, week)
# Filter games involving this team
for game in week_games:
if (game.away_team.abbrev.upper() == team_abbrev_upper or
game.home_team.abbrev.upper() == team_abbrev_upper):
team_games.append(game)
logger.info(f"Retrieved {len(team_games)} games for team {team_abbrev}")
return team_games
except Exception as e:
logger.error(f"Error getting team schedule for {team_abbrev}: {e}")
return []
async def get_recent_games(self, season: int, weeks_back: int = 2) -> List[Game]:
"""
Get recently completed games.
Args:
season: Season number
weeks_back: Number of weeks back to look
Returns:
List of completed Game instances
"""
try:
recent_games = []
# Get games from recent weeks
for week_offset in range(weeks_back):
# This is simplified - in production you'd want to determine current week
week = 10 - week_offset # Assuming we're around week 10
if week <= 0:
break
week_games = await self.get_week_schedule(season, week)
# Only include completed games
completed_games = [game for game in week_games if game.is_completed]
recent_games.extend(completed_games)
# Sort by week descending (most recent first)
recent_games.sort(key=lambda x: (x.week, x.game_num or 0), reverse=True)
logger.debug(f"Retrieved {len(recent_games)} recent games")
return recent_games
except Exception as e:
logger.error(f"Error getting recent games: {e}")
return []
async def get_upcoming_games(self, season: int, weeks_ahead: int = 6) -> List[Game]:
"""
Get upcoming scheduled games by scanning multiple weeks.
Args:
season: Season number
weeks_ahead: Number of weeks to scan ahead (default 6)
Returns:
List of upcoming Game instances
"""
try:
upcoming_games = []
# Scan through weeks to find games without scores
for week in range(1, 19): # Standard season length
week_games = await self.get_week_schedule(season, week)
# Find games without scores (not yet played)
upcoming_games_week = [game for game in week_games if not game.is_completed]
upcoming_games.extend(upcoming_games_week)
# If we found upcoming games, we can limit how many more weeks to check
if upcoming_games and len(upcoming_games) >= 20: # Reasonable limit
break
# Sort by week, then game number
upcoming_games.sort(key=lambda x: (x.week, x.game_num or 0))
logger.debug(f"Retrieved {len(upcoming_games)} upcoming games")
return upcoming_games
except Exception as e:
logger.error(f"Error getting upcoming games: {e}")
return []
async def get_series_by_teams(self, season: int, week: int, team1_abbrev: str, team2_abbrev: str) -> List[Game]:
"""
Get all games in a series between two teams for a specific week.
Args:
season: Season number
week: Week number
team1_abbrev: First team abbreviation
team2_abbrev: Second team abbreviation
Returns:
List of Game instances in the series
"""
try:
week_games = await self.get_week_schedule(season, week)
team1_upper = team1_abbrev.upper()
team2_upper = team2_abbrev.upper()
# Find games between these two teams
series_games = []
for game in week_games:
game_teams = {game.away_team.abbrev.upper(), game.home_team.abbrev.upper()}
if game_teams == {team1_upper, team2_upper}:
series_games.append(game)
# Sort by game number
series_games.sort(key=lambda x: x.game_num or 0)
logger.debug(f"Retrieved {len(series_games)} games in series between {team1_abbrev} and {team2_abbrev}")
return series_games
except Exception as e:
logger.error(f"Error getting series between {team1_abbrev} and {team2_abbrev}: {e}")
return []
def group_games_by_series(self, games: List[Game]) -> Dict[Tuple[str, str], List[Game]]:
"""
Group games by matchup (series).
Args:
games: List of Game instances
Returns:
Dictionary mapping (team1, team2) tuples to game lists
"""
series_games = {}
for game in games:
# Create consistent team pairing (alphabetical order)
teams = sorted([game.away_team.abbrev, game.home_team.abbrev])
series_key = (teams[0], teams[1])
if series_key not in series_games:
series_games[series_key] = []
series_games[series_key].append(game)
# Sort each series by game number
for series_key in series_games:
series_games[series_key].sort(key=lambda x: x.game_num or 0)
return series_games
# Global service instance
schedule_service = ScheduleService()