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