Add infrastructure components that were created during issue #24 but not properly committed: - Data access repositories and interfaces - Connection management infrastructure - Exception handling framework - Configuration management - Documentation from data access pattern improvements These files are essential infrastructure components that enable the repository pattern and improved data access strategies. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
400 lines
13 KiB
Python
400 lines
13 KiB
Python
"""
|
|
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 |