- Add rate_limit.py middleware with per-client throttling and cleanup task - Add pool_monitor.py for database connection pool health monitoring - Add custom exceptions module (GameEngineError, DatabaseError, etc.) - Add config settings for eviction intervals, session timeouts, memory limits - Add unit tests for rate limiting and pool monitoring 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
215 lines
5.9 KiB
Python
215 lines
5.9 KiB
Python
"""
|
|
Custom exceptions for the game engine.
|
|
|
|
Provides a hierarchy of specific exception types to replace broad `except Exception`
|
|
patterns, improving debugging and error handling precision.
|
|
|
|
Exception Hierarchy:
|
|
GameEngineError (base)
|
|
├── GameNotFoundError - Game doesn't exist in state manager
|
|
├── InvalidGameStateError - Game in wrong state for operation
|
|
├── SubstitutionError - Invalid player substitution
|
|
├── AuthorizationError - User lacks permission
|
|
├── DecisionTimeoutError - Decision not submitted in time
|
|
└── ExternalAPIError - External service call failed
|
|
└── PlayerDataError - Failed to fetch player data
|
|
|
|
Author: Claude
|
|
Date: 2025-11-27
|
|
"""
|
|
|
|
from uuid import UUID
|
|
|
|
|
|
class GameEngineError(Exception):
|
|
"""
|
|
Base exception for all game engine errors.
|
|
|
|
All game-specific exceptions should inherit from this class
|
|
to allow catching game errors separately from system errors.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class GameNotFoundError(GameEngineError):
|
|
"""
|
|
Raised when a game doesn't exist in state manager.
|
|
|
|
Attributes:
|
|
game_id: The UUID of the missing game
|
|
"""
|
|
|
|
def __init__(self, game_id: UUID | str):
|
|
self.game_id = game_id
|
|
super().__init__(f"Game not found: {game_id}")
|
|
|
|
|
|
class InvalidGameStateError(GameEngineError):
|
|
"""
|
|
Raised when game is in invalid state for requested operation.
|
|
|
|
Examples:
|
|
- Trying to submit decision when game is completed
|
|
- Rolling dice before both decisions submitted
|
|
- Starting an already-started game
|
|
|
|
Attributes:
|
|
message: Description of the state violation
|
|
current_state: Optional current state value for debugging
|
|
expected_state: Optional expected state value for debugging
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
current_state: str | None = None,
|
|
expected_state: str | None = None,
|
|
):
|
|
self.current_state = current_state
|
|
self.expected_state = expected_state
|
|
super().__init__(message)
|
|
|
|
|
|
class SubstitutionError(GameEngineError):
|
|
"""
|
|
Raised when a player substitution is invalid.
|
|
|
|
Used for substitution rule violations, not found errors.
|
|
|
|
Attributes:
|
|
message: Description of the substitution error
|
|
error_code: Machine-readable error code for frontend handling
|
|
"""
|
|
|
|
def __init__(self, message: str, error_code: str = "SUBSTITUTION_ERROR"):
|
|
self.error_code = error_code
|
|
super().__init__(message)
|
|
|
|
|
|
class AuthorizationError(GameEngineError):
|
|
"""
|
|
Raised when user lacks permission for an operation.
|
|
|
|
Note: Currently deferred (task 001) but defined for future use.
|
|
|
|
Attributes:
|
|
message: Description of the authorization failure
|
|
user_id: Optional user ID for logging
|
|
resource: Optional resource being accessed
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
user_id: int | None = None,
|
|
resource: str | None = None,
|
|
):
|
|
self.user_id = user_id
|
|
self.resource = resource
|
|
super().__init__(message)
|
|
|
|
|
|
class DecisionTimeoutError(GameEngineError):
|
|
"""
|
|
Raised when a decision is not submitted within the timeout period.
|
|
|
|
The game engine uses default decisions when this occurs,
|
|
so this exception is informational rather than fatal.
|
|
|
|
Attributes:
|
|
game_id: Game where timeout occurred
|
|
decision_type: Type of decision that timed out ('defensive' or 'offensive')
|
|
timeout_seconds: How long we waited
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
game_id: UUID | str,
|
|
decision_type: str,
|
|
timeout_seconds: int,
|
|
):
|
|
self.game_id = game_id
|
|
self.decision_type = decision_type
|
|
self.timeout_seconds = timeout_seconds
|
|
super().__init__(
|
|
f"Decision timeout for game {game_id}: "
|
|
f"{decision_type} decision not received within {timeout_seconds}s"
|
|
)
|
|
|
|
|
|
class ExternalAPIError(GameEngineError):
|
|
"""
|
|
Raised when an external API call fails.
|
|
|
|
Base class for external service errors.
|
|
|
|
Attributes:
|
|
service: Name of the external service
|
|
message: Error description
|
|
status_code: Optional HTTP status code
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
service: str,
|
|
message: str,
|
|
status_code: int | None = None,
|
|
):
|
|
self.service = service
|
|
self.status_code = status_code
|
|
super().__init__(f"{service} API error: {message}")
|
|
|
|
|
|
class PlayerDataError(ExternalAPIError):
|
|
"""
|
|
Raised when player data cannot be fetched from external API.
|
|
|
|
Specialized error for player data lookups (SBA API, PD API).
|
|
|
|
Attributes:
|
|
player_id: ID of the player that couldn't be fetched
|
|
service: Which API was called
|
|
"""
|
|
|
|
def __init__(self, player_id: int, service: str = "SBA API"):
|
|
self.player_id = player_id
|
|
super().__init__(
|
|
service=service,
|
|
message=f"Failed to fetch player data for ID {player_id}",
|
|
)
|
|
|
|
|
|
class DatabaseError(GameEngineError):
|
|
"""
|
|
Raised when a database operation fails.
|
|
|
|
Wraps SQLAlchemy exceptions with game context.
|
|
|
|
Attributes:
|
|
operation: What operation was attempted (e.g., 'save_play', 'create_substitution')
|
|
original_error: The underlying database exception
|
|
"""
|
|
|
|
def __init__(self, operation: str, original_error: Exception | None = None):
|
|
self.operation = operation
|
|
self.original_error = original_error
|
|
message = f"Database error during {operation}"
|
|
if original_error:
|
|
message += f": {original_error}"
|
|
super().__init__(message)
|
|
|
|
|
|
class LineupError(GameEngineError):
|
|
"""
|
|
Raised when lineup validation or lookup fails.
|
|
|
|
Attributes:
|
|
team_id: Team with the lineup issue
|
|
message: Description of the error
|
|
"""
|
|
|
|
def __init__(self, team_id: int, message: str):
|
|
self.team_id = team_id
|
|
super().__init__(f"Lineup error for team {team_id}: {message}")
|