""" Standardized exception hierarchy for data access operations. Provides structured error handling with context, operation tracking, and consistent error reporting across all data access layers. """ import traceback from typing import Optional, Dict, Any, List from dataclasses import dataclass, field from datetime import datetime from enum import Enum class ErrorSeverity(Enum): """Severity levels for data access errors.""" LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" class OperationType(Enum): """Types of data access operations.""" READ = "read" WRITE = "write" UPDATE = "update" DELETE = "delete" BATCH = "batch" TRANSACTION = "transaction" @dataclass class ErrorContext: """Context information for data access errors.""" operation_id: str operation_type: OperationType resource_type: str resource_id: Optional[str] = None user_id: Optional[str] = None timestamp: datetime = field(default_factory=datetime.utcnow) request_data: Optional[Dict[str, Any]] = None metadata: Dict[str, Any] = field(default_factory=dict) class DataAccessError(Exception): """ Base exception for all data access errors. Provides structured error context, operation tracking, and debugging information for data access failures. """ def __init__( self, message: str, context: Optional[ErrorContext] = None, severity: ErrorSeverity = ErrorSeverity.MEDIUM, cause: Optional[Exception] = None, recovery_suggestions: Optional[List[str]] = None ): super().__init__(message) self.message = message self.context = context self.severity = severity self.cause = cause self.recovery_suggestions = recovery_suggestions or [] self.traceback_info = traceback.format_exc() def to_dict(self) -> Dict[str, Any]: """Convert error to dictionary for logging/serialization.""" return { "error_type": self.__class__.__name__, "message": self.message, "severity": self.severity.value, "context": { "operation_id": self.context.operation_id if self.context else None, "operation_type": self.context.operation_type.value if self.context else None, "resource_type": self.context.resource_type if self.context else None, "resource_id": self.context.resource_id if self.context else None, "timestamp": self.context.timestamp.isoformat() if self.context else None, "metadata": self.context.metadata if self.context else {} }, "cause": str(self.cause) if self.cause else None, "recovery_suggestions": self.recovery_suggestions, "traceback": self.traceback_info } def __str__(self) -> str: """Provide detailed string representation.""" parts = [f"{self.__class__.__name__}: {self.message}"] if self.context: parts.append(f"Operation: {self.context.operation_type.value}") parts.append(f"Resource: {self.context.resource_type}") if self.context.resource_id: parts.append(f"ID: {self.context.resource_id}") if self.severity != ErrorSeverity.MEDIUM: parts.append(f"Severity: {self.severity.value}") if self.recovery_suggestions: parts.append(f"Suggestions: {', '.join(self.recovery_suggestions)}") return " | ".join(parts) # Repository-specific errors class RepositoryError(DataAccessError): """Base error for repository operations.""" pass class ResourceNotFoundError(RepositoryError): """Resource was not found in the data store.""" def __init__(self, resource_type: str, resource_id: str, context: Optional[ErrorContext] = None): message = f"{resource_type} with ID '{resource_id}' not found" super().__init__( message=message, context=context, severity=ErrorSeverity.LOW, recovery_suggestions=[ "Verify the resource ID is correct", "Check if the resource was deleted", "Refresh your data and try again" ] ) self.resource_type = resource_type self.resource_id = resource_id class DuplicateResourceError(RepositoryError): """Attempted to create a resource that already exists.""" def __init__(self, resource_type: str, identifier: str, context: Optional[ErrorContext] = None): message = f"{resource_type} with identifier '{identifier}' already exists" super().__init__( message=message, context=context, severity=ErrorSeverity.LOW, recovery_suggestions=[ "Use update operation instead of create", "Check for existing resources before creating", "Use upsert operation if available" ] ) self.resource_type = resource_type self.identifier = identifier class ValidationError(RepositoryError): """Data validation failed before repository operation.""" def __init__(self, field: str, value: Any, rule: str, context: Optional[ErrorContext] = None): message = f"Validation failed for field '{field}': {rule}" super().__init__( message=message, context=context, severity=ErrorSeverity.MEDIUM, recovery_suggestions=[ f"Correct the value for field '{field}'", "Review the validation rules", "Check the data format requirements" ] ) self.field = field self.value = value self.rule = rule class ConcurrencyError(RepositoryError): """Concurrent modification detected.""" def __init__(self, resource_type: str, resource_id: str, context: Optional[ErrorContext] = None): message = f"Concurrent modification detected for {resource_type} '{resource_id}'" super().__init__( message=message, context=context, severity=ErrorSeverity.HIGH, recovery_suggestions=[ "Retry the operation with fresh data", "Implement optimistic locking", "Use atomic operations where possible" ] ) self.resource_type = resource_type self.resource_id = resource_id # External service errors class ExternalServiceError(DataAccessError): """Base error for external service interactions.""" pass class GiteaApiError(ExternalServiceError): """Error communicating with Gitea API.""" def __init__( self, status_code: int, response_body: str, endpoint: str, context: Optional[ErrorContext] = None ): message = f"Gitea API error {status_code} at {endpoint}: {response_body}" severity = self._determine_severity(status_code) super().__init__( message=message, context=context, severity=severity, recovery_suggestions=self._get_recovery_suggestions(status_code) ) self.status_code = status_code self.response_body = response_body self.endpoint = endpoint def _determine_severity(self, status_code: int) -> ErrorSeverity: """Determine error severity based on HTTP status code.""" if status_code >= 500: return ErrorSeverity.HIGH elif status_code == 429: # Rate limited return ErrorSeverity.MEDIUM elif status_code >= 400: return ErrorSeverity.LOW else: return ErrorSeverity.MEDIUM def _get_recovery_suggestions(self, status_code: int) -> List[str]: """Get recovery suggestions based on HTTP status code.""" if status_code == 401: return ["Check API token is valid", "Verify authentication configuration"] elif status_code == 403: return ["Check API permissions", "Verify token has required scopes"] elif status_code == 404: return ["Verify the endpoint URL", "Check if the resource exists"] elif status_code == 429: return ["Implement rate limiting", "Wait before retrying", "Use exponential backoff"] elif status_code >= 500: return ["Retry the request", "Check Gitea service status", "Contact system administrator"] else: return ["Check request parameters", "Review API documentation"] class NetworkError(ExternalServiceError): """Network connectivity error.""" def __init__(self, operation: str, cause: Exception, context: Optional[ErrorContext] = None): message = f"Network error during {operation}: {str(cause)}" super().__init__( message=message, context=context, severity=ErrorSeverity.HIGH, cause=cause, recovery_suggestions=[ "Check network connectivity", "Verify service endpoints are reachable", "Retry with exponential backoff", "Check firewall and proxy settings" ] ) self.operation = operation # Database-specific errors class DatabaseError(DataAccessError): """Base error for database operations.""" pass class ConnectionError(DatabaseError): """Database connection error.""" def __init__(self, database: str, cause: Exception, context: Optional[ErrorContext] = None): message = f"Failed to connect to database '{database}': {str(cause)}" super().__init__( message=message, context=context, severity=ErrorSeverity.CRITICAL, cause=cause, recovery_suggestions=[ "Check database is running", "Verify connection string", "Check database permissions", "Verify network connectivity" ] ) self.database = database class TransactionError(DatabaseError): """Database transaction error.""" def __init__(self, operation: str, cause: Exception, context: Optional[ErrorContext] = None): message = f"Transaction failed during {operation}: {str(cause)}" super().__init__( message=message, context=context, severity=ErrorSeverity.HIGH, cause=cause, recovery_suggestions=[ "Retry the entire transaction", "Check for deadlocks", "Verify data constraints", "Review transaction isolation level" ] ) self.operation = operation class QueryError(DatabaseError): """Database query execution error.""" def __init__(self, query: str, parameters: Dict[str, Any], cause: Exception, context: Optional[ErrorContext] = None): message = f"Query execution failed: {str(cause)}" super().__init__( message=message, context=context, severity=ErrorSeverity.MEDIUM, cause=cause, recovery_suggestions=[ "Check query syntax", "Verify parameter types", "Check table/column names", "Review database schema" ] ) self.query = query self.parameters = parameters # Cache-specific errors class CacheError(DataAccessError): """Base error for cache operations.""" pass class CacheMissError(CacheError): """Requested item not found in cache.""" def __init__(self, cache_key: str, context: Optional[ErrorContext] = None): message = f"Cache miss for key '{cache_key}'" super().__init__( message=message, context=context, severity=ErrorSeverity.LOW, recovery_suggestions=[ "Load data from primary source", "Check cache key format", "Verify cache is populated" ] ) self.cache_key = cache_key class CacheInvalidationError(CacheError): """Failed to invalidate cache entries.""" def __init__(self, pattern: str, cause: Exception, context: Optional[ErrorContext] = None): message = f"Failed to invalidate cache pattern '{pattern}': {str(cause)}" super().__init__( message=message, context=context, severity=ErrorSeverity.MEDIUM, cause=cause, recovery_suggestions=[ "Retry cache invalidation", "Clear entire cache if needed", "Check cache connection", "Monitor cache consistency" ] ) self.pattern = pattern # Configuration errors class ConfigurationError(DataAccessError): """Configuration-related error.""" def __init__(self, setting: str, value: Any, context: Optional[ErrorContext] = None): message = f"Invalid configuration for '{setting}': {value}" super().__init__( message=message, context=context, severity=ErrorSeverity.CRITICAL, recovery_suggestions=[ f"Check configuration for '{setting}'", "Review environment variables", "Verify configuration file format", "Check default values" ] ) self.setting = setting self.value = value