## Player Model Migration - Migrate Player model from Discord app following Model/Service Architecture pattern - Extract all business logic from Player model to PlayerService - Create pure data model with PostgreSQL relationships (Cardset, PositionRating) - Implement comprehensive PlayerFactory with specialized methods for test data ## PlayerService Implementation - Extract 5 business logic methods from original Player model: - get_batter_card_url() - batting card URL retrieval - get_pitcher_card_url() - pitching card URL retrieval - generate_name_card_link() - markdown link generation - get_formatted_name_with_description() - name formatting - get_player_description() - description from object or dict - Follow BaseService pattern with dependency injection and logging ## Comprehensive Testing - 35 passing Player tests (14 model + 21 service tests) - PlayerFactory with specialized methods (batting/pitching cards, positions) - Test isolation following factory pattern and db_session guidelines - Fix PostgreSQL integer overflow in test ID generation ## Integration Test Infrastructure - Create integration test framework for improving service coverage - Design AIService integration tests targeting uncovered branches - Demonstrate real database query testing with proper isolation - Establish patterns for testing complex game scenarios ## Service Coverage Analysis - Current service coverage: 61% overall - PlayerService: 100% coverage (excellent migration example) - AIService: 60% coverage (improvement opportunities identified) - Integration test strategy designed to achieve 90%+ coverage ## Database Integration - Update Cardset model to include players relationship - Update PositionRating model with proper Player foreign key - Maintain all existing relationships and constraints - Demonstrate data isolation and automatic cleanup in tests ## Test Suite Status - 137 tests passing, 0 failures (maintained 100% pass rate) - Added 35 new tests while preserving all existing functionality - Integration test infrastructure ready for coverage improvements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
153 lines
4.8 KiB
Python
153 lines
4.8 KiB
Python
"""
|
|
Player Service
|
|
|
|
Business logic for player-related operations, extracted from Player model.
|
|
Handles URL generation, formatting, and player description logic.
|
|
"""
|
|
import logging
|
|
from typing import Dict, Literal, Optional, Union
|
|
|
|
from sqlmodel import Session
|
|
|
|
from ..models.player import Player
|
|
from .base_service import BaseService
|
|
|
|
|
|
class PlayerService(BaseService):
|
|
"""Service for player-related business logic."""
|
|
|
|
def __init__(self, session: Session):
|
|
super().__init__(session)
|
|
self.logger = logging.getLogger(f'{__name__}.{self.__class__.__name__}')
|
|
|
|
def get_batter_card_url(self, player: Player) -> Optional[str]:
|
|
"""
|
|
Get the batting card image URL for a player.
|
|
|
|
Migrated from Player.batter_card_url property.
|
|
|
|
Args:
|
|
player: Player instance
|
|
|
|
Returns:
|
|
str: Batting card URL if found, None otherwise
|
|
"""
|
|
self._log_operation("get_batter_card_url", f"player {player.name}")
|
|
|
|
if player.image and 'batting' in player.image:
|
|
return player.image
|
|
elif player.image2 and 'batting' in player.image2:
|
|
return player.image2
|
|
else:
|
|
return None
|
|
|
|
def get_pitcher_card_url(self, player: Player) -> Optional[str]:
|
|
"""
|
|
Get the pitching card image URL for a player.
|
|
|
|
Migrated from Player.pitcher_card_url property.
|
|
|
|
Args:
|
|
player: Player instance
|
|
|
|
Returns:
|
|
str: Pitching card URL if found, None otherwise
|
|
"""
|
|
self._log_operation("get_pitcher_card_url", f"player {player.name}")
|
|
|
|
if player.image and 'pitching' in player.image:
|
|
return player.image
|
|
elif player.image2 and 'pitching' in player.image2:
|
|
return player.image2
|
|
else:
|
|
return None
|
|
|
|
def generate_name_card_link(self, player: Player, card_type: Literal['pitching', 'batting']) -> str:
|
|
"""
|
|
Generate a markdown link with player name and card URL.
|
|
|
|
Migrated from Player.name_card_link() method.
|
|
|
|
Args:
|
|
player: Player instance
|
|
card_type: Type of card ('pitching' or 'batting')
|
|
|
|
Returns:
|
|
str: Markdown formatted link
|
|
|
|
Raises:
|
|
ValueError: If card URL is not available for the specified type
|
|
"""
|
|
self._log_operation("generate_name_card_link", f"player {player.name}, type {card_type}")
|
|
|
|
if card_type == 'pitching':
|
|
url = self.get_pitcher_card_url(player)
|
|
else:
|
|
url = self.get_batter_card_url(player)
|
|
|
|
if url is None:
|
|
raise ValueError(f"No {card_type} card URL available for player {player.name}")
|
|
|
|
return f'[{player.name}]({url})'
|
|
|
|
def get_formatted_name_with_description(self, player: Player) -> str:
|
|
"""
|
|
Get formatted player name with description.
|
|
|
|
Migrated from Player.name_with_desc property.
|
|
|
|
Args:
|
|
player: Player instance
|
|
|
|
Returns:
|
|
str: Formatted name with description
|
|
"""
|
|
self._log_operation("get_formatted_name_with_description", f"player {player.name}")
|
|
|
|
return f'{player.description} {player.name}'
|
|
|
|
def get_player_description(
|
|
self,
|
|
player: Optional[Player] = None,
|
|
player_dict: Optional[Dict[str, Union[str, int]]] = None
|
|
) -> str:
|
|
"""
|
|
Get full player description from Player object or dictionary.
|
|
|
|
Migrated from standalone player_description() function.
|
|
|
|
Args:
|
|
player: Player instance (optional)
|
|
player_dict: Dictionary with player data (optional)
|
|
|
|
Returns:
|
|
str: Full player description
|
|
|
|
Raises:
|
|
TypeError: If neither player nor player_dict is provided
|
|
KeyError: If required keys are missing from player_dict
|
|
"""
|
|
if player is None and player_dict is None:
|
|
err = 'One of "player" or "player_dict" must be included to get full description'
|
|
self._log_error("get_player_description", err)
|
|
raise TypeError(err)
|
|
|
|
if player is not None:
|
|
self._log_operation("get_player_description", f"from Player object: {player.name}")
|
|
return f'{player.description} {player.name}'
|
|
|
|
# Handle dictionary case
|
|
if 'description' not in player_dict:
|
|
err = 'player_dict must contain "description" key'
|
|
self._log_error("get_player_description", err)
|
|
raise KeyError(err)
|
|
|
|
r_val = f'{player_dict["description"]}'
|
|
|
|
if 'name' in player_dict:
|
|
r_val += f' {player_dict["name"]}'
|
|
elif 'p_name' in player_dict:
|
|
r_val += f' {player_dict["p_name"]}'
|
|
|
|
self._log_operation("get_player_description", f"from dict: {r_val}")
|
|
return r_val |