major-domo-v2/models/player.py
Cal Corum 2409c27c1d CLAUDE: Add comprehensive scorecard submission system
Implements full Google Sheets scorecard submission with:
- Complete game data extraction (68 play fields, pitching decisions, box score)
- Transaction rollback support at 3 states (plays/game/complete)
- Duplicate game detection with confirmation dialog
- Permission-based submission (GMs only)
- Automated results posting to news channel
- Automatic standings recalculation
- Key plays display with WPA sorting

New Components:
- Play, Decision, Game models with full validation
- SheetsService for Google Sheets integration
- GameService, PlayService, DecisionService for data management
- ConfirmationView for user confirmations
- Discord helper utilities for channel operations

Services Enhanced:
- StandingsService: Added recalculate_standings() method
- CustomCommandsService: Fixed creator endpoint path
- Team/Player models: Added helper methods for display

Configuration:
- Added SHEETS_CREDENTIALS_PATH environment variable
- Added SBA_NETWORK_NEWS_CHANNEL and role constants
- Enabled pygsheets dependency

Documentation:
- Comprehensive README updates across all modules
- Added command, service, model, and view documentation
- Detailed workflow and error handling documentation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 00:21:32 -05:00

122 lines
4.9 KiB
Python

"""
Player model for SBA players
Represents a player with team relationships and position information.
"""
from typing import Optional, List
from pydantic import Field
from models.base import SBABaseModel
from models.team import Team
from models.sbaplayer import SBAPlayer
class Player(SBABaseModel):
"""Player model representing an SBA player."""
# Override base model to make id required for database entities
id: int = Field(..., description="Player ID from database")
name: str = Field(..., description="Player full name")
wara: float = Field(..., description="Wins Above Replacement Average")
season: int = Field(..., description="Season number")
# Team relationship (team_id extracted from nested team object)
team_id: Optional[int] = Field(None, description="Team ID this player belongs to")
team: Optional[Team] = Field(None, description="Team object (populated from API)")
# Images and media
image: Optional[str] = Field(None, description="Primary player image URL")
image2: Optional[str] = Field(None, description="Secondary player image URL")
vanity_card: Optional[str] = Field(None, description="Custom vanity card URL")
headshot: Optional[str] = Field(None, description="Player headshot URL")
# Positions (up to 8 positions)
pos_1: str = Field(..., description="Primary position")
pos_2: Optional[str] = None
pos_3: Optional[str] = None
pos_4: Optional[str] = None
pos_5: Optional[str] = None
pos_6: Optional[str] = None
pos_7: Optional[str] = None
pos_8: Optional[str] = None
# Injury and status information
pitcher_injury: Optional[int] = Field(None, description="Pitcher injury rating")
injury_rating: Optional[str] = Field(None, description="General injury rating")
il_return: Optional[str] = Field(None, description="Injured list return date")
demotion_week: Optional[int] = Field(None, description="Week of demotion")
# Game tracking
last_game: Optional[str] = Field(None, description="Last game played")
last_game2: Optional[str] = Field(None, description="Second to last game played")
# External identifiers
strat_code: Optional[str] = Field(None, description="Strat-o-matic code")
bbref_id: Optional[str] = Field(None, description="Baseball Reference ID")
sbaplayer: Optional[SBAPlayer] = Field(None, description="SBA player data object")
@property
def positions(self) -> List[str]:
"""Return list of all positions this player can play."""
positions = []
for i in range(1, 9):
pos = getattr(self, f'pos_{i}', None)
if pos:
positions.append(pos)
return positions
@property
def primary_position(self) -> str:
"""Return the player's primary position."""
return self.pos_1
@classmethod
def from_api_data(cls, data: dict) -> 'Player':
"""
Create Player instance from API data, handling nested team structure.
The API returns team data as a nested object, but our model expects
both team_id (int) and team (optional Team object).
"""
# Make a copy to avoid modifying original data
player_data = data.copy()
# Handle team structure - can be nested object or just ID
if 'team' in player_data:
if isinstance(player_data['team'], dict):
# Nested team object from regular endpoints
team_data = player_data['team']
player_data['team_id'] = team_data.get('id')
if team_data.get('id'):
from models.team import Team
player_data['team'] = Team.from_api_data(team_data)
elif isinstance(player_data['team'], int):
# Team ID only from search endpoints
player_data['team_id'] = player_data['team']
player_data['team'] = None # No nested team object available
# Handle sbaplayer structure - can be nested object or just ID
if 'sbaplayer' in player_data:
if isinstance(player_data['sbaplayer'], dict):
# Nested sbaplayer object
sba_data = player_data['sbaplayer']
player_data['sbaplayer'] = SBAPlayer.from_api_data(sba_data)
elif isinstance(player_data['sbaplayer'], int):
# SBA player ID only from search endpoints
player_data['sbaplayer'] = None # No nested object available
return super().from_api_data(player_data)
@property
def is_pitcher(self) -> bool:
"""Check if player is a pitcher."""
return self.pos_1 in ['SP', 'RP', 'P']
@property
def display_name(self) -> str:
"""Return the player's display name (same as name)."""
return self.name
def __str__(self):
return f"{self.name} ({self.primary_position})"