diff --git a/doc/changelog.rst b/doc/changelog.rst index d37627d..b4b01f6 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,15 @@ Changelog ========= +[0.8.0] - 2026-02-05 +-------------------- + +Changed +^^^^^^^ +- **Breaking:** SCIM errors now raise :class:`~scim2_models.SCIMException` (and subclasses) from scim2-models instead of custom exceptions. :issue:`39` +- **Breaking:** ``SCIMRequestError``, ``RequestPayloadValidationError``, and ``SCIMResponseErrorObject`` have been removed. +- Exceptions renamed from ``*Error`` to ``*Exception`` suffix. Old names are deprecated (removal in 0.9). + [0.7.3] - 2026-02-04 -------------------- @@ -16,14 +25,14 @@ Fixed ^^^^^ - Skip ``Content-Type`` header validation for 204 responses. :issue:`34` -[0.7.1] - 2025-01-25 +[0.7.1] - 2026-01-25 -------------------- Fixed ^^^^^ - ``schemas`` is no longer included in GET query parameters per RFC 7644 ยง3.4.2. -[0.7.0] - 2025-01-25 +[0.7.0] - 2026-01-25 -------------------- Added diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 5adcfdd..b6d5a2a 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -93,19 +93,36 @@ Have a look at the :doc:`reference` to see usage examples and the exhaustive set response = scim.create(request) print(f"User {response.id} has been created!") -By default, if the server returns an error, a :class:`~scim2_client.SCIMResponseErrorObject` exception is raised. -The :meth:`~scim2_client.SCIMResponseErrorObject.to_error` method gives access to the :class:`~scim2_models.Error` object: +Error management +================ + +By default, if the server returns an error, a :class:`~scim2_models.SCIMException` exception is raised. +The :meth:`~scim2_models.SCIMException.to_error` method gives access to the :class:`~scim2_models.Error` object: .. code-block:: python - from scim2_client import SCIMResponseErrorObject + from scim2_models import SCIMException try: response = scim.create(request) - except SCIMResponseErrorObject as exc: + except SCIMException as exc: error = exc.to_error() print(f"SCIM error [{error.status}] {error.scim_type}: {error.detail}") +The :attr:`~scim2_models.SCIMException.scim_ctx` attribute indicates whether the error originated from request validation or server response: + +.. code-block:: python + + from scim2_models import Context, SCIMException + + try: + response = scim.create(request) + except SCIMException as exc: + if Context.is_request(exc.scim_ctx): + print("Local validation error") + else: + print("Server returned an error") + PATCH modifications =================== @@ -189,9 +206,8 @@ To achieve this, all the methods provide the following parameters, all are :data If :data:`False` the server response is returned as-is. - :code:`expected_status_codes`: The list of expected status codes in the response. If :data:`None` any status code is accepted. - If an unexpected status code is returned, a :class:`~scim2_client.errors.UnexpectedStatusCode` exception is raised. -- :paramref:`~scim2_client.SCIMClient.raise_scim_errors`: If :data:`True` (the default) and the server returned an :class:`~scim2_models.Error` object, a :class:`~scim2_client.SCIMResponseErrorObject` exception will be raised. - The :meth:`~scim2_client.SCIMResponseErrorObject.to_error` method gives access to the :class:`~scim2_models.Error` object. + If an unexpected status code is returned, a :class:`~scim2_client.errors.UnexpectedStatusCodeException` exception is raised. +- :paramref:`~scim2_client.SCIMClient.raise_scim_errors`: If :data:`True` (the default) and the server returned an :class:`~scim2_models.Error` object, a :class:`~scim2_models.SCIMException` exception will be raised. If :data:`False` the error object is returned directly. diff --git a/pyproject.toml b/pyproject.toml index 3217e20..95f9dda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ requires-python = ">= 3.10" dependencies = [ - "scim2-models>=0.6.1", + "scim2-models>=0.6.4", ] [project.optional-dependencies] diff --git a/scim2_client/__init__.py b/scim2_client/__init__.py index c1a0cb1..b82511e 100644 --- a/scim2_client/__init__.py +++ b/scim2_client/__init__.py @@ -1,27 +1,37 @@ from .client import BaseSyncSCIMClient from .client import SCIMClient from .errors import RequestNetworkError -from .errors import RequestPayloadValidationError +from .errors import RequestNetworkException from .errors import ResponsePayloadValidationError +from .errors import ResponsePayloadValidationException from .errors import SCIMClientError -from .errors import SCIMRequestError +from .errors import SCIMClientException from .errors import SCIMResponseError -from .errors import SCIMResponseErrorObject +from .errors import SCIMResponseException from .errors import UnexpectedContentFormat +from .errors import UnexpectedContentFormatException from .errors import UnexpectedContentType +from .errors import UnexpectedContentTypeException from .errors import UnexpectedStatusCode +from .errors import UnexpectedStatusCodeException __all__ = [ "SCIMClient", "BaseSyncSCIMClient", + # New exception classes + "SCIMClientException", + "SCIMResponseException", + "RequestNetworkException", + "UnexpectedStatusCodeException", + "UnexpectedContentTypeException", + "UnexpectedContentFormatException", + "ResponsePayloadValidationException", + # Deprecated aliases (will be removed in 0.9) "SCIMClientError", - "SCIMRequestError", "SCIMResponseError", - "SCIMResponseErrorObject", - "UnexpectedContentFormat", - "UnexpectedContentType", - "UnexpectedStatusCode", - "RequestPayloadValidationError", "RequestNetworkError", + "UnexpectedStatusCode", + "UnexpectedContentType", + "UnexpectedContentFormat", "ResponsePayloadValidationError", ] diff --git a/scim2_client/client.py b/scim2_client/client.py index e54b0c8..5e9f2fc 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -10,22 +10,20 @@ from scim2_models import Context from scim2_models import Error from scim2_models import Extension +from scim2_models import InvalidValueException from scim2_models import ListResponse from scim2_models import PatchOp from scim2_models import Resource from scim2_models import ResourceType from scim2_models import Schema +from scim2_models import SCIMException from scim2_models import SearchRequest from scim2_models import ServiceProviderConfig -from scim2_client.errors import RequestPayloadValidationError -from scim2_client.errors import ResponsePayloadValidationError -from scim2_client.errors import SCIMClientError -from scim2_client.errors import SCIMRequestError -from scim2_client.errors import SCIMResponseError -from scim2_client.errors import SCIMResponseErrorObject -from scim2_client.errors import UnexpectedContentType -from scim2_client.errors import UnexpectedStatusCode +from scim2_client.errors import ResponsePayloadValidationException +from scim2_client.errors import SCIMResponseException +from scim2_client.errors import UnexpectedContentTypeException +from scim2_client.errors import UnexpectedStatusCodeException ResourceT = TypeVar("ResourceT", bound=Resource) @@ -65,7 +63,7 @@ class SCIMClient: :param check_response_content_type: Whether to validate that the response content types are valid. :param check_response_status_codes: Whether to validate that the response status codes are valid. :param raise_scim_errors: If :data:`True` and the server returned an - :class:`~scim2_models.Error` object during a request, a :class:`~scim2_client.SCIMResponseErrorObject` + :class:`~scim2_models.Error` object during a request, a :class:`~scim2_models.SCIMException` exception will be raised. If :data:`False` the error object is returned. This value can be overwritten in methods. .. note:: @@ -211,8 +209,8 @@ def _check_resource_model( return if resource_model not in CONFIG_RESOURCES: - raise SCIMRequestError( - f"Unknown resource type: '{resource_model}'", source=payload + raise InvalidValueException( + detail=f"Unknown resource type: '{resource_model}'" ) def resource_endpoint(self, resource_model: type[Resource] | None) -> str: @@ -236,7 +234,9 @@ def resource_endpoint(self, resource_model: type[Resource] | None) -> str: if schema == resource_type.schema_: return resource_type.endpoint - raise SCIMRequestError(f"No ResourceType is matching the schema: {schema}") + raise InvalidValueException( + detail=f"No ResourceType is matching the schema: {schema}" + ) def register_naive_resource_types(self): """Register a *naive* :class:`~scim2_models.ResourceType` for each :paramref:`resource_model `. @@ -259,7 +259,7 @@ def _check_status_codes( and expected_status_codes and status_code not in expected_status_codes ): - raise UnexpectedStatusCode(status_code) + raise UnexpectedStatusCodeException(status_code) def _check_content_types(self, headers: dict): # Interoperability considerations: The "application/scim+json" media @@ -274,7 +274,7 @@ def _check_content_types(self, headers: dict): self.check_response_content_type and actual_content_type not in expected_response_content_types ): - raise UnexpectedContentType(content_type=actual_content_type) + raise UnexpectedContentTypeException(content_type=actual_content_type) def check_response( self, @@ -312,7 +312,7 @@ def check_response( if response_payload and response_payload.get("schemas") == [Error.__schema__]: error = Error.model_validate(response_payload) if raise_scim_errors: - raise SCIMResponseErrorObject(error) + raise SCIMException.from_error(error, scim_ctx=scim_ctx) return error self._check_status_codes(status_code, expected_status_codes) @@ -338,12 +338,12 @@ def check_response( f"Expected type {expected} but got undefined object with no schema" ) - raise SCIMResponseError(message) + raise SCIMResponseException(message) try: return actual_type.model_validate(response_payload, scim_ctx=scim_ctx) except ValidationError as exc: - scim_exc = ResponsePayloadValidationError() + scim_exc = ResponsePayloadValidationException() if sys.version_info >= (3, 11): # pragma: no cover scim_exc.add_note(str(exc)) raise scim_exc from exc @@ -373,17 +373,17 @@ def _prepare_create_request( else: resource_model = Resource.get_by_payload(self.resource_models, resource) if not resource_model: - raise SCIMRequestError( - "Cannot guess resource type from the payload" + raise InvalidValueException( + detail="Cannot guess resource type from the payload" ) try: resource = resource_model.model_validate(resource) except ValidationError as exc: - scim_validation_exc = RequestPayloadValidationError(source=resource) - if sys.version_info >= (3, 11): # pragma: no cover - scim_validation_exc.add_note(str(exc)) - raise scim_validation_exc from exc + errors = Error.from_validation_errors(exc) + raise SCIMException.from_error( + errors[0], scim_ctx=Context.RESOURCE_CREATION_REQUEST + ) from exc self._check_resource_model(resource_model, resource) req.expected_types = [resource.__class__] @@ -442,7 +442,9 @@ def _prepare_query_request( elif resource_model == ServiceProviderConfig: req.expected_types = [resource_model] if id: - raise SCIMClientError("ServiceProviderConfig cannot have an id") + raise InvalidValueException( + detail="ServiceProviderConfig cannot have an id" + ) elif id: req.expected_types = [resource_model] @@ -527,23 +529,22 @@ def _prepare_replace_request( else: resource_model = Resource.get_by_payload(self.resource_models, resource) if not resource_model: - raise SCIMRequestError( - "Cannot guess resource type from the payload", - source=resource, + raise InvalidValueException( + detail="Cannot guess resource type from the payload" ) try: resource = resource_model.model_validate(resource) except ValidationError as exc: - scim_validation_exc = RequestPayloadValidationError(source=resource) - if sys.version_info >= (3, 11): # pragma: no cover - scim_validation_exc.add_note(str(exc)) - raise scim_validation_exc from exc + errors = Error.from_validation_errors(exc) + raise SCIMException.from_error( + errors[0], scim_ctx=Context.RESOURCE_REPLACEMENT_REQUEST + ) from exc self._check_resource_model(resource_model, resource) if not resource.id: - raise SCIMRequestError("Resource must have an id", source=resource) + raise InvalidValueException(detail="Resource must have an id") req.expected_types = [resource.__class__] req.payload = resource.model_dump( @@ -576,7 +577,7 @@ def _prepare_patch_request( :param expected_status_codes: List of HTTP status codes expected for this request. :param raise_scim_errors: If :data:`True` and the server returned an :class:`~scim2_models.Error` object during a request, a - :class:`~scim2_client.SCIMResponseErrorObject` exception will be raised. + :class:`~scim2_models.SCIMException` exception will be raised. :param kwargs: Additional request parameters. :return: The prepared request payload. """ @@ -605,10 +606,10 @@ def _prepare_patch_request( scim_ctx=Context.RESOURCE_PATCH_REQUEST ) except ValidationError as exc: - scim_validation_exc = RequestPayloadValidationError(source=patch_op) - if sys.version_info >= (3, 11): # pragma: no cover - scim_validation_exc.add_note(str(exc)) - raise scim_validation_exc from exc + errors = Error.from_validation_errors(exc) + raise SCIMException.from_error( + errors[0], scim_ctx=Context.RESOURCE_PATCH_REQUEST + ) from exc req.url = req.request_kwargs.pop( "url", f"{self.resource_endpoint(resource_model)}/{id}" diff --git a/scim2_client/engines/httpx.py b/scim2_client/engines/httpx.py index d0cc7d7..8789e18 100644 --- a/scim2_client/engines/httpx.py +++ b/scim2_client/engines/httpx.py @@ -16,9 +16,9 @@ from scim2_client.client import BaseAsyncSCIMClient from scim2_client.client import BaseSyncSCIMClient -from scim2_client.errors import RequestNetworkError -from scim2_client.errors import SCIMClientError -from scim2_client.errors import UnexpectedContentFormat +from scim2_client.errors import RequestNetworkException +from scim2_client.errors import SCIMClientException +from scim2_client.errors import UnexpectedContentFormatException ResourceT = TypeVar("ResourceT", bound=Resource) @@ -29,7 +29,7 @@ def handle_request_error(payload=None): yield except RequestError as exc: - scim_network_exc = RequestNetworkError(source=payload) + scim_network_exc = RequestNetworkException(source=payload) if sys.version_info >= (3, 11): # pragma: no cover scim_network_exc.add_note(str(exc)) raise scim_network_exc from exc @@ -41,9 +41,9 @@ def handle_response_error(response: Response): yield except json.decoder.JSONDecodeError as exc: - raise UnexpectedContentFormat(source=response) from exc + raise UnexpectedContentFormatException(source=response) from exc - except SCIMClientError as exc: + except SCIMClientException as exc: exc.source = response raise exc @@ -60,7 +60,7 @@ class SyncSCIMClient(BaseSyncSCIMClient): :param check_response_payload: Whether to validate that the response payloads are valid. If set, the raw payload will be returned. This value can be overwritten in methods. :param raise_scim_errors: If :data:`True` and the server returned an - :class:`~scim2_models.Error` object during a request, a :class:`~scim2_client.SCIMResponseErrorObject` + :class:`~scim2_models.Error` object during a request, a :class:`~scim2_models.SCIMException` exception will be raised. If :data:`False` the error object is returned. This value can be overwritten in methods. """ @@ -283,7 +283,7 @@ class AsyncSCIMClient(BaseAsyncSCIMClient): :param check_response_payload: Whether to validate that the response payloads are valid. If set, the raw payload will be returned. This value can be overwritten in methods. :param raise_scim_errors: If :data:`True` and the server returned an - :class:`~scim2_models.Error` object during a request, a :class:`~scim2_client.SCIMResponseErrorObject` + :class:`~scim2_models.Error` object during a request, a :class:`~scim2_models.SCIMException` exception will be raised. If :data:`False` the error object is returned. This value can be overwritten in methods. """ diff --git a/scim2_client/engines/werkzeug.py b/scim2_client/engines/werkzeug.py index 6b28808..015a5eb 100644 --- a/scim2_client/engines/werkzeug.py +++ b/scim2_client/engines/werkzeug.py @@ -13,8 +13,8 @@ from werkzeug.test import Client from scim2_client.client import BaseSyncSCIMClient -from scim2_client.errors import SCIMClientError -from scim2_client.errors import UnexpectedContentFormat +from scim2_client.errors import SCIMClientException +from scim2_client.errors import UnexpectedContentFormatException ResourceT = TypeVar("ResourceT", bound=Resource) @@ -25,9 +25,9 @@ def handle_response_error(response): yield except json.decoder.JSONDecodeError as exc: - raise UnexpectedContentFormat(source=response) from exc + raise UnexpectedContentFormatException(source=response) from exc - except SCIMClientError as exc: + except SCIMClientException as exc: exc.source = response raise exc @@ -51,7 +51,7 @@ class TestSCIMClient(BaseSyncSCIMClient): :param check_response_payload: Whether to validate that the response payloads are valid. If set, the raw payload will be returned. This value can be overwritten in methods. :param raise_scim_errors: If :data:`True` and the server returned an - :class:`~scim2_models.Error` object during a request, a :class:`~scim2_client.SCIMResponseErrorObject` + :class:`~scim2_models.Error` object during a request, a :class:`~scim2_models.SCIMException` exception will be raised. If :data:`False` the error object is returned. This value can be overwritten in methods. .. code-block:: python diff --git a/scim2_client/errors.py b/scim2_client/errors.py index deb02a5..8450151 100644 --- a/scim2_client/errors.py +++ b/scim2_client/errors.py @@ -1,11 +1,8 @@ -from typing import TYPE_CHECKING +import warnings from typing import Any -if TYPE_CHECKING: - from scim2_models import Error - -class SCIMClientError(Exception): +class SCIMClientException(Exception): """Base exception for scim2-client. :param message: The exception reason. @@ -24,15 +21,12 @@ def __str__(self) -> str: return self.message or "UNKNOWN" -class SCIMRequestError(SCIMClientError): - """Base exception for errors happening during request payload building.""" - - -class RequestNetworkError(SCIMRequestError): - """Error raised when a network error happened during request. +class RequestNetworkException(SCIMClientException): + """Exception raised when a network error happened during request. - This error is raised when a :class:`httpx.RequestError` has been caught while performing a request. - The original :class:`~httpx.RequestError` is available with :attr:`~BaseException.__cause__`. + This exception is raised when a :class:`httpx.RequestError` has been caught + while performing a request. The original :class:`~httpx.RequestError` is + available with :attr:`~BaseException.__cause__`. """ def __init__(self, *args: Any, **kwargs: Any) -> None: @@ -40,60 +34,12 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(message, *args, **kwargs) -class RequestPayloadValidationError(SCIMRequestError): - """Error raised when an invalid request payload has been passed to SCIMClient. - - This error is raised when a :class:`pydantic.ValidationError` has been caught - while validating the client request payload. - The original :class:`~pydantic.ValidationError` is available with :attr:`~BaseException.__cause__`. - - .. code-block:: python - - try: - scim.create( - { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], - "active": "not-a-bool", - } - ) - except RequestPayloadValidationError as exc: - print("Original validation error cause", exc.__cause__) - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - message = kwargs.pop("message", "Server request payload validation error") - super().__init__(message, *args, **kwargs) - - -class SCIMResponseError(SCIMClientError): +class SCIMResponseException(SCIMClientException): """Base exception for errors happening during response payload validation.""" -class SCIMResponseErrorObject(SCIMResponseError): - """The server response returned a :class:`scim2_models.Error` object. - - Those errors are only raised when the :code:`raise_scim_errors` parameter is :data:`True`. - - :param error: The :class:`~scim2_models.Error` object returned by the server. - """ - - def __init__(self, error: "Error", *args: Any, **kwargs: Any) -> None: - self._error = error - parts = [] - if error.scim_type: - parts.append(error.scim_type + ":") - if error.detail: - parts.append(error.detail) - message = " ".join(parts) if parts else "SCIM Error" - super().__init__(message, *args, **kwargs) - - def to_error(self) -> "Error": - """Return the :class:`~scim2_models.Error` object returned by the server.""" - return self._error - - -class UnexpectedStatusCode(SCIMResponseError): - """Error raised when a server returned an unexpected status code for a given :class:`~scim2_models.Context`.""" +class UnexpectedStatusCodeException(SCIMResponseException): + """Exception raised when a server returned an unexpected status code.""" def __init__(self, status_code: int, *args: Any, **kwargs: Any) -> None: message = kwargs.pop( @@ -102,37 +48,134 @@ def __init__(self, status_code: int, *args: Any, **kwargs: Any) -> None: super().__init__(message, *args, **kwargs) -class UnexpectedContentType(SCIMResponseError): - """Error raised when a server returned an unexpected `Content-Type` header in a response.""" +class UnexpectedContentTypeException(SCIMResponseException): + """Exception raised when a server returned an unexpected `Content-Type` header.""" def __init__(self, content_type: str, *args: Any, **kwargs: Any) -> None: message = kwargs.pop("message", f"Unexpected content type: {content_type}") super().__init__(message, *args, **kwargs) -class UnexpectedContentFormat(SCIMResponseError): - """Error raised when a server returned a response in a non-JSON format.""" +class UnexpectedContentFormatException(SCIMResponseException): + """Exception raised when a server returned a response in a non-JSON format.""" def __init__(self, *args: Any, **kwargs: Any) -> None: message = kwargs.pop("message", "Unexpected response content format") super().__init__(message, *args, **kwargs) -class ResponsePayloadValidationError(SCIMResponseError): - """Error raised when the server returned a payload that cannot be validated. +class ResponsePayloadValidationException(SCIMResponseException): + """Exception raised when the server returned a payload that cannot be validated. - This error is raised when a :class:`pydantic.ValidationError` has been caught + This exception is raised when a :class:`pydantic.ValidationError` has been caught while validating the server response payload. - The original :class:`~pydantic.ValidationError` is available with :attr:`~BaseException.__cause__`. + The original :class:`~pydantic.ValidationError` is available with + :attr:`~BaseException.__cause__`. .. code-block:: python try: scim.query(User, "foobar") - except ResponsePayloadValidationError as exc: + except ResponsePayloadValidationException as exc: print("Original validation error cause", exc.__cause__) """ def __init__(self, *args: Any, **kwargs: Any) -> None: message = kwargs.pop("message", "Server response payload validation error") super().__init__(message, *args, **kwargs) + + +# Deprecated aliases - will be removed in 0.9 + + +class SCIMClientError(SCIMClientException): + """Deprecated: Use :class:`SCIMClientException` instead.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "SCIMClientError is deprecated, use SCIMClientException instead. " + "It will be removed in version 0.9.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + + +class SCIMResponseError(SCIMResponseException): + """Deprecated: Use :class:`SCIMResponseException` instead.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "SCIMResponseError is deprecated, use SCIMResponseException instead. " + "It will be removed in version 0.9.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + + +class RequestNetworkError(RequestNetworkException): + """Deprecated: Use :class:`RequestNetworkException` instead.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "RequestNetworkError is deprecated, use RequestNetworkException instead. " + "It will be removed in version 0.9.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + + +class UnexpectedStatusCode(UnexpectedStatusCodeException): + """Deprecated: Use :class:`UnexpectedStatusCodeException` instead.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "UnexpectedStatusCode is deprecated, use UnexpectedStatusCodeException " + "instead. It will be removed in version 0.9.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + + +class UnexpectedContentType(UnexpectedContentTypeException): + """Deprecated: Use :class:`UnexpectedContentTypeException` instead.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "UnexpectedContentType is deprecated, use UnexpectedContentTypeException " + "instead. It will be removed in version 0.9.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + + +class UnexpectedContentFormat(UnexpectedContentFormatException): + """Deprecated: Use :class:`UnexpectedContentFormatException` instead.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "UnexpectedContentFormat is deprecated, use " + "UnexpectedContentFormatException instead. " + "It will be removed in version 0.9.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + + +class ResponsePayloadValidationError(ResponsePayloadValidationException): + """Deprecated: Use :class:`ResponsePayloadValidationException` instead.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "ResponsePayloadValidationError is deprecated, use " + "ResponsePayloadValidationException instead. " + "It will be removed in version 0.9.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) diff --git a/tests/engines/test_httpx.py b/tests/engines/test_httpx.py index 8d001a7..7ae940e 100644 --- a/tests/engines/test_httpx.py +++ b/tests/engines/test_httpx.py @@ -7,12 +7,12 @@ from httpx import Client from scim2_models import PatchOp from scim2_models import PatchOperation +from scim2_models import SCIMException from scim2_models import SearchRequest from scim2_models import ServiceProviderConfig from scim2_client.engines.httpx import AsyncSCIMClient from scim2_client.engines.httpx import SyncSCIMClient -from scim2_client.errors import SCIMResponseErrorObject scim2_server = pytest.importorskip("scim2_server") from scim2_server.backend import InMemoryBackend # noqa: E402 @@ -93,7 +93,7 @@ def test_sync_engine(server): assert queried_user.display_name == "patched name" scim_client.delete(User, response_user.id) - with pytest.raises(SCIMResponseErrorObject): + with pytest.raises(SCIMException): scim_client.query(User, response_user.id) @@ -152,5 +152,5 @@ async def test_async_engine(server): assert queried_user.display_name == "async patched name" await scim_client.delete(User, response_user.id) - with pytest.raises(SCIMResponseErrorObject): + with pytest.raises(SCIMException): await scim_client.query(User, response_user.id) diff --git a/tests/engines/test_werkzeug.py b/tests/engines/test_werkzeug.py index e194bcf..e9d8aab 100644 --- a/tests/engines/test_werkzeug.py +++ b/tests/engines/test_werkzeug.py @@ -1,6 +1,7 @@ import pytest from scim2_models import PatchOp from scim2_models import PatchOperation +from scim2_models import SCIMException from scim2_models import SearchRequest from scim2_models import User from werkzeug.test import Client @@ -8,8 +9,7 @@ from werkzeug.wrappers import Response from scim2_client.engines.werkzeug import TestSCIMClient -from scim2_client.errors import SCIMResponseErrorObject -from scim2_client.errors import UnexpectedContentFormat +from scim2_client.errors import UnexpectedContentFormatException scim2_server = pytest.importorskip("scim2_server") from scim2_server.backend import InMemoryBackend # noqa: E402 @@ -73,7 +73,7 @@ def test_werkzeug_engine(scim_client): assert queried_user.display_name == "werkzeug patched" scim_client.delete(User, response_user.id) - with pytest.raises(SCIMResponseErrorObject): + with pytest.raises(SCIMException): scim_client.query(User, response_user.id) @@ -87,10 +87,29 @@ def application(request): werkzeug_client = Client(application) scim_client = TestSCIMClient(client=werkzeug_client, resource_models=(User,)) scim_client.register_naive_resource_types() - with pytest.raises(UnexpectedContentFormat): + with pytest.raises(UnexpectedContentFormatException): scim_client.query(url="/") +def test_invalid_payload(): + """Test that a response with invalid SCIM payload raises a ResponsePayloadValidationException.""" + from scim2_client.errors import ResponsePayloadValidationException + + @Request.application + def application(request): + # Return valid JSON but with invalid SCIM data (missing required fields) + return Response( + '{"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "active": "not-a-bool"}', + content_type="application/scim+json", + ) + + werkzeug_client = Client(application) + scim_client = TestSCIMClient(client=werkzeug_client, resource_models=(User,)) + scim_client.register_naive_resource_types() + with pytest.raises(ResponsePayloadValidationException): + scim_client.query(url="/Users/1234") + + def test_environ(scim_client): @Request.application def application(request): diff --git a/tests/test_create.py b/tests/test_create.py index 9b0d24c..ce19447 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -2,15 +2,14 @@ import pytest from scim2_models import Error +from scim2_models import InvalidValueException from scim2_models import Meta from scim2_models import Resource +from scim2_models import SCIMException from scim2_models import User -from scim2_client import RequestNetworkError -from scim2_client import RequestPayloadValidationError -from scim2_client import SCIMClientError -from scim2_client import SCIMRequestError -from scim2_client import UnexpectedStatusCode +from scim2_client import RequestNetworkException +from scim2_client import UnexpectedStatusCodeException def test_create_user(httpserver, sync_client): @@ -119,7 +118,7 @@ def test_create_dict_user_bad_schema(httpserver, sync_client): } with pytest.raises( - SCIMClientError, match="Cannot guess resource type from the payload" + InvalidValueException, match="Cannot guess resource type from the payload" ): sync_client.create(user_request) @@ -224,7 +223,7 @@ def test_no_200(httpserver, sync_client): user_request = User(user_name="bjensen@example.com") - with pytest.raises(UnexpectedStatusCode): + with pytest.raises(UnexpectedStatusCodeException): sync_client.create(user_request) sync_client.create(user_request, expected_status_codes=None) sync_client.create(user_request, expected_status_codes=[200, 201]) @@ -260,15 +259,13 @@ class MyResource(Resource): __schema__ = "urn:ietf:params:scim:schemas:core:2.0:MyResource" display_name: str | None = None - with pytest.raises(SCIMRequestError, match=r"Unknown resource type"): + with pytest.raises(InvalidValueException, match=r"Unknown resource type"): sync_client.create(MyResource(display_name="foobar")) def test_request_validation_error(sync_client): - """Test that incorrect input raise a RequestPayloadValidationError.""" - with pytest.raises( - RequestPayloadValidationError, match="Server request payload validation error" - ): + """Test that incorrect input raise a SCIMException.""" + with pytest.raises(SCIMException): sync_client.create( { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], @@ -278,9 +275,9 @@ def test_request_validation_error(sync_client): def test_request_network_error(sync_client): - """Test that httpx exceptions are transformed in RequestNetworkError.""" + """Test that httpx exceptions are transformed in RequestNetworkException.""" user_request = User(user_name="bjensen@example.com") with pytest.raises( - RequestNetworkError, match="Network error happened during request" + RequestNetworkException, match="Network error happened during request" ): sync_client.create(user_request, url="http://invalid.test") diff --git a/tests/test_delete.py b/tests/test_delete.py index 465e3d5..cb6009c 100644 --- a/tests/test_delete.py +++ b/tests/test_delete.py @@ -1,10 +1,10 @@ import pytest from scim2_models import Error +from scim2_models import InvalidValueException from scim2_models import Resource from scim2_models import User -from scim2_client import RequestNetworkError -from scim2_client import SCIMRequestError +from scim2_client import RequestNetworkException class UnregisteredResource(Resource): @@ -58,7 +58,7 @@ def test_errors(httpserver, code, sync_client): def test_invalid_resource_model(httpserver, sync_client): """Test that resource_models passed to the method must be part of SCIMClient.resource_models.""" - with pytest.raises(SCIMRequestError, match=r"Unknown resource type"): + with pytest.raises(InvalidValueException, match=r"Unknown resource type"): sync_client.delete(UnregisteredResource, id="foobar") @@ -86,8 +86,8 @@ def test_dont_check_response_payload(httpserver, sync_client): def test_request_network_error(httpserver, sync_client): - """Test that httpx exceptions are transformed in RequestNetworkError.""" + """Test that httpx exceptions are transformed in RequestNetworkException.""" with pytest.raises( - RequestNetworkError, match="Network error happened during request" + RequestNetworkException, match="Network error happened during request" ): sync_client.delete(User, "anything", url="http://invalid.test") diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py new file mode 100644 index 0000000..c0e333b --- /dev/null +++ b/tests/test_deprecations.py @@ -0,0 +1,55 @@ +import pytest + +from scim2_client.errors import RequestNetworkError +from scim2_client.errors import ResponsePayloadValidationError +from scim2_client.errors import SCIMClientError +from scim2_client.errors import SCIMResponseError +from scim2_client.errors import UnexpectedContentFormat +from scim2_client.errors import UnexpectedContentType +from scim2_client.errors import UnexpectedStatusCode + + +def test_scim_client_error_deprecation(): + """Test that SCIMClientError emits a deprecation warning.""" + with pytest.warns(DeprecationWarning, match="SCIMClientError is deprecated"): + SCIMClientError("test") + + +def test_scim_response_error_deprecation(): + """Test that SCIMResponseError emits a deprecation warning.""" + with pytest.warns(DeprecationWarning, match="SCIMResponseError is deprecated"): + SCIMResponseError("test") + + +def test_request_network_error_deprecation(): + """Test that RequestNetworkError emits a deprecation warning.""" + with pytest.warns(DeprecationWarning, match="RequestNetworkError is deprecated"): + RequestNetworkError() + + +def test_unexpected_status_code_deprecation(): + """Test that UnexpectedStatusCode emits a deprecation warning.""" + with pytest.warns(DeprecationWarning, match="UnexpectedStatusCode is deprecated"): + UnexpectedStatusCode(404) + + +def test_unexpected_content_type_deprecation(): + """Test that UnexpectedContentType emits a deprecation warning.""" + with pytest.warns(DeprecationWarning, match="UnexpectedContentType is deprecated"): + UnexpectedContentType("text/html") + + +def test_unexpected_content_format_deprecation(): + """Test that UnexpectedContentFormat emits a deprecation warning.""" + with pytest.warns( + DeprecationWarning, match="UnexpectedContentFormat is deprecated" + ): + UnexpectedContentFormat() + + +def test_response_payload_validation_error_deprecation(): + """Test that ResponsePayloadValidationError emits a deprecation warning.""" + with pytest.warns( + DeprecationWarning, match="ResponsePayloadValidationError is deprecated" + ): + ResponsePayloadValidationError() diff --git a/tests/test_modify.py b/tests/test_modify.py index cef4956..fdffb47 100644 --- a/tests/test_modify.py +++ b/tests/test_modify.py @@ -1,14 +1,14 @@ import pytest from scim2_models import Error from scim2_models import Group +from scim2_models import InvalidValueException from scim2_models import PatchOp from scim2_models import PatchOperation from scim2_models import ResourceType +from scim2_models import SCIMException from scim2_models import User -from scim2_client import RequestNetworkError -from scim2_client import RequestPayloadValidationError -from scim2_client import SCIMRequestError +from scim2_client import RequestNetworkException def test_modify_user_200(httpserver, sync_client): @@ -327,7 +327,7 @@ def test_invalid_resource_model(httpserver, sync_client): ) patch_op = PatchOp[Group](operations=[operation]) - with pytest.raises(SCIMRequestError, match=r"Unknown resource type"): + with pytest.raises(InvalidValueException, match=r"Unknown resource type"): sync_client.modify(Group, "some-id", patch_op) @@ -335,7 +335,7 @@ def test_request_validation_error(httpserver, sync_client): """Test that incorrect PatchOp creation raises a validation error.""" # Test with a PatchOp that has invalid data - this should fail during model_dump in prepare_patch_request with pytest.raises( - (RequestPayloadValidationError, ValueError, TypeError), + (SCIMException, ValueError, TypeError), match=r"(?i)(validation|invalid|error)", ): # Create a PatchOp with invalid enum value by bypassing normal validation @@ -348,14 +348,14 @@ def test_request_validation_error(httpserver, sync_client): def test_request_network_error(httpserver, sync_client): - """Test that httpx exceptions are transformed in RequestNetworkError.""" + """Test that httpx exceptions are transformed in RequestNetworkException.""" operation = PatchOperation( op=PatchOperation.Op.replace_, path="displayName", value="Test" ) patch_op = PatchOp[User](operations=[operation]) with pytest.raises( - RequestNetworkError, match="Network error happened during request" + RequestNetworkException, match="Network error happened during request" ): sync_client.modify(User, "some-id", patch_op, url="http://invalid.test") @@ -422,8 +422,5 @@ def test_modify_validation_error(httpserver, sync_client): invalid_patch_op.model_dump.side_effect = exc_info.value - with pytest.raises( - RequestPayloadValidationError, - match="Server request payload validation error", - ): + with pytest.raises(SCIMException): sync_client.modify(User, "some-id", invalid_patch_op) diff --git a/tests/test_query.py b/tests/test_query.py index 0496a13..01f8603 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -3,23 +3,23 @@ import pytest from scim2_models import Error from scim2_models import Group +from scim2_models import InvalidValueException from scim2_models import ListResponse from scim2_models import Meta from scim2_models import Resource from scim2_models import ResourceType +from scim2_models import SCIMException from scim2_models import SearchRequest from scim2_models import ServiceProviderConfig +from scim2_models import UniquenessException from scim2_models import User -from scim2_client import SCIMRequestError -from scim2_client.errors import RequestNetworkError -from scim2_client.errors import ResponsePayloadValidationError -from scim2_client.errors import SCIMClientError -from scim2_client.errors import SCIMResponseError -from scim2_client.errors import SCIMResponseErrorObject -from scim2_client.errors import UnexpectedContentFormat -from scim2_client.errors import UnexpectedContentType -from scim2_client.errors import UnexpectedStatusCode +from scim2_client.errors import RequestNetworkException +from scim2_client.errors import ResponsePayloadValidationException +from scim2_client.errors import SCIMResponseException +from scim2_client.errors import UnexpectedContentFormatException +from scim2_client.errors import UnexpectedContentTypeException +from scim2_client.errors import UnexpectedStatusCodeException @pytest.fixture @@ -325,21 +325,17 @@ def test_user_with_invalid_id(sync_client): def test_raise_scim_errors(sync_client): """Test that querying an user with an invalid id raises an exception.""" with pytest.raises( - SCIMResponseErrorObject, + SCIMException, match="Resource unknown not found", - ) as exc_info: + ): sync_client.query(User, "unknown", raise_scim_errors=True) - assert exc_info.value.to_error() == Error( - detail="Resource unknown not found", status=404 - ) - def test_raise_scim_errors_with_scim_type(sync_client): """Test that the exception message includes scim_type when present.""" with pytest.raises( - SCIMResponseErrorObject, - match="uniqueness: User already exists", + UniquenessException, + match="User already exists", ) as exc_info: sync_client.query(User, "conflict", raise_scim_errors=True) @@ -350,14 +346,9 @@ def test_raise_scim_errors_with_scim_type(sync_client): def test_raise_scim_errors_without_detail(sync_client): """Test that the exception works when the error has no detail.""" - with pytest.raises( - SCIMResponseErrorObject, - match="SCIM Error", - ) as exc_info: + with pytest.raises(SCIMException): sync_client.query(User, "no-detail", raise_scim_errors=True) - assert exc_info.value.to_error() == Error(status=500) - def test_all_users(sync_client): """Test that querying all existing users instantiate a ListResponse object.""" @@ -447,12 +438,12 @@ class Foobar(Resource): def test_bad_resource_model(sync_client): - """Test querying a resource unknown from the client raise a SCIMResponseError.""" + """Test querying a resource unknown from the client raise a SCIMResponseException.""" sync_client.resource_models = (User,) sync_client.resource_types = [ResourceType.from_resource(User)] with pytest.raises( - SCIMResponseError, + SCIMResponseException, match="Expected type User but got unknown resource with schemas: urn:ietf:params:scim:schemas:core:2.0:Group", ): sync_client.query(User, "its-a-group") @@ -469,26 +460,27 @@ def test_all(sync_client): def test_all_unexpected_type(sync_client): - """Test retrieving a payload for an object which type has not been passed in parameters raise a ResponsePayloadValidationError.""" + """Test retrieving a payload for an object which type has not been passed in parameters raise a ResponsePayloadValidationException.""" sync_client.resource_models = (User,) sync_client.resource_types = [ResourceType.from_resource(User)] with pytest.raises( - ResponsePayloadValidationError, match="Server response payload validation error" + ResponsePayloadValidationException, + match="Server response payload validation error", ): sync_client.query() def test_response_is_not_json(sync_client): """Test situations where servers return an invalid JSON object.""" - with pytest.raises(UnexpectedContentFormat): + with pytest.raises(UnexpectedContentFormatException): sync_client.query(User, "not-json") def test_not_a_scim_object(sync_client): """Test retrieving a valid JSON object without a schema.""" with pytest.raises( - SCIMResponseError, + SCIMResponseException, match="Expected type User but got undefined object with no schema", ): sync_client.query(User, "not-a-scim-object") @@ -504,7 +496,7 @@ def test_dont_check_response_payload(sync_client): def test_response_bad_status_code(sync_client): """Test situations where servers return an invalid status code.""" - with pytest.raises(UnexpectedStatusCode): + with pytest.raises(UnexpectedStatusCodeException): sync_client.query(User, "status-201") sync_client.query(User, "status-201", expected_status_codes=None) @@ -517,7 +509,7 @@ def test_response_content_type_with_charset(sync_client): def test_response_bad_content_type(sync_client): """Test situations where servers return an invalid content-type response.""" - with pytest.raises(UnexpectedContentType): + with pytest.raises(UnexpectedContentTypeException): sync_client.query(User, "bad-content-type") @@ -596,7 +588,7 @@ def test_invalid_resource_model(sync_client): sync_client.resource_models = (User,) sync_client.resource_types = [ResourceType.from_resource(User)] - with pytest.raises(SCIMRequestError, match=r"Unknown resource type"): + with pytest.raises(InvalidValueException, match=r"Unknown resource type"): sync_client.query(Group) @@ -609,14 +601,14 @@ def test_service_provider_config_endpoint(sync_client): def test_service_provider_config_endpoint_with_an_id(sync_client): """Test that querying the /ServiceProviderConfig with an id raise an exception.""" with pytest.raises( - SCIMClientError, match="ServiceProviderConfig cannot have an id" + InvalidValueException, match="ServiceProviderConfig cannot have an id" ): sync_client.query(ServiceProviderConfig, "dummy") def test_request_network_error(sync_client): - """Test that httpx exceptions are transformed in RequestNetworkError.""" + """Test that httpx exceptions are transformed in RequestNetworkException.""" with pytest.raises( - RequestNetworkError, match="Network error happened during request" + RequestNetworkException, match="Network error happened during request" ): sync_client.query(url="http://invalid.test") diff --git a/tests/test_replace.py b/tests/test_replace.py index 0372dcc..91250e7 100644 --- a/tests/test_replace.py +++ b/tests/test_replace.py @@ -3,14 +3,13 @@ import pytest from scim2_models import Error from scim2_models import Group +from scim2_models import InvalidValueException from scim2_models import Meta from scim2_models import ResourceType +from scim2_models import SCIMException from scim2_models import User -from scim2_client import RequestNetworkError -from scim2_client import RequestPayloadValidationError -from scim2_client import SCIMClientError -from scim2_client import SCIMRequestError +from scim2_client import RequestNetworkException def test_replace_user(httpserver, sync_client): @@ -122,7 +121,7 @@ def test_replace_user_dict_bad_schema(httpserver, sync_client): } with pytest.raises( - SCIMClientError, match="Cannot guess resource type from the payload" + InvalidValueException, match="Cannot guess resource type from the payload" ): sync_client.replace(payload) @@ -277,7 +276,7 @@ def test_user_with_no_id(httpserver, sync_client): ), ) - with pytest.raises(SCIMClientError, match="Resource must have an id"): + with pytest.raises(InvalidValueException, match="Resource must have an id"): sync_client.replace(user) @@ -286,15 +285,13 @@ def test_invalid_resource_model(httpserver, sync_client): sync_client.resource_models = (User,) sync_client.resource_types = [ResourceType.from_resource(User)] - with pytest.raises(SCIMRequestError, match=r"Unknown resource type"): + with pytest.raises(InvalidValueException, match=r"Unknown resource type"): sync_client.replace(Group(display_name="foobar")) def test_request_validation_error(httpserver, sync_client): - """Test that incorrect input raise a RequestPayloadValidationError.""" - with pytest.raises( - RequestPayloadValidationError, match="Server request payload validation error" - ): + """Test that incorrect input raise a SCIMException.""" + with pytest.raises(SCIMException): sync_client.replace( { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], @@ -304,9 +301,9 @@ def test_request_validation_error(httpserver, sync_client): def test_request_network_error(httpserver, sync_client): - """Test that httpx exceptions are transformed in RequestNetworkError.""" + """Test that httpx exceptions are transformed in RequestNetworkException.""" user_request = User(user_name="bjensen@example.com", id="anything") with pytest.raises( - RequestNetworkError, match="Network error happened during request" + RequestNetworkException, match="Network error happened during request" ): sync_client.replace(user_request, url="http://invalid.test") diff --git a/tests/test_search.py b/tests/test_search.py index e1540bc..bbd293c 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -9,7 +9,7 @@ from scim2_models import SearchRequest from scim2_models import User -from scim2_client import RequestNetworkError +from scim2_client import RequestNetworkException from scim2_client.engines.httpx import SyncSCIMClient @@ -244,10 +244,10 @@ def test_errors(httpserver, code): def test_request_network_error(httpserver): - """Test that httpx exceptions are transformed in RequestNetworkError.""" + """Test that httpx exceptions are transformed in RequestNetworkException.""" client = Client(base_url=f"http://localhost:{httpserver.port}") scim_client = SyncSCIMClient(client, resource_models=(User,)) with pytest.raises( - RequestNetworkError, match="Network error happened during request" + RequestNetworkException, match="Network error happened during request" ): scim_client.search(url="http://invalid.test") diff --git a/tests/test_utils.py b/tests/test_utils.py index e605db8..ddc9f87 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,13 +1,13 @@ import pytest from scim2_models import EnterpriseUser from scim2_models import Group +from scim2_models import InvalidValueException from scim2_models import Resource from scim2_models import ResourceType from scim2_models import Schema from scim2_models import ServiceProviderConfig from scim2_models import User -from scim2_client import SCIMRequestError from scim2_client.engines.httpx import SyncSCIMClient @@ -32,7 +32,7 @@ class Foobar(Resource): # This one is special as it does not take an ending 's' assert client.resource_endpoint(ServiceProviderConfig) == "/ServiceProviderConfig" - with pytest.raises(SCIMRequestError): + with pytest.raises(InvalidValueException): client.resource_endpoint(Foobar) diff --git a/uv.lock b/uv.lock index 6d3ac88..8154bba 100644 --- a/uv.lock +++ b/uv.lock @@ -1271,7 +1271,7 @@ doc = [ [package.metadata] requires-dist = [ { name = "httpx", marker = "extra == 'httpx'", specifier = ">=0.28.0" }, - { name = "scim2-models", specifier = ">=0.6.1" }, + { name = "scim2-models", specifier = ">=0.6.4" }, { name = "werkzeug", marker = "extra == 'werkzeug'", specifier = ">=3.1.3" }, ] provides-extras = ["httpx", "werkzeug"] @@ -1312,14 +1312,14 @@ wheels = [ [[package]] name = "scim2-models" -version = "0.6.3" +version = "0.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", extra = ["email"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/86/ebcf14dd5f69f80c787849869f45364587fedcce0a382308167914b26876/scim2_models-0.6.3.tar.gz", hash = "sha256:527f6f54b35bb5ded120229b4160845b7a5e1ca6ae8bc4b97537543c79343057", size = 45720, upload-time = "2026-01-29T21:13:30.994Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/36/367d2b0e4a5c4f9a9724ffbd088097829dc822c77123a8dad9379eb6b4df/scim2_models-0.6.4.tar.gz", hash = "sha256:7064c62ae58b9dc73a29dd337222b4719ce7bdbf739418f831c4697161b38a5c", size = 45777, upload-time = "2026-02-05T13:38:41.713Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/7b/ddb6179bf6daad3b927c09762aa11fbfa39a128d0ac63be923b9fb0a3b1e/scim2_models-0.6.3-py3-none-any.whl", hash = "sha256:bc8656ad3d7caccc70ec9407ce3a2711ef9862161922267c24c36ea5de9f8566", size = 57102, upload-time = "2026-01-29T21:13:29.8Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/dbbd8661642e00f572b8493a5acf6888818c43b1f9bd57df4b64af088133/scim2_models-0.6.4-py3-none-any.whl", hash = "sha256:94be14533167a526a206ff38638c7264641dca0c6ac0c202c9e7873cca9ddad0", size = 57193, upload-time = "2026-02-05T13:38:40.376Z" }, ] [[package]] @@ -1666,28 +1666,28 @@ wheels = [ [[package]] name = "uv" -version = "0.9.29" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/97/71/8a5bf591f3d9674e0a9144567d2e0a16fd04a33b4ab8ecfc902f1551c709/uv-0.9.29.tar.gz", hash = "sha256:140422df01de34dc335bd29827ae6aec6ecb2b92c2ee8ed6bc6dbeee50ac2f4e", size = 3838234, upload-time = "2026-02-03T19:39:06.702Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/35/a8d744a2866d176a16c02ead8d277e0b02ae587a68c89cb2b5b9b8bcf602/uv-0.9.29-py3-none-linux_armv6l.whl", hash = "sha256:54fc0056a8f41b43e41c4c677632f751842f5d94b91dea4d547086448a8325eb", size = 21998377, upload-time = "2026-02-03T19:38:24.678Z" }, - { url = "https://files.pythonhosted.org/packages/8b/82/92b539e445c75706cbc8b9ac00291ee2923602e68109d73dffa9ab412257/uv-0.9.29-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66a5f5c5ecf62f32b8d71383760a422aa9a2a2798cbb6424fb25ccfa8fd53a81", size = 21032721, upload-time = "2026-02-03T19:38:44.791Z" }, - { url = "https://files.pythonhosted.org/packages/55/e8/0489cb87d25a9b06ec3b867fecfd32a9a054dcef8c889662c153d20bba3d/uv-0.9.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11aad2d15a9e78551f656886ce604810f872fa2452127216f8ff5d75febae26e", size = 19824587, upload-time = "2026-02-03T19:38:17.32Z" }, - { url = "https://files.pythonhosted.org/packages/ef/09/8e06484d3f1713170926b356913deb0cf25f14ba6c77d765afdbac33e07c/uv-0.9.29-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4f118141a84862b96f4a4f2bf5e2436f65a8b572706861e0d4585f4bc87fdac0", size = 21616388, upload-time = "2026-02-03T19:38:52.269Z" }, - { url = "https://files.pythonhosted.org/packages/04/da/0c5cfd9d0296c78968fb588ca5a018a6b0e0132bdf3b0fca712cd0ffa938/uv-0.9.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ca5effa2b227a989f341197248551be00919d3dbd13e9d03fabd1af26a9f9d41", size = 21622407, upload-time = "2026-02-03T19:39:12.999Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3f/7c14c282b3d258a274d382c0e03b13fafac99483590476ceb01ca54e2b9d/uv-0.9.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d227644f94c66abf82eb51f33cb03a3e2e50f00d502438bc2f0bae1f4ae0e5a5", size = 21585617, upload-time = "2026-02-03T19:38:21.272Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d9/4db58a2f5d311a0549d1f0855e1f650364265e743709ef81824cf86c7ae6/uv-0.9.29-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14a6c27f7c61ca1dc9c6edf53d39e9f289531873c8488ed24bd15e49353a485c", size = 22794114, upload-time = "2026-02-03T19:38:59.809Z" }, - { url = "https://files.pythonhosted.org/packages/8c/41/4d4df6dd7e88bea33557c3b6fd36e054e057cf8dfd64b8e97b4f40c8d170/uv-0.9.29-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5c99fd20ae5a98066c03e06a8f4c5a68e71acf8029d1ab7eba682f7166696b52", size = 24121009, upload-time = "2026-02-03T19:38:13.137Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ef/9a82a1bf3c5d23dd4ecf3c8778fc8ffc241e671fef519e3e7722884e93ba/uv-0.9.29-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113cbe21a39fa2cfbe146333141561e015a67dfaec7d12415c7ec6ff9f878754", size = 23655975, upload-time = "2026-02-03T19:38:28.713Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f5/6158eaf6558962ca6a7c17ecbe14a2434166d5a0dae9712aca16b8520f46/uv-0.9.29-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d36fe3f9de3a37f7d712ee51ebf42d97df7a00ec901b02b6306c7ebbab8c6a76", size = 22881973, upload-time = "2026-02-03T19:39:03.854Z" }, - { url = "https://files.pythonhosted.org/packages/7b/fa/e725329efb484997fd60018d62f931901f3d25a04b95278845c1ad25b00d/uv-0.9.29-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae09db1bbdad5c38c508876a5903a322951539146f14c7567448bdcdea67e1fe", size = 22760712, upload-time = "2026-02-03T19:38:33.372Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/8a2e4ad9a8024ceb10c04a9c386220d53107e6f3bff7a246fe36622b5342/uv-0.9.29-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:aaf650ddf20a6029a59c136eaeade720655c07bfbbd4e7867cc9b6167b0abae9", size = 21721267, upload-time = "2026-02-03T19:38:09.623Z" }, - { url = "https://files.pythonhosted.org/packages/3e/05/8a3b8a190b5ffb9b0d07d10f6f962e29e0f5aa4209415e78bf0514e2394a/uv-0.9.29-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:4095f5763c69d75f324d81e799d90c682f63f4789f7b8ad4297484262ecdeffd", size = 22426985, upload-time = "2026-02-03T19:38:48.4Z" }, - { url = "https://files.pythonhosted.org/packages/41/1d/af83aeebb75062c8539ffdeaa7474ff3c7acb6263d6d7ead28219c71f5d8/uv-0.9.29-py3-none-musllinux_1_1_i686.whl", hash = "sha256:52a6934cbbb3dd339c24e8de1cdd0d3239b82ce5e65289e0b13055009abf2bc1", size = 22051690, upload-time = "2026-02-03T19:39:09.552Z" }, - { url = "https://files.pythonhosted.org/packages/91/65/fe381859f237a5d2b271bc69215ebc5b87cbfd156ad901927921ef82b2e1/uv-0.9.29-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:367cb2a7ab2138b796caf5b402e343ef47f93329ae5d08a05d7bcfeca51b19e7", size = 22968942, upload-time = "2026-02-03T19:38:05.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/04/155263d673c980da9b513673d9a61bb8a5a98547c8e42af3613881ca54e1/uv-0.9.29-py3-none-win32.whl", hash = "sha256:fcb17d9576598f536a04139beefd82187e84db3e6d11a16fa5507f5d3d414f28", size = 20890568, upload-time = "2026-02-03T19:38:40.928Z" }, - { url = "https://files.pythonhosted.org/packages/78/0a/450bd74385c4da3d83639946eaf39ca5bbcb69e73a0433d3bcc65af096d0/uv-0.9.29-py3-none-win_amd64.whl", hash = "sha256:b823c17132b851bf452e38f68e5dd39de9b433c39e2cd3aec2a1734b1594c295", size = 23465607, upload-time = "2026-02-03T19:38:37.411Z" }, - { url = "https://files.pythonhosted.org/packages/ad/2a/0d4a615f36d53a7cf1992351c395b17367783cface5afa5976db4c96675d/uv-0.9.29-py3-none-win_arm64.whl", hash = "sha256:22ab5e68d2d6a283a0a290e9b4a3ce53fef55f6ae197a5f6a58b7f4c605f21c8", size = 21911432, upload-time = "2026-02-03T19:38:55.987Z" }, +version = "0.9.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a0/63cea38fe839fb89592728b91928ee6d15705f1376a7940fee5bbc77fea0/uv-0.9.30.tar.gz", hash = "sha256:03ebd4b22769e0a8d825fa09d038e31cbab5d3d48edf755971cb0cec7920ab95", size = 3846526, upload-time = "2026-02-04T21:45:37.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/3c/71be72f125f0035348b415468559cc3b335ec219376d17a3d242d2bd9b23/uv-0.9.30-py3-none-linux_armv6l.whl", hash = "sha256:a5467dddae1cd5f4e093f433c0f0d9a0df679b92696273485ec91bbb5a8620e6", size = 21927585, upload-time = "2026-02-04T21:46:14.935Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fd/8070b5423a77d4058d14e48a970aa075762bbff4c812dda3bb3171543e44/uv-0.9.30-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ec38ae29aa83a37c6e50331707eac8ecc90cf2b356d60ea6382a94de14973be", size = 21050392, upload-time = "2026-02-04T21:45:55.649Z" }, + { url = "https://files.pythonhosted.org/packages/42/5f/3ccc9415ef62969ed01829572338ea7bdf4c5cf1ffb9edc1f8cb91b571f3/uv-0.9.30-py3-none-macosx_11_0_arm64.whl", hash = "sha256:777ecd117cf1d8d6bb07de8c9b7f6c5f3e802415b926cf059d3423699732eb8c", size = 19817085, upload-time = "2026-02-04T21:45:40.881Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3f/76b44e2a224f4c4a8816fc92686ef6d4c2656bc5fc9d4f673816162c994d/uv-0.9.30-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:93049ba3c41fa2cc38b467cb78ef61b2ddedca34b6be924a5481d7750c8111c6", size = 21620537, upload-time = "2026-02-04T21:45:47.846Z" }, + { url = "https://files.pythonhosted.org/packages/60/2a/50f7e8c6d532af8dd327f77bdc75ce4652322ac34f5e29f79a8e04ea3cc8/uv-0.9.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:f295604fee71224ebe2685a0f1f4ff7a45c77211a60bd57133a4a02056d7c775", size = 21550855, upload-time = "2026-02-04T21:46:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/0e/10/f823d4af1125fae559194b356757dc7d4a8ac79d10d11db32c2d4c9e2f63/uv-0.9.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2faf84e1f3b6fc347a34c07f1291d11acf000b0dd537a61d541020f22b17ccd9", size = 21516576, upload-time = "2026-02-04T21:46:03.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/f3/64b02db11f38226ed34458c7fbdb6f16b6d4fd951de24c3e51acf02b30f8/uv-0.9.30-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b3b3700ecf64a09a07fd04d10ec35f0973ec15595d38bbafaa0318252f7e31f", size = 22718097, upload-time = "2026-02-04T21:45:51.875Z" }, + { url = "https://files.pythonhosted.org/packages/28/21/a48d1872260f04a68bb5177b0f62ddef62ab892d544ed1922f2d19fd2b00/uv-0.9.30-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b176fc2937937dd81820445cb7e7e2e3cd1009a003c512f55fa0ae10064c8a38", size = 24107844, upload-time = "2026-02-04T21:46:19.032Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c6/d7e5559bfe1ab7a215a7ad49c58c8a5701728f2473f7f436ef00b4664e88/uv-0.9.30-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:180e8070b8c438b9a3fb3fde8a37b365f85c3c06e17090f555dc68fdebd73333", size = 23685378, upload-time = "2026-02-04T21:46:07.166Z" }, + { url = "https://files.pythonhosted.org/packages/a8/bf/b937bbd50d14c6286e353fd4c7bdc09b75f6b3a26bd4e2f3357e99891f28/uv-0.9.30-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4125a9aa2a751e1589728f6365cfe204d1be41499148ead44b6180b7df576f27", size = 22848471, upload-time = "2026-02-04T21:45:18.728Z" }, + { url = "https://files.pythonhosted.org/packages/6a/57/12a67c569e69b71508ad669adad266221f0b1d374be88eaf60109f551354/uv-0.9.30-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4366dd740ac9ad3ec50a58868a955b032493bb7d7e6ed368289e6ced8bbc70f3", size = 22774258, upload-time = "2026-02-04T21:46:10.798Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b8/a26cc64685dddb9fb13f14c3dc1b12009f800083405f854f84eb8c86b494/uv-0.9.30-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:33e50f208e01a0c20b3c5f87d453356a5cbcfd68f19e47a28b274cd45618881c", size = 21699573, upload-time = "2026-02-04T21:45:44.365Z" }, + { url = "https://files.pythonhosted.org/packages/c8/59/995af0c5f0740f8acb30468e720269e720352df1d204e82c2d52d9a8c586/uv-0.9.30-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5e7a6fa7a3549ce893cf91fe4b06629e3e594fc1dca0a6050aba2ea08722e964", size = 22460799, upload-time = "2026-02-04T21:45:26.658Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0b/6affe815ecbaebf38b35d6230fbed2f44708c67d5dd5720f81f2ec8f96ff/uv-0.9.30-py3-none-musllinux_1_1_i686.whl", hash = "sha256:62d7e408d41e392b55ffa4cf9b07f7bbd8b04e0929258a42e19716c221ac0590", size = 22001777, upload-time = "2026-02-04T21:45:34.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b6/47a515171c891b0d29f8e90c8a1c0e233e4813c95a011799605cfe04c74c/uv-0.9.30-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6dc65c24f5b9cdc78300fa6631368d3106e260bbffa66fb1e831a318374da2df", size = 22968416, upload-time = "2026-02-04T21:45:22.863Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3a/c1df8615385138bb7c43342586431ca32b77466c5fb086ac0ed14ab6ca28/uv-0.9.30-py3-none-win32.whl", hash = "sha256:74e94c65d578657db94a753d41763d0364e5468ec0d368fb9ac8ddab0fb6e21f", size = 20889232, upload-time = "2026-02-04T21:46:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a8/e8761c8414a880d70223723946576069e042765475f73b4436d78b865dba/uv-0.9.30-py3-none-win_amd64.whl", hash = "sha256:88a2190810684830a1ba4bb1cf8fb06b0308988a1589559404259d295260891c", size = 23432208, upload-time = "2026-02-04T21:45:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/6f2ebab941ec559f97110bbbae1279cd0333d6bc352b55f6fa3fefb020d9/uv-0.9.30-py3-none-win_arm64.whl", hash = "sha256:7fde83a5b5ea027315223c33c30a1ab2f2186910b933d091a1b7652da879e230", size = 21887273, upload-time = "2026-02-04T21:45:59.787Z" }, ] [[package]]