anaplan-sdk 0.4.5__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.
- anaplan_sdk/__init__.py +2 -0
- anaplan_sdk/_async_clients/__init__.py +4 -0
- anaplan_sdk/_async_clients/_alm.py +257 -44
- anaplan_sdk/_async_clients/_audit.py +31 -21
- anaplan_sdk/_async_clients/_bulk.py +270 -173
- anaplan_sdk/_async_clients/_cloud_works.py +61 -41
- anaplan_sdk/_async_clients/_cw_flow.py +26 -18
- anaplan_sdk/_async_clients/_scim.py +148 -0
- anaplan_sdk/_async_clients/_transactional.py +265 -56
- anaplan_sdk/_clients/__init__.py +12 -1
- anaplan_sdk/_clients/_alm.py +255 -45
- anaplan_sdk/_clients/_audit.py +32 -22
- anaplan_sdk/_clients/_bulk.py +265 -157
- anaplan_sdk/_clients/_cloud_works.py +59 -40
- anaplan_sdk/_clients/_cw_flow.py +24 -16
- anaplan_sdk/_clients/_scim.py +145 -0
- anaplan_sdk/_clients/_transactional.py +260 -50
- anaplan_sdk/_services.py +277 -0
- anaplan_sdk/_utils.py +188 -0
- anaplan_sdk/models/__init__.py +49 -2
- anaplan_sdk/models/_alm.py +64 -6
- anaplan_sdk/models/_bulk.py +22 -13
- anaplan_sdk/models/_transactional.py +221 -4
- anaplan_sdk/models/cloud_works.py +6 -2
- anaplan_sdk/models/scim.py +282 -0
- {anaplan_sdk-0.4.5.dist-info → anaplan_sdk-0.5.0.dist-info}/METADATA +2 -2
- anaplan_sdk-0.5.0.dist-info/RECORD +34 -0
- anaplan_sdk/_base.py +0 -297
- anaplan_sdk-0.4.5.dist-info/RECORD +0 -30
- {anaplan_sdk-0.4.5.dist-info → anaplan_sdk-0.5.0.dist-info}/WHEEL +0 -0
- {anaplan_sdk-0.4.5.dist-info → anaplan_sdk-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,8 @@
|
|
1
|
+
import logging
|
1
2
|
from typing import Any, Literal
|
2
3
|
|
3
|
-
import
|
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
|
32
|
-
def __init__(self,
|
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(
|
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
|
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.
|
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.
|
63
|
+
res = self._http.post(
|
63
64
|
f"{self._url}/connections", json=construct_payload(ConnectionInput, con_info)
|
64
65
|
)
|
65
|
-
|
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.
|
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.
|
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.
|
94
|
+
self._http.delete(f"{self._url}/connections/{con_id}")
|
95
|
+
logger.info(f"Deleted connection '{con_id}'.")
|
92
96
|
|
93
|
-
def
|
94
|
-
self,
|
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
|
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": "
|
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.
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
299
|
+
res = self._http.post(
|
284
300
|
f"{self._url}/notification", json=construct_payload(NotificationInput, config)
|
285
301
|
)
|
286
|
-
|
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
|
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.
|
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.
|
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.
|
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.
|
361
|
+
return self._http.get_binary(f"{self._url}/run/{run_id}/process/import/{action_id}/dumps")
|
anaplan_sdk/_clients/_cw_flow.py
CHANGED
@@ -1,17 +1,19 @@
|
|
1
|
+
import logging
|
1
2
|
from typing import Any
|
2
3
|
|
3
|
-
import
|
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
|
10
|
-
def __init__(self,
|
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
|
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.
|
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.
|
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.
|
48
|
+
self._http.post(url, json={"stepsToRun": only_steps})
|
47
49
|
if only_steps
|
48
|
-
else self.
|
50
|
+
else self._http.post_empty(url)
|
49
51
|
)
|
50
|
-
|
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.
|
61
|
-
|
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.
|
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.
|
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
|