scim2-client 0.1.5__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scim2-client
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Pythonically build SCIM requests and parse SCIM responses
5
5
  License: MIT
6
6
  Keywords: scim,scim2,provisioning,httpx,api
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
22
22
  Requires-Dist: httpx (>=0.27.0,<0.28.0)
23
- Requires-Dist: scim2-models (>=0.1.5,<0.2.0)
23
+ Requires-Dist: scim2-models (>=0.1.8,<0.2.0)
24
24
  Description-Content-Type: text/markdown
25
25
 
26
26
  # scim2-client
@@ -67,9 +67,9 @@ assert user.meta.last_updated == datetime.datetime(
67
67
  )
68
68
 
69
69
  # Update resources
70
- user.display_name = "Babes Jensen"
70
+ user.display_name = "Babs Jensen"
71
71
  user = scim.replace(user)
72
- assert user.display_name == "Babes Jensen"
72
+ assert user.display_name == "Babs Jensen"
73
73
  assert user.meta.last_updated == datetime.datetime(
74
74
  2024, 4, 13, 12, 0, 30, tzinfo=datetime.timezone.utc
75
75
  )
@@ -42,9 +42,9 @@ assert user.meta.last_updated == datetime.datetime(
42
42
  )
43
43
 
44
44
  # Update resources
45
- user.display_name = "Babes Jensen"
45
+ user.display_name = "Babs Jensen"
46
46
  user = scim.replace(user)
47
- assert user.display_name == "Babes Jensen"
47
+ assert user.display_name == "Babs Jensen"
48
48
  assert user.meta.last_updated == datetime.datetime(
49
49
  2024, 4, 13, 12, 0, 30, tzinfo=datetime.timezone.utc
50
50
  )
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "scim2-client"
7
- version = "0.1.5"
7
+ version = "0.1.7"
8
8
  description = "Pythonically build SCIM requests and parse SCIM responses"
9
9
  authors = ["Yaal Coop <contact@yaal.coop>"]
10
10
  license = "MIT"
@@ -27,7 +27,7 @@ classifiers = [
27
27
  [tool.poetry.dependencies]
28
28
  python = "^3.9"
29
29
  httpx = "^0.27.0"
30
- scim2-models = "^0.1.5"
30
+ scim2-models = "^0.1.8"
31
31
 
32
32
  [tool.poetry.group.doc]
33
33
  optional = true
@@ -38,10 +38,10 @@ pytest-coverage = "^0.0"
38
38
  pytest-httpserver = "^1.0.10"
39
39
 
40
40
  [tool.poetry.group.doc.dependencies]
41
+ autodoc-pydantic = "^2.2.0"
42
+ myst-parser = "^3.0.1"
41
43
  shibuya = "^2024.5.15"
42
44
  sphinx = "^7.3.7"
43
- myst-parser = "^3.0.1"
44
- autodoc-pydantic = "^2.2.0"
45
45
 
46
46
  [tool.coverage.run]
47
47
  source = [
@@ -5,6 +5,7 @@ from .errors import ResponsePayloadValidationError
5
5
  from .errors import SCIMClientError
6
6
  from .errors import SCIMRequestError
7
7
  from .errors import SCIMResponseError
8
+ from .errors import SCIMResponseErrorObject
8
9
  from .errors import UnexpectedContentFormat
9
10
  from .errors import UnexpectedContentType
10
11
  from .errors import UnexpectedStatusCode
@@ -14,6 +15,7 @@ __all__ = [
14
15
  "SCIMClientError",
15
16
  "SCIMRequestError",
16
17
  "SCIMResponseError",
18
+ "SCIMResponseErrorObject",
17
19
  "UnexpectedContentFormat",
18
20
  "UnexpectedContentType",
19
21
  "UnexpectedStatusCode",
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import json.decoder
3
+ import sys
3
4
  from typing import Dict
4
5
  from typing import List
5
6
  from typing import Optional
@@ -28,6 +29,7 @@ from .errors import ResponsePayloadValidationError
28
29
  from .errors import SCIMClientError
29
30
  from .errors import SCIMRequestError
30
31
  from .errors import SCIMResponseError
32
+ from .errors import SCIMResponseErrorObject
31
33
  from .errors import UnexpectedContentFormat
32
34
  from .errors import UnexpectedContentType
33
35
  from .errors import UnexpectedStatusCode
@@ -61,12 +63,18 @@ class SCIMClient:
61
63
  404,
62
64
  500,
63
65
  ]
64
- """Resource creation HTTP codes defined at :rfc:`RFC7644 §3.3
65
- <7644#section-3.3>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
66
+ """Resource creation HTTP codes.
67
+
68
+ As defined at :rfc:`RFC7644 §3.3 <7644#section-3.3>` and
69
+ :rfc:`RFC7644 §3.12 <7644#section-3.12>`.
70
+ """
66
71
 
67
72
  QUERY_RESPONSE_STATUS_CODES: List[int] = [200, 400, 307, 308, 401, 403, 404, 500]
68
- """Resource querying HTTP codes defined at :rfc:`RFC7644 §3.4.2
69
- <7644#section-3.4.2>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
73
+ """Resource querying HTTP codes.
74
+
75
+ As defined at :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2>` and
76
+ :rfc:`RFC7644 §3.12 <7644#section-3.12>`.
77
+ """
70
78
 
71
79
  SEARCH_RESPONSE_STATUS_CODES: List[int] = [
72
80
  200,
@@ -81,8 +89,11 @@ class SCIMClient:
81
89
  500,
82
90
  501,
83
91
  ]
84
- """Resource querying HTTP codes defined at :rfc:`RFC7644 §3.4.3
85
- <7644#section-3.4.3>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
92
+ """Resource querying HTTP codes.
93
+
94
+ As defined at :rfc:`RFC7644 §3.4.3 <7644#section-3.4.3>` and
95
+ :rfc:`RFC7644 §3.12 <7644#section-3.12>`.
96
+ """
86
97
 
87
98
  DELETION_RESPONSE_STATUS_CODES: List[int] = [
88
99
  204,
@@ -96,8 +107,11 @@ class SCIMClient:
96
107
  500,
97
108
  501,
98
109
  ]
99
- """Resource deletion HTTP codes defined at :rfc:`RFC7644 §3.6
100
- <7644#section-3.6>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
110
+ """Resource deletion HTTP codes.
111
+
112
+ As defined at :rfc:`RFC7644 §3.6 <7644#section-3.6>` and
113
+ :rfc:`RFC7644 §3.12 <7644#section-3.12>`.
114
+ """
101
115
 
102
116
  REPLACEMENT_RESPONSE_STATUS_CODES: List[int] = [
103
117
  200,
@@ -112,8 +126,11 @@ class SCIMClient:
112
126
  500,
113
127
  501,
114
128
  ]
115
- """Resource querying HTTP codes defined at :rfc:`RFC7644 §3.4.2
116
- <7644#section-3.4.2>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
129
+ """Resource querying HTTP codes.
130
+
131
+ As defined at :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2>` and
132
+ :rfc:`RFC7644 §3.12 <7644#section-3.12>`.
133
+ """
117
134
 
118
135
  def __init__(self, client: Client, resource_types: Optional[Tuple[Type]] = None):
119
136
  self.client = client
@@ -146,6 +163,7 @@ class SCIMClient:
146
163
  expected_status_codes: List[int],
147
164
  expected_types: Optional[Type] = None,
148
165
  check_response_payload: bool = True,
166
+ raise_scim_errors: bool = False,
149
167
  scim_ctx: Optional[Context] = None,
150
168
  ):
151
169
  if expected_status_codes and response.status_code not in expected_status_codes:
@@ -178,10 +196,14 @@ class SCIMClient:
178
196
  if not check_response_payload:
179
197
  return response_payload
180
198
 
181
- try:
182
- return Error.model_validate(response_payload)
183
- except ValidationError:
184
- pass
199
+ if (
200
+ response_payload
201
+ and response_payload.get("schemas") == Error.model_fields["schemas"].default
202
+ ):
203
+ error = Error.model_validate(response_payload)
204
+ if raise_scim_errors:
205
+ raise SCIMResponseErrorObject(source=error)
206
+ return error
185
207
 
186
208
  if not expected_types:
187
209
  return response_payload
@@ -206,7 +228,7 @@ class SCIMClient:
206
228
  return actual_type.model_validate(response_payload, scim_ctx=scim_ctx)
207
229
  except ValidationError as exc:
208
230
  scim_exc = ResponsePayloadValidationError(source=response)
209
- if hasattr(scim_exc, "add_note"): # pragma: no cover
231
+ if sys.version_info >= (3, 11): # pragma: no cover
210
232
  scim_exc.add_note(str(exc))
211
233
  raise scim_exc from exc
212
234
 
@@ -216,6 +238,7 @@ class SCIMClient:
216
238
  check_request_payload: bool = True,
217
239
  check_response_payload: bool = True,
218
240
  check_status_code: bool = True,
241
+ raise_scim_errors: bool = False,
219
242
  **kwargs,
220
243
  ) -> Union[AnyResource, Error, Dict]:
221
244
  """Perform a POST request to create, as defined in :rfc:`RFC7644 §3.3
@@ -228,6 +251,9 @@ class SCIMClient:
228
251
  :param check_response_payload: Whether to validate that the response payload is valid.
229
252
  If set, the raw payload will be returned.
230
253
  :param check_status_code: Whether to validate that the response status code is valid.
254
+ :param raise_scim_errors: If :data:`True` and the server returned an
255
+ :class:`~scim2_models.Error` object, a :class:`~scim2_client.SCIMResponseErrorObject`
256
+ exception will be raised. If :data:`False` the error object is returned.
231
257
  :param kwargs: Additional parameters passed to the underlying HTTP request
232
258
  library.
233
259
 
@@ -252,7 +278,6 @@ class SCIMClient:
252
278
  which value will excluded from the request payload, and which values are expected in
253
279
  the response payload.
254
280
  """
255
-
256
281
  if not check_request_payload:
257
282
  payload = resource
258
283
  url = kwargs.pop("url", None)
@@ -272,7 +297,7 @@ class SCIMClient:
272
297
  resource = resource_type.model_validate(resource)
273
298
  except ValidationError as exc:
274
299
  scim_exc = RequestPayloadValidationError(source=resource)
275
- if hasattr(scim_exc, "add_note"): # pragma: no cover
300
+ if sys.version_info >= (3, 11): # pragma: no cover
276
301
  scim_exc.add_note(str(exc))
277
302
  raise scim_exc from exc
278
303
 
@@ -284,7 +309,7 @@ class SCIMClient:
284
309
  response = self.client.post(url, json=payload, **kwargs)
285
310
  except RequestError as exc:
286
311
  scim_exc = RequestNetworkError(source=payload)
287
- if hasattr(scim_exc, "add_note"): # pragma: no cover
312
+ if sys.version_info >= (3, 11): # pragma: no cover
288
313
  scim_exc.add_note(str(exc))
289
314
  raise scim_exc from exc
290
315
 
@@ -295,6 +320,7 @@ class SCIMClient:
295
320
  ),
296
321
  expected_types=([resource.__class__] if check_request_payload else None),
297
322
  check_response_payload=check_response_payload,
323
+ raise_scim_errors=raise_scim_errors,
298
324
  scim_ctx=Context.RESOURCE_CREATION_RESPONSE,
299
325
  )
300
326
 
@@ -306,6 +332,7 @@ class SCIMClient:
306
332
  check_request_payload: bool = True,
307
333
  check_response_payload: bool = True,
308
334
  check_status_code: bool = True,
335
+ raise_scim_errors: bool = False,
309
336
  **kwargs,
310
337
  ) -> Union[AnyResource, ListResponse[AnyResource], Error, Dict]:
311
338
  """Perform a GET request to read resources, as defined in :rfc:`RFC7644
@@ -322,6 +349,9 @@ class SCIMClient:
322
349
  :param check_response_payload: Whether to validate that the response payload is valid.
323
350
  If set, the raw payload will be returned.
324
351
  :param check_status_code: Whether to validate that the response status code is valid.
352
+ :param raise_scim_errors: If :data:`True` and the server returned an
353
+ :class:`~scim2_models.Error` object, a :class:`~scim2_client.SCIMResponseErrorObject`
354
+ exception will be raised. If :data:`False` the error object is returned.
325
355
  :param kwargs: Additional parameters passed to the underlying HTTP request library.
326
356
 
327
357
  :return:
@@ -368,7 +398,6 @@ class SCIMClient:
368
398
  which value will excluded from the request payload, and which values are expected in
369
399
  the response payload.
370
400
  """
371
-
372
401
  if resource_type and check_request_payload:
373
402
  self.check_resource_type(resource_type)
374
403
 
@@ -409,7 +438,7 @@ class SCIMClient:
409
438
  response = self.client.get(url, params=payload, **kwargs)
410
439
  except RequestError as exc:
411
440
  scim_exc = RequestNetworkError(source=payload)
412
- if hasattr(scim_exc, "add_note"): # pragma: no cover
441
+ if sys.version_info >= (3, 11): # pragma: no cover
413
442
  scim_exc.add_note(str(exc))
414
443
  raise scim_exc from exc
415
444
 
@@ -420,6 +449,7 @@ class SCIMClient:
420
449
  ),
421
450
  expected_types=expected_types,
422
451
  check_response_payload=check_response_payload,
452
+ raise_scim_errors=raise_scim_errors,
423
453
  scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
424
454
  )
425
455
 
@@ -429,6 +459,7 @@ class SCIMClient:
429
459
  check_request_payload: bool = True,
430
460
  check_response_payload: bool = True,
431
461
  check_status_code: bool = True,
462
+ raise_scim_errors: bool = False,
432
463
  **kwargs,
433
464
  ) -> Union[AnyResource, ListResponse[AnyResource], Error, Dict]:
434
465
  """Perform a POST search request to read all available resources, as
@@ -442,6 +473,9 @@ class SCIMClient:
442
473
  :param check_response_payload: Whether to validate that the response payload is valid.
443
474
  If set, the raw payload will be returned.
444
475
  :param check_status_code: Whether to validate that the response status code is valid.
476
+ :param raise_scim_errors: If :data:`True` and the server returned an
477
+ :class:`~scim2_models.Error` object, a :class:`~scim2_client.SCIMResponseErrorObject`
478
+ exception will be raised. If :data:`False` the error object is returned.
445
479
  :param kwargs: Additional parameters passed to the underlying
446
480
  HTTP request library.
447
481
 
@@ -467,7 +501,6 @@ class SCIMClient:
467
501
  which value will excluded from the request payload, and which values are expected in
468
502
  the response payload.
469
503
  """
470
-
471
504
  if not check_request_payload:
472
505
  payload = search_request
473
506
 
@@ -486,7 +519,7 @@ class SCIMClient:
486
519
  response = self.client.post(url, json=payload)
487
520
  except RequestError as exc:
488
521
  scim_exc = RequestNetworkError(source=payload)
489
- if hasattr(scim_exc, "add_note"): # pragma: no cover
522
+ if sys.version_info >= (3, 11): # pragma: no cover
490
523
  scim_exc.add_note(str(exc))
491
524
  raise scim_exc from exc
492
525
 
@@ -497,6 +530,7 @@ class SCIMClient:
497
530
  ),
498
531
  expected_types=[ListResponse[Union[self.resource_types]]],
499
532
  check_response_payload=check_response_payload,
533
+ raise_scim_errors=raise_scim_errors,
500
534
  scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
501
535
  )
502
536
 
@@ -506,6 +540,7 @@ class SCIMClient:
506
540
  id: str,
507
541
  check_response_payload: bool = True,
508
542
  check_status_code: bool = True,
543
+ raise_scim_errors: bool = False,
509
544
  **kwargs,
510
545
  ) -> Optional[Union[Error, Dict]]:
511
546
  """Perform a DELETE request to create, as defined in :rfc:`RFC7644 §3.6
@@ -516,6 +551,9 @@ class SCIMClient:
516
551
  :param check_response_payload: Whether to validate that the response payload is valid.
517
552
  If set, the raw payload will be returned.
518
553
  :param check_status_code: Whether to validate that the response status code is valid.
554
+ :param raise_scim_errors: If :data:`True` and the server returned an
555
+ :class:`~scim2_models.Error` object, a :class:`~scim2_client.SCIMResponseErrorObject`
556
+ exception will be raised. If :data:`False` the error object is returned.
519
557
  :param kwargs: Additional parameters passed to the underlying
520
558
  HTTP request library.
521
559
 
@@ -533,7 +571,6 @@ class SCIMClient:
533
571
  response = scim.delete(User, "foobar")
534
572
  # 'response' may be None, or an Error object
535
573
  """
536
-
537
574
  self.check_resource_type(resource_type)
538
575
  delete_url = self.resource_endpoint(resource_type) + f"/{id}"
539
576
  url = kwargs.pop("url", delete_url)
@@ -542,7 +579,7 @@ class SCIMClient:
542
579
  response = self.client.delete(url, **kwargs)
543
580
  except RequestError as exc:
544
581
  scim_exc = RequestNetworkError()
545
- if hasattr(scim_exc, "add_note"): # pragma: no cover
582
+ if sys.version_info >= (3, 11): # pragma: no cover
546
583
  scim_exc.add_note(str(exc))
547
584
  raise scim_exc from exc
548
585
 
@@ -552,6 +589,7 @@ class SCIMClient:
552
589
  self.DELETION_RESPONSE_STATUS_CODES if check_status_code else None
553
590
  ),
554
591
  check_response_payload=check_response_payload,
592
+ raise_scim_errors=raise_scim_errors,
555
593
  )
556
594
 
557
595
  def replace(
@@ -560,6 +598,7 @@ class SCIMClient:
560
598
  check_request_payload: bool = True,
561
599
  check_response_payload: bool = True,
562
600
  check_status_code: bool = True,
601
+ raise_scim_errors: bool = False,
563
602
  **kwargs,
564
603
  ) -> Union[AnyResource, Error, Dict]:
565
604
  """Perform a PUT request to replace a resource, as defined in
@@ -572,6 +611,9 @@ class SCIMClient:
572
611
  :param check_response_payload: Whether to validate that the response payload is valid.
573
612
  If set, the raw payload will be returned.
574
613
  :param check_status_code: Whether to validate that the response status code is valid.
614
+ :param raise_scim_errors: If :data:`True` and the server returned an
615
+ :class:`~scim2_models.Error` object, a :class:`~scim2_client.SCIMResponseErrorObject`
616
+ exception will be raised. If :data:`False` the error object is returned.
575
617
  :param kwargs: Additional parameters passed to the underlying
576
618
  HTTP request library.
577
619
 
@@ -598,7 +640,6 @@ class SCIMClient:
598
640
  which value will excluded from the request payload, and which values are expected in
599
641
  the response payload.
600
642
  """
601
-
602
643
  if not check_request_payload:
603
644
  payload = resource
604
645
  url = kwargs.pop("url", None)
@@ -619,7 +660,7 @@ class SCIMClient:
619
660
  resource = resource_type.model_validate(resource)
620
661
  except ValidationError as exc:
621
662
  scim_exc = RequestPayloadValidationError(source=resource)
622
- if hasattr(scim_exc, "add_note"): # pragma: no cover
663
+ if sys.version_info >= (3, 11): # pragma: no cover
623
664
  scim_exc.add_note(str(exc))
624
665
  raise scim_exc from exc
625
666
 
@@ -637,7 +678,7 @@ class SCIMClient:
637
678
  response = self.client.put(url, json=payload, **kwargs)
638
679
  except RequestError as exc:
639
680
  scim_exc = RequestNetworkError(source=payload)
640
- if hasattr(scim_exc, "add_note"): # pragma: no cover
681
+ if sys.version_info >= (3, 11): # pragma: no cover
641
682
  scim_exc.add_note(str(exc))
642
683
  raise scim_exc from exc
643
684
 
@@ -648,6 +689,7 @@ class SCIMClient:
648
689
  ),
649
690
  expected_types=([resource.__class__] if check_request_payload else None),
650
691
  check_response_payload=check_response_payload,
692
+ raise_scim_errors=raise_scim_errors,
651
693
  scim_ctx=Context.RESOURCE_REPLACEMENT_RESPONSE,
652
694
  )
653
695
 
@@ -60,6 +60,20 @@ class SCIMResponseError(SCIMClientError):
60
60
  validation."""
61
61
 
62
62
 
63
+ class SCIMResponseErrorObject(SCIMResponseError):
64
+ """The server response returned a :class:`scim2_models.Error` object.
65
+
66
+ Those errors are only raised when the :code:`raise_scim_errors` parameter is :data:`True`.
67
+ """
68
+
69
+ def __init__(self, *args, **kwargs):
70
+ message = kwargs.pop(
71
+ "message",
72
+ f"The server returned a SCIM Error object: {kwargs['source'].detail}",
73
+ )
74
+ super().__init__(message, *args, **kwargs)
75
+
76
+
63
77
  class UnexpectedStatusCode(SCIMResponseError):
64
78
  """Error raised when a server returned an unexpected status code for a
65
79
  given :class:`~scim2_models.Context`."""