"""Pydantic models for roster link type safety Provides league-specific type-safe models for roster operations: - PdRosterLinkData: PD league card-based rosters - SbaRosterLinkData: SBA league player-based rosters """ from abc import ABC, abstractmethod from uuid import UUID from pydantic import BaseModel, ConfigDict, computed_field, field_validator class BaseRosterLinkData(BaseModel, ABC): """Abstract base for roster link data Common fields shared across all leagues """ model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True) id: int | None = None # Database ID (populated after save) game_id: UUID team_id: int @abstractmethod def get_entity_id(self) -> int: """Get the entity ID (card_id or player_id)""" pass @abstractmethod def get_entity_type(self) -> str: """Get entity type identifier ('card' or 'player')""" pass class PdRosterLinkData(BaseRosterLinkData): """PD league roster link - tracks cards Used for Paper Dynasty league games where rosters are composed of cards. Each card represents a player with detailed scouting data. """ card_id: int @field_validator("card_id") @classmethod def validate_card_id(cls, v: int) -> int: if v <= 0: raise ValueError("card_id must be positive") return v def get_entity_id(self) -> int: return self.card_id def get_entity_type(self) -> str: return "card" class SbaRosterLinkData(BaseRosterLinkData): """SBA league roster link - tracks players Used for SBA league games where rosters are composed of players. Players are identified directly by player_id without a card system. The player_positions field stores the player's natural positions from the API for use in substitution UI filtering (batters vs pitchers). This supports two-way players like Shohei Ohtani who have both pitching and batting positions. The player_data field caches essential SbaPlayer fields (name, image, headshot) to avoid runtime API calls when loading bench players. """ player_id: int player_positions: list[str] = [] player_data: dict | None = None # Cached SbaPlayer fields: {name, image, headshot} @field_validator("player_id") @classmethod def validate_player_id(cls, v: int) -> int: if v <= 0: raise ValueError("player_id must be positive") return v def get_entity_id(self) -> int: return self.player_id def get_entity_type(self) -> str: return "player" # Pitcher positions used for filtering # CP = Closer Pitcher (alternate notation for CL) _PITCHER_POSITIONS: set[str] = {"P", "SP", "RP", "CL", "CP"} @computed_field @property def is_pitcher(self) -> bool: """True if player has any pitching position (supports two-way players) Examples: - ["SP"] -> True - ["SP", "DH"] -> True (two-way player like Ohtani) - ["CF", "DH"] -> False """ return any(pos in self._PITCHER_POSITIONS for pos in self.player_positions) @computed_field @property def is_batter(self) -> bool: """True if player has any non-pitching position (supports two-way players) Examples: - ["CF", "DH"] -> True - ["SP", "DH"] -> True (two-way player like Ohtani) - ["SP"] -> False (pitcher-only) """ return any(pos not in self._PITCHER_POSITIONS for pos in self.player_positions) class RosterLinkCreate(BaseModel): """Request model for creating a roster link""" game_id: UUID team_id: int card_id: int | None = None player_id: int | None = None player_positions: list[str] = [] # Natural positions for substitution filtering @field_validator("team_id") @classmethod def validate_team_id(cls, v: int) -> int: if v <= 0: raise ValueError("team_id must be positive") return v def model_post_init(self, __context) -> None: """Validate that exactly one ID is populated""" has_card = self.card_id is not None has_player = self.player_id is not None if has_card == has_player: # XOR check (both True or both False = invalid) raise ValueError("Exactly one of card_id or player_id must be provided") def to_pd_data(self) -> PdRosterLinkData: """Convert to PD roster data (validates card_id is present)""" if self.card_id is None: raise ValueError("card_id required for PD roster") return PdRosterLinkData( game_id=self.game_id, team_id=self.team_id, card_id=self.card_id ) def to_sba_data(self) -> SbaRosterLinkData: """Convert to SBA roster data (validates player_id is present)""" if self.player_id is None: raise ValueError("player_id required for SBA roster") return SbaRosterLinkData( game_id=self.game_id, team_id=self.team_id, player_id=self.player_id, player_positions=self.player_positions, )