⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -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
--------------------

Expand All @@ -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
Expand Down
30 changes: 23 additions & 7 deletions doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===================

Expand Down Expand Up @@ -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.


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ classifiers = [

requires-python = ">= 3.10"
dependencies = [
"scim2-models>=0.6.1",
"scim2-models>=0.6.4",
]

[project.optional-dependencies]
Expand Down
28 changes: 19 additions & 9 deletions scim2_client/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
75 changes: 38 additions & 37 deletions scim2_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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::
Expand Down Expand Up @@ -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:
Expand All @@ -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 <scim2_client.SCIMClient.resource_models>`.
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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__]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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}"
Expand Down
16 changes: 8 additions & 8 deletions scim2_client/engines/httpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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.
"""

Expand Down Expand Up @@ -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.

"""
Expand Down
Loading
Loading