major-domo-v2/services/player_service.py
Cal Corum bf374c1b98 CLAUDE: Fix /set-image command to use query parameters for API updates
The /set-image command was failing to persist player image updates to the
database. Investigation revealed a mismatch between how the bot sent PATCH
data versus how the database API expected it.

Root Cause:
- Database API endpoint (/api/v3/players/{id}) expects PATCH data as URL
  query parameters, not JSON body
- Bot was sending: PATCH /api/v3/players/12288 {"vanity_card": "url"}
- API expected: PATCH /api/v3/players/12288?vanity_card=url

Changes Made:

1. api/client.py:
   - Added use_query_params parameter to patch() method
   - When enabled, sends data as URL query parameters instead of JSON body
   - Maintains backward compatibility (defaults to JSON body)

2. services/base_service.py:
   - Added use_query_params parameter to patch() method
   - Passes parameter through to API client

3. services/player_service.py:
   - Updated update_player() to use use_query_params=True
   - Added documentation note about query parameter requirement

4. commands/profile/images.py:
   - Fixed autocomplete to use correct utility function
   - Changed from non-existent player_autocomplete_with_team_priority
   - Now uses player_autocomplete from utils/autocomplete.py

Documentation Updates:

5. commands/profile/README.md:
   - Updated API Integration section
   - Documented PATCH endpoint uses query parameters
   - Added note about automatic handling in player_service

6. services/README.md:
   - Added PATCH vs PUT operations documentation
   - Documented use_query_params parameter
   - Included usage examples for both modes

Testing:
- Verified /set-image command now successfully persists image URLs
- Confirmed API returns updated player with vanity_card populated
- Validated both fancy-card and headshot updates work correctly

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 17:52:14 -05:00

304 lines
10 KiB
Python

"""
Player service for Discord Bot v2.0
Handles player-related operations with team population and search functionality.
"""
import logging
from typing import Optional, List, TYPE_CHECKING
from services.base_service import BaseService
from models.player import Player
from constants import FREE_AGENT_TEAM_ID, SBA_CURRENT_SEASON
from exceptions import APIException
if TYPE_CHECKING:
from services.team_service import TeamService
logger = logging.getLogger(f'{__name__}.PlayerService')
class PlayerService(BaseService[Player]):
"""
Service for player-related operations.
Features:
- Player retrieval with team population
- Team roster queries
- Name-based search with exact matching
- Season-specific filtering
- Free agent handling via constants
"""
def __init__(self, team_service: Optional['TeamService'] = None):
"""Initialize player service."""
super().__init__(Player, 'players')
self._team_service = team_service
logger.debug("PlayerService initialized")
async def get_player(self, player_id: int) -> Optional[Player]:
"""
Get player by ID with error handling.
Args:
player_id: Unique player identifier
Returns:
Player instance or None if not found
"""
try:
return await self.get_by_id(player_id)
except APIException:
logger.error(f"Failed to get player {player_id}")
return None
except Exception as e:
logger.error(f"Unexpected error getting player {player_id}: {e}")
return None
async def get_players_by_team(self, team_id: int, season: int, sort: Optional[str] = None) -> List[Player]:
"""
Get all players for a specific team.
Args:
team_id: Team identifier
season: Season number (required)
sort: Sort order - 'cost-asc', 'cost-desc', 'name-asc', 'name-desc' (optional)
Returns:
List of players on the team, optionally sorted
"""
try:
params = [
('season', str(season)),
('team_id', str(team_id))
]
# Add sort parameter if specified
if sort:
valid_sorts = ['cost-asc', 'cost-desc', 'name-asc', 'name-desc']
if sort in valid_sorts:
params.append(('sort', sort))
logger.debug(f"Applying sort '{sort}' to team {team_id} players")
else:
logger.warning(f"Invalid sort parameter '{sort}' - ignoring")
players = await self.get_all_items(params=params)
logger.debug(f"Retrieved {len(players)} players for team {team_id} in season {season}")
return players
except Exception as e:
logger.error(f"Failed to get players for team {team_id}: {e}")
return []
async def get_players_by_name(self, name: str, season: int) -> List[Player]:
"""
Search for players by name (partial match).
Args:
name: Player name or partial name
season: Season number (required)
Returns:
List of matching players
"""
try:
params = [
('season', str(season)),
('name', name)
]
players = await self.get_all_items(params=params)
logger.debug(f"Found {len(players)} players matching '{name}' in season {season}")
return players
except Exception as e:
logger.error(f"Failed to search players by name '{name}': {e}")
return []
async def get_player_by_name_exact(self, name: str, season: int) -> Optional[Player]:
"""
Get player by exact name match (case-insensitive).
Args:
name: Exact player name
season: Season number (required)
Returns:
Player instance or None if not found
"""
try:
players = await self.get_players_by_name(name, season)
# Look for exact case-insensitive match
name_lower = name.lower()
for player in players:
if player.name.lower() == name_lower:
logger.debug(f"Found exact match for '{name}': {player.name}")
return player
logger.debug(f"No exact match found for '{name}'")
return None
except Exception as e:
logger.error(f"Error finding exact player match for '{name}': {e}")
return None
async def search_players(self, query: str, limit: int = 10, season: Optional[int] = None) -> List[Player]:
"""
Search for players using the dedicated /v3/players/search endpoint.
Args:
query: Search query for player name
limit: Maximum number of results to return (1-50)
season: Season to search in (defaults to current season)
Returns:
List of matching players (up to limit)
"""
try:
params = [('q', query), ('limit', str(limit))]
if season is not None:
params.append(('season', str(season)))
client = await self.get_client()
data = await client.get('players/search', params=params)
if not data:
logger.debug(f"No players found for search query '{query}'")
return []
# Handle API response format: {'count': int, 'players': [...]}
items, count = self._extract_items_and_count_from_response(data)
players = [self.model_class.from_api_data(item) for item in items]
logger.debug(f"Search '{query}' returned {len(players)} of {count} matches")
return players
except Exception as e:
logger.error(f"Error in player search for '{query}': {e}")
return []
async def search_players_fuzzy(self, query: str, limit: int = 10, season: Optional[int] = None) -> List[Player]:
"""
Fuzzy search for players by name with limit using existing name search functionality.
Args:
query: Search query
limit: Maximum results to return
season: Season to search in (defaults to current season)
Returns:
List of matching players (up to limit)
"""
try:
if season is None:
from constants import SBA_CURRENT_SEASON
season = SBA_CURRENT_SEASON
# Use the existing name-based search that actually works
players = await self.get_players_by_name(query, season)
# Sort by relevance (exact matches first, then partial)
query_lower = query.lower()
exact_matches = []
partial_matches = []
for player in players:
name_lower = player.name.lower()
if name_lower == query_lower:
exact_matches.append(player)
elif query_lower in name_lower:
partial_matches.append(player)
# Combine and limit results
results = exact_matches + partial_matches
limited_results = results[:limit]
logger.debug(f"Fuzzy search '{query}' returned {len(limited_results)} of {len(results)} matches")
return limited_results
except Exception as e:
logger.error(f"Error in fuzzy search for '{query}': {e}")
return []
async def get_free_agents(self, season: int) -> List[Player]:
"""
Get all free agent players.
Args:
season: Season number (required)
Returns:
List of free agent players
"""
try:
params = [('team_id', FREE_AGENT_TEAM_ID), ('season', str(season))]
players = await self.get_all_items(params=params)
logger.debug(f"Retrieved {len(players)} free agents")
return players
except Exception as e:
logger.error(f"Failed to get free agents: {e}")
return []
async def is_free_agent(self, player: Player) -> bool:
"""
Check if a player is a free agent.
Args:
player: Player instance to check
Returns:
True if player is a free agent
"""
return player.team_id == FREE_AGENT_TEAM_ID
async def get_players_by_position(self, position: str, season: int) -> List[Player]:
"""
Get players by position.
Args:
position: Player position (e.g., 'C', '1B', 'OF')
season: Season number (required)
Returns:
List of players at the position
"""
try:
params = [('position', position), ('season', str(season))]
players = await self.get_all_items(params=params)
logger.debug(f"Retrieved {len(players)} players at position {position}")
return players
except Exception as e:
logger.error(f"Failed to get players by position {position}: {e}")
return []
async def update_player(self, player_id: int, updates: dict) -> Optional[Player]:
"""
Update player information.
Args:
player_id: Player ID to update
updates: Dictionary of fields to update
Returns:
Updated player instance or None
Note:
The player PATCH endpoint uses query parameters instead of JSON body,
so we pass use_query_params=True to the patch method.
"""
try:
return await self.patch(player_id, updates, use_query_params=True)
except Exception as e:
logger.error(f"Failed to update player {player_id}: {e}")
return None
# Global service instance - will be properly initialized in __init__.py
player_service = PlayerService()