- Add Model Mixins: TimestampMixin, SoftDeleteMixin, AuditMixin, etc. - Add Base Services: BaseService, BulkOperationsMixin, QueryOptimizerMixin - Add Base ViewSets with bulk operations - Add BackgroundJob model for Celery task tracking - Add BaseAppCommand for management commands - Add permissions, pagination, filters, cache, logging - Migrate tests to factory_boy + faker - Add CHANGELOG.md - 297 tests passing
170 lines
4.2 KiB
Python
170 lines
4.2 KiB
Python
"""
|
|
Core exceptions for the application.
|
|
|
|
Provides a hierarchy of business logic exceptions that are automatically
|
|
converted to appropriate API responses by the exception handler.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
|
|
class BaseAPIException(Exception):
|
|
"""
|
|
Base exception for all API-related errors.
|
|
|
|
Attributes:
|
|
message: Human-readable error message
|
|
code: Machine-readable error code (e.g., 'validation_error')
|
|
status_code: HTTP status code
|
|
details: Additional error details (optional)
|
|
"""
|
|
|
|
message: str = "An error occurred"
|
|
code: str = "error"
|
|
status_code: int = 400
|
|
details: dict[str, Any] | None = None
|
|
|
|
def __init__(
|
|
self,
|
|
message: str | None = None,
|
|
code: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
self.message = message or self.message
|
|
self.code = code or self.code
|
|
self.details = details
|
|
super().__init__(self.message)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Convert exception to dictionary for API response."""
|
|
result = {
|
|
"code": self.code,
|
|
"message": self.message,
|
|
}
|
|
if self.details:
|
|
result["details"] = self.details
|
|
return result
|
|
|
|
|
|
# ============================================================================
|
|
# Client Errors (4xx)
|
|
# ============================================================================
|
|
|
|
|
|
class ValidationError(BaseAPIException):
|
|
"""Raised when input data fails validation."""
|
|
|
|
message = "Validation error"
|
|
code = "validation_error"
|
|
status_code = 400
|
|
|
|
|
|
class BadRequestError(BaseAPIException):
|
|
"""Raised when request is malformed or invalid."""
|
|
|
|
message = "Bad request"
|
|
code = "bad_request"
|
|
status_code = 400
|
|
|
|
|
|
class AuthenticationError(BaseAPIException):
|
|
"""Raised when authentication fails."""
|
|
|
|
message = "Authentication failed"
|
|
code = "authentication_error"
|
|
status_code = 401
|
|
|
|
|
|
class PermissionDeniedError(BaseAPIException):
|
|
"""Raised when user lacks required permissions."""
|
|
|
|
message = "Permission denied"
|
|
code = "permission_denied"
|
|
status_code = 403
|
|
|
|
|
|
class NotFoundError(BaseAPIException):
|
|
"""Raised when requested resource is not found."""
|
|
|
|
message = "Resource not found"
|
|
code = "not_found"
|
|
status_code = 404
|
|
|
|
|
|
class ConflictError(BaseAPIException):
|
|
"""Raised when action conflicts with current state."""
|
|
|
|
message = "Conflict with current state"
|
|
code = "conflict"
|
|
status_code = 409
|
|
|
|
|
|
class RateLimitError(BaseAPIException):
|
|
"""Raised when rate limit is exceeded."""
|
|
|
|
message = "Rate limit exceeded"
|
|
code = "rate_limit_exceeded"
|
|
status_code = 429
|
|
|
|
|
|
# ============================================================================
|
|
# Server Errors (5xx)
|
|
# ============================================================================
|
|
|
|
|
|
class InternalError(BaseAPIException):
|
|
"""Raised for unexpected internal errors."""
|
|
|
|
message = "Internal server error"
|
|
code = "internal_error"
|
|
status_code = 500
|
|
|
|
|
|
class ServiceUnavailableError(BaseAPIException):
|
|
"""Raised when a dependent service is unavailable."""
|
|
|
|
message = "Service temporarily unavailable"
|
|
code = "service_unavailable"
|
|
status_code = 503
|
|
|
|
|
|
# ============================================================================
|
|
# Business Logic Errors
|
|
# ============================================================================
|
|
|
|
|
|
class BusinessLogicError(BaseAPIException):
|
|
"""
|
|
Base class for business logic errors.
|
|
|
|
Use this for domain-specific errors that should return 400/422.
|
|
"""
|
|
|
|
message = "Business logic error"
|
|
code = "business_error"
|
|
status_code = 400
|
|
|
|
|
|
class InvalidStateError(BusinessLogicError):
|
|
"""Raised when entity is in invalid state for requested operation."""
|
|
|
|
message = "Invalid state for this operation"
|
|
code = "invalid_state"
|
|
status_code = 400
|
|
|
|
|
|
class DuplicateError(BusinessLogicError):
|
|
"""Raised when attempting to create a duplicate resource."""
|
|
|
|
message = "Resource already exists"
|
|
code = "duplicate"
|
|
status_code = 409
|
|
|
|
|
|
class QuotaExceededError(BusinessLogicError):
|
|
"""Raised when a resource quota is exceeded."""
|
|
|
|
message = "Quota exceeded"
|
|
code = "quota_exceeded"
|
|
status_code = 400
|