Files
orion/app/modules/cms/exceptions.py
Samir Boulahtit 1b8a40f1ff
All checks were successful
CI / dependency-scanning (push) Successful in 27s
CI / docs (push) Successful in 35s
CI / ruff (push) Successful in 8s
CI / pytest (push) Successful in 34m22s
CI / validate (push) Successful in 19s
CI / deploy (push) Successful in 2m25s
feat(validators): add noqa suppression support to security and performance validators
- Add centralized _is_noqa_suppressed() to BaseValidator with normalization
  (accepts both SEC001 and SEC-001 formats for ruff compatibility)
- Wire noqa support into all 21 security and 18 performance check functions
- Add ruff external config for SEC/PERF/MOD/EXC codes in pyproject.toml
- Convert all 280 Python noqa comments to dashless format (ruff-compatible)
- Add site/ to IGNORE_PATTERNS (excludes mkdocs build output)
- Suppress 152 false positive findings (test passwords, seed data, validator
  self-references, Apple Wallet SHA1, etc.)
- Security: 79 errors → 0, 60 warnings → 0
- Performance: 80 warnings → 77 (3 test script suppressions)
- Add proposal doc with noqa inventory and remaining findings recommendations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 22:56:56 +01:00

372 lines
12 KiB
Python

# app/modules/cms/exceptions.py
"""
CMS module exceptions.
This module provides exception classes for CMS operations including:
- Content page management
- Media file handling
- Store theme customization
"""
from typing import Any
from app.exceptions.base import (
AuthorizationException,
BusinessLogicException,
ConflictException,
ResourceNotFoundException,
ValidationException,
)
__all__ = [
# Content page exceptions
"ContentPageNotFoundException",
"ContentPageAlreadyExistsException",
"ContentPageSlugReservedException",
"ContentPageNotPublishedException",
"UnauthorizedContentPageAccessException",
"StoreNotAssociatedException",
"NoPlatformSubscriptionException",
"ContentPageValidationException",
# Media exceptions
"MediaNotFoundException",
"MediaUploadException",
"MediaValidationException",
"UnsupportedMediaTypeException",
"MediaFileTooLargeException",
"MediaOptimizationException",
"MediaDeleteException",
# Theme exceptions
"StoreThemeNotFoundException",
"InvalidThemeDataException",
"ThemePresetNotFoundException",
"ThemeValidationException",
"ThemePresetAlreadyAppliedException",
"InvalidColorFormatException",
"InvalidFontFamilyException",
"ThemeOperationException",
]
# =============================================================================
# Content Page Exceptions
# =============================================================================
class ContentPageNotFoundException(ResourceNotFoundException):
"""Raised when a content page is not found."""
def __init__(self, identifier: str | int | None = None):
if identifier:
message = f"Content page not found: {identifier}"
else:
message = "Content page not found"
super().__init__(
message=message,
resource_type="ContentPage",
identifier=str(identifier) if identifier else "unknown",
error_code="CONTENT_PAGE_NOT_FOUND",
)
class ContentPageAlreadyExistsException(ConflictException): # noqa: MOD025
"""Raised when a content page with the same slug already exists."""
def __init__(self, slug: str, store_id: int | None = None):
if store_id:
message = f"Content page with slug '{slug}' already exists for this store"
else:
message = f"Platform content page with slug '{slug}' already exists"
super().__init__(
message=message,
error_code="CONTENT_PAGE_ALREADY_EXISTS",
details={"slug": slug, "store_id": store_id} if store_id else {"slug": slug},
)
class ContentPageSlugReservedException(ValidationException): # noqa: MOD025
"""Raised when trying to use a reserved slug."""
def __init__(self, slug: str):
super().__init__(
message=f"Content page slug '{slug}' is reserved",
field="slug",
details={"slug": slug},
)
self.error_code = "CONTENT_PAGE_SLUG_RESERVED"
class ContentPageNotPublishedException(BusinessLogicException): # noqa: MOD025
"""Raised when trying to access an unpublished content page."""
def __init__(self, slug: str):
super().__init__(
message=f"Content page '{slug}' is not published",
error_code="CONTENT_PAGE_NOT_PUBLISHED",
details={"slug": slug},
)
class UnauthorizedContentPageAccessException(AuthorizationException):
"""Raised when a user tries to access/modify a content page they don't own."""
def __init__(self, action: str = "access"):
super().__init__(
message=f"Cannot {action} content pages from other stores",
error_code="CONTENT_PAGE_ACCESS_DENIED",
details={"required_permission": f"content_page:{action}"},
)
class StoreNotAssociatedException(AuthorizationException): # noqa: MOD025
"""Raised when a user is not associated with a store."""
def __init__(self):
super().__init__(
message="User is not associated with a store",
error_code="STORE_NOT_ASSOCIATED",
details={"required_permission": "store:member"},
)
class NoPlatformSubscriptionException(BusinessLogicException):
"""Raised when a store is not subscribed to any platform."""
def __init__(self, store_id: int | None = None):
details = {}
if store_id:
details["store_id"] = store_id
super().__init__(
message="Store is not subscribed to any platform",
error_code="NO_PLATFORM_SUBSCRIPTION",
details=details if details else None,
)
class ContentPageValidationException(ValidationException): # noqa: MOD025
"""Raised when content page data validation fails."""
def __init__(self, field: str, message: str, value: str | None = None):
details = {}
if value:
details["value"] = value
super().__init__(message=message, field=field, details=details if details else None)
self.error_code = "CONTENT_PAGE_VALIDATION_FAILED"
# =============================================================================
# Media Exceptions
# =============================================================================
class MediaNotFoundException(ResourceNotFoundException):
"""Raised when a media file is not found."""
def __init__(self, media_id: int | str | None = None):
if media_id:
message = f"Media file '{media_id}' not found"
else:
message = "Media file not found"
super().__init__(
resource_type="MediaFile",
identifier=str(media_id) if media_id else "unknown",
message=message,
error_code="MEDIA_NOT_FOUND",
)
class MediaUploadException(BusinessLogicException): # noqa: MOD025
"""Raised when media upload fails."""
def __init__(self, message: str = "Media upload failed", details: dict[str, Any] | None = None):
super().__init__(
message=message,
error_code="MEDIA_UPLOAD_FAILED",
details=details,
)
class MediaValidationException(ValidationException):
"""Raised when media validation fails (file type, size, etc.)."""
def __init__(
self,
message: str = "Media validation failed",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(message=message, field=field, details=details)
self.error_code = "MEDIA_VALIDATION_FAILED"
class UnsupportedMediaTypeException(ValidationException):
"""Raised when media file type is not supported."""
def __init__(self, file_type: str, allowed_types: list[str] | None = None):
details = {"file_type": file_type}
if allowed_types:
details["allowed_types"] = allowed_types
super().__init__(
message=f"Unsupported media type: {file_type}",
field="file",
details=details,
)
self.error_code = "UNSUPPORTED_MEDIA_TYPE"
class MediaFileTooLargeException(ValidationException):
"""Raised when media file exceeds size limit."""
def __init__(self, file_size: int, max_size: int, media_type: str = "file"):
super().__init__(
message=f"File size ({file_size} bytes) exceeds maximum allowed ({max_size} bytes) for {media_type}",
field="file",
details={
"file_size": file_size,
"max_size": max_size,
"media_type": media_type,
},
)
self.error_code = "MEDIA_FILE_TOO_LARGE"
class MediaOptimizationException(BusinessLogicException):
"""Raised when media optimization fails."""
def __init__(self, message: str = "Only images can be optimized"):
super().__init__(
message=message,
error_code="MEDIA_OPTIMIZATION_FAILED",
)
class MediaDeleteException(BusinessLogicException): # noqa: MOD025
"""Raised when media deletion fails."""
def __init__(self, message: str, details: dict[str, Any] | None = None):
super().__init__(
message=message,
error_code="MEDIA_DELETE_FAILED",
details=details,
)
# =============================================================================
# Theme Exceptions
# =============================================================================
class StoreThemeNotFoundException(ResourceNotFoundException):
"""Raised when a store theme is not found."""
def __init__(self, store_identifier: str):
super().__init__(
resource_type="StoreTheme",
identifier=store_identifier,
message=f"Theme for store '{store_identifier}' not found",
error_code="STORE_THEME_NOT_FOUND",
)
class InvalidThemeDataException(ValidationException): # noqa: MOD025
"""Raised when theme data is invalid."""
def __init__(
self,
message: str = "Invalid theme data",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_THEME_DATA"
class ThemePresetNotFoundException(ResourceNotFoundException):
"""Raised when a theme preset is not found."""
def __init__(self, preset_name: str, available_presets: list | None = None):
super().__init__(
resource_type="ThemePreset",
identifier=preset_name,
message=f"Theme preset '{preset_name}' not found",
error_code="THEME_PRESET_NOT_FOUND",
)
if available_presets:
self.details["available_presets"] = available_presets
class ThemeValidationException(ValidationException):
"""Raised when theme validation fails."""
def __init__(
self,
message: str = "Theme validation failed",
field: str | None = None,
validation_errors: dict[str, str] | None = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "THEME_VALIDATION_FAILED"
class ThemePresetAlreadyAppliedException(BusinessLogicException): # noqa: MOD025
"""Raised when trying to apply the same preset that's already active."""
def __init__(self, preset_name: str, store_code: str):
super().__init__(
message=f"Preset '{preset_name}' is already applied to store '{store_code}'",
error_code="THEME_PRESET_ALREADY_APPLIED",
details={"preset_name": preset_name, "store_code": store_code},
)
class InvalidColorFormatException(ValidationException):
"""Raised when color format is invalid."""
def __init__(self, color_value: str, field: str):
super().__init__(
message=f"Invalid color format: {color_value}",
field=field,
details={"color_value": color_value},
)
self.error_code = "INVALID_COLOR_FORMAT"
class InvalidFontFamilyException(ValidationException):
"""Raised when font family is invalid."""
def __init__(self, font_value: str, field: str):
super().__init__(
message=f"Invalid font family: {font_value}",
field=field,
details={"font_value": font_value},
)
self.error_code = "INVALID_FONT_FAMILY"
class ThemeOperationException(BusinessLogicException):
"""Raised when theme operation fails."""
def __init__(self, operation: str, store_code: str, reason: str):
super().__init__(
message=f"Theme operation '{operation}' failed for store '{store_code}': {reason}",
error_code="THEME_OPERATION_FAILED",
details={
"operation": operation,
"store_code": store_code,
"reason": reason,
},
)