scim2-client 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

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.
scim2_client/client.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import json.decoder
3
+ from typing import Dict
3
4
  from typing import List
4
5
  from typing import Optional
5
6
  from typing import Tuple
@@ -29,6 +30,71 @@ BASE_HEADERS = {
29
30
  class SCIMClient:
30
31
  """An object that perform SCIM requests and validate responses."""
31
32
 
33
+ CREATION_RESPONSE_STATUS_CODES: List[int] = [
34
+ 201,
35
+ 409,
36
+ 307,
37
+ 308,
38
+ 400,
39
+ 401,
40
+ 403,
41
+ 404,
42
+ 500,
43
+ ]
44
+ """Resource creation HTTP codes defined at :rfc:`RFC7644 §3.3
45
+ <7644#section-3.3>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
46
+
47
+ QUERY_RESPONSE_STATUS_CODES: List[int] = [200, 400, 307, 308, 401, 403, 404, 500]
48
+ """Resource querying HTTP codes defined at :rfc:`RFC7644 §3.4.2
49
+ <7644#section-3.4.2>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
50
+
51
+ SEARCH_RESPONSE_STATUS_CODES: List[int] = [
52
+ 200,
53
+ 307,
54
+ 308,
55
+ 400,
56
+ 401,
57
+ 403,
58
+ 404,
59
+ 409,
60
+ 413,
61
+ 500,
62
+ 501,
63
+ ]
64
+ """Resource querying HTTP codes defined at :rfc:`RFC7644 §3.4.3
65
+ <7644#section-3.4.3>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
66
+
67
+ DELETION_RESPONSE_STATUS_CODES: List[int] = [
68
+ 204,
69
+ 307,
70
+ 308,
71
+ 400,
72
+ 401,
73
+ 403,
74
+ 404,
75
+ 412,
76
+ 500,
77
+ 501,
78
+ ]
79
+ """Resource deletion HTTP codes defined at :rfc:`RFC7644 §3.6
80
+ <7644#section-3.6>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
81
+
82
+ REPLACEMENT_RESPONSE_STATUS_CODES: List[int] = [
83
+ 200,
84
+ 307,
85
+ 308,
86
+ 400,
87
+ 401,
88
+ 403,
89
+ 404,
90
+ 409,
91
+ 412,
92
+ 500,
93
+ 501,
94
+ ]
95
+ """Resource querying HTTP codes defined at :rfc:`RFC7644 §3.4.2
96
+ <7644#section-3.4.2>` and :rfc:`RFC7644 §3.12 <7644#section-3.12>`"""
97
+
32
98
  def __init__(self, client: Client, resource_types: Optional[Tuple[Type]] = None):
33
99
  self.client = client
34
100
  self.resource_types = resource_types or ()
@@ -47,7 +113,7 @@ class SCIMClient:
47
113
  expected_type: Optional[Type] = None,
48
114
  scim_ctx: Optional[Context] = None,
49
115
  ):
50
- if response.status_code not in expected_status_codes:
116
+ if expected_status_codes and response.status_code not in expected_status_codes:
51
117
  raise UnexpectedStatusCode(response)
52
118
 
53
119
  # Interoperability considerations: The "application/scim+json" media
@@ -64,7 +130,8 @@ class SCIMClient:
64
130
  # the errors in the body of the response in a JSON format
65
131
  # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
66
132
 
67
- if response.status_code in (204, 205):
133
+ no_content_status_codes = [204, 205]
134
+ if response.status_code in no_content_status_codes:
68
135
  response_payload = None
69
136
 
70
137
  else:
@@ -79,15 +146,31 @@ class SCIMClient:
79
146
  pass
80
147
 
81
148
  if expected_type:
82
- return expected_type.model_validate(response_payload, scim_ctx=scim_ctx)
149
+ try:
150
+ return expected_type.model_validate(response_payload, scim_ctx=scim_ctx)
151
+ except ValidationError as exc:
152
+ exc.response_payload = response_payload
153
+ raise exc
83
154
 
84
155
  return response_payload
85
156
 
86
- def create(self, resource: AnyResource, **kwargs) -> Union[AnyResource, Error]:
157
+ def create(
158
+ self,
159
+ resource: Union[AnyResource, Dict],
160
+ check_request_payload: bool = True,
161
+ check_response_payload: bool = True,
162
+ check_status_code: bool = True,
163
+ **kwargs,
164
+ ) -> Union[AnyResource, Error, Dict]:
87
165
  """Perform a POST request to create, as defined in :rfc:`RFC7644 §3.3
88
166
  <7644#section-3.3>`.
89
167
 
90
168
  :param resource: The resource to create
169
+ :param check_request_payload: If :data:`False`,
170
+ :code:`resource` is expected to be a dict that will be passed as-is in the request.
171
+ :param check_response_payload: Whether to validate that the response payload is valid.
172
+ If set, the raw payload will be returned.
173
+ :param check_status_code: Whether to validate that the response status code is valid.
91
174
  :param kwargs: Additional parameters passed to the underlying HTTP request
92
175
  library.
93
176
 
@@ -96,30 +179,23 @@ class SCIMClient:
96
179
  - The created object as returned by the server in case of success.
97
180
  """
98
181
 
99
- self.check_resource_type(resource.__class__)
100
- url = self.resource_endpoint(resource.__class__)
101
- dump = resource.model_dump(scim_ctx=Context.RESOURCE_CREATION_REQUEST)
102
- response = self.client.post(url, json=dump, **kwargs)
103
-
104
- expected_status_codes = [
105
- # Resource creation HTTP codes defined at:
106
- # https://datatracker.ietf.org/doc/html/rfc7644#section-3.3
107
- 201,
108
- 409,
109
- # Default HTTP codes defined at:
110
- # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
111
- 307,
112
- 308,
113
- 400,
114
- 401,
115
- 403,
116
- 404,
117
- 500,
118
- ]
182
+ if not check_request_payload:
183
+ payload = resource
184
+ url = kwargs.pop("url", None)
185
+
186
+ else:
187
+ self.check_resource_type(resource.__class__)
188
+ url = kwargs.pop("url", self.resource_endpoint(resource.__class__))
189
+ payload = resource.model_dump(scim_ctx=Context.RESOURCE_CREATION_REQUEST)
190
+
191
+ response = self.client.post(url, json=payload, **kwargs)
192
+
119
193
  return self.check_response(
120
194
  response,
121
- expected_status_codes,
122
- resource.__class__,
195
+ self.CREATION_RESPONSE_STATUS_CODES if check_status_code else None,
196
+ resource.__class__
197
+ if check_request_payload and check_response_payload
198
+ else None,
123
199
  scim_ctx=Context.RESOURCE_CREATION_RESPONSE,
124
200
  )
125
201
 
@@ -127,9 +203,12 @@ class SCIMClient:
127
203
  self,
128
204
  resource_type: Type,
129
205
  id: Optional[str] = None,
130
- search_request: Optional[SearchRequest] = None,
206
+ search_request: Optional[Union[SearchRequest, Dict]] = None,
207
+ check_request_payload: bool = True,
208
+ check_response_payload: bool = True,
209
+ check_status_code: bool = True,
131
210
  **kwargs,
132
- ) -> Union[AnyResource, ListResponse[AnyResource], Error]:
211
+ ) -> Union[AnyResource, ListResponse[AnyResource], Error, Dict]:
133
212
  """Perform a GET request to read resources, as defined in :rfc:`RFC7644
134
213
  §3.4.2 <7644#section-3.4.2>`.
135
214
 
@@ -139,6 +218,11 @@ class SCIMClient:
139
218
  :param resource_type: A :class:`~scim2_models.Resource` subtype or :data:`None`
140
219
  :param id: The SCIM id of an object to get, or :data:`None`
141
220
  :param search_request: An object detailing the search query parameters.
221
+ :param check_request_payload: If :data:`False`,
222
+ :code:`search_request` is expected to be a dict that will be passed as-is in the request.
223
+ :param check_response_payload: Whether to validate that the response payload is valid.
224
+ If set, the raw payload will be returned.
225
+ :param check_status_code: Whether to validate that the response status code is valid.
142
226
  :param kwargs: Additional parameters passed to the underlying HTTP request library.
143
227
 
144
228
  :return:
@@ -148,14 +232,18 @@ class SCIMClient:
148
232
  """
149
233
 
150
234
  self.check_resource_type(resource_type)
151
- payload = (
152
- search_request.model_dump(
153
- exclude_unset=True,
154
- scim_ctx=Context.RESOURCE_QUERY_REQUEST,
235
+ if not check_request_payload:
236
+ payload = search_request
237
+
238
+ else:
239
+ payload = (
240
+ search_request.model_dump(
241
+ exclude_unset=True,
242
+ scim_ctx=Context.RESOURCE_QUERY_REQUEST,
243
+ )
244
+ if search_request
245
+ else None
155
246
  )
156
- if search_request
157
- else None
158
- )
159
247
 
160
248
  if not id:
161
249
  expected_type = ListResponse[resource_type]
@@ -165,37 +253,31 @@ class SCIMClient:
165
253
  expected_type = resource_type
166
254
  url = self.resource_endpoint(resource_type) + f"/{id}"
167
255
 
168
- expected_status_codes = [
169
- # Resource querying HTTP codes defined at:
170
- # https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2
171
- 200,
172
- 400,
173
- # Default HTTP codes defined at:
174
- # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
175
- 307,
176
- 308,
177
- 401,
178
- 403,
179
- 404,
180
- 500,
181
- ]
182
256
  response = self.client.get(url, params=payload, **kwargs)
183
257
  return self.check_response(
184
258
  response,
185
- expected_status_codes,
186
- expected_type,
259
+ self.QUERY_RESPONSE_STATUS_CODES if check_status_code else None,
260
+ expected_type if check_response_payload else None,
187
261
  scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
188
262
  )
189
263
 
190
264
  def query_all(
191
265
  self,
192
266
  search_request: Optional[SearchRequest] = None,
267
+ check_request_payload: bool = True,
268
+ check_response_payload: bool = True,
269
+ check_status_code: bool = True,
193
270
  **kwargs,
194
- ) -> Union[AnyResource, ListResponse[AnyResource], Error]:
271
+ ) -> Union[AnyResource, ListResponse[AnyResource], Error, Dict]:
195
272
  """Perform a GET request to read all available resources, as defined in
196
273
  :rfc:`RFC7644 §3.4.2.1 <7644#section-3.4.2.1>`.
197
274
 
198
275
  :param search_request: An object detailing the search query parameters.
276
+ :param check_request_payload: If :data:`False`,
277
+ :code:`search_request` is expected to be a dict that will be passed as-is in the request.
278
+ :param check_response_payload: Whether to validate that the response payload is valid.
279
+ If set, the raw payload will be returned.
280
+ :param check_status_code: Whether to validate that the response status code is valid.
199
281
  :param kwargs: Additional parameters passed to the underlying
200
282
  HTTP request library.
201
283
 
@@ -208,49 +290,48 @@ class SCIMClient:
208
290
  # server SHALL be included, subject to filtering.
209
291
  # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.4.2.1
210
292
 
211
- payload = (
212
- search_request.model_dump(
213
- exclude_unset=True, scim_ctx=Context.RESOURCE_QUERY_REQUEST
293
+ if not check_request_payload:
294
+ payload = search_request
295
+
296
+ else:
297
+ payload = (
298
+ search_request.model_dump(
299
+ exclude_unset=True, scim_ctx=Context.RESOURCE_QUERY_REQUEST
300
+ )
301
+ if search_request
302
+ else None
214
303
  )
215
- if search_request
216
- else None
217
- )
218
- response = self.client.get("/", params=payload)
219
304
 
220
- expected_status_codes = [
221
- # Resource querying HTTP codes defined at:
222
- # https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2
223
- 200,
224
- 400,
225
- # Default HTTP codes defined at:
226
- # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
227
- 307,
228
- 308,
229
- 401,
230
- 403,
231
- 404,
232
- 500,
233
- 501,
234
- ]
305
+ response = self.client.get("/", params=payload)
235
306
 
236
307
  return self.check_response(
237
308
  response,
238
- expected_status_codes,
239
- ListResponse[Union[self.resource_types]],
309
+ self.QUERY_RESPONSE_STATUS_CODES if check_status_code else None,
310
+ ListResponse[Union[self.resource_types]]
311
+ if check_response_payload
312
+ else None,
240
313
  scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
241
314
  )
242
315
 
243
316
  def search(
244
317
  self,
245
318
  search_request: Optional[SearchRequest] = None,
319
+ check_request_payload: bool = True,
320
+ check_response_payload: bool = True,
321
+ check_status_code: bool = True,
246
322
  **kwargs,
247
- ) -> Union[AnyResource, ListResponse[AnyResource], Error]:
323
+ ) -> Union[AnyResource, ListResponse[AnyResource], Error, Dict]:
248
324
  """Perform a POST search request to read all available resources, as
249
325
  defined in :rfc:`RFC7644 §3.4.3 <7644#section-3.4.3>`.
250
326
 
251
327
  :param resource_types: Resource type or union of types expected
252
328
  to be read from the response.
253
329
  :param search_request: An object detailing the search query parameters.
330
+ :param check_request_payload: If :data:`False`,
331
+ :code:`search_request` is expected to be a dict that will be passed as-is in the request.
332
+ :param check_response_payload: Whether to validate that the response payload is valid.
333
+ If set, the raw payload will be returned.
334
+ :param check_status_code: Whether to validate that the response status code is valid.
254
335
  :param kwargs: Additional parameters passed to the underlying
255
336
  HTTP request library.
256
337
 
@@ -259,43 +340,36 @@ class SCIMClient:
259
340
  - A :class:`~scim2_models.ListResponse[resource_type]` object in case of success.
260
341
  """
261
342
 
262
- payload = (
263
- search_request.model_dump(
264
- exclude_unset=True, scim_ctx=Context.RESOURCE_QUERY_RESPONSE
343
+ if not check_request_payload:
344
+ payload = search_request
345
+
346
+ else:
347
+ payload = (
348
+ search_request.model_dump(
349
+ exclude_unset=True, scim_ctx=Context.RESOURCE_QUERY_RESPONSE
350
+ )
351
+ if search_request
352
+ else None
265
353
  )
266
- if search_request
267
- else None
268
- )
354
+
269
355
  response = self.client.post("/.search", params=payload)
270
356
 
271
- expected_status_codes = [
272
- # Resource querying HTTP codes defined at:
273
- # https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.3
274
- 200,
275
- # Default HTTP codes defined at:
276
- # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
277
- 307,
278
- 308,
279
- 400,
280
- 401,
281
- 403,
282
- 404,
283
- 409,
284
- 413,
285
- 500,
286
- 501,
287
- ]
288
357
  return self.check_response(
289
358
  response,
290
- expected_status_codes,
291
- ListResponse[Union[self.resource_types]],
359
+ self.SEARCH_RESPONSE_STATUS_CODES if check_status_code else None,
360
+ ListResponse[Union[self.resource_types]]
361
+ if check_response_payload
362
+ else None,
292
363
  scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
293
364
  )
294
365
 
295
- def delete(self, resource_type: Type, id: str, **kwargs) -> Optional[Error]:
366
+ def delete(
367
+ self, resource_type: Type, id: str, check_status_code: bool = True, **kwargs
368
+ ) -> Optional[Union[Error, Dict]]:
296
369
  """Perform a DELETE request to create, as defined in :rfc:`RFC7644 §3.6
297
370
  <7644#section-3.6>`.
298
371
 
372
+ :param check_status_code: Whether to validate that the response status code is valid.
299
373
  :param kwargs: Additional parameters passed to the underlying
300
374
  HTTP request library.
301
375
 
@@ -308,29 +382,27 @@ class SCIMClient:
308
382
  url = self.resource_endpoint(resource_type) + f"/{id}"
309
383
  response = self.client.delete(url, **kwargs)
310
384
 
311
- expected_status_codes = [
312
- # Resource deletion HTTP codes defined at:
313
- # https://datatracker.ietf.org/doc/html/rfc7644#section-3.6
314
- 204,
315
- # Default HTTP codes defined at:
316
- # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
317
- 307,
318
- 308,
319
- 400,
320
- 401,
321
- 403,
322
- 404,
323
- 412,
324
- 500,
325
- 501,
326
- ]
327
- return self.check_response(response, expected_status_codes)
328
-
329
- def replace(self, resource: AnyResource, **kwargs) -> Union[AnyResource, Error]:
385
+ return self.check_response(
386
+ response, self.DELETION_RESPONSE_STATUS_CODES if check_status_code else None
387
+ )
388
+
389
+ def replace(
390
+ self,
391
+ resource: Union[AnyResource, Dict],
392
+ check_request_payload: bool = True,
393
+ check_response_payload: bool = True,
394
+ check_status_code: bool = True,
395
+ **kwargs,
396
+ ) -> Union[AnyResource, Error, Dict]:
330
397
  """Perform a PUT request to replace a resource, as defined in
331
398
  :rfc:`RFC7644 §3.5.1 <7644#section-3.5.1>`.
332
399
 
333
400
  :param resource: The new state of the resource to replace.
401
+ :param check_request_payload: If :data:`False`,
402
+ :code:`resource` is expected to be a dict that will be passed as-is in the request.
403
+ :param check_response_payload: Whether to validate that the response payload is valid.
404
+ If set, the raw payload will be returned.
405
+ :param check_status_code: Whether to validate that the response status code is valid.
334
406
  :param kwargs: Additional parameters passed to the underlying
335
407
  HTTP request library.
336
408
 
@@ -339,39 +411,32 @@ class SCIMClient:
339
411
  - The updated object as returned by the server in case of success.
340
412
  """
341
413
 
342
- self.check_resource_type(resource.__class__)
343
- if not resource.id:
344
- raise Exception("Resource must have an id")
345
-
346
- dump = resource.model_dump(scim_ctx=Context.RESOURCE_REPLACEMENT_REQUEST)
347
- url = self.resource_endpoint(resource.__class__) + f"/{resource.id}"
348
- response = self.client.put(url, json=dump, **kwargs)
349
-
350
- expected_status_codes = [
351
- # Resource querying HTTP codes defined at:
352
- # https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2
353
- 200,
354
- # Default HTTP codes defined at:
355
- # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
356
- 307,
357
- 308,
358
- 400,
359
- 401,
360
- 403,
361
- 404,
362
- 409,
363
- 412,
364
- 500,
365
- 501,
366
- ]
414
+ if not check_request_payload:
415
+ payload = resource
416
+ url = kwargs.pop("url")
417
+
418
+ else:
419
+ self.check_resource_type(resource.__class__)
420
+ if not resource.id:
421
+ raise Exception("Resource must have an id")
422
+
423
+ payload = resource.model_dump(scim_ctx=Context.RESOURCE_REPLACEMENT_REQUEST)
424
+ url = kwargs.pop(
425
+ "url", self.resource_endpoint(resource.__class__) + f"/{resource.id}"
426
+ )
427
+
428
+ response = self.client.put(url, json=payload, **kwargs)
429
+
367
430
  return self.check_response(
368
431
  response,
369
- expected_status_codes,
370
- resource.__class__,
432
+ self.REPLACEMENT_RESPONSE_STATUS_CODES if check_status_code else None,
433
+ resource.__class__
434
+ if check_request_payload and check_response_payload
435
+ else None,
371
436
  scim_ctx=Context.RESOURCE_REPLACEMENT_RESPONSE,
372
437
  )
373
438
 
374
439
  def modify(
375
- self, resource: AnyResource, op: PatchOp, **kwargs
376
- ) -> Optional[AnyResource]:
440
+ self, resource: Union[AnyResource, Dict], op: PatchOp, **kwargs
441
+ ) -> Optional[Union[AnyResource, Dict]]:
377
442
  raise NotImplementedError()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scim2-client
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Pythonically build SCIM requests and parse SCIM responses
5
5
  License: MIT
6
6
  Keywords: scim,scim2,provisioning,httpx,api
@@ -42,7 +42,7 @@ Here is an example of usage:
42
42
 
43
43
  ```python
44
44
  import datetime
45
- from httpx impont Client
45
+ from httpx import Client
46
46
  from scim2_models import User, EnterpriseUserUser, Group, Error
47
47
  from scim2_client import SCIMClient
48
48
 
@@ -0,0 +1,6 @@
1
+ scim2_client/__init__.py,sha256=2UNsl6HNtVUv5LVcnmLaCHyT4SqAUUFOIWW2r5XGv6A,338
2
+ scim2_client/client.py,sha256=_ZjMd6_Kh5SYEcstm5OkMIu0z-N93R4SWdG0lGd4u7s,16538
3
+ scim2_client/errors.py,sha256=uOOAwsD8rDrC8BQbwPid051YyWtexTcS8b7i6QC6-CM,1175
4
+ scim2_client-0.1.2.dist-info/METADATA,sha256=4h7QD3jpYRhSQCBmLX9AFhHJ9R68_ObP45V9_kV51LI,2654
5
+ scim2_client-0.1.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
6
+ scim2_client-0.1.2.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- scim2_client/__init__.py,sha256=2UNsl6HNtVUv5LVcnmLaCHyT4SqAUUFOIWW2r5XGv6A,338
2
- scim2_client/client.py,sha256=LsG_omH6Mz4mxcqLLA6ApnVJqInAWo8GjaVRXuyf08c,13199
3
- scim2_client/errors.py,sha256=uOOAwsD8rDrC8BQbwPid051YyWtexTcS8b7i6QC6-CM,1175
4
- scim2_client-0.1.1.dist-info/METADATA,sha256=s1A7Uk7fz_9n0PgU7V2EqxGZMEonmfCzVod5a7l_sJ4,2654
5
- scim2_client-0.1.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
6
- scim2_client-0.1.1.dist-info/RECORD,,