""" 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 config import get_config 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 get_color_int(self, default_color: Optional[str] = None) -> int: if self.color is not None: return int(self.color, 16) if default_color is not None: return int(default_color, 16) config = get_config() return int(config.sba_color, 16) def __str__(self): return f"{self.abbrev} - {self.lname}"