scim2-client 0.1.1__tar.gz → 0.1.2__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.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
 
@@ -17,7 +17,7 @@ Here is an example of usage:
17
17
 
18
18
  ```python
19
19
  import datetime
20
- from httpx impont Client
20
+ from httpx import Client
21
21
  from scim2_models import User, EnterpriseUserUser, Group, Error
22
22
  from scim2_client import SCIMClient
23
23
 
@@ -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.1"
7
+ version = "0.1.2"
8
8
  description = "Pythonically build SCIM requests and parse SCIM responses"
9
9
  authors = ["Yaal Coop <contact@yaal.coop>"]
10
10
  license = "MIT"
@@ -0,0 +1,442 @@
1
+ import json
2
+ import json.decoder
3
+ from typing import Dict
4
+ from typing import List
5
+ from typing import Optional
6
+ from typing import Tuple
7
+ from typing import Type
8
+ from typing import Union
9
+
10
+ from httpx import Client
11
+ from httpx import Response
12
+ from pydantic import ValidationError
13
+ from scim2_models import AnyResource
14
+ from scim2_models import Context
15
+ from scim2_models import Error
16
+ from scim2_models import ListResponse
17
+ from scim2_models import PatchOp
18
+ from scim2_models import SearchRequest
19
+
20
+ from .errors import UnexpectedContentFormat
21
+ from .errors import UnexpectedContentType
22
+ from .errors import UnexpectedStatusCode
23
+
24
+ BASE_HEADERS = {
25
+ "Accept": "application/scim+json",
26
+ "Content-Type": "application/scim+json",
27
+ }
28
+
29
+
30
+ class SCIMClient:
31
+ """An object that perform SCIM requests and validate responses."""
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
+
98
+ def __init__(self, client: Client, resource_types: Optional[Tuple[Type]] = None):
99
+ self.client = client
100
+ self.resource_types = resource_types or ()
101
+
102
+ def check_resource_type(self, resource_type):
103
+ if resource_type not in self.resource_types:
104
+ raise ValueError(f"Unknown resource type: '{resource_type}'")
105
+
106
+ def resource_endpoint(self, resource_type: Type):
107
+ return f"/{resource_type.__name__}s"
108
+
109
+ def check_response(
110
+ self,
111
+ response: Response,
112
+ expected_status_codes: List[int],
113
+ expected_type: Optional[Type] = None,
114
+ scim_ctx: Optional[Context] = None,
115
+ ):
116
+ if expected_status_codes and response.status_code not in expected_status_codes:
117
+ raise UnexpectedStatusCode(response)
118
+
119
+ # Interoperability considerations: The "application/scim+json" media
120
+ # type is intended to identify JSON structure data that conforms to
121
+ # the SCIM protocol and schema specifications. Older versions of
122
+ # SCIM are known to informally use "application/json".
123
+ # https://datatracker.ietf.org/doc/html/rfc7644.html#section-8.1
124
+
125
+ expected_response_content_types = ("application/scim+json", "application/json")
126
+ if response.headers.get("content-type") not in expected_response_content_types:
127
+ raise UnexpectedContentType(response)
128
+
129
+ # In addition to returning an HTTP response code, implementers MUST return
130
+ # the errors in the body of the response in a JSON format
131
+ # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
132
+
133
+ no_content_status_codes = [204, 205]
134
+ if response.status_code in no_content_status_codes:
135
+ response_payload = None
136
+
137
+ else:
138
+ try:
139
+ response_payload = response.json()
140
+ except json.decoder.JSONDecodeError as exc:
141
+ raise UnexpectedContentFormat(response) from exc
142
+
143
+ try:
144
+ return Error.model_validate(response_payload)
145
+ except ValidationError:
146
+ pass
147
+
148
+ if expected_type:
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
154
+
155
+ return response_payload
156
+
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]:
165
+ """Perform a POST request to create, as defined in :rfc:`RFC7644 §3.3
166
+ <7644#section-3.3>`.
167
+
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.
174
+ :param kwargs: Additional parameters passed to the underlying HTTP request
175
+ library.
176
+
177
+ :return:
178
+ - An :class:`~scim2_models.Error` object in case of error.
179
+ - The created object as returned by the server in case of success.
180
+ """
181
+
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
+
193
+ return self.check_response(
194
+ response,
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,
199
+ scim_ctx=Context.RESOURCE_CREATION_RESPONSE,
200
+ )
201
+
202
+ def query(
203
+ self,
204
+ resource_type: Type,
205
+ id: Optional[str] = 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,
210
+ **kwargs,
211
+ ) -> Union[AnyResource, ListResponse[AnyResource], Error, Dict]:
212
+ """Perform a GET request to read resources, as defined in :rfc:`RFC7644
213
+ §3.4.2 <7644#section-3.4.2>`.
214
+
215
+ - If `id` is not :data:`None`, the resource with the exact id will be reached.
216
+ - If `id` is :data:`None`, all the resources with the given type will be reached.
217
+
218
+ :param resource_type: A :class:`~scim2_models.Resource` subtype or :data:`None`
219
+ :param id: The SCIM id of an object to get, or :data:`None`
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.
226
+ :param kwargs: Additional parameters passed to the underlying HTTP request library.
227
+
228
+ :return:
229
+ - A :class:`~scim2_models.Error` object in case of error.
230
+ - A `resource_type` object in case of success if `id` is not :data:`None`
231
+ - A :class:`~scim2_models.ListResponse[resource_type]` object in case of success if `id` is :data:`None`
232
+ """
233
+
234
+ self.check_resource_type(resource_type)
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
246
+ )
247
+
248
+ if not id:
249
+ expected_type = ListResponse[resource_type]
250
+ url = self.resource_endpoint(resource_type)
251
+
252
+ else:
253
+ expected_type = resource_type
254
+ url = self.resource_endpoint(resource_type) + f"/{id}"
255
+
256
+ response = self.client.get(url, params=payload, **kwargs)
257
+ return self.check_response(
258
+ response,
259
+ self.QUERY_RESPONSE_STATUS_CODES if check_status_code else None,
260
+ expected_type if check_response_payload else None,
261
+ scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
262
+ )
263
+
264
+ def query_all(
265
+ self,
266
+ search_request: Optional[SearchRequest] = None,
267
+ check_request_payload: bool = True,
268
+ check_response_payload: bool = True,
269
+ check_status_code: bool = True,
270
+ **kwargs,
271
+ ) -> Union[AnyResource, ListResponse[AnyResource], Error, Dict]:
272
+ """Perform a GET request to read all available resources, as defined in
273
+ :rfc:`RFC7644 §3.4.2.1 <7644#section-3.4.2.1>`.
274
+
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.
281
+ :param kwargs: Additional parameters passed to the underlying
282
+ HTTP request library.
283
+
284
+ :return:
285
+ - A :class:`~scim2_models.Error` object in case of error.
286
+ - A :class:`~scim2_models.ListResponse[resource_type]` object in case of success.
287
+ """
288
+
289
+ # A query against a server root indicates that all resources within the
290
+ # server SHALL be included, subject to filtering.
291
+ # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.4.2.1
292
+
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
303
+ )
304
+
305
+ response = self.client.get("/", params=payload)
306
+
307
+ return self.check_response(
308
+ response,
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,
313
+ scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
314
+ )
315
+
316
+ def search(
317
+ self,
318
+ search_request: Optional[SearchRequest] = None,
319
+ check_request_payload: bool = True,
320
+ check_response_payload: bool = True,
321
+ check_status_code: bool = True,
322
+ **kwargs,
323
+ ) -> Union[AnyResource, ListResponse[AnyResource], Error, Dict]:
324
+ """Perform a POST search request to read all available resources, as
325
+ defined in :rfc:`RFC7644 §3.4.3 <7644#section-3.4.3>`.
326
+
327
+ :param resource_types: Resource type or union of types expected
328
+ to be read from the response.
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.
335
+ :param kwargs: Additional parameters passed to the underlying
336
+ HTTP request library.
337
+
338
+ :return:
339
+ - A :class:`~scim2_models.Error` object in case of error.
340
+ - A :class:`~scim2_models.ListResponse[resource_type]` object in case of success.
341
+ """
342
+
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
353
+ )
354
+
355
+ response = self.client.post("/.search", params=payload)
356
+
357
+ return self.check_response(
358
+ response,
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,
363
+ scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
364
+ )
365
+
366
+ def delete(
367
+ self, resource_type: Type, id: str, check_status_code: bool = True, **kwargs
368
+ ) -> Optional[Union[Error, Dict]]:
369
+ """Perform a DELETE request to create, as defined in :rfc:`RFC7644 §3.6
370
+ <7644#section-3.6>`.
371
+
372
+ :param check_status_code: Whether to validate that the response status code is valid.
373
+ :param kwargs: Additional parameters passed to the underlying
374
+ HTTP request library.
375
+
376
+ :return:
377
+ - A :class:`~scim2_models.Error` object in case of error.
378
+ - :data:`None` in case of success.
379
+ """
380
+
381
+ self.check_resource_type(resource_type)
382
+ url = self.resource_endpoint(resource_type) + f"/{id}"
383
+ response = self.client.delete(url, **kwargs)
384
+
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]:
397
+ """Perform a PUT request to replace a resource, as defined in
398
+ :rfc:`RFC7644 §3.5.1 <7644#section-3.5.1>`.
399
+
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.
406
+ :param kwargs: Additional parameters passed to the underlying
407
+ HTTP request library.
408
+
409
+ :return:
410
+ - An :class:`~scim2_models.Error` object in case of error.
411
+ - The updated object as returned by the server in case of success.
412
+ """
413
+
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
+
430
+ return self.check_response(
431
+ response,
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,
436
+ scim_ctx=Context.RESOURCE_REPLACEMENT_RESPONSE,
437
+ )
438
+
439
+ def modify(
440
+ self, resource: Union[AnyResource, Dict], op: PatchOp, **kwargs
441
+ ) -> Optional[Union[AnyResource, Dict]]:
442
+ raise NotImplementedError()
@@ -1,377 +0,0 @@
1
- import json
2
- import json.decoder
3
- from typing import List
4
- from typing import Optional
5
- from typing import Tuple
6
- from typing import Type
7
- from typing import Union
8
-
9
- from httpx import Client
10
- from httpx import Response
11
- from pydantic import ValidationError
12
- from scim2_models import AnyResource
13
- from scim2_models import Context
14
- from scim2_models import Error
15
- from scim2_models import ListResponse
16
- from scim2_models import PatchOp
17
- from scim2_models import SearchRequest
18
-
19
- from .errors import UnexpectedContentFormat
20
- from .errors import UnexpectedContentType
21
- from .errors import UnexpectedStatusCode
22
-
23
- BASE_HEADERS = {
24
- "Accept": "application/scim+json",
25
- "Content-Type": "application/scim+json",
26
- }
27
-
28
-
29
- class SCIMClient:
30
- """An object that perform SCIM requests and validate responses."""
31
-
32
- def __init__(self, client: Client, resource_types: Optional[Tuple[Type]] = None):
33
- self.client = client
34
- self.resource_types = resource_types or ()
35
-
36
- def check_resource_type(self, resource_type):
37
- if resource_type not in self.resource_types:
38
- raise ValueError(f"Unknown resource type: '{resource_type}'")
39
-
40
- def resource_endpoint(self, resource_type: Type):
41
- return f"/{resource_type.__name__}s"
42
-
43
- def check_response(
44
- self,
45
- response: Response,
46
- expected_status_codes: List[int],
47
- expected_type: Optional[Type] = None,
48
- scim_ctx: Optional[Context] = None,
49
- ):
50
- if response.status_code not in expected_status_codes:
51
- raise UnexpectedStatusCode(response)
52
-
53
- # Interoperability considerations: The "application/scim+json" media
54
- # type is intended to identify JSON structure data that conforms to
55
- # the SCIM protocol and schema specifications. Older versions of
56
- # SCIM are known to informally use "application/json".
57
- # https://datatracker.ietf.org/doc/html/rfc7644.html#section-8.1
58
-
59
- expected_response_content_types = ("application/scim+json", "application/json")
60
- if response.headers.get("content-type") not in expected_response_content_types:
61
- raise UnexpectedContentType(response)
62
-
63
- # In addition to returning an HTTP response code, implementers MUST return
64
- # the errors in the body of the response in a JSON format
65
- # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
66
-
67
- if response.status_code in (204, 205):
68
- response_payload = None
69
-
70
- else:
71
- try:
72
- response_payload = response.json()
73
- except json.decoder.JSONDecodeError as exc:
74
- raise UnexpectedContentFormat(response) from exc
75
-
76
- try:
77
- return Error.model_validate(response_payload)
78
- except ValidationError:
79
- pass
80
-
81
- if expected_type:
82
- return expected_type.model_validate(response_payload, scim_ctx=scim_ctx)
83
-
84
- return response_payload
85
-
86
- def create(self, resource: AnyResource, **kwargs) -> Union[AnyResource, Error]:
87
- """Perform a POST request to create, as defined in :rfc:`RFC7644 §3.3
88
- <7644#section-3.3>`.
89
-
90
- :param resource: The resource to create
91
- :param kwargs: Additional parameters passed to the underlying HTTP request
92
- library.
93
-
94
- :return:
95
- - An :class:`~scim2_models.Error` object in case of error.
96
- - The created object as returned by the server in case of success.
97
- """
98
-
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
- ]
119
- return self.check_response(
120
- response,
121
- expected_status_codes,
122
- resource.__class__,
123
- scim_ctx=Context.RESOURCE_CREATION_RESPONSE,
124
- )
125
-
126
- def query(
127
- self,
128
- resource_type: Type,
129
- id: Optional[str] = None,
130
- search_request: Optional[SearchRequest] = None,
131
- **kwargs,
132
- ) -> Union[AnyResource, ListResponse[AnyResource], Error]:
133
- """Perform a GET request to read resources, as defined in :rfc:`RFC7644
134
- §3.4.2 <7644#section-3.4.2>`.
135
-
136
- - If `id` is not :data:`None`, the resource with the exact id will be reached.
137
- - If `id` is :data:`None`, all the resources with the given type will be reached.
138
-
139
- :param resource_type: A :class:`~scim2_models.Resource` subtype or :data:`None`
140
- :param id: The SCIM id of an object to get, or :data:`None`
141
- :param search_request: An object detailing the search query parameters.
142
- :param kwargs: Additional parameters passed to the underlying HTTP request library.
143
-
144
- :return:
145
- - A :class:`~scim2_models.Error` object in case of error.
146
- - A `resource_type` object in case of success if `id` is not :data:`None`
147
- - A :class:`~scim2_models.ListResponse[resource_type]` object in case of success if `id` is :data:`None`
148
- """
149
-
150
- self.check_resource_type(resource_type)
151
- payload = (
152
- search_request.model_dump(
153
- exclude_unset=True,
154
- scim_ctx=Context.RESOURCE_QUERY_REQUEST,
155
- )
156
- if search_request
157
- else None
158
- )
159
-
160
- if not id:
161
- expected_type = ListResponse[resource_type]
162
- url = self.resource_endpoint(resource_type)
163
-
164
- else:
165
- expected_type = resource_type
166
- url = self.resource_endpoint(resource_type) + f"/{id}"
167
-
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
- response = self.client.get(url, params=payload, **kwargs)
183
- return self.check_response(
184
- response,
185
- expected_status_codes,
186
- expected_type,
187
- scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
188
- )
189
-
190
- def query_all(
191
- self,
192
- search_request: Optional[SearchRequest] = None,
193
- **kwargs,
194
- ) -> Union[AnyResource, ListResponse[AnyResource], Error]:
195
- """Perform a GET request to read all available resources, as defined in
196
- :rfc:`RFC7644 §3.4.2.1 <7644#section-3.4.2.1>`.
197
-
198
- :param search_request: An object detailing the search query parameters.
199
- :param kwargs: Additional parameters passed to the underlying
200
- HTTP request library.
201
-
202
- :return:
203
- - A :class:`~scim2_models.Error` object in case of error.
204
- - A :class:`~scim2_models.ListResponse[resource_type]` object in case of success.
205
- """
206
-
207
- # A query against a server root indicates that all resources within the
208
- # server SHALL be included, subject to filtering.
209
- # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.4.2.1
210
-
211
- payload = (
212
- search_request.model_dump(
213
- exclude_unset=True, scim_ctx=Context.RESOURCE_QUERY_REQUEST
214
- )
215
- if search_request
216
- else None
217
- )
218
- response = self.client.get("/", params=payload)
219
-
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
- ]
235
-
236
- return self.check_response(
237
- response,
238
- expected_status_codes,
239
- ListResponse[Union[self.resource_types]],
240
- scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
241
- )
242
-
243
- def search(
244
- self,
245
- search_request: Optional[SearchRequest] = None,
246
- **kwargs,
247
- ) -> Union[AnyResource, ListResponse[AnyResource], Error]:
248
- """Perform a POST search request to read all available resources, as
249
- defined in :rfc:`RFC7644 §3.4.3 <7644#section-3.4.3>`.
250
-
251
- :param resource_types: Resource type or union of types expected
252
- to be read from the response.
253
- :param search_request: An object detailing the search query parameters.
254
- :param kwargs: Additional parameters passed to the underlying
255
- HTTP request library.
256
-
257
- :return:
258
- - A :class:`~scim2_models.Error` object in case of error.
259
- - A :class:`~scim2_models.ListResponse[resource_type]` object in case of success.
260
- """
261
-
262
- payload = (
263
- search_request.model_dump(
264
- exclude_unset=True, scim_ctx=Context.RESOURCE_QUERY_RESPONSE
265
- )
266
- if search_request
267
- else None
268
- )
269
- response = self.client.post("/.search", params=payload)
270
-
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
- return self.check_response(
289
- response,
290
- expected_status_codes,
291
- ListResponse[Union[self.resource_types]],
292
- scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
293
- )
294
-
295
- def delete(self, resource_type: Type, id: str, **kwargs) -> Optional[Error]:
296
- """Perform a DELETE request to create, as defined in :rfc:`RFC7644 §3.6
297
- <7644#section-3.6>`.
298
-
299
- :param kwargs: Additional parameters passed to the underlying
300
- HTTP request library.
301
-
302
- :return:
303
- - A :class:`~scim2_models.Error` object in case of error.
304
- - :data:`None` in case of success.
305
- """
306
-
307
- self.check_resource_type(resource_type)
308
- url = self.resource_endpoint(resource_type) + f"/{id}"
309
- response = self.client.delete(url, **kwargs)
310
-
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]:
330
- """Perform a PUT request to replace a resource, as defined in
331
- :rfc:`RFC7644 §3.5.1 <7644#section-3.5.1>`.
332
-
333
- :param resource: The new state of the resource to replace.
334
- :param kwargs: Additional parameters passed to the underlying
335
- HTTP request library.
336
-
337
- :return:
338
- - An :class:`~scim2_models.Error` object in case of error.
339
- - The updated object as returned by the server in case of success.
340
- """
341
-
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
- ]
367
- return self.check_response(
368
- response,
369
- expected_status_codes,
370
- resource.__class__,
371
- scim_ctx=Context.RESOURCE_REPLACEMENT_RESPONSE,
372
- )
373
-
374
- def modify(
375
- self, resource: AnyResource, op: PatchOp, **kwargs
376
- ) -> Optional[AnyResource]:
377
- raise NotImplementedError()