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.
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
+ )