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>
203 lines
7.1 KiB
Python
203 lines
7.1 KiB
Python
"""
|
|
Standings service for Discord Bot v2.0
|
|
|
|
Handles team standings retrieval and processing.
|
|
"""
|
|
import logging
|
|
from typing import Optional, List, Dict
|
|
|
|
from services.base_service import BaseService
|
|
from models.standings import TeamStandings
|
|
from exceptions import APIException
|
|
|
|
logger = logging.getLogger(f'{__name__}.StandingsService')
|
|
|
|
|
|
class StandingsService:
|
|
"""
|
|
Service for team standings operations.
|
|
|
|
Features:
|
|
- League standings retrieval
|
|
- Division-based filtering
|
|
- Season-specific data
|
|
- Playoff positioning
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize standings service."""
|
|
from api.client import get_global_client
|
|
self._get_client = get_global_client
|
|
logger.debug("StandingsService initialized")
|
|
|
|
async def get_client(self):
|
|
"""Get the API client."""
|
|
return await self._get_client()
|
|
|
|
async def get_league_standings(self, season: int) -> List[TeamStandings]:
|
|
"""
|
|
Get complete league standings for a season.
|
|
|
|
Args:
|
|
season: Season number
|
|
|
|
Returns:
|
|
List of TeamStandings ordered by record
|
|
"""
|
|
try:
|
|
client = await self.get_client()
|
|
|
|
params = [('season', str(season))]
|
|
response = await client.get('standings', params=params)
|
|
|
|
if not response or 'standings' not in response:
|
|
logger.warning(f"No standings data found for season {season}")
|
|
return []
|
|
|
|
standings_list = response['standings']
|
|
if not standings_list:
|
|
logger.warning(f"Empty standings for season {season}")
|
|
return []
|
|
|
|
# Convert to model objects
|
|
standings = []
|
|
for standings_data in standings_list:
|
|
try:
|
|
team_standings = TeamStandings.from_api_data(standings_data)
|
|
standings.append(team_standings)
|
|
except Exception as e:
|
|
logger.error(f"Error parsing standings data for team: {e}")
|
|
continue
|
|
|
|
logger.info(f"Retrieved standings for {len(standings)} teams in season {season}")
|
|
return standings
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting league standings for season {season}: {e}")
|
|
return []
|
|
|
|
async def get_standings_by_division(self, season: int) -> Dict[str, List[TeamStandings]]:
|
|
"""
|
|
Get standings grouped by division.
|
|
|
|
Args:
|
|
season: Season number
|
|
|
|
Returns:
|
|
Dictionary mapping division names to team standings
|
|
"""
|
|
try:
|
|
all_standings = await self.get_league_standings(season)
|
|
|
|
if not all_standings:
|
|
return {}
|
|
|
|
# Group by division
|
|
divisions = {}
|
|
for team_standings in all_standings:
|
|
if hasattr(team_standings.team, 'division') and team_standings.team.division:
|
|
div_name = team_standings.team.division.division_name
|
|
if div_name not in divisions:
|
|
divisions[div_name] = []
|
|
divisions[div_name].append(team_standings)
|
|
else:
|
|
# Handle teams without division
|
|
if "No Division" not in divisions:
|
|
divisions["No Division"] = []
|
|
divisions["No Division"].append(team_standings)
|
|
|
|
# Sort each division by record (wins descending, then by winning percentage)
|
|
for div_name in divisions:
|
|
divisions[div_name].sort(
|
|
key=lambda x: (x.wins, x.winning_percentage),
|
|
reverse=True
|
|
)
|
|
|
|
logger.debug(f"Grouped standings into {len(divisions)} divisions")
|
|
return divisions
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error grouping standings by division: {e}")
|
|
return {}
|
|
|
|
async def get_team_standings(self, team_abbrev: str, season: int) -> Optional[TeamStandings]:
|
|
"""
|
|
Get standings for a specific team.
|
|
|
|
Args:
|
|
team_abbrev: Team abbreviation (e.g., 'NYY')
|
|
season: Season number
|
|
|
|
Returns:
|
|
TeamStandings instance or None if not found
|
|
"""
|
|
try:
|
|
all_standings = await self.get_league_standings(season)
|
|
|
|
# Find team by abbreviation
|
|
team_abbrev_upper = team_abbrev.upper()
|
|
for team_standings in all_standings:
|
|
if team_standings.team.abbrev.upper() == team_abbrev_upper:
|
|
logger.debug(f"Found standings for {team_abbrev}: {team_standings}")
|
|
return team_standings
|
|
|
|
logger.warning(f"No standings found for team {team_abbrev} in season {season}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting standings for team {team_abbrev}: {e}")
|
|
return None
|
|
|
|
async def get_playoff_picture(self, season: int) -> Dict[str, List[TeamStandings]]:
|
|
"""
|
|
Get playoff picture with division leaders and wild card contenders.
|
|
|
|
Args:
|
|
season: Season number
|
|
|
|
Returns:
|
|
Dictionary with 'division_leaders' and 'wild_card' lists
|
|
"""
|
|
try:
|
|
divisions = await self.get_standings_by_division(season)
|
|
|
|
if not divisions:
|
|
return {"division_leaders": [], "wild_card": []}
|
|
|
|
# Get division leaders (first place in each division)
|
|
division_leaders = []
|
|
wild_card_candidates = []
|
|
|
|
for div_name, teams in divisions.items():
|
|
if teams: # Division has teams
|
|
# First team is division leader
|
|
division_leaders.append(teams[0])
|
|
|
|
# Rest are potential wild card candidates
|
|
for team in teams[1:]:
|
|
wild_card_candidates.append(team)
|
|
|
|
# Sort wild card candidates by record
|
|
wild_card_candidates.sort(
|
|
key=lambda x: (x.wins, x.winning_percentage),
|
|
reverse=True
|
|
)
|
|
|
|
# Take top wild card contenders (typically top 6-8 teams)
|
|
wild_card_contenders = wild_card_candidates[:8]
|
|
|
|
logger.debug(f"Playoff picture: {len(division_leaders)} division leaders, "
|
|
f"{len(wild_card_contenders)} wild card contenders")
|
|
|
|
return {
|
|
"division_leaders": division_leaders,
|
|
"wild_card": wild_card_contenders
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating playoff picture: {e}")
|
|
return {"division_leaders": [], "wild_card": []}
|
|
|
|
|
|
# Global service instance
|
|
standings_service = StandingsService() |