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
|
-
_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
|
32
|
-
def __init__(self,
|
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(
|
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
|
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.
|
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.
|
63
|
+
res = await 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
|
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.
|
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.
|
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.
|
98
|
+
await self._http.delete(f"{self._url}/connections/{con_id}")
|
99
|
+
logger.info(f"Deleted connection '{con_id}'.")
|
94
100
|
|
95
|
-
async def
|
96
|
-
self,
|
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
|
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": "
|
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.
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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.
|
185
|
-
"
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
300
|
+
res = await self._http.post(
|
286
301
|
f"{self._url}/notification", json=construct_payload(NotificationInput, config)
|
287
302
|
)
|
288
|
-
|
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.
|
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.
|
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.
|
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.
|
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
|
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
|
10
|
-
def __init__(self,
|
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
|
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.
|
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(
|
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.
|
50
|
+
self._http.post(url, json={"stepsToRun": only_steps})
|
49
51
|
if only_steps
|
50
|
-
else self.
|
52
|
+
else self._http.post_empty(url)
|
51
53
|
)
|
52
|
-
|
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.
|
63
|
-
|
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.
|
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.
|
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
|