major-domo-v2/models/team.py
Cal Corum 7aa454f619 CLAUDE: Reorganize data storage and enhance team/roster displays
Standardize data file locations to data/ directory and improve command organization with better UI for team rosters, pagination for team lists, and refactored chart commands into logical command groups.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 19:05:51 -05:00

216 lines
8.0 KiB
Python

"""
Team model for SBA teams
Represents a team in the league with all associated metadata.
"""
from typing import Optional
from enum import Enum
from pydantic import Field
from models.base import SBABaseModel
from models.division import Division
from models.manager import Manager
class RosterType(Enum):
"""Roster designation types."""
MAJOR_LEAGUE = "ml"
MINOR_LEAGUE = "mil"
INJURED_LIST = "il"
FREE_AGENCY = "fa"
class Team(SBABaseModel):
"""Team model representing an SBA team."""
# Override base model to make id required for database entities
id: int = Field(..., description="Team ID from database")
abbrev: str = Field(..., description="Team abbreviation (e.g., 'NYY')")
sname: str = Field(..., description="Short team name")
lname: str = Field(..., description="Long team name")
season: int = Field(..., description="Season number")
# Manager information
gmid: Optional[int] = Field(None, description="Primary general manager ID")
gmid2: Optional[int] = Field(None, description="Secondary general manager ID")
manager1_id: Optional[int] = Field(None, description="Primary manager ID")
manager1: Optional[Manager] = Field(None, description="Manager object")
manager2_id: Optional[int] = Field(None, description="Secondary manager ID")
manager2: Optional[Manager] = Field(None, description="Manager object")
# Team metadata
division_id: Optional[int] = Field(None, description="Division ID")
division: Optional[Division] = Field(None, description="Division object (populated from API)")
stadium: Optional[str] = Field(None, description="Home stadium name")
thumbnail: Optional[str] = Field(None, description="Team thumbnail URL")
color: Optional[str] = Field(None, description="Primary team color")
dice_color: Optional[str] = Field(None, description="Dice rolling color")
@classmethod
def from_api_data(cls, data: dict) -> 'Team':
"""
Create Team instance from API data, handling nested division structure.
The API returns division data as a nested object, but our model expects
both division_id (int) and division (optional Division object).
"""
# Make a copy to avoid modifying original data
team_data = data.copy()
# Handle nested division structure
if 'division' in team_data and isinstance(team_data['division'], dict):
division_data = team_data['division']
# Extract division_id from nested division object
team_data['division_id'] = division_data.get('id')
# Keep division object for optional population
if division_data.get('id'):
team_data['division'] = Division.from_api_data(division_data)
return super().from_api_data(team_data)
def roster_type(self) -> RosterType:
"""Determine the roster type based on team abbreviation and name."""
if len(self.abbrev) <= 3:
return RosterType.MAJOR_LEAGUE
# Use sname as the definitive source of truth for IL teams
# If "IL" is in sname and abbrev ends in "IL" → Injured List
if self.abbrev.upper().endswith('IL') and 'IL' in self.sname:
return RosterType.INJURED_LIST
# If abbrev ends with "MiL" (exact case) and "IL" not in sname → Minor League
if self.abbrev.endswith('MiL') and 'IL' not in self.sname:
return RosterType.MINOR_LEAGUE
# Handle other patterns
abbrev_lower = self.abbrev.lower()
if abbrev_lower.endswith('mil'):
return RosterType.MINOR_LEAGUE
elif abbrev_lower.endswith('il'):
return RosterType.INJURED_LIST
else:
return RosterType.MAJOR_LEAGUE
def _get_base_abbrev(self) -> str:
"""
Extract the base team abbreviation from potentially extended abbreviation.
Returns:
Base team abbreviation (typically 3 characters)
"""
abbrev_lower = self.abbrev.lower()
# If 3 chars or less, it's already the base team
if len(self.abbrev) <= 3:
return self.abbrev
# Handle teams ending in 'mil' - use sname to determine if IL or MiL
if abbrev_lower.endswith('mil'):
# If "IL" is in sname and abbrev ends in "IL" → It's [Team]IL
if self.abbrev.upper().endswith('IL') and 'IL' in self.sname:
return self.abbrev[:-2] # Remove 'IL'
# Otherwise it's minor league → remove 'MIL'
return self.abbrev[:-3]
# Handle injured list: ends with 'il' but not 'mil'
if abbrev_lower.endswith('il'):
return self.abbrev[:-2] # Remove 'IL'
# Unknown pattern, return as-is
return self.abbrev
async def major_league_affiliate(self) -> 'Team':
"""
Get the major league team for this organization via API call.
Returns:
Team instance representing the major league affiliate
Raises:
APIException: If the affiliate team cannot be found
"""
from services.team_service import team_service
base_abbrev = self._get_base_abbrev()
if base_abbrev == self.abbrev:
return self # Already the major league team
team = await team_service.get_team_by_abbrev(base_abbrev, self.season)
if team is None:
raise ValueError(f"Major league affiliate not found for team {self.abbrev} (looking for {base_abbrev})")
return team
async def minor_league_affiliate(self) -> 'Team':
"""
Get the minor league team for this organization via API call.
Returns:
Team instance representing the minor league affiliate
Raises:
APIException: If the affiliate team cannot be found
"""
from services.team_service import team_service
base_abbrev = self._get_base_abbrev()
mil_abbrev = f"{base_abbrev}MIL"
if mil_abbrev == self.abbrev:
return self # Already the minor league team
team = await team_service.get_team_by_abbrev(mil_abbrev, self.season)
if team is None:
raise ValueError(f"Minor league affiliate not found for team {self.abbrev} (looking for {mil_abbrev})")
return team
async def injured_list_affiliate(self) -> 'Team':
"""
Get the injured list team for this organization via API call.
Returns:
Team instance representing the injured list affiliate
Raises:
APIException: If the affiliate team cannot be found
"""
from services.team_service import team_service
base_abbrev = self._get_base_abbrev()
il_abbrev = f"{base_abbrev}IL"
if il_abbrev == self.abbrev:
return self # Already the injured list team
team = await team_service.get_team_by_abbrev(il_abbrev, self.season)
if team is None:
raise ValueError(f"Injured list affiliate not found for team {self.abbrev} (looking for {il_abbrev})")
return team
def is_same_organization(self, other_team: 'Team') -> bool:
"""
Check if this team and another team are from the same organization.
Args:
other_team: Another team to compare
Returns:
True if both teams are from the same organization
"""
return self._get_base_abbrev() == other_team._get_base_abbrev()
def gm_names(self) -> str:
if any([self.manager1, self.manager2]):
names = ''
if self.manager1:
names += f'{self.manager1}'
if self.manager2:
names += f', {self.manager2}'
return names
if any([self.manager1_id, self.manager2_id]):
mgr_count = sum(1 for x in [self.manager1_id, self.manager2_id] if x is not None)
return f'{mgr_count} GM{"s" if mgr_count > 1 else ""}'
return 'Unknown'
def __str__(self):
return f"{self.abbrev} - {self.lname}"