anaplan-sdk 0.2.11__tar.gz → 0.3.1b1__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.
Files changed (70) hide show
  1. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/.gitignore +1 -0
  2. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/PKG-INFO +2 -2
  3. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/README.md +1 -1
  4. anaplan_sdk-0.3.1b1/anaplan_sdk/_async_clients/_audit.py +42 -0
  5. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_async_clients/_bulk.py +43 -31
  6. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_async_clients/_transactional.py +15 -4
  7. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_base.py +60 -9
  8. anaplan_sdk-0.3.1b1/anaplan_sdk/_clients/_audit.py +43 -0
  9. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_clients/_bulk.py +38 -23
  10. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_clients/_transactional.py +19 -6
  11. anaplan_sdk-0.3.1b1/anaplan_sdk/models.py +329 -0
  12. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/async_transactional_client.md +2 -2
  13. anaplan_sdk-0.3.1b1/docs/css/styles.css +64 -0
  14. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/guides/bulk.md +62 -13
  15. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/guides/logging.md +1 -1
  16. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/guides/multiple_models.md +2 -2
  17. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/guides/transactional.md +17 -17
  18. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/index.md +1 -1
  19. anaplan_sdk-0.3.1b1/docs/js/assets/hljs.js +1242 -0
  20. anaplan_sdk-0.3.1b1/docs/js/assets/python.js +334 -0
  21. anaplan_sdk-0.3.1b1/docs/js/highlight.js +12 -0
  22. anaplan_sdk-0.3.1b1/docs/quickstart.md +162 -0
  23. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/mkdocs.yml +9 -2
  24. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/pyproject.toml +88 -87
  25. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/async/conftest.py +2 -2
  26. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/async/test_async_client.py +36 -10
  27. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/sync/conftest.py +2 -2
  28. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/sync/test_client.py +36 -10
  29. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/uv.lock +1152 -1100
  30. anaplan_sdk-0.2.11/anaplan_sdk/_async_clients/_audit.py +0 -87
  31. anaplan_sdk-0.2.11/anaplan_sdk/_clients/_audit.py +0 -86
  32. anaplan_sdk-0.2.11/anaplan_sdk/models.py +0 -318
  33. anaplan_sdk-0.2.11/docs/css/styles.css +0 -13
  34. anaplan_sdk-0.2.11/docs/quickstart.md +0 -102
  35. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/.github/dependabot.yml +0 -0
  36. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/.github/workflows/docs.yml +0 -0
  37. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/.github/workflows/lint.yml +0 -0
  38. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/.github/workflows/tests.yml +0 -0
  39. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/.pre-commit-config.yaml +0 -0
  40. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/LICENSE +0 -0
  41. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/__init__.py +0 -0
  42. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_async_clients/__init__.py +0 -0
  43. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_async_clients/_alm.py +0 -0
  44. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_auth.py +0 -0
  45. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_clients/__init__.py +0 -0
  46. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/_clients/_alm.py +0 -0
  47. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/anaplan_sdk/exceptions.py +0 -0
  48. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/anaplan_explained.md +0 -0
  49. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/alm_client.md +0 -0
  50. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/async_alm_client.md +0 -0
  51. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/async_audit_client.md +0 -0
  52. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/async_client.md +0 -0
  53. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/audit_client.md +0 -0
  54. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/client.md +0 -0
  55. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/exceptions.md +0 -0
  56. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/models.md +0 -0
  57. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/api/transactional_client.md +0 -0
  58. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/guides/alm.md +0 -0
  59. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/guides/audit.md +0 -0
  60. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/guides/bulk_vs_transactional.md +0 -0
  61. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/img/anaplan-overview.webp +0 -0
  62. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/img/anaplan-sdk.webp +0 -0
  63. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/docs/installation.md +0 -0
  64. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/async/test_async_alm_client.py +0 -0
  65. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/async/test_async_audit_client.py +0 -0
  66. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/async/test_async_transactional_client.py +0 -0
  67. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/conftest.py +0 -0
  68. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/sync/test_alm_client.py +0 -0
  69. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/sync/test_audit_client.py +0 -0
  70. {anaplan_sdk-0.2.11 → anaplan_sdk-0.3.1b1}/tests/sync/test_transactional_client.py +0 -0
@@ -1,3 +1,4 @@
1
+ .prettierrc
1
2
  # Byte-compiled / optimized / DLL files
2
3
  __pycache__/
3
4
  *.py[cod]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anaplan-sdk
3
- Version: 0.2.11
3
+ Version: 0.3.1b1
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 APsI](https://help.anaplan.com/application-lifecycle-management-api-2565cfa6-e0c2-4e24-884e-d0df957184d6),
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 APsI](https://help.anaplan.com/application-lifecycle-management-api-2565cfa6-e0c2-4e24-884e-d0df957184d6),
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 time
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 Action, Export, File, Import, Model, Process, Workspace
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
- await self._get("https://api.anaplan.com/2/0/workspaces?tenantDetails=true")
187
- ).get("workspaces", [])
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 (await self._get("https://api.anaplan.com/2/0/models?modelDetails=true")).get(
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 (await self._get(f"{self._url}/files")).get("files", [])
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 (await self._get(f"{self._url}/actions")).get("actions", [])
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 (await self._get(f"{self._url}/processes")).get("processes", [])
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 (await self._get(f"{self._url}/imports")).get("imports", [])
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 (await self._get(f"{self._url}/exports")).get("exports", [])
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) -> None:
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 "COMPLETE" not in task_status.get("taskState"):
272
- time.sleep(self.status_poll_delay)
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.get("taskState") == "COMPLETE" and not task_status.get("result").get(
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 (await self._get(f"{self._url}/{action_url(action_id)}/{action_id}/tasks")).get(
400
- "tasks", []
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 as returned by the API. For more information
411
- see: https://anaplan.docs.apiary.io.
421
+ :return: The status of the task.
412
422
  """
413
- return (
414
- await self._get(f"{self._url}/{action_url(action_id)}/{action_id}/tasks/{task_id}")
415
- ).get("task")
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 (await self._get(f"{self._url}/modules")).get("modules")
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 (await self._get(f"{self._url}/lists")).get("lists")
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 write_to_module(
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 typing import Any, Callable, Coroutine, Literal
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, float | int | str | list | dict | bool]:
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, float | int | str | list | dict | bool]:
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 Action, Export, File, Import, Model, Process, Workspace
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._get("https://api.anaplan.com/2/0/workspaces?tenantDetails=true").get(
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._get("https://api.anaplan.com/2/0/models?modelDetails=true").get(
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._get(f"{self._url}/files").get("files", [])]
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 (self._get(f"{self._url}/actions")).get("actions", [])
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 (self._get(f"{self._url}/processes")).get("processes", [])
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 (self._get(f"{self._url}/imports")).get("imports", [])
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) -> None:
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 "COMPLETE" not in task_status.get("taskState"):
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.get("taskState") == "COMPLETE" and not task_status.get("result").get(
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 self._get(f"{self._url}/{action_url(action_id)}/{action_id}/tasks").get("tasks", [])
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 as returned by the API. For more information
402
- see: https://anaplan.docs.apiary.io.
415
+ :return: The status of the task.
403
416
  """
404
- return self._get(f"{self._url}/{action_url(action_id)}/{action_id}/tasks/{task_id}").get(
405
- "task"
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 [Module.model_validate(e) for e in self._get(f"{self._url}/modules").get("modules")]
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 Model.
58
+ :return: All Lists on this model.
57
59
  """
58
- return [List.model_validate(e) for e in self._get(f"{self._url}/lists").get("lists")]
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 write_to_module(self, module_id: int, data: list[dict[str, Any]]) -> int | dict[str, Any]:
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: