Files
mcma-backend/app/domain/errors.py
T
Цвылев Александр Вадимович a263272935 feat(api): finish Group A stubbed endpoints
Implements previously-stubbed /api/v1 endpoints (hexagonal: ports -> repos
-> services -> routers wired in deps):

- playlists: GET /{id}/cover (serves stored cover, 404 when absent)
- settings: GET/PATCH /settings + GET/PUT /settings/scrobbling — lazy
  per-user row, write-only Fernet-encrypted scrobble session key; adds
  user_settings table + migration (chains off dc126696f5a6)
- storage: GET /duplicates, /broken, /missing-metadata + admin POST
  /cleanup (arq cleanup_storage worker; reconciles local refs only,
  guarded against a storage-outage mass delete)
- admin: GET /services, /sources, /settings + POST /reindex; PATCH
  /settings and /sources/{source} return 501 (config is env-managed)

Adds NotSupportedError (-> HTTP 501). Integration tests for each surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 14:33:12 +03:00

87 lines
2.1 KiB
Python

"""Domain exception hierarchy — framework-agnostic.
Services raise these; the API layer maps them to HTTP responses
(see ``app.api.errors``). The domain never references HTTP status codes.
"""
class DomainError(Exception):
"""Base for all expected, business-meaningful failures.
``code`` is a stable, machine-readable identifier returned to clients.
"""
code: str = "domain_error"
def __init__(self, message: str | None = None) -> None:
super().__init__(message or self.__class__.__doc__ or self.code)
self.message = str(self)
class NotFoundError(DomainError):
"""Requested resource does not exist."""
code = "not_found"
class AlreadyExistsError(DomainError):
"""Resource conflicts with an existing one (e.g. duplicate)."""
code = "already_exists"
class ConflictError(DomainError):
"""Operation conflicts with current state (e.g. stale version on write)."""
code = "conflict"
class ValidationError(DomainError):
"""Input is well-formed but violates a business rule."""
code = "validation_error"
class AuthenticationError(DomainError):
"""Caller could not be authenticated."""
code = "authentication_error"
class PermissionDeniedError(DomainError):
"""Caller authenticated but not authorized for this action."""
code = "permission_denied"
class NotSupportedError(DomainError):
"""Operation is intentionally unsupported (e.g. a config knob that is managed
via environment, not mutable at runtime)."""
code = "not_supported"
class DependencyUnavailableError(DomainError):
"""An external dependency (source, ML, MusicBrainz) is unavailable.
Callers should degrade gracefully rather than propagate as a hard 500.
"""
code = "dependency_unavailable"
class StorageError(DomainError):
"""File storage operation failed."""
code = "storage_error"
class RangeNotSatisfiableError(DomainError):
"""Requested byte range cannot be satisfied."""
code = "range_not_satisfiable"
def __init__(self, total_size: int) -> None:
super().__init__("Requested range is not satisfiable.")
self.total_size = total_size