scim2-client 0.2.1__py3-none-any.whl → 0.3.0__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/__init__.py +4 -2
- scim2_client/client.py +560 -250
- scim2_client/engines/__init__.py +0 -0
- scim2_client/engines/httpx.py +430 -0
- scim2_client/engines/werkzeug.py +245 -0
- scim2_client/errors.py +8 -5
- scim2_client/py.typed +0 -0
- {scim2_client-0.2.1.dist-info → scim2_client-0.3.0.dist-info}/METADATA +11 -8
- scim2_client-0.3.0.dist-info/RECORD +11 -0
- {scim2_client-0.2.1.dist-info → scim2_client-0.3.0.dist-info}/WHEEL +1 -1
- scim2_client-0.2.1.dist-info/RECORD +0 -7
- {scim2_client-0.2.1.dist-info → scim2_client-0.3.0.dist-info}/licenses/LICENSE.md +0 -0
|
File without changes
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
from httpx import Client
|
|
8
|
+
from httpx import RequestError
|
|
9
|
+
from httpx import Response
|
|
10
|
+
from scim2_models import AnyResource
|
|
11
|
+
from scim2_models import Context
|
|
12
|
+
from scim2_models import Error
|
|
13
|
+
from scim2_models import ListResponse
|
|
14
|
+
from scim2_models import Resource
|
|
15
|
+
from scim2_models import SearchRequest
|
|
16
|
+
|
|
17
|
+
from scim2_client.client import BaseAsyncSCIMClient
|
|
18
|
+
from scim2_client.client import BaseSyncSCIMClient
|
|
19
|
+
from scim2_client.errors import RequestNetworkError
|
|
20
|
+
from scim2_client.errors import SCIMClientError
|
|
21
|
+
from scim2_client.errors import UnexpectedContentFormat
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@contextmanager
|
|
25
|
+
def handle_request_error(payload=None):
|
|
26
|
+
try:
|
|
27
|
+
yield
|
|
28
|
+
|
|
29
|
+
except RequestError as exc:
|
|
30
|
+
scim_network_exc = RequestNetworkError(source=payload)
|
|
31
|
+
if sys.version_info >= (3, 11): # pragma: no cover
|
|
32
|
+
scim_network_exc.add_note(str(exc))
|
|
33
|
+
raise scim_network_exc from exc
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@contextmanager
|
|
37
|
+
def handle_response_error(response: Response):
|
|
38
|
+
try:
|
|
39
|
+
yield
|
|
40
|
+
|
|
41
|
+
except json.decoder.JSONDecodeError as exc:
|
|
42
|
+
raise UnexpectedContentFormat(source=response) from exc
|
|
43
|
+
|
|
44
|
+
except SCIMClientError as exc:
|
|
45
|
+
exc.source = response
|
|
46
|
+
raise exc
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SyncSCIMClient(BaseSyncSCIMClient):
|
|
50
|
+
"""Perform SCIM requests over the network and validate responses.
|
|
51
|
+
|
|
52
|
+
:param client: A :class:`httpx.Client` instance that will be used to send requests.
|
|
53
|
+
:param resource_models: A tuple of :class:`~scim2_models.Resource` types expected to be handled by the SCIM client.
|
|
54
|
+
If a request payload describe a resource that is not in this list, an exception will be raised.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self, client: Client, resource_models: Optional[tuple[type[Resource]]] = None
|
|
59
|
+
):
|
|
60
|
+
super().__init__(resource_models=resource_models)
|
|
61
|
+
self.client = client
|
|
62
|
+
|
|
63
|
+
def create(
|
|
64
|
+
self,
|
|
65
|
+
resource: Union[AnyResource, dict],
|
|
66
|
+
check_request_payload: bool = True,
|
|
67
|
+
check_response_payload: bool = True,
|
|
68
|
+
expected_status_codes: Optional[
|
|
69
|
+
list[int]
|
|
70
|
+
] = BaseSyncSCIMClient.CREATION_RESPONSE_STATUS_CODES,
|
|
71
|
+
raise_scim_errors: bool = True,
|
|
72
|
+
**kwargs,
|
|
73
|
+
) -> Union[AnyResource, Error, dict]:
|
|
74
|
+
req = self.prepare_create_request(
|
|
75
|
+
resource=resource,
|
|
76
|
+
check_request_payload=check_request_payload,
|
|
77
|
+
expected_status_codes=expected_status_codes,
|
|
78
|
+
raise_scim_errors=raise_scim_errors,
|
|
79
|
+
**kwargs,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
with handle_request_error(req.payload):
|
|
83
|
+
response = self.client.post(req.url, json=req.payload, **req.request_kwargs)
|
|
84
|
+
|
|
85
|
+
with handle_response_error(req.payload):
|
|
86
|
+
return self.check_response(
|
|
87
|
+
payload=response.json() if response.text else None,
|
|
88
|
+
status_code=response.status_code,
|
|
89
|
+
headers=response.headers,
|
|
90
|
+
expected_status_codes=req.expected_status_codes,
|
|
91
|
+
expected_types=req.expected_types,
|
|
92
|
+
check_response_payload=check_response_payload,
|
|
93
|
+
raise_scim_errors=raise_scim_errors,
|
|
94
|
+
scim_ctx=Context.RESOURCE_CREATION_RESPONSE,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def query(
|
|
98
|
+
self,
|
|
99
|
+
resource_model: Optional[type[Resource]] = None,
|
|
100
|
+
id: Optional[str] = None,
|
|
101
|
+
search_request: Optional[Union[SearchRequest, dict]] = None,
|
|
102
|
+
check_request_payload: bool = True,
|
|
103
|
+
check_response_payload: bool = True,
|
|
104
|
+
expected_status_codes: Optional[
|
|
105
|
+
list[int]
|
|
106
|
+
] = BaseSyncSCIMClient.QUERY_RESPONSE_STATUS_CODES,
|
|
107
|
+
raise_scim_errors: bool = True,
|
|
108
|
+
**kwargs,
|
|
109
|
+
):
|
|
110
|
+
req = self.prepare_query_request(
|
|
111
|
+
resource_model=resource_model,
|
|
112
|
+
id=id,
|
|
113
|
+
search_request=search_request,
|
|
114
|
+
check_request_payload=check_request_payload,
|
|
115
|
+
expected_status_codes=expected_status_codes,
|
|
116
|
+
raise_scim_errors=raise_scim_errors,
|
|
117
|
+
**kwargs,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
with handle_request_error(req.payload):
|
|
121
|
+
response = self.client.get(
|
|
122
|
+
req.url, params=req.payload, **req.request_kwargs
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
with handle_response_error(req.payload):
|
|
126
|
+
return self.check_response(
|
|
127
|
+
payload=response.json() if response.text else None,
|
|
128
|
+
status_code=response.status_code,
|
|
129
|
+
headers=response.headers,
|
|
130
|
+
expected_status_codes=req.expected_status_codes,
|
|
131
|
+
expected_types=req.expected_types,
|
|
132
|
+
check_response_payload=check_response_payload,
|
|
133
|
+
raise_scim_errors=raise_scim_errors,
|
|
134
|
+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def search(
|
|
138
|
+
self,
|
|
139
|
+
search_request: Optional[SearchRequest] = None,
|
|
140
|
+
check_request_payload: bool = True,
|
|
141
|
+
check_response_payload: bool = True,
|
|
142
|
+
expected_status_codes: Optional[
|
|
143
|
+
list[int]
|
|
144
|
+
] = BaseSyncSCIMClient.SEARCH_RESPONSE_STATUS_CODES,
|
|
145
|
+
raise_scim_errors: bool = True,
|
|
146
|
+
**kwargs,
|
|
147
|
+
) -> Union[AnyResource, ListResponse[AnyResource], Error, dict]:
|
|
148
|
+
req = self.prepare_search_request(
|
|
149
|
+
search_request=search_request,
|
|
150
|
+
check_request_payload=check_request_payload,
|
|
151
|
+
expected_status_codes=expected_status_codes,
|
|
152
|
+
raise_scim_errors=raise_scim_errors,
|
|
153
|
+
**kwargs,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
with handle_request_error(req.payload):
|
|
157
|
+
response = self.client.post(req.url, json=req.payload, **req.request_kwargs)
|
|
158
|
+
|
|
159
|
+
with handle_response_error(response):
|
|
160
|
+
return self.check_response(
|
|
161
|
+
payload=response.json() if response.text else None,
|
|
162
|
+
status_code=response.status_code,
|
|
163
|
+
headers=response.headers,
|
|
164
|
+
expected_status_codes=req.expected_status_codes,
|
|
165
|
+
expected_types=req.expected_types,
|
|
166
|
+
check_response_payload=check_response_payload,
|
|
167
|
+
raise_scim_errors=raise_scim_errors,
|
|
168
|
+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def delete(
|
|
172
|
+
self,
|
|
173
|
+
resource_model: type,
|
|
174
|
+
id: str,
|
|
175
|
+
check_response_payload: bool = True,
|
|
176
|
+
expected_status_codes: Optional[
|
|
177
|
+
list[int]
|
|
178
|
+
] = BaseSyncSCIMClient.DELETION_RESPONSE_STATUS_CODES,
|
|
179
|
+
raise_scim_errors: bool = True,
|
|
180
|
+
**kwargs,
|
|
181
|
+
) -> Optional[Union[Error, dict]]:
|
|
182
|
+
req = self.prepare_delete_request(
|
|
183
|
+
resource_model=resource_model,
|
|
184
|
+
id=id,
|
|
185
|
+
expected_status_codes=expected_status_codes,
|
|
186
|
+
raise_scim_errors=raise_scim_errors,
|
|
187
|
+
**kwargs,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
with handle_request_error():
|
|
191
|
+
response = self.client.delete(req.url, **req.request_kwargs)
|
|
192
|
+
|
|
193
|
+
with handle_response_error(response):
|
|
194
|
+
return self.check_response(
|
|
195
|
+
payload=response.json() if response.text else None,
|
|
196
|
+
status_code=response.status_code,
|
|
197
|
+
headers=response.headers,
|
|
198
|
+
expected_status_codes=expected_status_codes,
|
|
199
|
+
check_response_payload=check_response_payload,
|
|
200
|
+
raise_scim_errors=raise_scim_errors,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def replace(
|
|
204
|
+
self,
|
|
205
|
+
resource: Union[AnyResource, dict],
|
|
206
|
+
check_request_payload: bool = True,
|
|
207
|
+
check_response_payload: bool = True,
|
|
208
|
+
expected_status_codes: Optional[
|
|
209
|
+
list[int]
|
|
210
|
+
] = BaseSyncSCIMClient.REPLACEMENT_RESPONSE_STATUS_CODES,
|
|
211
|
+
raise_scim_errors: bool = True,
|
|
212
|
+
**kwargs,
|
|
213
|
+
) -> Union[AnyResource, Error, dict]:
|
|
214
|
+
req = self.prepare_replace_request(
|
|
215
|
+
resource=resource,
|
|
216
|
+
check_request_payload=check_request_payload,
|
|
217
|
+
expected_status_codes=expected_status_codes,
|
|
218
|
+
raise_scim_errors=raise_scim_errors,
|
|
219
|
+
**kwargs,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
with handle_request_error(req.payload):
|
|
223
|
+
response = self.client.put(req.url, json=req.payload, **req.request_kwargs)
|
|
224
|
+
|
|
225
|
+
with handle_response_error(response):
|
|
226
|
+
return self.check_response(
|
|
227
|
+
payload=response.json() if response.text else None,
|
|
228
|
+
status_code=response.status_code,
|
|
229
|
+
headers=response.headers,
|
|
230
|
+
expected_status_codes=req.expected_status_codes,
|
|
231
|
+
expected_types=req.expected_types,
|
|
232
|
+
check_response_payload=check_response_payload,
|
|
233
|
+
raise_scim_errors=raise_scim_errors,
|
|
234
|
+
scim_ctx=Context.RESOURCE_REPLACEMENT_RESPONSE,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class AsyncSCIMClient(BaseAsyncSCIMClient):
|
|
239
|
+
"""Perform SCIM requests over the network and validate responses.
|
|
240
|
+
|
|
241
|
+
:param client: A :class:`httpx.AsyncClient` instance that will be used to send requests.
|
|
242
|
+
:param resource_models: A tuple of :class:`~scim2_models.Resource` types expected to be handled by the SCIM client.
|
|
243
|
+
If a request payload describe a resource that is not in this list, an exception will be raised.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(
|
|
247
|
+
self, client: Client, resource_models: Optional[tuple[type[Resource]]] = None
|
|
248
|
+
):
|
|
249
|
+
super().__init__(resource_models=resource_models)
|
|
250
|
+
self.client = client
|
|
251
|
+
|
|
252
|
+
async def create(
|
|
253
|
+
self,
|
|
254
|
+
resource: Union[AnyResource, dict],
|
|
255
|
+
check_request_payload: bool = True,
|
|
256
|
+
check_response_payload: bool = True,
|
|
257
|
+
expected_status_codes: Optional[
|
|
258
|
+
list[int]
|
|
259
|
+
] = BaseAsyncSCIMClient.CREATION_RESPONSE_STATUS_CODES,
|
|
260
|
+
raise_scim_errors: bool = True,
|
|
261
|
+
**kwargs,
|
|
262
|
+
) -> Union[AnyResource, Error, dict]:
|
|
263
|
+
req = self.prepare_create_request(
|
|
264
|
+
resource=resource,
|
|
265
|
+
check_request_payload=check_request_payload,
|
|
266
|
+
expected_status_codes=expected_status_codes,
|
|
267
|
+
raise_scim_errors=raise_scim_errors,
|
|
268
|
+
**kwargs,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
with handle_request_error(req.payload):
|
|
272
|
+
response = await self.client.post(
|
|
273
|
+
req.url, json=req.payload, **req.request_kwargs
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
with handle_response_error(req.payload):
|
|
277
|
+
return self.check_response(
|
|
278
|
+
payload=response.json() if response.text else None,
|
|
279
|
+
status_code=response.status_code,
|
|
280
|
+
headers=response.headers,
|
|
281
|
+
expected_status_codes=req.expected_status_codes,
|
|
282
|
+
expected_types=req.expected_types,
|
|
283
|
+
check_response_payload=check_response_payload,
|
|
284
|
+
raise_scim_errors=raise_scim_errors,
|
|
285
|
+
scim_ctx=Context.RESOURCE_CREATION_RESPONSE,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
async def query(
|
|
289
|
+
self,
|
|
290
|
+
resource_model: Optional[type[Resource]] = None,
|
|
291
|
+
id: Optional[str] = None,
|
|
292
|
+
search_request: Optional[Union[SearchRequest, dict]] = None,
|
|
293
|
+
check_request_payload: bool = True,
|
|
294
|
+
check_response_payload: bool = True,
|
|
295
|
+
expected_status_codes: Optional[
|
|
296
|
+
list[int]
|
|
297
|
+
] = BaseAsyncSCIMClient.QUERY_RESPONSE_STATUS_CODES,
|
|
298
|
+
raise_scim_errors: bool = True,
|
|
299
|
+
**kwargs,
|
|
300
|
+
):
|
|
301
|
+
req = self.prepare_query_request(
|
|
302
|
+
resource_model=resource_model,
|
|
303
|
+
id=id,
|
|
304
|
+
search_request=search_request,
|
|
305
|
+
check_request_payload=check_request_payload,
|
|
306
|
+
expected_status_codes=expected_status_codes,
|
|
307
|
+
raise_scim_errors=raise_scim_errors,
|
|
308
|
+
**kwargs,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
with handle_request_error(req.payload):
|
|
312
|
+
response = await self.client.get(
|
|
313
|
+
req.url, params=req.payload, **req.request_kwargs
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
with handle_response_error(req.payload):
|
|
317
|
+
return self.check_response(
|
|
318
|
+
payload=response.json() if response.text else None,
|
|
319
|
+
status_code=response.status_code,
|
|
320
|
+
headers=response.headers,
|
|
321
|
+
expected_status_codes=req.expected_status_codes,
|
|
322
|
+
expected_types=req.expected_types,
|
|
323
|
+
check_response_payload=check_response_payload,
|
|
324
|
+
raise_scim_errors=raise_scim_errors,
|
|
325
|
+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
async def search(
|
|
329
|
+
self,
|
|
330
|
+
search_request: Optional[SearchRequest] = None,
|
|
331
|
+
check_request_payload: bool = True,
|
|
332
|
+
check_response_payload: bool = True,
|
|
333
|
+
expected_status_codes: Optional[
|
|
334
|
+
list[int]
|
|
335
|
+
] = BaseAsyncSCIMClient.SEARCH_RESPONSE_STATUS_CODES,
|
|
336
|
+
raise_scim_errors: bool = True,
|
|
337
|
+
**kwargs,
|
|
338
|
+
) -> Union[AnyResource, ListResponse[AnyResource], Error, dict]:
|
|
339
|
+
req = self.prepare_search_request(
|
|
340
|
+
search_request=search_request,
|
|
341
|
+
check_request_payload=check_request_payload,
|
|
342
|
+
expected_status_codes=expected_status_codes,
|
|
343
|
+
raise_scim_errors=raise_scim_errors,
|
|
344
|
+
**kwargs,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
with handle_request_error(req.payload):
|
|
348
|
+
response = await self.client.post(
|
|
349
|
+
req.url, json=req.payload, **req.request_kwargs
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
with handle_response_error(response):
|
|
353
|
+
return self.check_response(
|
|
354
|
+
payload=response.json() if response.text else None,
|
|
355
|
+
status_code=response.status_code,
|
|
356
|
+
headers=response.headers,
|
|
357
|
+
expected_status_codes=req.expected_status_codes,
|
|
358
|
+
expected_types=req.expected_types,
|
|
359
|
+
check_response_payload=check_response_payload,
|
|
360
|
+
raise_scim_errors=raise_scim_errors,
|
|
361
|
+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
async def delete(
|
|
365
|
+
self,
|
|
366
|
+
resource_model: type,
|
|
367
|
+
id: str,
|
|
368
|
+
check_response_payload: bool = True,
|
|
369
|
+
expected_status_codes: Optional[
|
|
370
|
+
list[int]
|
|
371
|
+
] = BaseAsyncSCIMClient.DELETION_RESPONSE_STATUS_CODES,
|
|
372
|
+
raise_scim_errors: bool = True,
|
|
373
|
+
**kwargs,
|
|
374
|
+
) -> Optional[Union[Error, dict]]:
|
|
375
|
+
req = self.prepare_delete_request(
|
|
376
|
+
resource_model=resource_model,
|
|
377
|
+
id=id,
|
|
378
|
+
expected_status_codes=expected_status_codes,
|
|
379
|
+
raise_scim_errors=raise_scim_errors,
|
|
380
|
+
**kwargs,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
with handle_request_error():
|
|
384
|
+
response = await self.client.delete(req.url, **req.request_kwargs)
|
|
385
|
+
|
|
386
|
+
with handle_response_error(response):
|
|
387
|
+
return self.check_response(
|
|
388
|
+
payload=response.json() if response.text else None,
|
|
389
|
+
status_code=response.status_code,
|
|
390
|
+
headers=response.headers,
|
|
391
|
+
expected_status_codes=expected_status_codes,
|
|
392
|
+
check_response_payload=check_response_payload,
|
|
393
|
+
raise_scim_errors=raise_scim_errors,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
async def replace(
|
|
397
|
+
self,
|
|
398
|
+
resource: Union[AnyResource, dict],
|
|
399
|
+
check_request_payload: bool = True,
|
|
400
|
+
check_response_payload: bool = True,
|
|
401
|
+
expected_status_codes: Optional[
|
|
402
|
+
list[int]
|
|
403
|
+
] = BaseAsyncSCIMClient.REPLACEMENT_RESPONSE_STATUS_CODES,
|
|
404
|
+
raise_scim_errors: bool = True,
|
|
405
|
+
**kwargs,
|
|
406
|
+
) -> Union[AnyResource, Error, dict]:
|
|
407
|
+
req = self.prepare_replace_request(
|
|
408
|
+
resource=resource,
|
|
409
|
+
check_request_payload=check_request_payload,
|
|
410
|
+
expected_status_codes=expected_status_codes,
|
|
411
|
+
raise_scim_errors=raise_scim_errors,
|
|
412
|
+
**kwargs,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
with handle_request_error(req.payload):
|
|
416
|
+
response = await self.client.put(
|
|
417
|
+
req.url, json=req.payload, **req.request_kwargs
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
with handle_response_error(response):
|
|
421
|
+
return self.check_response(
|
|
422
|
+
payload=response.json() if response.text else None,
|
|
423
|
+
status_code=response.status_code,
|
|
424
|
+
headers=response.headers,
|
|
425
|
+
expected_status_codes=req.expected_status_codes,
|
|
426
|
+
expected_types=req.expected_types,
|
|
427
|
+
check_response_payload=check_response_payload,
|
|
428
|
+
raise_scim_errors=raise_scim_errors,
|
|
429
|
+
scim_ctx=Context.RESOURCE_REPLACEMENT_RESPONSE,
|
|
430
|
+
)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from typing import Union
|
|
4
|
+
from urllib.parse import urlencode
|
|
5
|
+
|
|
6
|
+
from scim2_models import AnyResource
|
|
7
|
+
from scim2_models import Context
|
|
8
|
+
from scim2_models import Error
|
|
9
|
+
from scim2_models import ListResponse
|
|
10
|
+
from scim2_models import Resource
|
|
11
|
+
from scim2_models import SearchRequest
|
|
12
|
+
from werkzeug.test import Client
|
|
13
|
+
|
|
14
|
+
from scim2_client.client import BaseSyncSCIMClient
|
|
15
|
+
from scim2_client.errors import SCIMClientError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@contextmanager
|
|
19
|
+
def handle_response_error(response):
|
|
20
|
+
try:
|
|
21
|
+
yield
|
|
22
|
+
|
|
23
|
+
except SCIMClientError as exc:
|
|
24
|
+
exc.source = response
|
|
25
|
+
raise exc
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestSCIMClient(BaseSyncSCIMClient):
|
|
29
|
+
"""A client based on :class:`Werkzeug test Client <werkzeug.test.Client>` for application development purposes.
|
|
30
|
+
|
|
31
|
+
This is helpful for developers of SCIM servers.
|
|
32
|
+
This client avoids to perform real HTTP requests and directly execute the server code instead.
|
|
33
|
+
This allows to dynamically catch the exceptions if something gets wrong.
|
|
34
|
+
|
|
35
|
+
:param client: A WSGI application instance that will be used to send requests.
|
|
36
|
+
:param scim_prefix: The scim root endpoint in the application.
|
|
37
|
+
:param resource_models: A tuple of :class:`~scim2_models.Resource` types expected to be handled by the SCIM client.
|
|
38
|
+
If a request payload describe a resource that is not in this list, an exception will be raised.
|
|
39
|
+
|
|
40
|
+
.. code-block:: python
|
|
41
|
+
|
|
42
|
+
from scim2_client.engines.werkzeug import TestSCIMClient
|
|
43
|
+
from scim2_models import User, Group
|
|
44
|
+
|
|
45
|
+
scim_provider = myapp.create_app()
|
|
46
|
+
testclient = TestSCIMClient(app=scim_provider, resource_models=(User, Group))
|
|
47
|
+
|
|
48
|
+
request_user = User(user_name="foo", display_name="bar")
|
|
49
|
+
response_user = scim_client.create(request_user)
|
|
50
|
+
assert response_user.user_name == "foo"
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
app,
|
|
56
|
+
scim_prefix: str = "",
|
|
57
|
+
resource_models: Optional[tuple[type[Resource]]] = None,
|
|
58
|
+
):
|
|
59
|
+
super().__init__(resource_models=resource_models)
|
|
60
|
+
self.client = Client(app)
|
|
61
|
+
self.scim_prefix = scim_prefix
|
|
62
|
+
|
|
63
|
+
def make_url(self, url: Optional[str]) -> str:
|
|
64
|
+
prefix = (
|
|
65
|
+
self.scim_prefix[:-1]
|
|
66
|
+
if self.scim_prefix.endswith("/")
|
|
67
|
+
else self.scim_prefix
|
|
68
|
+
)
|
|
69
|
+
return f"{prefix}{url or ''}"
|
|
70
|
+
|
|
71
|
+
def create(
|
|
72
|
+
self,
|
|
73
|
+
resource: Union[AnyResource, dict],
|
|
74
|
+
check_request_payload: bool = True,
|
|
75
|
+
check_response_payload: bool = True,
|
|
76
|
+
expected_status_codes: Optional[
|
|
77
|
+
list[int]
|
|
78
|
+
] = BaseSyncSCIMClient.CREATION_RESPONSE_STATUS_CODES,
|
|
79
|
+
raise_scim_errors: bool = True,
|
|
80
|
+
**kwargs,
|
|
81
|
+
) -> Union[AnyResource, Error, dict]:
|
|
82
|
+
req = self.prepare_create_request(
|
|
83
|
+
resource=resource,
|
|
84
|
+
check_request_payload=check_request_payload,
|
|
85
|
+
expected_status_codes=expected_status_codes,
|
|
86
|
+
raise_scim_errors=raise_scim_errors,
|
|
87
|
+
**kwargs,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
response = self.client.post(
|
|
91
|
+
self.make_url(req.url), json=req.payload, **req.request_kwargs
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
with handle_response_error(req.payload):
|
|
95
|
+
return self.check_response(
|
|
96
|
+
payload=response.json if response.text else None,
|
|
97
|
+
status_code=response.status_code,
|
|
98
|
+
headers=response.headers,
|
|
99
|
+
expected_status_codes=req.expected_status_codes,
|
|
100
|
+
expected_types=req.expected_types,
|
|
101
|
+
check_response_payload=check_response_payload,
|
|
102
|
+
raise_scim_errors=raise_scim_errors,
|
|
103
|
+
scim_ctx=Context.RESOURCE_CREATION_RESPONSE,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def query(
|
|
107
|
+
self,
|
|
108
|
+
resource_model: Optional[type[Resource]] = None,
|
|
109
|
+
id: Optional[str] = None,
|
|
110
|
+
search_request: Optional[Union[SearchRequest, dict]] = None,
|
|
111
|
+
check_request_payload: bool = True,
|
|
112
|
+
check_response_payload: bool = True,
|
|
113
|
+
expected_status_codes: Optional[
|
|
114
|
+
list[int]
|
|
115
|
+
] = BaseSyncSCIMClient.QUERY_RESPONSE_STATUS_CODES,
|
|
116
|
+
raise_scim_errors: bool = True,
|
|
117
|
+
**kwargs,
|
|
118
|
+
):
|
|
119
|
+
req = self.prepare_query_request(
|
|
120
|
+
resource_model=resource_model,
|
|
121
|
+
id=id,
|
|
122
|
+
search_request=search_request,
|
|
123
|
+
check_request_payload=check_request_payload,
|
|
124
|
+
expected_status_codes=expected_status_codes,
|
|
125
|
+
raise_scim_errors=raise_scim_errors,
|
|
126
|
+
**kwargs,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
query_string = urlencode(req.payload, doseq=False) if req.payload else None
|
|
130
|
+
response = self.client.get(
|
|
131
|
+
self.make_url(req.url), query_string=query_string, **req.request_kwargs
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
with handle_response_error(req.payload):
|
|
135
|
+
return self.check_response(
|
|
136
|
+
payload=response.json if response.text else None,
|
|
137
|
+
status_code=response.status_code,
|
|
138
|
+
headers=response.headers,
|
|
139
|
+
expected_status_codes=req.expected_status_codes,
|
|
140
|
+
expected_types=req.expected_types,
|
|
141
|
+
check_response_payload=check_response_payload,
|
|
142
|
+
raise_scim_errors=raise_scim_errors,
|
|
143
|
+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def search(
|
|
147
|
+
self,
|
|
148
|
+
search_request: Optional[SearchRequest] = None,
|
|
149
|
+
check_request_payload: bool = True,
|
|
150
|
+
check_response_payload: bool = True,
|
|
151
|
+
expected_status_codes: Optional[
|
|
152
|
+
list[int]
|
|
153
|
+
] = BaseSyncSCIMClient.SEARCH_RESPONSE_STATUS_CODES,
|
|
154
|
+
raise_scim_errors: bool = True,
|
|
155
|
+
**kwargs,
|
|
156
|
+
) -> Union[AnyResource, ListResponse[AnyResource], Error, dict]:
|
|
157
|
+
req = self.prepare_search_request(
|
|
158
|
+
search_request=search_request,
|
|
159
|
+
check_request_payload=check_request_payload,
|
|
160
|
+
expected_status_codes=expected_status_codes,
|
|
161
|
+
raise_scim_errors=raise_scim_errors,
|
|
162
|
+
**kwargs,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
response = self.client.post(
|
|
166
|
+
self.make_url(req.url), json=req.payload, **req.request_kwargs
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
with handle_response_error(response):
|
|
170
|
+
return self.check_response(
|
|
171
|
+
payload=response.json if response.text else None,
|
|
172
|
+
status_code=response.status_code,
|
|
173
|
+
headers=response.headers,
|
|
174
|
+
expected_status_codes=req.expected_status_codes,
|
|
175
|
+
expected_types=req.expected_types,
|
|
176
|
+
check_response_payload=check_response_payload,
|
|
177
|
+
raise_scim_errors=raise_scim_errors,
|
|
178
|
+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def delete(
|
|
182
|
+
self,
|
|
183
|
+
resource_model: type,
|
|
184
|
+
id: str,
|
|
185
|
+
check_response_payload: bool = True,
|
|
186
|
+
expected_status_codes: Optional[
|
|
187
|
+
list[int]
|
|
188
|
+
] = BaseSyncSCIMClient.DELETION_RESPONSE_STATUS_CODES,
|
|
189
|
+
raise_scim_errors: bool = True,
|
|
190
|
+
**kwargs,
|
|
191
|
+
) -> Optional[Union[Error, dict]]:
|
|
192
|
+
req = self.prepare_delete_request(
|
|
193
|
+
resource_model=resource_model,
|
|
194
|
+
id=id,
|
|
195
|
+
expected_status_codes=expected_status_codes,
|
|
196
|
+
raise_scim_errors=raise_scim_errors,
|
|
197
|
+
**kwargs,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
response = self.client.delete(self.make_url(req.url), **req.request_kwargs)
|
|
201
|
+
|
|
202
|
+
with handle_response_error(response):
|
|
203
|
+
return self.check_response(
|
|
204
|
+
payload=response.json if response.text else None,
|
|
205
|
+
status_code=response.status_code,
|
|
206
|
+
headers=response.headers,
|
|
207
|
+
expected_status_codes=req.expected_status_codes,
|
|
208
|
+
check_response_payload=check_response_payload,
|
|
209
|
+
raise_scim_errors=raise_scim_errors,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def replace(
|
|
213
|
+
self,
|
|
214
|
+
resource: Union[AnyResource, dict],
|
|
215
|
+
check_request_payload: bool = True,
|
|
216
|
+
check_response_payload: bool = True,
|
|
217
|
+
expected_status_codes: Optional[
|
|
218
|
+
list[int]
|
|
219
|
+
] = BaseSyncSCIMClient.REPLACEMENT_RESPONSE_STATUS_CODES,
|
|
220
|
+
raise_scim_errors: bool = True,
|
|
221
|
+
**kwargs,
|
|
222
|
+
) -> Union[AnyResource, Error, dict]:
|
|
223
|
+
req = self.prepare_replace_request(
|
|
224
|
+
resource=resource,
|
|
225
|
+
check_request_payload=check_request_payload,
|
|
226
|
+
expected_status_codes=expected_status_codes,
|
|
227
|
+
raise_scim_errors=raise_scim_errors,
|
|
228
|
+
**kwargs,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
response = self.client.put(
|
|
232
|
+
self.make_url(req.url), json=req.payload, **req.request_kwargs
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
with handle_response_error(response):
|
|
236
|
+
return self.check_response(
|
|
237
|
+
payload=response.json if response.text else None,
|
|
238
|
+
status_code=response.status_code,
|
|
239
|
+
headers=response.headers,
|
|
240
|
+
expected_status_codes=req.expected_status_codes,
|
|
241
|
+
expected_types=req.expected_types,
|
|
242
|
+
check_response_payload=check_response_payload,
|
|
243
|
+
raise_scim_errors=raise_scim_errors,
|
|
244
|
+
scim_ctx=Context.RESOURCE_REPLACEMENT_RESPONSE,
|
|
245
|
+
)
|