diff --git a/doc/changelog.rst b/doc/changelog.rst index e165cf1..cd8389c 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,14 @@ Changelog ========= +[Unreleased] +------------ + +Changed +^^^^^^^ +- :class:`~scim2_client.SCIMResponseErrorObject` now exposes a :meth:`~scim2_client.SCIMResponseErrorObject.to_error` method + returning the :class:`~scim2_models.Error` object from the server. :issue:`37` + [0.7.2] - 2026-02-03 -------------------- diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 397b868..5adcfdd 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -89,14 +89,22 @@ Have a look at the :doc:`reference` to see usage examples and the exhaustive set .. code-block:: python - from scim2_models import Error - request = User(user_name="bjensen@example.com") response = scim.create(request) - if isinstance(response, Error): - raise SomethingIsWrong(response.detail) + 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: + +.. code-block:: python + + from scim2_client import SCIMResponseErrorObject - return f"User {user.id} have been created!" + try: + response = scim.create(request) + except SCIMResponseErrorObject as exc: + error = exc.to_error() + print(f"SCIM error [{error.status}] {error.scim_type}: {error.detail}") PATCH modifications =================== @@ -183,7 +191,8 @@ To achieve this, all the methods provide the following parameters, all are :data 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. - If :data:`False` the error object is returned. + The :meth:`~scim2_client.SCIMResponseErrorObject.to_error` method gives access to the :class:`~scim2_models.Error` object. + If :data:`False` the error object is returned directly. .. tip:: diff --git a/pyproject.toml b/pyproject.toml index 80878ac..d2595ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ exclude_lines = [ "@pytest.mark.skip", "pragma: no cover", "raise NotImplementedError", + "if TYPE_CHECKING:", "\\.\\.\\.\\s*$", # ignore ellipsis ] diff --git a/scim2_client/client.py b/scim2_client/client.py index f1ea455..e54b0c8 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -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(obj=error.detail, source=error) + raise SCIMResponseErrorObject(error) return error self._check_status_codes(status_code, expected_status_codes) diff --git a/scim2_client/errors.py b/scim2_client/errors.py index f854ef1..deb02a5 100644 --- a/scim2_client/errors.py +++ b/scim2_client/errors.py @@ -1,5 +1,9 @@ +from typing import TYPE_CHECKING from typing import Any +if TYPE_CHECKING: + from scim2_models import Error + class SCIMClientError(Exception): """Base exception for scim2-client. @@ -69,14 +73,24 @@ 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, obj: Any, *args: Any, **kwargs: Any) -> None: - message = kwargs.pop( - "message", f"The server returned a SCIM Error object: {obj}" - ) + 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`.""" diff --git a/tests/test_query.py b/tests/test_query.py index 9c6c452..0496a13 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -60,6 +60,24 @@ def httpserver(httpserver): status=400, ) + httpserver.expect_request("/Users/conflict").respond_with_json( + { + "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], + "scimType": "uniqueness", + "detail": "User already exists", + "status": "409", + }, + status=409, + ) + + httpserver.expect_request("/Users/no-detail").respond_with_json( + { + "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status": "500", + }, + status=500, + ) + httpserver.expect_request("/Users/status-201").respond_with_json( { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], @@ -305,13 +323,41 @@ def test_user_with_invalid_id(sync_client): def test_raise_scim_errors(sync_client): - """Test that querying an user with an invalid id instantiate an Error object.""" + """Test that querying an user with an invalid id raises an exception.""" with pytest.raises( SCIMResponseErrorObject, - match="The server returned a SCIM Error object: Resource unknown not found", - ): + 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", + ) as exc_info: + sync_client.query(User, "conflict", raise_scim_errors=True) + + assert exc_info.value.to_error() == Error( + detail="User already exists", status=409, scim_type="uniqueness" + ) + + +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: + 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."""