feat(core): add core module with mixins, services, and background jobs
- 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
This commit is contained in:
169
src/apps/core/exceptions.py
Normal file
169
src/apps/core/exceptions.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user