""" Decision Service Manages pitching decision operations for game submission. """ from typing import List, Dict, Any, Optional, Tuple from utils.logging import get_contextual_logger from api.client import get_global_client from models.player import Player from exceptions import APIException class DecisionService: """Pitching decision management service.""" def __init__(self): """Initialize decision service.""" self.logger = get_contextual_logger(f"{__name__}.DecisionService") self._get_client = get_global_client async def get_client(self): """Get the API client.""" return await self._get_client() async def create_decisions_batch(self, decisions: List[Dict[str, Any]]) -> bool: """ POST batch of decisions to /decisions endpoint. Args: decisions: List of decision dictionaries Returns: True if successful Raises: APIException: If POST fails """ try: client = await self.get_client() payload = {"decisions": decisions} # Trailing slash required: without it, the server returns a 307 redirect # and aiohttp drops the POST body when following the redirect await client.post("decisions/", payload) self.logger.info(f"Created {len(decisions)} decisions") return True except Exception as e: self.logger.error(f"Failed to create decisions batch: {e}") error_msg = self._parse_api_error(e) raise APIException(error_msg) from e async def delete_decisions_for_game(self, game_id: int) -> bool: """ Delete all decisions for a specific game. Calls DELETE /decisions/game/{game_id} Args: game_id: Game ID to delete decisions for Returns: True if successful Raises: APIException: If deletion fails """ try: client = await self.get_client() await client.delete(f"decisions/game/{game_id}") self.logger.info(f"Deleted decisions for game {game_id}") return True except Exception as e: self.logger.error(f"Failed to delete decisions for game {game_id}: {e}") raise APIException(f"Failed to delete decisions: {e}") async def find_winning_losing_pitchers( self, decisions_data: List[Dict[str, Any]] ) -> Tuple[ Optional[Player], Optional[Player], Optional[Player], List[Player], List[Player] ]: """ Extract WP, LP, SV, Holds, Blown Saves from decisions list and fetch Player objects. Args: decisions_data: List of decision dictionaries from scorecard Returns: Tuple of (wp, lp, sv, holders, blown_saves) wp: Winning pitcher Player object (or None) lp: Losing pitcher Player object (or None) sv: Save pitcher Player object (or None) holders: List of Player objects with holds blown_saves: List of Player objects with blown saves Raises: APIException: If any player lookup fails """ from services.player_service import player_service wp_id = None lp_id = None sv_id = None hold_ids = [] bsv_ids = [] # First pass: Extract IDs for decision in decisions_data: pitcher_id = int(decision.get("pitcher_id", 0)) if int(decision.get("win", 0)) == 1: wp_id = pitcher_id if int(decision.get("loss", 0)) == 1: lp_id = pitcher_id if int(decision.get("is_save", 0)) == 1: sv_id = pitcher_id if int(decision.get("hold", 0)) == 1: hold_ids.append(pitcher_id) if int(decision.get("b_save", 0)) == 1: bsv_ids.append(pitcher_id) # Second pass: Fetch Player objects wp = await player_service.get_player(wp_id) if wp_id else None lp = await player_service.get_player(lp_id) if lp_id else None sv = await player_service.get_player(sv_id) if sv_id else None holders = [] for hold_id in hold_ids: holder = await player_service.get_player(hold_id) if holder: holders.append(holder) blown_saves = [] for bsv_id in bsv_ids: bsv = await player_service.get_player(bsv_id) if bsv: blown_saves.append(bsv) return wp, lp, sv, holders, blown_saves def _parse_api_error(self, error: Exception) -> str: """ Parse API error into user-friendly message. Args: error: Exception from API call Returns: User-friendly error message """ error_str = str(error) if "Player ID" in error_str and "not found" in error_str: return "Invalid pitcher ID in decision data." elif "Game ID" in error_str and "not found" in error_str: return "Game not found for decisions." else: return f"Error submitting decisions: {error_str}" # Global service instance decision_service = DecisionService()