Skip to content

Commit ecc7cc6

Browse files
authored
feat: SCIM Exceptions provide errors objects (#38)
1 parent b2cb89d commit ecc7cc6

File tree

6 files changed

+92
-14
lines changed

6 files changed

+92
-14
lines changed

doc/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Changelog
22
=========
33

4+
[Unreleased]
5+
------------
6+
7+
Changed
8+
^^^^^^^
9+
- :class:`~scim2_client.SCIMResponseErrorObject` now exposes a :meth:`~scim2_client.SCIMResponseErrorObject.to_error` method
10+
returning the :class:`~scim2_models.Error` object from the server. :issue:`37`
11+
412
[0.7.2] - 2026-02-03
513
--------------------
614

doc/tutorial.rst

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,22 @@ Have a look at the :doc:`reference` to see usage examples and the exhaustive set
8989

9090
.. code-block:: python
9191
92-
from scim2_models import Error
93-
9492
request = User(user_name="bjensen@example.com")
9593
response = scim.create(request)
96-
if isinstance(response, Error):
97-
raise SomethingIsWrong(response.detail)
94+
print(f"User {response.id} has been created!")
95+
96+
By default, if the server returns an error, a :class:`~scim2_client.SCIMResponseErrorObject` exception is raised.
97+
The :meth:`~scim2_client.SCIMResponseErrorObject.to_error` method gives access to the :class:`~scim2_models.Error` object:
98+
99+
.. code-block:: python
100+
101+
from scim2_client import SCIMResponseErrorObject
98102
99-
return f"User {user.id} have been created!"
103+
try:
104+
response = scim.create(request)
105+
except SCIMResponseErrorObject as exc:
106+
error = exc.to_error()
107+
print(f"SCIM error [{error.status}] {error.scim_type}: {error.detail}")
100108
101109
PATCH modifications
102110
===================
@@ -183,7 +191,8 @@ To achieve this, all the methods provide the following parameters, all are :data
183191
If :data:`None` any status code is accepted.
184192
If an unexpected status code is returned, a :class:`~scim2_client.errors.UnexpectedStatusCode` exception is raised.
185193
- :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.
186-
If :data:`False` the error object is returned.
194+
The :meth:`~scim2_client.SCIMResponseErrorObject.to_error` method gives access to the :class:`~scim2_models.Error` object.
195+
If :data:`False` the error object is returned directly.
187196

188197

189198
.. tip::

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ exclude_lines = [
8383
"@pytest.mark.skip",
8484
"pragma: no cover",
8585
"raise NotImplementedError",
86+
"if TYPE_CHECKING:",
8687
"\\.\\.\\.\\s*$", # ignore ellipsis
8788
]
8889

scim2_client/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ def check_response(
312312
if response_payload and response_payload.get("schemas") == [Error.__schema__]:
313313
error = Error.model_validate(response_payload)
314314
if raise_scim_errors:
315-
raise SCIMResponseErrorObject(obj=error.detail, source=error)
315+
raise SCIMResponseErrorObject(error)
316316
return error
317317

318318
self._check_status_codes(status_code, expected_status_codes)

scim2_client/errors.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from typing import TYPE_CHECKING
12
from typing import Any
23

4+
if TYPE_CHECKING:
5+
from scim2_models import Error
6+
37

48
class SCIMClientError(Exception):
59
"""Base exception for scim2-client.
@@ -69,14 +73,24 @@ class SCIMResponseErrorObject(SCIMResponseError):
6973
"""The server response returned a :class:`scim2_models.Error` object.
7074
7175
Those errors are only raised when the :code:`raise_scim_errors` parameter is :data:`True`.
76+
77+
:param error: The :class:`~scim2_models.Error` object returned by the server.
7278
"""
7379

74-
def __init__(self, obj: Any, *args: Any, **kwargs: Any) -> None:
75-
message = kwargs.pop(
76-
"message", f"The server returned a SCIM Error object: {obj}"
77-
)
80+
def __init__(self, error: "Error", *args: Any, **kwargs: Any) -> None:
81+
self._error = error
82+
parts = []
83+
if error.scim_type:
84+
parts.append(error.scim_type + ":")
85+
if error.detail:
86+
parts.append(error.detail)
87+
message = " ".join(parts) if parts else "SCIM Error"
7888
super().__init__(message, *args, **kwargs)
7989

90+
def to_error(self) -> "Error":
91+
"""Return the :class:`~scim2_models.Error` object returned by the server."""
92+
return self._error
93+
8094

8195
class UnexpectedStatusCode(SCIMResponseError):
8296
"""Error raised when a server returned an unexpected status code for a given :class:`~scim2_models.Context`."""

tests/test_query.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ def httpserver(httpserver):
6060
status=400,
6161
)
6262

63+
httpserver.expect_request("/Users/conflict").respond_with_json(
64+
{
65+
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
66+
"scimType": "uniqueness",
67+
"detail": "User already exists",
68+
"status": "409",
69+
},
70+
status=409,
71+
)
72+
73+
httpserver.expect_request("/Users/no-detail").respond_with_json(
74+
{
75+
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
76+
"status": "500",
77+
},
78+
status=500,
79+
)
80+
6381
httpserver.expect_request("/Users/status-201").respond_with_json(
6482
{
6583
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -305,13 +323,41 @@ def test_user_with_invalid_id(sync_client):
305323

306324

307325
def test_raise_scim_errors(sync_client):
308-
"""Test that querying an user with an invalid id instantiate an Error object."""
326+
"""Test that querying an user with an invalid id raises an exception."""
309327
with pytest.raises(
310328
SCIMResponseErrorObject,
311-
match="The server returned a SCIM Error object: Resource unknown not found",
312-
):
329+
match="Resource unknown not found",
330+
) as exc_info:
313331
sync_client.query(User, "unknown", raise_scim_errors=True)
314332

333+
assert exc_info.value.to_error() == Error(
334+
detail="Resource unknown not found", status=404
335+
)
336+
337+
338+
def test_raise_scim_errors_with_scim_type(sync_client):
339+
"""Test that the exception message includes scim_type when present."""
340+
with pytest.raises(
341+
SCIMResponseErrorObject,
342+
match="uniqueness: User already exists",
343+
) as exc_info:
344+
sync_client.query(User, "conflict", raise_scim_errors=True)
345+
346+
assert exc_info.value.to_error() == Error(
347+
detail="User already exists", status=409, scim_type="uniqueness"
348+
)
349+
350+
351+
def test_raise_scim_errors_without_detail(sync_client):
352+
"""Test that the exception works when the error has no detail."""
353+
with pytest.raises(
354+
SCIMResponseErrorObject,
355+
match="SCIM Error",
356+
) as exc_info:
357+
sync_client.query(User, "no-detail", raise_scim_errors=True)
358+
359+
assert exc_info.value.to_error() == Error(status=500)
360+
315361

316362
def test_all_users(sync_client):
317363
"""Test that querying all existing users instantiate a ListResponse object."""

0 commit comments

Comments
 (0)