Source code for scim2_models.exceptions

"""SCIM exceptions corresponding to RFC 7644 error types.

This module provides a hierarchy of exceptions that map to SCIM protocol errors.
Each exception can be converted to a :class:`~scim2_models.Error` response object
or to a :class:`~pydantic_core.PydanticCustomError` for use in Pydantic validators.
"""

from typing import TYPE_CHECKING
from typing import Any

from pydantic_core import PydanticCustomError

from .context import Context

if TYPE_CHECKING:
    from .messages.error import Error


[docs] class SCIMException(Exception): """Base exception for SCIM protocol errors. Each subclass corresponds to a scimType defined in :rfc:`RFC 7644 Table 9 <7644#section-3.12>`. :param detail: The error detail message. :param scim_ctx: The SCIM context in which the exception occurred. """ status: int = 400 scim_type: str = "" _default_detail: str = "A SCIM error occurred" def __init__( self, *, detail: str | None = None, scim_ctx: Context | None = None, **context: Any, ): self.context = context self._detail = detail self.scim_ctx = scim_ctx super().__init__(detail or self._default_detail) @property def detail(self) -> str: """The error detail message.""" return self._detail or self._default_detail
[docs] def to_error(self) -> "Error": """Convert this exception to a SCIM Error response object.""" from .messages.error import Error return Error( status=self.status, scim_type=self.scim_type or None, detail=str(self), )
[docs] def as_pydantic_error(self) -> PydanticCustomError: """Convert to PydanticCustomError for use in Pydantic validators.""" return PydanticCustomError( f"scim_{self.scim_type}" if self.scim_type else "scim_error", str(self), {"scim_type": self.scim_type, "status": self.status, **self.context}, )
[docs] @classmethod def from_error( cls, error: "Error", scim_ctx: Context | None = None ) -> "SCIMException": """Create an exception from a SCIM Error object. :param error: The SCIM Error object to convert. :param scim_ctx: The SCIM context in which the exception occurred. :return: The appropriate SCIMException subclass instance. """ from .messages.error import Error if not isinstance(error, Error): raise TypeError(f"Expected Error, got {type(error).__name__}") exception_class = _SCIM_TYPE_TO_EXCEPTION.get(error.scim_type or "", cls) return exception_class(detail=error.detail, scim_ctx=scim_ctx)
[docs] class InvalidFilterException(SCIMException): """The specified filter syntax was invalid. Corresponds to scimType ``invalidFilter`` with HTTP status 400. :rfc:`RFC 7644 Section 3.4.2.2 <7644#section-3.4.2.2>` """ status = 400 scim_type = "invalidFilter" _default_detail = ( "The specified filter syntax was invalid, " "or the specified attribute and filter comparison combination is not supported" ) def __init__(self, *, filter: str | None = None, **kw: Any): self.filter = filter super().__init__(**kw)
[docs] class TooManyException(SCIMException): """The specified filter yields too many results. Corresponds to scimType ``tooMany`` with HTTP status 400. :rfc:`RFC 7644 Section 3.4.2.2 <7644#section-3.4.2.2>` """ status = 400 scim_type = "tooMany" _default_detail = ( "The specified filter yields many more results " "than the server is willing to calculate or process" )
[docs] class UniquenessException(SCIMException): """One or more attribute values are already in use or reserved. Corresponds to scimType ``uniqueness`` with HTTP status 409. :rfc:`RFC 7644 Section 3.3.1 <7644#section-3.3.1>` """ status = 409 scim_type = "uniqueness" _default_detail = ( "One or more of the attribute values are already in use or are reserved" ) def __init__( self, *, attribute: str | None = None, value: Any | None = None, **kw: Any ): self.attribute = attribute self.value = value super().__init__(**kw)
[docs] class MutabilityException(SCIMException): """The attempted modification is not compatible with the attribute's mutability. Corresponds to scimType ``mutability`` with HTTP status 400. :rfc:`RFC 7644 Section 3.5.2 <7644#section-3.5.2>` """ status = 400 scim_type = "mutability" _default_detail = ( "The attempted modification is not compatible with the target attribute's " "mutability or current state" ) def __init__( self, *, attribute: str | None = None, mutability: str | None = None, operation: str | None = None, **kw: Any, ): self.attribute = attribute self.mutability = mutability self.operation = operation super().__init__(**kw)
[docs] class InvalidSyntaxException(SCIMException): """The request body message structure was invalid. Corresponds to scimType ``invalidSyntax`` with HTTP status 400. :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>` """ status = 400 scim_type = "invalidSyntax" _default_detail = ( "The request body message structure was invalid " "or did not conform to the request schema" )
[docs] class InvalidPathException(SCIMException): """The path attribute was invalid or malformed. Corresponds to scimType ``invalidPath`` with HTTP status 400. :rfc:`RFC 7644 Section 3.5.2 <7644#section-3.5.2>` """ status = 400 scim_type = "invalidPath" _default_detail = "The path attribute was invalid or malformed" def __init__(self, *, path: str | None = None, **kw: Any): self.path = path super().__init__(**kw)
[docs] class PathNotFoundException(InvalidPathException): """The path references a non-existent field. This is a specialized form of :class:`InvalidPathException`. """ _default_detail = "The specified path references a non-existent field" def __init__(self, *, path: str | None = None, field: str | None = None, **kw: Any): self.field = field super().__init__(path=path, **kw) def __str__(self) -> str: if self._detail: return self._detail if self.field: return f"Field not found: {self.field}" return self._default_detail
[docs] class NoTargetException(SCIMException): """The specified path did not yield a target that could be operated on. Corresponds to scimType ``noTarget`` with HTTP status 400. :rfc:`RFC 7644 Section 3.5.2 <7644#section-3.5.2>` """ status = 400 scim_type = "noTarget" _default_detail = ( "The specified path did not yield an attribute or attribute value " "that could be operated on" ) def __init__(self, *, path: str | None = None, **kw: Any): self.path = path super().__init__(**kw)
[docs] class InvalidValueException(SCIMException): """A required value was missing or the value was not compatible. Corresponds to scimType ``invalidValue`` with HTTP status 400. :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>` """ status = 400 scim_type = "invalidValue" _default_detail = ( "A required value was missing, or the value specified was not compatible " "with the operation or attribute type, or resource schema" ) def __init__( self, *, attribute: str | None = None, reason: str | None = None, **kw: Any ): self.attribute = attribute self.reason = reason super().__init__(**kw)
[docs] class InvalidVersionException(SCIMException): """The specified SCIM protocol version is not supported. Corresponds to scimType ``invalidVers`` with HTTP status 400. :rfc:`RFC 7644 Section 3.13 <7644#section-3.13>` """ status = 400 scim_type = "invalidVers" _default_detail = "The specified SCIM protocol version is not supported"
[docs] class SensitiveException(SCIMException): """The request cannot be completed due to sensitive information in the URI. Corresponds to scimType ``sensitive`` with HTTP status 400. :rfc:`RFC 7644 Section 7.5.2 <7644#section-7.5.2>` """ status = 400 scim_type = "sensitive" _default_detail = ( "The specified request cannot be completed, due to the passing of sensitive " "information in a request URI" )
_SCIM_TYPE_TO_EXCEPTION: dict[str, type[SCIMException]] = { "invalidFilter": InvalidFilterException, "tooMany": TooManyException, "uniqueness": UniquenessException, "mutability": MutabilityException, "invalidSyntax": InvalidSyntaxException, "invalidPath": InvalidPathException, "noTarget": NoTargetException, "invalidValue": InvalidValueException, "invalidVers": InvalidVersionException, "sensitive": SensitiveException, }