strat-gameplay-webapp/backend/app/core/exceptions.py
Cal Corum 2a392b87f8 CLAUDE: Add rate limiting, pool monitoring, and exception infrastructure
- 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>
2025-11-28 12:06:10 -06:00

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}")