anaplan-sdk 0.2.11__tar.gz → 0.3.1__tar.gz
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-0.2.11 → anaplan_sdk-0.3.1}/.gitignore +1 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/PKG-INFO +2 -2
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/README.md +1 -1
- anaplan_sdk-0.3.1/anaplan_sdk/_async_clients/_audit.py +42 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_async_clients/_bulk.py +43 -31
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_async_clients/_transactional.py +15 -4
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_base.py +60 -9
- anaplan_sdk-0.3.1/anaplan_sdk/_clients/_audit.py +43 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_clients/_bulk.py +38 -23
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_clients/_transactional.py +19 -6
- anaplan_sdk-0.3.1/anaplan_sdk/models.py +329 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/async_transactional_client.md +2 -2
- anaplan_sdk-0.3.1/docs/css/styles.css +64 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/guides/bulk.md +62 -13
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/guides/logging.md +1 -1
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/guides/multiple_models.md +2 -2
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/guides/transactional.md +17 -17
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/index.md +1 -1
- anaplan_sdk-0.3.1/docs/js/assets/hljs.js +1242 -0
- anaplan_sdk-0.3.1/docs/js/assets/python.js +334 -0
- anaplan_sdk-0.3.1/docs/js/highlight.js +12 -0
- anaplan_sdk-0.3.1/docs/quickstart.md +162 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/mkdocs.yml +9 -2
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/pyproject.toml +2 -1
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/async/conftest.py +2 -2
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/async/test_async_client.py +36 -10
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/sync/conftest.py +2 -2
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/sync/test_client.py +36 -10
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/uv.lock +188 -136
- anaplan_sdk-0.2.11/anaplan_sdk/_async_clients/_audit.py +0 -87
- anaplan_sdk-0.2.11/anaplan_sdk/_clients/_audit.py +0 -86
- anaplan_sdk-0.2.11/anaplan_sdk/models.py +0 -318
- anaplan_sdk-0.2.11/docs/css/styles.css +0 -13
- anaplan_sdk-0.2.11/docs/quickstart.md +0 -102
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/.github/dependabot.yml +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/.github/workflows/docs.yml +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/.github/workflows/lint.yml +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/.github/workflows/tests.yml +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/.pre-commit-config.yaml +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/LICENSE +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/__init__.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_async_clients/__init__.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_async_clients/_alm.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_auth.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_clients/__init__.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/_clients/_alm.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/anaplan_sdk/exceptions.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/anaplan_explained.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/alm_client.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/async_alm_client.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/async_audit_client.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/async_client.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/audit_client.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/client.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/exceptions.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/models.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/api/transactional_client.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/guides/alm.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/guides/audit.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/guides/bulk_vs_transactional.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/img/anaplan-overview.webp +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/img/anaplan-sdk.webp +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/docs/installation.md +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/async/test_async_alm_client.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/async/test_async_audit_client.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/async/test_async_transactional_client.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/conftest.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/sync/test_alm_client.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/sync/test_audit_client.py +0 -0
- {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1}/tests/sync/test_transactional_client.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: anaplan-sdk
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.1
|
4
4
|
Summary: Provides pythonic access to the Anaplan API
|
5
5
|
Project-URL: Homepage, https://vinzenzklass.github.io/anaplan-sdk/
|
6
6
|
Project-URL: Repository, https://github.com/VinzenzKlass/anaplan-sdk
|
@@ -38,7 +38,7 @@ implementation details like authentication, error handling, chunking, compressio
|
|
38
38
|
This Projects supports
|
39
39
|
the [Bulk APIs](https://help.anaplan.com/use-the-bulk-apis-93218e5e-00e5-406e-8361-09ab861889a7),
|
40
40
|
the [Transactional APIs](https://help.anaplan.com/use-the-transactional-apis-cc1c1e91-39fc-4272-a4b5-16bc91e9c313) and
|
41
|
-
the [ALM
|
41
|
+
the [ALM APIs](https://help.anaplan.com/application-lifecycle-management-api-2565cfa6-e0c2-4e24-884e-d0df957184d6),
|
42
42
|
the [Audit APIs](https://auditservice.docs.apiary.io/#),
|
43
43
|
providing both synchronous and asynchronous Clients.
|
44
44
|
|
@@ -21,7 +21,7 @@ implementation details like authentication, error handling, chunking, compressio
|
|
21
21
|
This Projects supports
|
22
22
|
the [Bulk APIs](https://help.anaplan.com/use-the-bulk-apis-93218e5e-00e5-406e-8361-09ab861889a7),
|
23
23
|
the [Transactional APIs](https://help.anaplan.com/use-the-transactional-apis-cc1c1e91-39fc-4272-a4b5-16bc91e9c313) and
|
24
|
-
the [ALM
|
24
|
+
the [ALM APIs](https://help.anaplan.com/application-lifecycle-management-api-2565cfa6-e0c2-4e24-884e-d0df957184d6),
|
25
25
|
the [Audit APIs](https://auditservice.docs.apiary.io/#),
|
26
26
|
providing both synchronous and asynchronous Clients.
|
27
27
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
3
|
+
import httpx
|
4
|
+
|
5
|
+
from anaplan_sdk._base import _AsyncBaseClient
|
6
|
+
from anaplan_sdk.models import User
|
7
|
+
|
8
|
+
Event = Literal["all", "byok", "user_activity"]
|
9
|
+
|
10
|
+
|
11
|
+
class _AsyncAuditClient(_AsyncBaseClient):
|
12
|
+
def __init__(self, client: httpx.AsyncClient, retry_count: int) -> None:
|
13
|
+
self._client = client
|
14
|
+
self._limit = 10_000
|
15
|
+
self._url = "https://audit.anaplan.com/audit/api/1/events"
|
16
|
+
super().__init__(retry_count, client)
|
17
|
+
|
18
|
+
async def list_users(self) -> list[User]:
|
19
|
+
"""
|
20
|
+
Lists all the Users in the authenticated users default tenant.
|
21
|
+
:return: The List of Users.
|
22
|
+
"""
|
23
|
+
return [
|
24
|
+
User.model_validate(e)
|
25
|
+
for e in await self._get_paginated("https://api.anaplan.com/2/0/users", "users")
|
26
|
+
]
|
27
|
+
|
28
|
+
async def get_events(self, days_into_past: int = 30, event_type: Event = "all") -> list:
|
29
|
+
"""
|
30
|
+
Get audit events from Anaplan Audit API.
|
31
|
+
:param days_into_past: The nuber of days into the past to get events for. The API provides
|
32
|
+
data for up to 30 days.
|
33
|
+
:param event_type: The type of events to get.
|
34
|
+
:return: A list of audit events.
|
35
|
+
"""
|
36
|
+
return list(
|
37
|
+
await self._get_paginated(
|
38
|
+
self._url,
|
39
|
+
"response",
|
40
|
+
params={"type": event_type, "intervalInHours": days_into_past * 24},
|
41
|
+
)
|
42
|
+
)
|
@@ -3,8 +3,7 @@ Asynchronous Client.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import logging
|
6
|
-
import
|
7
|
-
from asyncio import gather
|
6
|
+
from asyncio import gather, sleep
|
8
7
|
from copy import copy
|
9
8
|
from typing import AsyncIterator, Iterator
|
10
9
|
|
@@ -14,7 +13,17 @@ from typing_extensions import Self
|
|
14
13
|
from anaplan_sdk._auth import AnaplanBasicAuth, AnaplanCertAuth, get_certificate, get_private_key
|
15
14
|
from anaplan_sdk._base import _AsyncBaseClient, action_url
|
16
15
|
from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
|
17
|
-
from anaplan_sdk.models import
|
16
|
+
from anaplan_sdk.models import (
|
17
|
+
Action,
|
18
|
+
Export,
|
19
|
+
File,
|
20
|
+
Import,
|
21
|
+
Model,
|
22
|
+
Process,
|
23
|
+
TaskStatus,
|
24
|
+
TaskSummary,
|
25
|
+
Workspace,
|
26
|
+
)
|
18
27
|
|
19
28
|
from ._alm import _AsyncAlmClient
|
20
29
|
from ._audit import _AsyncAuditClient
|
@@ -182,9 +191,11 @@ class AsyncClient(_AsyncBaseClient):
|
|
182
191
|
"""
|
183
192
|
return [
|
184
193
|
Workspace.model_validate(e)
|
185
|
-
for e in (
|
186
|
-
|
187
|
-
|
194
|
+
for e in await self._get_paginated(
|
195
|
+
"https://api.anaplan.com/2/0/workspaces",
|
196
|
+
"workspaces",
|
197
|
+
params={"tenantDetails": "true"},
|
198
|
+
)
|
188
199
|
]
|
189
200
|
|
190
201
|
async def list_models(self) -> list[Model]:
|
@@ -194,8 +205,8 @@ class AsyncClient(_AsyncBaseClient):
|
|
194
205
|
"""
|
195
206
|
return [
|
196
207
|
Model.model_validate(e)
|
197
|
-
for e in
|
198
|
-
"models",
|
208
|
+
for e in await self._get_paginated(
|
209
|
+
"https://api.anaplan.com/2/0/models", "models", params={"modelDetails": "true"}
|
199
210
|
)
|
200
211
|
]
|
201
212
|
|
@@ -205,7 +216,7 @@ class AsyncClient(_AsyncBaseClient):
|
|
205
216
|
:return: The List of Files.
|
206
217
|
"""
|
207
218
|
return [
|
208
|
-
File.model_validate(e) for e in
|
219
|
+
File.model_validate(e) for e in await self._get_paginated(f"{self._url}/files", "files")
|
209
220
|
]
|
210
221
|
|
211
222
|
async def list_actions(self) -> list[Action]:
|
@@ -217,7 +228,7 @@ class AsyncClient(_AsyncBaseClient):
|
|
217
228
|
"""
|
218
229
|
return [
|
219
230
|
Action.model_validate(e)
|
220
|
-
for e in
|
231
|
+
for e in await self._get_paginated(f"{self._url}/actions", "actions")
|
221
232
|
]
|
222
233
|
|
223
234
|
async def list_processes(self) -> list[Process]:
|
@@ -227,7 +238,7 @@ class AsyncClient(_AsyncBaseClient):
|
|
227
238
|
"""
|
228
239
|
return [
|
229
240
|
Process.model_validate(e)
|
230
|
-
for e in
|
241
|
+
for e in await self._get_paginated(f"{self._url}/processes", "processes")
|
231
242
|
]
|
232
243
|
|
233
244
|
async def list_imports(self) -> list[Import]:
|
@@ -237,7 +248,7 @@ class AsyncClient(_AsyncBaseClient):
|
|
237
248
|
"""
|
238
249
|
return [
|
239
250
|
Import.model_validate(e)
|
240
|
-
for e in
|
251
|
+
for e in await self._get_paginated(f"{self._url}/imports", "imports")
|
241
252
|
]
|
242
253
|
|
243
254
|
async def list_exports(self) -> list[Export]:
|
@@ -247,10 +258,10 @@ class AsyncClient(_AsyncBaseClient):
|
|
247
258
|
"""
|
248
259
|
return [
|
249
260
|
Export.model_validate(e)
|
250
|
-
for e in
|
261
|
+
for e in await self._get_paginated(f"{self._url}/exports", "exports")
|
251
262
|
]
|
252
263
|
|
253
|
-
async def run_action(self, action_id: int) ->
|
264
|
+
async def run_action(self, action_id: int) -> TaskStatus:
|
254
265
|
"""
|
255
266
|
Runs the specified Anaplan Action and validates the spawned task. If the Action fails or
|
256
267
|
completes with errors, will raise an :py:class:`AnaplanActionError`. Failed Tasks are
|
@@ -268,16 +279,15 @@ class AsyncClient(_AsyncBaseClient):
|
|
268
279
|
task_id = await self.invoke_action(action_id)
|
269
280
|
task_status = await self.get_task_status(action_id, task_id)
|
270
281
|
|
271
|
-
while
|
272
|
-
|
282
|
+
while task_status.task_state != "COMPLETE":
|
283
|
+
await sleep(self.status_poll_delay)
|
273
284
|
task_status = await self.get_task_status(action_id, task_id)
|
274
285
|
|
275
|
-
if task_status.
|
276
|
-
"successful"
|
277
|
-
):
|
286
|
+
if task_status.task_state == "COMPLETE" and not task_status.result.successful:
|
278
287
|
raise AnaplanActionError(f"Task '{task_id}' completed with errors.")
|
279
288
|
|
280
289
|
logger.info(f"Task '{task_id}' completed successfully.")
|
290
|
+
return task_status
|
281
291
|
|
282
292
|
async def get_file(self, file_id: int) -> bytes:
|
283
293
|
"""
|
@@ -390,29 +400,31 @@ class AsyncClient(_AsyncBaseClient):
|
|
390
400
|
await self.run_action(action_id)
|
391
401
|
return await self.get_file(action_id)
|
392
402
|
|
393
|
-
async def list_task_status(self, action_id: int) -> list:
|
403
|
+
async def list_task_status(self, action_id: int) -> list[TaskSummary]:
|
394
404
|
"""
|
395
405
|
Retrieves the status of all tasks spawned by the specified action.
|
396
406
|
:param action_id: The identifier of the action that was invoked.
|
397
407
|
:return: The list of tasks spawned by the action.
|
398
408
|
"""
|
399
|
-
return
|
400
|
-
|
401
|
-
|
409
|
+
return [
|
410
|
+
TaskSummary.model_validate(e)
|
411
|
+
for e in await self._get_paginated(
|
412
|
+
f"{self._url}/{action_url(action_id)}/{action_id}/tasks", "tasks"
|
413
|
+
)
|
414
|
+
]
|
402
415
|
|
403
|
-
async def get_task_status(
|
404
|
-
self, action_id: int, task_id: str
|
405
|
-
) -> dict[str, float | int | str | list | dict | bool]:
|
416
|
+
async def get_task_status(self, action_id: int, task_id: str) -> TaskStatus:
|
406
417
|
"""
|
407
418
|
Retrieves the status of the specified task.
|
408
419
|
:param action_id: The identifier of the action that was invoked.
|
409
420
|
:param task_id: The identifier of the spawned task.
|
410
|
-
:return: The status of the task
|
411
|
-
see: https://anaplan.docs.apiary.io.
|
421
|
+
:return: The status of the task.
|
412
422
|
"""
|
413
|
-
return (
|
414
|
-
|
415
|
-
|
423
|
+
return TaskStatus.model_validate(
|
424
|
+
(
|
425
|
+
await self._get(f"{self._url}/{action_url(action_id)}/{action_id}/tasks/{task_id}")
|
426
|
+
).get("task")
|
427
|
+
)
|
416
428
|
|
417
429
|
async def invoke_action(self, action_id: int) -> str:
|
418
430
|
"""
|
@@ -32,7 +32,7 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
|
|
32
32
|
"""
|
33
33
|
return [
|
34
34
|
Module.model_validate(e)
|
35
|
-
for e in
|
35
|
+
for e in await self._get_paginated(f"{self._url}/modules", "modules")
|
36
36
|
]
|
37
37
|
|
38
38
|
async def get_model_status(self) -> ModelStatus:
|
@@ -55,7 +55,7 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
|
|
55
55
|
if only_module_id
|
56
56
|
else f"{self._url}/lineItems?includeAll=true"
|
57
57
|
)
|
58
|
-
return [LineItem.model_validate(e) for e in (await self._get(url)).get("items")]
|
58
|
+
return [LineItem.model_validate(e) for e in (await self._get(url)).get("items", [])]
|
59
59
|
|
60
60
|
async def list_lists(self) -> list[List]:
|
61
61
|
"""
|
@@ -63,7 +63,7 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
|
|
63
63
|
:return: All Lists on this model.
|
64
64
|
"""
|
65
65
|
return [
|
66
|
-
List.model_validate(e) for e in
|
66
|
+
List.model_validate(e) for e in await self._get_paginated(f"{self._url}/lists", "lists")
|
67
67
|
]
|
68
68
|
|
69
69
|
async def get_list_metadata(self, list_id: int) -> ListMetadata:
|
@@ -156,7 +156,7 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
|
|
156
156
|
"""
|
157
157
|
await self._post_empty(f"{self._url}/lists/{list_id}/resetIndex")
|
158
158
|
|
159
|
-
async def
|
159
|
+
async def update_module_data(
|
160
160
|
self, module_id: int, data: list[dict[str, Any]]
|
161
161
|
) -> int | dict[str, Any]:
|
162
162
|
"""
|
@@ -171,6 +171,17 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
|
|
171
171
|
res = await self._post(f"{self._url}/modules/{module_id}/data", json=data)
|
172
172
|
return res if "failures" in res else res["numberOfCellsChanged"]
|
173
173
|
|
174
|
+
async def write_to_module(
|
175
|
+
self, module_id: int, data: list[dict[str, Any]]
|
176
|
+
) -> int | dict[str, Any]:
|
177
|
+
warnings.warn(
|
178
|
+
"`write_to_module()` is deprecated and will be removed in a future version. "
|
179
|
+
"Use `update_module_data()` instead.",
|
180
|
+
DeprecationWarning,
|
181
|
+
stacklevel=1,
|
182
|
+
)
|
183
|
+
return await self.update_module_data(module_id, data)
|
184
|
+
|
174
185
|
async def add_items_to_list(
|
175
186
|
self, list_id: int, items: list[dict[str, str | int | dict]]
|
176
187
|
) -> InsertionResult:
|
@@ -6,8 +6,12 @@ import asyncio
|
|
6
6
|
import logging
|
7
7
|
import random
|
8
8
|
import time
|
9
|
+
from asyncio import gather
|
10
|
+
from concurrent.futures import ThreadPoolExecutor
|
9
11
|
from gzip import compress
|
10
|
-
from
|
12
|
+
from itertools import chain
|
13
|
+
from math import ceil
|
14
|
+
from typing import Any, Callable, Coroutine, Iterator, Literal
|
11
15
|
|
12
16
|
import httpx
|
13
17
|
from httpx import HTTPError, Response
|
@@ -26,15 +30,13 @@ class _BaseClient:
|
|
26
30
|
self._retry_count = retry_count
|
27
31
|
self._client = client
|
28
32
|
|
29
|
-
def _get(self, url: str, **kwargs) -> dict[str,
|
33
|
+
def _get(self, url: str, **kwargs) -> dict[str, Any]:
|
30
34
|
return self._run_with_retry(self._client.get, url, **kwargs).json()
|
31
35
|
|
32
36
|
def _get_binary(self, url: str) -> bytes:
|
33
37
|
return self._run_with_retry(self._client.get, url).content
|
34
38
|
|
35
|
-
def _post(
|
36
|
-
self, url: str, json: dict | list
|
37
|
-
) -> dict[str, float | int | str | list | dict | bool]:
|
39
|
+
def _post(self, url: str, json: dict | list) -> dict[str, Any]:
|
38
40
|
return self._run_with_retry(
|
39
41
|
self._client.post, url, headers={"Content-Type": "application/json"}, json=json
|
40
42
|
).json()
|
@@ -50,6 +52,30 @@ class _BaseClient:
|
|
50
52
|
content=compress(content),
|
51
53
|
)
|
52
54
|
|
55
|
+
def __get_page(self, url: str, limit: int, offset: int, result_key: str, **kwargs) -> list:
|
56
|
+
kwargs["params"] = kwargs.get("params", {}) | {"limit": limit, "offset": offset}
|
57
|
+
return self._get(url, **kwargs).get(result_key, [])
|
58
|
+
|
59
|
+
def __get_first_page(self, url: str, limit: int, result_key: str, **kwargs) -> tuple[list, int]:
|
60
|
+
kwargs["params"] = kwargs.get("params", {}) | {"limit": limit}
|
61
|
+
res = self._get(url, **kwargs)
|
62
|
+
return res.get(result_key, []), res["meta"]["paging"]["totalSize"]
|
63
|
+
|
64
|
+
def _get_paginated(
|
65
|
+
self, url: str, result_key: str, page_size: int = 5_000, **kwargs
|
66
|
+
) -> Iterator[dict[str, Any]]:
|
67
|
+
first_page, total_items = self.__get_first_page(url, page_size, result_key, **kwargs)
|
68
|
+
if total_items <= page_size:
|
69
|
+
return iter(first_page)
|
70
|
+
|
71
|
+
with ThreadPoolExecutor() as executor:
|
72
|
+
pages = executor.map(
|
73
|
+
lambda n: self.__get_page(url, page_size, n * page_size, result_key, **kwargs),
|
74
|
+
range(1, ceil(total_items / page_size)),
|
75
|
+
)
|
76
|
+
|
77
|
+
return chain(first_page, *pages)
|
78
|
+
|
53
79
|
def _run_with_retry(self, func: Callable[..., Response], *args, **kwargs) -> Response:
|
54
80
|
for i in range(max(self._retry_count, 1)):
|
55
81
|
try:
|
@@ -77,15 +103,13 @@ class _AsyncBaseClient:
|
|
77
103
|
self._retry_count = retry_count
|
78
104
|
self._client = client
|
79
105
|
|
80
|
-
async def _get(self, url: str, **kwargs) -> dict[str,
|
106
|
+
async def _get(self, url: str, **kwargs) -> dict[str, Any]:
|
81
107
|
return (await self._run_with_retry(self._client.get, url, **kwargs)).json()
|
82
108
|
|
83
109
|
async def _get_binary(self, url: str) -> bytes:
|
84
110
|
return (await self._run_with_retry(self._client.get, url)).content
|
85
111
|
|
86
|
-
async def _post(
|
87
|
-
self, url: str, json: dict | list
|
88
|
-
) -> dict[str, float | int | str | list | dict | bool]:
|
112
|
+
async def _post(self, url: str, json: dict | list) -> dict[str, Any]:
|
89
113
|
return (
|
90
114
|
await self._run_with_retry(
|
91
115
|
self._client.post, url, headers={"Content-Type": "application/json"}, json=json
|
@@ -103,6 +127,33 @@ class _AsyncBaseClient:
|
|
103
127
|
content=compress(content),
|
104
128
|
)
|
105
129
|
|
130
|
+
async def __get_page(
|
131
|
+
self, url: str, limit: int, offset: int, result_key: str, **kwargs
|
132
|
+
) -> list:
|
133
|
+
kwargs["params"] = kwargs.get("params", {}) | {"limit": limit, "offset": offset}
|
134
|
+
return (await self._get(url, **kwargs)).get(result_key, [])
|
135
|
+
|
136
|
+
async def __get_first_page(
|
137
|
+
self, url: str, limit: int, result_key: str, **kwargs
|
138
|
+
) -> tuple[list, int]:
|
139
|
+
kwargs["params"] = kwargs.get("params", {}) | {"limit": limit}
|
140
|
+
res = await self._get(url, **kwargs)
|
141
|
+
return res.get(result_key, []), res["meta"]["paging"]["totalSize"]
|
142
|
+
|
143
|
+
async def _get_paginated(
|
144
|
+
self, url: str, result_key: str, page_size: int = 5_000, **kwargs
|
145
|
+
) -> Iterator[dict[str, Any]]:
|
146
|
+
first_page, total_items = await self.__get_first_page(url, page_size, result_key, **kwargs)
|
147
|
+
if total_items <= page_size:
|
148
|
+
return iter(first_page)
|
149
|
+
pages = await gather(
|
150
|
+
*(
|
151
|
+
self.__get_page(url, page_size, n * page_size, result_key, **kwargs)
|
152
|
+
for n in range(1, ceil(total_items / page_size))
|
153
|
+
)
|
154
|
+
)
|
155
|
+
return chain(first_page, *pages)
|
156
|
+
|
106
157
|
async def _run_with_retry(
|
107
158
|
self, func: Callable[..., Coroutine[Any, Any, Response]], *args, **kwargs
|
108
159
|
) -> Response:
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
3
|
+
import httpx
|
4
|
+
|
5
|
+
from anaplan_sdk._base import _BaseClient
|
6
|
+
from anaplan_sdk.models import User
|
7
|
+
|
8
|
+
Event = Literal["all", "byok", "user_activity"]
|
9
|
+
|
10
|
+
|
11
|
+
class _AuditClient(_BaseClient):
|
12
|
+
def __init__(self, client: httpx.Client, retry_count: int, thread_count: int) -> None:
|
13
|
+
self._client = client
|
14
|
+
self._limit = 10_000
|
15
|
+
self._thread_count = thread_count
|
16
|
+
self._url = "https://audit.anaplan.com/audit/api/1/events"
|
17
|
+
super().__init__(retry_count, client)
|
18
|
+
|
19
|
+
def list_users(self) -> list[User]:
|
20
|
+
"""
|
21
|
+
Lists all the Users in the authenticated users default tenant.
|
22
|
+
:return: The List of Users.
|
23
|
+
"""
|
24
|
+
return [
|
25
|
+
User.model_validate(e)
|
26
|
+
for e in self._get_paginated("https://api.anaplan.com/2/0/users", "users")
|
27
|
+
]
|
28
|
+
|
29
|
+
def get_events(self, days_into_past: int = 30, event_type: Event = "all") -> list:
|
30
|
+
"""
|
31
|
+
Get audit events from Anaplan Audit API.
|
32
|
+
:param days_into_past: The nuber of days into the past to get events for. The API provides
|
33
|
+
data for up to 30 days.
|
34
|
+
:param event_type: The type of events to get.
|
35
|
+
:return: A list of audit events.
|
36
|
+
"""
|
37
|
+
return list(
|
38
|
+
self._get_paginated(
|
39
|
+
self._url,
|
40
|
+
"response",
|
41
|
+
params={"type": event_type, "intervalInHours": days_into_past * 24},
|
42
|
+
)
|
43
|
+
)
|
@@ -15,7 +15,17 @@ from typing_extensions import Self
|
|
15
15
|
from anaplan_sdk._auth import AnaplanBasicAuth, AnaplanCertAuth, get_certificate, get_private_key
|
16
16
|
from anaplan_sdk._base import _BaseClient, action_url
|
17
17
|
from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
|
18
|
-
from anaplan_sdk.models import
|
18
|
+
from anaplan_sdk.models import (
|
19
|
+
Action,
|
20
|
+
Export,
|
21
|
+
File,
|
22
|
+
Import,
|
23
|
+
Model,
|
24
|
+
Process,
|
25
|
+
TaskStatus,
|
26
|
+
TaskSummary,
|
27
|
+
Workspace,
|
28
|
+
)
|
19
29
|
|
20
30
|
from ._alm import _AlmClient
|
21
31
|
from ._audit import _AuditClient
|
@@ -189,8 +199,10 @@ class Client(_BaseClient):
|
|
189
199
|
"""
|
190
200
|
return [
|
191
201
|
Workspace.model_validate(e)
|
192
|
-
for e in self.
|
193
|
-
"workspaces",
|
202
|
+
for e in self._get_paginated(
|
203
|
+
"https://api.anaplan.com/2/0/workspaces",
|
204
|
+
"workspaces",
|
205
|
+
params={"tenantDetails": "true"},
|
194
206
|
)
|
195
207
|
]
|
196
208
|
|
@@ -201,8 +213,8 @@ class Client(_BaseClient):
|
|
201
213
|
"""
|
202
214
|
return [
|
203
215
|
Model.model_validate(e)
|
204
|
-
for e in self.
|
205
|
-
"models",
|
216
|
+
for e in self._get_paginated(
|
217
|
+
"https://api.anaplan.com/2/0/models", "models", params={"modelDetails": "true"}
|
206
218
|
)
|
207
219
|
]
|
208
220
|
|
@@ -211,7 +223,7 @@ class Client(_BaseClient):
|
|
211
223
|
Lists all the Files in the Model.
|
212
224
|
:return: The List of Files.
|
213
225
|
"""
|
214
|
-
return [File.model_validate(e) for e in self.
|
226
|
+
return [File.model_validate(e) for e in self._get_paginated(f"{self._url}/files", "files")]
|
215
227
|
|
216
228
|
def list_actions(self) -> list[Action]:
|
217
229
|
"""
|
@@ -221,7 +233,7 @@ class Client(_BaseClient):
|
|
221
233
|
:return: The List of Actions.
|
222
234
|
"""
|
223
235
|
return [
|
224
|
-
Action.model_validate(e) for e in
|
236
|
+
Action.model_validate(e) for e in self._get_paginated(f"{self._url}/actions", "actions")
|
225
237
|
]
|
226
238
|
|
227
239
|
def list_processes(self) -> list[Process]:
|
@@ -231,7 +243,7 @@ class Client(_BaseClient):
|
|
231
243
|
"""
|
232
244
|
return [
|
233
245
|
Process.model_validate(e)
|
234
|
-
for e in
|
246
|
+
for e in self._get_paginated(f"{self._url}/processes", "processes")
|
235
247
|
]
|
236
248
|
|
237
249
|
def list_imports(self) -> list[Import]:
|
@@ -240,7 +252,7 @@ class Client(_BaseClient):
|
|
240
252
|
:return: The List of Imports.
|
241
253
|
"""
|
242
254
|
return [
|
243
|
-
Import.model_validate(e) for e in
|
255
|
+
Import.model_validate(e) for e in self._get_paginated(f"{self._url}/imports", "imports")
|
244
256
|
]
|
245
257
|
|
246
258
|
def list_exports(self) -> list[Export]:
|
@@ -252,7 +264,7 @@ class Client(_BaseClient):
|
|
252
264
|
Export.model_validate(e) for e in (self._get(f"{self._url}/exports")).get("exports", [])
|
253
265
|
]
|
254
266
|
|
255
|
-
def run_action(self, action_id: int) ->
|
267
|
+
def run_action(self, action_id: int) -> TaskStatus:
|
256
268
|
"""
|
257
269
|
Runs the specified Anaplan Action and validates the spawned task. If the Action fails or
|
258
270
|
completes with errors, will raise an :py:class:`AnaplanActionError`. Failed Tasks are
|
@@ -270,16 +282,15 @@ class Client(_BaseClient):
|
|
270
282
|
task_id = self.invoke_action(action_id)
|
271
283
|
task_status = self.get_task_status(action_id, task_id)
|
272
284
|
|
273
|
-
while
|
285
|
+
while task_status.task_state != "COMPLETE":
|
274
286
|
time.sleep(self.status_poll_delay)
|
275
287
|
task_status = self.get_task_status(action_id, task_id)
|
276
288
|
|
277
|
-
if task_status.
|
278
|
-
"successful"
|
279
|
-
):
|
289
|
+
if task_status.task_state == "COMPLETE" and not task_status.result.successful:
|
280
290
|
raise AnaplanActionError(f"Task '{task_id}' completed with errors.")
|
281
291
|
|
282
292
|
logger.info(f"Task '{task_id}' completed successfully.")
|
293
|
+
return task_status
|
283
294
|
|
284
295
|
def get_file(self, file_id: int) -> bytes:
|
285
296
|
"""
|
@@ -383,26 +394,30 @@ class Client(_BaseClient):
|
|
383
394
|
self.run_action(action_id)
|
384
395
|
return self.get_file(action_id)
|
385
396
|
|
386
|
-
def list_task_status(self, action_id: int) -> list:
|
397
|
+
def list_task_status(self, action_id: int) -> list[TaskSummary]:
|
387
398
|
"""
|
388
399
|
Retrieves the status of all tasks spawned by the specified action.
|
389
400
|
:param action_id: The identifier of the action that was invoked.
|
390
401
|
:return: The list of tasks spawned by the action.
|
391
402
|
"""
|
392
|
-
return
|
403
|
+
return [
|
404
|
+
TaskSummary.model_validate(e)
|
405
|
+
for e in self._get_paginated(
|
406
|
+
f"{self._url}/{action_url(action_id)}/{action_id}/tasks", "tasks"
|
407
|
+
)
|
408
|
+
]
|
393
409
|
|
394
|
-
def get_task_status(
|
395
|
-
self, action_id: int, task_id: str
|
396
|
-
) -> dict[str, float | int | str | list | dict | bool]:
|
410
|
+
def get_task_status(self, action_id: int, task_id: str) -> TaskStatus:
|
397
411
|
"""
|
398
412
|
Retrieves the status of the specified task.
|
399
413
|
:param action_id: The identifier of the action that was invoked.
|
400
414
|
:param task_id: The identifier of the spawned task.
|
401
|
-
:return: The status of the task
|
402
|
-
see: https://anaplan.docs.apiary.io.
|
415
|
+
:return: The status of the task.
|
403
416
|
"""
|
404
|
-
return
|
405
|
-
"
|
417
|
+
return TaskStatus.model_validate(
|
418
|
+
self._get(f"{self._url}/{action_url(action_id)}/{action_id}/tasks/{task_id}").get(
|
419
|
+
"task"
|
420
|
+
)
|
406
421
|
)
|
407
422
|
|
408
423
|
def invoke_action(self, action_id: int) -> str:
|
@@ -28,7 +28,9 @@ class _TransactionalClient(_BaseClient):
|
|
28
28
|
Lists all the Modules in the Model.
|
29
29
|
:return: The List of Modules.
|
30
30
|
"""
|
31
|
-
return [
|
31
|
+
return [
|
32
|
+
Module.model_validate(e) for e in self._get_paginated(f"{self._url}/modules", "modules")
|
33
|
+
]
|
32
34
|
|
33
35
|
def get_model_status(self) -> ModelStatus:
|
34
36
|
"""
|
@@ -48,14 +50,14 @@ class _TransactionalClient(_BaseClient):
|
|
48
50
|
if only_module_id
|
49
51
|
else f"{self._url}/lineItems?includeAll=true"
|
50
52
|
)
|
51
|
-
return [LineItem.model_validate(e) for e in self._get(url).get("items")]
|
53
|
+
return [LineItem.model_validate(e) for e in self._get(url).get("items", [])]
|
52
54
|
|
53
55
|
def list_lists(self) -> list[List]:
|
54
56
|
"""
|
55
57
|
Lists all the Lists in the Model.
|
56
|
-
:return: All Lists on this
|
58
|
+
:return: All Lists on this model.
|
57
59
|
"""
|
58
|
-
return [List.model_validate(e) for e in self.
|
60
|
+
return [List.model_validate(e) for e in self._get_paginated(f"{self._url}/lists", "lists")]
|
59
61
|
|
60
62
|
def get_list_metadata(self, list_id: int) -> ListMetadata:
|
61
63
|
"""
|
@@ -76,7 +78,7 @@ class _TransactionalClient(_BaseClient):
|
|
76
78
|
return [
|
77
79
|
ListItem.model_validate(e)
|
78
80
|
for e in self._get(f"{self._url}/lists/{list_id}/items?includeAll=true").get(
|
79
|
-
"listItems"
|
81
|
+
"listItems", []
|
80
82
|
)
|
81
83
|
]
|
82
84
|
|
@@ -149,7 +151,9 @@ class _TransactionalClient(_BaseClient):
|
|
149
151
|
"""
|
150
152
|
self._post_empty(f"{self._url}/lists/{list_id}/resetIndex")
|
151
153
|
|
152
|
-
def
|
154
|
+
def update_module_data(
|
155
|
+
self, module_id: int, data: list[dict[str, Any]]
|
156
|
+
) -> int | dict[str, Any]:
|
153
157
|
"""
|
154
158
|
Write the passed items to the specified module. If successful, the number of cells changed
|
155
159
|
is returned, if only partially successful or unsuccessful, the response with the according
|
@@ -162,6 +166,15 @@ class _TransactionalClient(_BaseClient):
|
|
162
166
|
res = self._post(f"{self._url}/modules/{module_id}/data", json=data)
|
163
167
|
return res if "failures" in res else res["numberOfCellsChanged"]
|
164
168
|
|
169
|
+
def write_to_module(self, module_id: int, data: list[dict[str, Any]]) -> int | dict[str, Any]:
|
170
|
+
warnings.warn(
|
171
|
+
"`write_to_module()` is deprecated and will be removed in a future version. "
|
172
|
+
"Use `update_module_data()` instead.",
|
173
|
+
DeprecationWarning,
|
174
|
+
stacklevel=1,
|
175
|
+
)
|
176
|
+
return self.update_module_data(module_id, data)
|
177
|
+
|
165
178
|
def add_items_to_list(
|
166
179
|
self, list_id: int, items: list[dict[str, str | int | dict]]
|
167
180
|
) -> InsertionResult:
|