anaplan-sdk 0.4.4a4__py3-none-any.whl → 0.5.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.
@@ -1,9 +1,8 @@
1
+ import logging
1
2
  from typing import Any, Literal
2
3
 
3
- import httpx
4
-
5
- from anaplan_sdk._base import (
6
- _AsyncBaseClient,
4
+ from anaplan_sdk._services import _AsyncHttpService
5
+ from anaplan_sdk._utils import (
7
6
  connection_body_payload,
8
7
  construct_payload,
9
8
  integration_payload,
@@ -27,12 +26,14 @@ from anaplan_sdk.models.cloud_works import (
27
26
 
28
27
  from ._cw_flow import _AsyncFlowClient
29
28
 
29
+ logger = logging.getLogger("anaplan_sdk")
30
+
30
31
 
31
- class _AsyncCloudWorksClient(_AsyncBaseClient):
32
- def __init__(self, client: httpx.AsyncClient, retry_count: int) -> None:
32
+ class _AsyncCloudWorksClient:
33
+ def __init__(self, http: _AsyncHttpService) -> None:
34
+ self._http = http
33
35
  self._url = "https://api.cloudworks.anaplan.com/2/0/integrations"
34
- self._flow = _AsyncFlowClient(client, retry_count)
35
- super().__init__(retry_count, client)
36
+ self._flow = _AsyncFlowClient(http)
36
37
 
37
38
  @property
38
39
  def flows(self) -> _AsyncFlowClient:
@@ -41,14 +42,14 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
41
42
  """
42
43
  return self._flow
43
44
 
44
- async def list_connections(self) -> list[Connection]:
45
+ async def get_connections(self) -> list[Connection]:
45
46
  """
46
47
  List all Connections available in CloudWorks.
47
48
  :return: A list of connections.
48
49
  """
49
50
  return [
50
51
  Connection.model_validate(e)
51
- for e in await self._get_paginated(f"{self._url}/connections", "connections")
52
+ for e in await self._http.get_paginated(f"{self._url}/connections", "connections")
52
53
  ]
53
54
 
54
55
  async def create_connection(self, con_info: ConnectionInput | dict[str, Any]) -> str:
@@ -59,10 +60,12 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
59
60
  against the ConnectionInput model before sending the request.
60
61
  :return: The ID of the new connection.
61
62
  """
62
- res = await self._post(
63
+ res = await self._http.post(
63
64
  f"{self._url}/connections", json=construct_payload(ConnectionInput, con_info)
64
65
  )
65
- return res["connections"]["connectionId"]
66
+ connection_id = res["connections"]["connectionId"]
67
+ logger.info(f"Created connection '{connection_id}'.")
68
+ return connection_id
66
69
 
67
70
  async def update_connection(
68
71
  self, con_id: str, con_info: ConnectionBody | dict[str, Any]
@@ -74,7 +77,9 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
74
77
  as when initially creating the connection again. If you want to update only some of
75
78
  the details, use the `patch_connection` method instead.
76
79
  """
77
- await self._put(f"{self._url}/connections/{con_id}", json=connection_body_payload(con_info))
80
+ await self._http.put(
81
+ f"{self._url}/connections/{con_id}", json=connection_body_payload(con_info)
82
+ )
78
83
 
79
84
  async def patch_connection(self, con_id: str, body: dict[str, Any]) -> None:
80
85
  """
@@ -83,27 +88,29 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
83
88
  :param body: The name and details of the connection. You can pass all the same details as
84
89
  when initially creating the connection again, or just any one of them.
85
90
  """
86
- await self._patch(f"{self._url}/connections/{con_id}", json=body)
91
+ await self._http.patch(f"{self._url}/connections/{con_id}", json=body)
87
92
 
88
93
  async def delete_connection(self, con_id: str) -> None:
89
94
  """
90
95
  Delete an existing connection in CloudWorks.
91
96
  :param con_id: The ID of the connection to delete.
92
97
  """
93
- await self._delete(f"{self._url}/connections/{con_id}")
98
+ await self._http.delete(f"{self._url}/connections/{con_id}")
99
+ logger.info(f"Deleted connection '{con_id}'.")
94
100
 
95
- async def list_integrations(
96
- self, sort_by_name: Literal["ascending", "descending"] = "ascending"
101
+ async def get_integrations(
102
+ self, sort_by: Literal["name"] | None = None, descending: bool = False
97
103
  ) -> list[Integration]:
98
104
  """
99
105
  List all integrations in CloudWorks.
100
- :param sort_by_name: Sort the integrations by name in ascending or descending order.
106
+ :param sort_by: The field to sort the results by.
107
+ :param descending: If True, the results will be sorted in descending order.
101
108
  :return: A list of integrations.
102
109
  """
103
- params = {"sortBy": "name" if sort_by_name == "ascending" else "-name"}
110
+ params = {"sortBy": f"{'-' if descending else ''}{sort_by}"} if sort_by else None
104
111
  return [
105
112
  Integration.model_validate(e)
106
- for e in await self._get_paginated(f"{self._url}", "integrations", params=params)
113
+ for e in await self._http.get_paginated(f"{self._url}", "integrations", params=params)
107
114
  ]
108
115
 
109
116
  async def get_integration(self, integration_id: str) -> SingleIntegration:
@@ -116,7 +123,7 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
116
123
  :return: The details of the integration, without the integration type.
117
124
  """
118
125
  return SingleIntegration.model_validate(
119
- (await self._get(f"{self._url}/{integration_id}"))["integration"]
126
+ (await self._http.get(f"{self._url}/{integration_id}"))["integration"]
120
127
  )
121
128
 
122
129
  async def create_integration(
@@ -143,7 +150,10 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
143
150
  :return: The ID of the new integration.
144
151
  """
145
152
  json = integration_payload(body)
146
- return (await self._post(f"{self._url}", json=json))["integration"]["integrationId"]
153
+ res = await self._http.post(f"{self._url}", json=json)
154
+ integration_id = res["integration"]["integrationId"]
155
+ logger.info(f"Created integration '{integration_id}'.")
156
+ return integration_id
147
157
 
148
158
  async def update_integration(
149
159
  self, integration_id: str, body: IntegrationInput | IntegrationProcessInput | dict[str, Any]
@@ -156,7 +166,7 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
156
166
  of the details, use the `patch_integration` method instead.
157
167
  """
158
168
  json = integration_payload(body)
159
- await self._put(f"{self._url}/{integration_id}", json=json)
169
+ await self._http.put(f"{self._url}/{integration_id}", json=json)
160
170
 
161
171
  async def run_integration(self, integration_id: str) -> str:
162
172
  """
@@ -164,14 +174,17 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
164
174
  :param integration_id: The ID of the integration to run.
165
175
  :return: The ID of the run instance.
166
176
  """
167
- return (await self._post_empty(f"{self._url}/{integration_id}/run"))["run"]["id"]
177
+ run_id = (await self._http.post_empty(f"{self._url}/{integration_id}/run"))["run"]["id"]
178
+ logger.info(f"Started integration run '{run_id}' for integration '{integration_id}'.")
179
+ return run_id
168
180
 
169
181
  async def delete_integration(self, integration_id: str) -> None:
170
182
  """
171
183
  Delete an existing integration in CloudWorks.
172
184
  :param integration_id: The ID of the integration to delete.
173
185
  """
174
- await self._delete(f"{self._url}/{integration_id}")
186
+ await self._http.delete(f"{self._url}/{integration_id}")
187
+ logger.info(f"Deleted integration '{integration_id}'.")
175
188
 
176
189
  async def get_run_history(self, integration_id: str) -> list[RunSummary]:
177
190
  """
@@ -181,9 +194,9 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
181
194
  """
182
195
  return [
183
196
  RunSummary.model_validate(e)
184
- for e in (await self._get(f"{self._url}/runs/{integration_id}"))["history_of_runs"].get(
185
- "runs", []
186
- )
197
+ for e in (await self._http.get(f"{self._url}/runs/{integration_id}"))[
198
+ "history_of_runs"
199
+ ].get("runs", [])
187
200
  ]
188
201
 
189
202
  async def get_run_status(self, run_id: str) -> RunStatus:
@@ -192,7 +205,7 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
192
205
  :param run_id: The ID of the run to retrieve.
193
206
  :return: The details of the run.
194
207
  """
195
- return RunStatus.model_validate((await self._get(f"{self._url}/run/{run_id}"))["run"])
208
+ return RunStatus.model_validate((await self._http.get(f"{self._url}/run/{run_id}"))["run"])
196
209
 
197
210
  async def get_run_error(self, run_id: str) -> RunError | None:
198
211
  """
@@ -201,7 +214,7 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
201
214
  :param run_id: The ID of the run to retrieve.
202
215
  :return: The details of the run error.
203
216
  """
204
- run = await self._get(f"{self._url}/runerror/{run_id}")
217
+ run = await self._http.get(f"{self._url}/runerror/{run_id}")
205
218
  return RunError.model_validate(run["runs"]) if run.get("runs") else None
206
219
 
207
220
  async def create_schedule(
@@ -214,10 +227,11 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
214
227
  dictionary as per the documentation. If a dictionary is passed, it will be validated
215
228
  against the ScheduleInput model before sending the request.
216
229
  """
217
- await self._post(
230
+ await self._http.post(
218
231
  f"{self._url}/{integration_id}/schedule",
219
232
  json=schedule_payload(integration_id, schedule),
220
233
  )
234
+ logger.info(f"Created schedule for integration '{integration_id}'.")
221
235
 
222
236
  async def update_schedule(
223
237
  self, integration_id: str, schedule: ScheduleInput | dict[str, Any]
@@ -229,7 +243,7 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
229
243
  dictionary as per the documentation. If a dictionary is passed, it will be validated
230
244
  against the ScheduleInput model before sending the request.
231
245
  """
232
- await self._put(
246
+ await self._http.put(
233
247
  f"{self._url}/{integration_id}/schedule",
234
248
  json=schedule_payload(integration_id, schedule),
235
249
  )
@@ -242,14 +256,15 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
242
256
  :param integration_id: The ID of the integration to schedule.
243
257
  :param status: The status of the schedule. This can be either "enabled" or "disabled".
244
258
  """
245
- await self._post_empty(f"{self._url}/{integration_id}/schedule/status/{status}")
259
+ await self._http.post_empty(f"{self._url}/{integration_id}/schedule/status/{status}")
246
260
 
247
261
  async def delete_schedule(self, integration_id: str) -> None:
248
262
  """
249
263
  Delete an integration schedule in CloudWorks. A schedule must already exist.
250
264
  :param integration_id: The ID of the integration to schedule.
251
265
  """
252
- await self._delete(f"{self._url}/{integration_id}/schedule")
266
+ await self._http.delete(f"{self._url}/{integration_id}/schedule")
267
+ logger.info(f"Deleted schedule for integration '{integration_id}'.")
253
268
 
254
269
  async def get_notification_config(
255
270
  self, notification_id: str | None = None, integration_id: str | None = None
@@ -268,7 +283,7 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
268
283
  if integration_id:
269
284
  notification_id = (await self.get_integration(integration_id)).notification_id
270
285
  return NotificationConfig.model_validate(
271
- (await self._get(f"{self._url}/notification/{notification_id}"))["notifications"]
286
+ (await self._http.get(f"{self._url}/notification/{notification_id}"))["notifications"]
272
287
  )
273
288
 
274
289
  async def create_notification_config(self, config: NotificationInput | dict[str, Any]) -> str:
@@ -282,10 +297,12 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
282
297
  validated against the NotificationConfig model before sending the request.
283
298
  :return: The ID of the new notification configuration.
284
299
  """
285
- res = await self._post(
300
+ res = await self._http.post(
286
301
  f"{self._url}/notification", json=construct_payload(NotificationInput, config)
287
302
  )
288
- return res["notification"]["notificationId"]
303
+ notification_id = res["notification"]["notificationId"]
304
+ logger.info(f"Created notification configuration '{notification_id}'.")
305
+ return notification_id
289
306
 
290
307
  async def update_notification_config(
291
308
  self, notification_id: str, config: NotificationInput | dict[str, Any]
@@ -300,7 +317,7 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
300
317
  a dictionary as per the documentation. If a dictionary is passed, it will be
301
318
  validated against the NotificationConfig model before sending the request.
302
319
  """
303
- await self._put(
320
+ await self._http.put(
304
321
  f"{self._url}/notification/{notification_id}",
305
322
  json=construct_payload(NotificationInput, config),
306
323
  )
@@ -319,7 +336,8 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
319
336
  raise ValueError("Either notification_id or integration_id must be specified.")
320
337
  if integration_id:
321
338
  notification_id = (await self.get_integration(integration_id)).notification_id
322
- await self._delete(f"{self._url}/notification/{notification_id}")
339
+ await self._http.delete(f"{self._url}/notification/{notification_id}")
340
+ logger.info(f"Deleted notification configuration '{notification_id}'.")
323
341
 
324
342
  async def get_import_error_dump(self, run_id: str) -> bytes:
325
343
  """
@@ -331,7 +349,7 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
331
349
  :param run_id: The ID of the run to retrieve.
332
350
  :return: The error dump.
333
351
  """
334
- return await self._get_binary(f"{self._url}/run/{run_id}/dump")
352
+ return await self._http.get_binary(f"{self._url}/run/{run_id}/dump")
335
353
 
336
354
  async def get_process_error_dump(self, run_id: str, action_id: int | str) -> bytes:
337
355
  """
@@ -341,4 +359,6 @@ class _AsyncCloudWorksClient(_AsyncBaseClient):
341
359
  :param action_id: The ID of the action to retrieve. This can be found in the RunError.
342
360
  :return: The error dump.
343
361
  """
344
- return await self._get_binary(f"{self._url}/run/{run_id}/process/import/{action_id}/dumps")
362
+ return await self._http.get_binary(
363
+ f"{self._url}/run/{run_id}/process/import/{action_id}/dumps"
364
+ )
@@ -1,17 +1,19 @@
1
+ import logging
1
2
  from typing import Any
2
3
 
3
- import httpx
4
-
5
- from anaplan_sdk._base import _AsyncBaseClient, construct_payload
4
+ from anaplan_sdk._services import _AsyncHttpService
5
+ from anaplan_sdk._utils import construct_payload
6
6
  from anaplan_sdk.models.flows import Flow, FlowInput, FlowSummary
7
7
 
8
+ logger = logging.getLogger("anaplan_sdk")
9
+
8
10
 
9
- class _AsyncFlowClient(_AsyncBaseClient):
10
- def __init__(self, client: httpx.AsyncClient, retry_count: int) -> None:
11
+ class _AsyncFlowClient:
12
+ def __init__(self, http: _AsyncHttpService) -> None:
13
+ self._http = http
11
14
  self._url = "https://api.cloudworks.anaplan.com/2/0/integrationflows"
12
- super().__init__(retry_count, client)
13
15
 
14
- async def list_flows(self, current_user_only: bool = False) -> list[FlowSummary]:
16
+ async def get_flows(self, current_user_only: bool = False) -> list[FlowSummary]:
15
17
  """
16
18
  List all flows in CloudWorks.
17
19
  :param current_user_only: Filters the flows to only those created by the current user.
@@ -20,9 +22,7 @@ class _AsyncFlowClient(_AsyncBaseClient):
20
22
  params = {"myIntegrations": 1 if current_user_only else 0}
21
23
  return [
22
24
  FlowSummary.model_validate(e)
23
- for e in await self._get_paginated(
24
- self._url, "integrationFlows", page_size=25, params=params
25
- )
25
+ for e in await self._http.get_paginated(self._url, "integrationFlows", params=params)
26
26
  ]
27
27
 
28
28
  async def get_flow(self, flow_id: str) -> Flow:
@@ -32,7 +32,9 @@ class _AsyncFlowClient(_AsyncBaseClient):
32
32
  :param flow_id: The ID of the flow to get.
33
33
  :return: The Flow object.
34
34
  """
35
- return Flow.model_validate((await self._get(f"{self._url}/{flow_id}"))["integrationFlow"])
35
+ return Flow.model_validate(
36
+ (await self._http.get(f"{self._url}/{flow_id}"))["integrationFlow"]
37
+ )
36
38
 
37
39
  async def run_flow(self, flow_id: str, only_steps: list[str] = None) -> str:
38
40
  """
@@ -45,11 +47,13 @@ class _AsyncFlowClient(_AsyncBaseClient):
45
47
  """
46
48
  url = f"{self._url}/{flow_id}/run"
47
49
  res = await (
48
- self._post(url, json={"stepsToRun": only_steps})
50
+ self._http.post(url, json={"stepsToRun": only_steps})
49
51
  if only_steps
50
- else self._post_empty(url)
52
+ else self._http.post_empty(url)
51
53
  )
52
- return res["run"]["id"]
54
+ run_id = res["run"]["id"]
55
+ logger.info(f"Started flow run '{run_id}' for flow '{flow_id}'.")
56
+ return run_id
53
57
 
54
58
  async def create_flow(self, flow: FlowInput | dict[str, Any]) -> str:
55
59
  """
@@ -59,8 +63,10 @@ class _AsyncFlowClient(_AsyncBaseClient):
59
63
  :param flow: The flow to create. This can be a FlowInput object or a dictionary.
60
64
  :return: The ID of the created flow.
61
65
  """
62
- res = await self._post(self._url, json=construct_payload(FlowInput, flow))
63
- return res["integrationFlow"]["integrationFlowId"]
66
+ res = await self._http.post(self._url, json=construct_payload(FlowInput, flow))
67
+ flow_id = res["integrationFlow"]["integrationFlowId"]
68
+ logger.info(f"Created flow '{flow_id}'.")
69
+ return flow_id
64
70
 
65
71
  async def update_flow(self, flow_id: str, flow: FlowInput | dict[str, Any]) -> None:
66
72
  """
@@ -69,7 +75,8 @@ class _AsyncFlowClient(_AsyncBaseClient):
69
75
  :param flow_id: The ID of the flow to update.
70
76
  :param flow: The flow to update. This can be a FlowInput object or a dictionary.
71
77
  """
72
- await self._put(f"{self._url}/{flow_id}", json=construct_payload(FlowInput, flow))
78
+ await self._http.put(f"{self._url}/{flow_id}", json=construct_payload(FlowInput, flow))
79
+ logger.info(f"Updated flow '{flow_id}'.")
73
80
 
74
81
  async def delete_flow(self, flow_id: str) -> None:
75
82
  """
@@ -77,4 +84,5 @@ class _AsyncFlowClient(_AsyncBaseClient):
77
84
  the flow is running or if it has any running steps.
78
85
  :param flow_id: The ID of the flow to delete.
79
86
  """
80
- await self._delete(f"{self._url}/{flow_id}")
87
+ await self._http.delete(f"{self._url}/{flow_id}")
88
+ logger.info(f"Deleted flow '{flow_id}'.")
@@ -0,0 +1,148 @@
1
+ import logging
2
+ from asyncio import gather
3
+ from itertools import chain
4
+ from typing import Any
5
+
6
+ from anaplan_sdk._services import _AsyncHttpService
7
+ from anaplan_sdk._utils import construct_payload
8
+ from anaplan_sdk.models.scim import (
9
+ Operation,
10
+ ReplaceUserInput,
11
+ Resource,
12
+ Schema,
13
+ ServiceProviderConfig,
14
+ User,
15
+ UserInput,
16
+ field,
17
+ )
18
+
19
+ logger = logging.getLogger("anaplan_sdk")
20
+
21
+
22
+ class _AsyncScimClient:
23
+ def __init__(self, http: _AsyncHttpService) -> None:
24
+ self._http = http
25
+ self._url = "https://api.anaplan.com/scim/1/0/v2"
26
+
27
+ async def get_service_provider_config(self) -> ServiceProviderConfig:
28
+ """
29
+ Get the SCIM Service Provider Configuration.
30
+ :return: The ServiceProviderConfig object describing the available SCIM features.
31
+ """
32
+ res = await self._http.get(f"{self._url}/ServiceProviderConfig")
33
+ return ServiceProviderConfig.model_validate(res)
34
+
35
+ async def get_resource_types(self) -> list[Resource]:
36
+ """
37
+ Get the SCIM Resource Types.
38
+ :return: A list of Resource objects describing the SCIM resource types.
39
+ """
40
+ res = await self._http.get(f"{self._url}/ResourceTypes")
41
+ return [Resource.model_validate(e) for e in res.get("Resources", [])]
42
+
43
+ async def get_resource_schemas(self) -> list[Schema]:
44
+ """
45
+ Get the SCIM Resource Schemas.
46
+ :return: A list of Schema objects describing the SCIM resource schemas.
47
+ """
48
+ res = await self._http.get(f"{self._url}/Schemas")
49
+ return [Schema.model_validate(e) for e in res.get("Resources", [])]
50
+
51
+ async def get_users(self, predicate: str | field = None, page_size: int = 100) -> list[User]:
52
+ """
53
+ Get a list of users, optionally filtered by a predicate. Keep in mind that this will only
54
+ return internal users. To get a list of all users in the tenant, use the `get_users()`
55
+ in the `audit` namespace instead.
56
+ :param predicate: A filter predicate to filter the users. This can either be a string,
57
+ in which case it will be passed as-is, or an expression. Anaplan supports filtering
58
+ on the following fields: "id", "externalId", "userName", "name.familyName",
59
+ "name.givenName" and "active". It supports the operators "eq", "ne", "gt", "ge",
60
+ "lt", "le" and "pr". It supports logical operators "and" and "or", "not" is not
61
+ supported. It supports grouping with parentheses.
62
+ :param page_size: The number of users to fetch per page. Values above 100 will error.
63
+ :return: The internal users optionally matching the filter.
64
+ """
65
+ params: dict[str, int | str] = {"startIndex": 1, "count": page_size}
66
+ if predicate is not None:
67
+ _predicate = predicate if isinstance(predicate, str) else str(predicate)
68
+ logger.debug(f"Searching for users with predicate: {_predicate}")
69
+ params["filter"] = _predicate
70
+ res = await self._http.get(f"{self._url}/Users", params=params)
71
+ users = [User.model_validate(e) for e in res.get("Resources", [])]
72
+ if (total := res["totalResults"]) <= page_size:
73
+ return users
74
+ pages = await gather(
75
+ *(
76
+ self._http.get(
77
+ f"{self._url}/Users", params=(params | {"startIndex": i, "count": page_size})
78
+ )
79
+ for i in range(page_size + 1, total + 1, page_size)
80
+ )
81
+ )
82
+ for user in chain(*(p.get("Resources", []) for p in pages)):
83
+ users.append(User.model_validate(user))
84
+ return users
85
+
86
+ async def get_user(self, user_id: str) -> User:
87
+ """
88
+ Get a user by their ID.
89
+ :param user_id: The ID of the user to fetch.
90
+ :return: The User object.
91
+ """
92
+ res = await self._http.get(f"{self._url}/Users/{user_id}")
93
+ return User.model_validate(res)
94
+
95
+ async def add_user(self, user: UserInput | dict[str, Any]) -> User:
96
+ """
97
+ Add a new user to your Anaplan tenant.
98
+ :param user: The user info to add. Can either be a UserInput object or a dict. If you pass
99
+ a dict, it will be validated against the UserInput model before sending. If the info
100
+ you provided is invalid or incomplete, this will raise a pydantic.ValidationError.
101
+ :return: The created User object.
102
+ """
103
+ res = await self._http.post(f"{self._url}/Users", json=construct_payload(UserInput, user))
104
+ user = User.model_validate(res)
105
+ logger.info(f"Added user '{user.user_name}' with ID '{user.id}'.")
106
+ return user
107
+
108
+ async def replace_user(self, user_id: str, user: ReplaceUserInput | dict[str, Any]):
109
+ """
110
+ Replace an existing user with new information. Note that this will replace all fields of the
111
+ :param user_id: ID of the user to replace.
112
+ :param user: The new user info. Can either be a ReplaceUserInput object or a dict. If you
113
+ pass a dict, it will be validated against the ReplaceUserInput model before sending.
114
+ If the info you provided is invalid or incomplete, this will raise a
115
+ pydantic.ValidationError.
116
+ :return: The updated User object.
117
+ """
118
+ res = await self._http.put(
119
+ f"{self._url}/Users/{user_id}", json=construct_payload(ReplaceUserInput, user)
120
+ )
121
+ user = User.model_validate(res)
122
+ logger.info(f"Replaced user with ID '{user_id}' with '{user.user_name}'.")
123
+ return user
124
+
125
+ async def update_user(
126
+ self, user_id: str, operations: list[Operation] | list[dict[str, Any]]
127
+ ) -> User:
128
+ """
129
+ Update an existing user with a list of operations. This allows you to update only specific
130
+ fields of the user without replacing the entire user.
131
+ :param user_id: The ID of the user to update.
132
+ :param operations: A list of operations to perform on the user. Each operation can either be
133
+ an Operation object or a dict. If you pass a dict, it will be validated against
134
+ the Operation model before sending. If the operation is invalid, this will raise a
135
+ pydantic.ValidationError. You can also use the models Replace, Add and Remove which
136
+ are subclasses of Operation and provide a more convenient way to create operations.
137
+ :return: The updated User object.
138
+ """
139
+ res = await self._http.patch(
140
+ f"{self._url}/Users/{user_id}",
141
+ json={
142
+ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
143
+ "Operations": [construct_payload(Operation, e) for e in operations],
144
+ },
145
+ )
146
+ user = User.model_validate(res)
147
+ logger.info(f"Updated user with ID '{user_id}'.")
148
+ return user