Source code for loom.core.errors.errors
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from loom.core.errors.codes import ErrorCode
[docs]
class LoomError(Exception):
"""Base class for all framework errors.
Carries a machine-readable ``code`` used by transport adapters to
produce appropriate responses (HTTP status, dead-letter routing, etc.)
without coupling the domain to any transport layer.
Args:
message: Human-readable description of the error.
code: Machine-readable discriminator (e.g. ``"not_found"``).
Example::
raise NotFound("User", id=42)
"""
def __init__(self, message: str, *, code: str) -> None:
self.message = message
self.code = code
super().__init__(message)
[docs]
class DomainError(LoomError):
"""Base class for all business-logic errors.
Raised inside UseCase execution. Transport adapters catch and convert
these to their own error representations.
"""
[docs]
class SystemError(LoomError):
"""Base class for infrastructure and unexpected errors.
Raised when a repository, external service, or internal subsystem
fails. Transport adapters may retry on SystemError and dead-letter
on DomainError.
Args:
message: Description of the system failure.
"""
def __init__(self, message: str) -> None:
super().__init__(message, code=ErrorCode.SYSTEM_ERROR)
[docs]
class NotFound(DomainError):
"""Raised when a requested entity does not exist.
Emitted automatically by ``LoadStep`` when a repository returns
``None``. May also be raised explicitly inside ``execute`` for
semantic not-found conditions.
Args:
entity: Name of the missing entity type (e.g. ``"User"``).
id: Lookup key that yielded no result.
Example::
raise NotFound("User", id=42)
"""
def __init__(self, entity: str, *, id: Any) -> None:
self.entity = entity
self.id = id
super().__init__(f"{entity} with id={id!r} not found", code=ErrorCode.NOT_FOUND)
[docs]
class Forbidden(DomainError):
"""Raised when an action is not permitted for the caller.
Args:
message: Description of the access restriction.
"""
def __init__(self, message: str = "Forbidden") -> None:
super().__init__(message, code=ErrorCode.FORBIDDEN)
[docs]
class Conflict(DomainError):
"""Raised when an operation conflicts with existing state.
Args:
message: Description of the conflicting condition.
"""
def __init__(self, message: str) -> None:
super().__init__(message, code=ErrorCode.CONFLICT)
[docs]
class RuleViolation(DomainError):
"""Raised when a single business rule is violated.
Args:
field: The field that caused the violation.
message: Human-readable description of the violation.
Example::
raise RuleViolation("email", "Disposable emails not allowed")
"""
def __init__(self, field: str, message: str) -> None:
self.field = field
# Override message attr with just the violation message (not field-prefixed)
# so adapters can access field and message independently.
super().__init__(f"{field}: {message}", code=ErrorCode.RULE_VIOLATION)
self.message = message
[docs]
class RuleViolations(DomainError):
"""Raised when one or more business rules are violated.
Accumulates all violations from rule evaluation instead of failing
fast on the first violation.
Args:
violations: Sequence of individual rule violations.
"""
def __init__(self, violations: Sequence[RuleViolation]) -> None:
self.violations: tuple[RuleViolation, ...] = tuple(violations)
messages = "; ".join(str(v) for v in self.violations)
super().__init__(messages, code=ErrorCode.RULE_VIOLATIONS)