Files
markitect-main/infrastructure/exceptions.py
tegwick f782ac1f69 fix: Add missing infrastructure files from data access improvements
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>
2025-09-27 08:35:34 +02:00

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