anaplan-sdk 0.5.0a3__tar.gz → 0.5.0a4__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 (97) hide show
  1. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/PKG-INFO +1 -1
  2. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_async_clients/_alm.py +12 -4
  3. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_async_clients/_audit.py +17 -9
  4. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_async_clients/_bulk.py +17 -27
  5. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_async_clients/_cloud_works.py +4 -3
  6. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_async_clients/_transactional.py +5 -7
  7. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_clients/_alm.py +12 -4
  8. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_clients/_audit.py +19 -11
  9. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_clients/_bulk.py +71 -45
  10. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_clients/_cloud_works.py +4 -3
  11. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_clients/_transactional.py +25 -11
  12. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_services.py +16 -2
  13. anaplan_sdk-0.5.0a4/docs/guides/sorting.md +41 -0
  14. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/mkdocs.yml +1 -0
  15. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/pyproject.toml +2 -4
  16. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/async/test_async_cloud_works_client.py +1 -1
  17. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/sync/test_cloud_works_client.py +1 -1
  18. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/.github/dependabot.yml +0 -0
  19. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/.github/workflows/docs.yml +0 -0
  20. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/.github/workflows/lint.yml +0 -0
  21. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/.github/workflows/tests.yml +0 -0
  22. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/.gitignore +0 -0
  23. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/.pre-commit-config.yaml +0 -0
  24. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/LICENSE +0 -0
  25. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/README.md +0 -0
  26. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/__init__.py +0 -0
  27. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_async_clients/__init__.py +0 -0
  28. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_async_clients/_cw_flow.py +0 -0
  29. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_auth.py +0 -0
  30. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_clients/__init__.py +0 -0
  31. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_clients/_cw_flow.py +0 -0
  32. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/_oauth.py +0 -0
  33. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/exceptions.py +0 -0
  34. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/models/__init__.py +0 -0
  35. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/models/_alm.py +0 -0
  36. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/models/_base.py +0 -0
  37. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/models/_bulk.py +0 -0
  38. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/models/_transactional.py +0 -0
  39. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/models/cloud_works.py +0 -0
  40. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/anaplan_sdk/models/flows.py +0 -0
  41. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/async/async_alm_client.md +0 -0
  42. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/async/async_audit_client.md +0 -0
  43. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/async/async_client.md +0 -0
  44. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/async/async_cw_client.md +0 -0
  45. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/async/async_flows_client.md +0 -0
  46. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/async/async_oauth_client.md +0 -0
  47. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/async/async_transactional_client.md +0 -0
  48. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/exceptions.md +0 -0
  49. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/models/alm.md +0 -0
  50. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/models/bulk.md +0 -0
  51. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/models/cloud_works.md +0 -0
  52. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/models/flows.md +0 -0
  53. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/models/transactional.md +0 -0
  54. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/sync/sync_alm_client.md +0 -0
  55. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/sync/sync_audit_client.md +0 -0
  56. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/sync/sync_client.md +0 -0
  57. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/sync/sync_cw_client.md +0 -0
  58. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/sync/sync_flows_client.md +0 -0
  59. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/sync/sync_oauth_client.md +0 -0
  60. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/api/sync/sync_transactional_client.md +0 -0
  61. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/assets/overview.html +0 -0
  62. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/concepts.md +0 -0
  63. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/css/styles.css +0 -0
  64. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/alm.md +0 -0
  65. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/audit.md +0 -0
  66. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/authentication.md +0 -0
  67. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/bulk.md +0 -0
  68. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/bulk_vs_transactional.md +0 -0
  69. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/cloud_works.md +0 -0
  70. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/index.md +0 -0
  71. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/logging.md +0 -0
  72. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/multiple_models.md +0 -0
  73. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/guides/transactional.md +0 -0
  74. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/img/anaplan-sdk.webp +0 -0
  75. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/index.md +0 -0
  76. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/installation.md +0 -0
  77. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/js/assets/hljs.js +0 -0
  78. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/js/assets/hljs.min.js +0 -0
  79. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/js/assets/python.js +0 -0
  80. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/js/assets/python.min.js +0 -0
  81. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/js/highlight.js +0 -0
  82. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/js/highlight.min.js +0 -0
  83. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/docs/quickstart.md +0 -0
  84. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/async/conftest.py +0 -0
  85. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/async/test_async_alm_client.py +0 -0
  86. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/async/test_async_audit_client.py +0 -0
  87. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/async/test_async_client.py +0 -0
  88. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/async/test_async_flows_client.py +0 -0
  89. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/async/test_async_transactional_client.py +0 -0
  90. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/conftest.py +0 -0
  91. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/sync/conftest.py +0 -0
  92. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/sync/test_alm_client.py +0 -0
  93. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/sync/test_audit_client.py +0 -0
  94. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/sync/test_client.py +0 -0
  95. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/sync/test_flows_client.py +0 -0
  96. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/tests/sync/test_transactional_client.py +0 -0
  97. {anaplan_sdk-0.5.0a3 → anaplan_sdk-0.5.0a4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anaplan-sdk
3
- Version: 0.5.0a3
3
+ Version: 0.5.0a4
4
4
  Summary: Streamlined Python Interface for 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
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from typing import Literal, overload
3
3
 
4
- from anaplan_sdk._services import _AsyncHttpService
4
+ from anaplan_sdk._services import _AsyncHttpService, sort_params
5
5
  from anaplan_sdk.exceptions import AnaplanActionError
6
6
  from anaplan_sdk.models import (
7
7
  ModelRevision,
@@ -29,13 +29,21 @@ class _AsyncAlmClient:
29
29
  logger.info(f"Changed model status to '{status}' for model {self._model_id}.")
30
30
  await self._http.put(f"{self._url}/onlineStatus", json={"status": status})
31
31
 
32
- async def get_revisions(self) -> list[Revision]:
32
+ async def get_revisions(
33
+ self,
34
+ sort_by: Literal["id", "name", "applied_on", "created_on"] | None = None,
35
+ descending: bool = False,
36
+ ) -> list[Revision]:
33
37
  """
34
38
  Use this call to return a list of revisions for a specific model.
39
+ :param sort_by: The field to sort the results by.
40
+ :param descending: If True, the results will be sorted in descending order.
35
41
  :return: A list of revisions for a specific model.
36
42
  """
37
- res = await self._http.get(f"{self._url}/alm/revisions")
38
- return [Revision.model_validate(e) for e in res.get("revisions", [])]
43
+ res = await self._http.get_paginated(
44
+ f"{self._url}/alm/revisions", "revisions", params=sort_params(sort_by, descending)
45
+ )
46
+ return [Revision.model_validate(e) for e in res]
39
47
 
40
48
  async def get_latest_revision(self) -> Revision | None:
41
49
  """
@@ -1,9 +1,10 @@
1
1
  from typing import Any, Literal
2
2
 
3
- from anaplan_sdk._services import _AsyncHttpService
3
+ from anaplan_sdk._services import _AsyncHttpService, sort_params
4
4
  from anaplan_sdk.models import User
5
5
 
6
6
  Event = Literal["all", "byok", "user_activity"]
7
+ UserSortBy = Literal["first_name", "last_name", "email", "active", "last_login_date"] | None
7
8
 
8
9
 
9
10
  class _AsyncAuditClient:
@@ -12,22 +13,29 @@ class _AsyncAuditClient:
12
13
  self._limit = 10_000
13
14
  self._url = "https://audit.anaplan.com/audit/api/1/events"
14
15
 
15
- async def get_users(self, search_pattern: str | None = None) -> list[User]:
16
+ async def get_users(
17
+ self,
18
+ search_pattern: str | None = None,
19
+ sort_by: UserSortBy = None,
20
+ descending: bool = False,
21
+ ) -> list[User]:
16
22
  """
17
23
  Lists all the Users in the authenticated users default tenant.
18
24
  :param search_pattern: Optionally filter for specific users. When provided,
19
25
  case-insensitive matches users with emails or names containing this string.
20
26
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
21
27
  When None (default), returns all users.
28
+ :param sort_by: The field to sort the results by.
29
+ :param descending: If True, the results will be sorted in descending order.
22
30
  :return: The List of Users.
23
31
  """
24
- params = {"s": search_pattern} if search_pattern else None
25
- return [
26
- User.model_validate(e)
27
- for e in await self._http.get_paginated(
28
- "https://api.anaplan.com/2/0/users", "users", params=params
29
- )
30
- ]
32
+ params = sort_params(sort_by, descending)
33
+ if search_pattern:
34
+ params["s"] = search_pattern
35
+ res = await self._http.get_paginated(
36
+ "https://api.anaplan.com/2/0/users", "users", params=params
37
+ )
38
+ return [User.model_validate(e) for e in res]
31
39
 
32
40
  async def get_user(self, user_id: str = "me") -> User:
33
41
  """
@@ -27,6 +27,8 @@ from ._audit import _AsyncAuditClient
27
27
  from ._cloud_works import _AsyncCloudWorksClient
28
28
  from ._transactional import _AsyncTransactionalClient
29
29
 
30
+ SortBy = Literal["id", "name"] | None
31
+
30
32
  logger = logging.getLogger("anaplan_sdk")
31
33
 
32
34
 
@@ -228,7 +230,7 @@ class AsyncClient:
228
230
  async def get_workspaces(
229
231
  self,
230
232
  search_pattern: str | None = None,
231
- sort_by: Literal["size_allowance", "name"] = "name",
233
+ sort_by: Literal["size_allowance", "name"] | None = None,
232
234
  descending: bool = False,
233
235
  ) -> list[Workspace]:
234
236
  """
@@ -244,17 +246,15 @@ class AsyncClient:
244
246
  params = {"tenantDetails": "true"} | sort_params(sort_by, descending)
245
247
  if search_pattern:
246
248
  params["s"] = search_pattern
247
- return [
248
- Workspace.model_validate(e)
249
- for e in await self._http.get_paginated(
250
- "https://api.anaplan.com/2/0/workspaces", "workspaces", params=params
251
- )
252
- ]
249
+ res = await self._http.get_paginated(
250
+ "https://api.anaplan.com/2/0/workspaces", "workspaces", params=params
251
+ )
252
+ return [Workspace.model_validate(e) for e in res]
253
253
 
254
254
  async def get_models(
255
255
  self,
256
256
  search_pattern: str | None = None,
257
- sort_by: Literal["active_state", "name"] = "name",
257
+ sort_by: Literal["active_state", "name"] | None = None,
258
258
  descending: bool = False,
259
259
  ) -> list[Model]:
260
260
  """
@@ -270,12 +270,10 @@ class AsyncClient:
270
270
  params = {"modelDetails": "true"} | sort_params(sort_by, descending)
271
271
  if search_pattern:
272
272
  params["s"] = search_pattern
273
- return [
274
- Model.model_validate(e)
275
- for e in await self._http.get_paginated(
276
- "https://api.anaplan.com/2/0/models", "models", params=params
277
- )
278
- ]
273
+ res = await self._http.get_paginated(
274
+ "https://api.anaplan.com/2/0/models", "models", params=params
275
+ )
276
+ return [Model.model_validate(e) for e in res]
279
277
 
280
278
  async def delete_models(self, model_ids: list[str]) -> ModelDeletionResult:
281
279
  """
@@ -291,9 +289,7 @@ class AsyncClient:
291
289
  )
292
290
  return ModelDeletionResult.model_validate(res)
293
291
 
294
- async def get_files(
295
- self, sort_by: Literal["id", "name"] = "id", descending: bool = False
296
- ) -> list[File]:
292
+ async def get_files(self, sort_by: SortBy = None, descending: bool = False) -> list[File]:
297
293
  """
298
294
  Lists all the Files in the Model.
299
295
  :param sort_by: The field to sort the results by.
@@ -305,9 +301,7 @@ class AsyncClient:
305
301
  )
306
302
  return [File.model_validate(e) for e in res]
307
303
 
308
- async def get_actions(
309
- self, sort_by: Literal["id", "name"] = "id", descending: bool = False
310
- ) -> list[Action]:
304
+ async def get_actions(self, sort_by: SortBy = None, descending: bool = False) -> list[Action]:
311
305
  """
312
306
  Lists all the Actions in the Model. This will only return the Actions listed under
313
307
  `Other Actions` in Anaplan. For Imports, exports, and processes, see their respective
@@ -322,7 +316,7 @@ class AsyncClient:
322
316
  return [Action.model_validate(e) for e in res]
323
317
 
324
318
  async def get_processes(
325
- self, sort_by: Literal["id", "name"] = "id", descending: bool = False
319
+ self, sort_by: SortBy = None, descending: bool = False
326
320
  ) -> list[Process]:
327
321
  """
328
322
  Lists all the Processes in the Model.
@@ -335,9 +329,7 @@ class AsyncClient:
335
329
  )
336
330
  return [Process.model_validate(e) for e in res]
337
331
 
338
- async def get_imports(
339
- self, sort_by: Literal["id", "name"] = "id", descending: bool = False
340
- ) -> list[Import]:
332
+ async def get_imports(self, sort_by: SortBy = None, descending: bool = False) -> list[Import]:
341
333
  """
342
334
  Lists all the Imports in the Model.
343
335
  :param sort_by: The field to sort the results by.
@@ -349,9 +341,7 @@ class AsyncClient:
349
341
  )
350
342
  return [Import.model_validate(e) for e in res]
351
343
 
352
- async def get_exports(
353
- self, sort_by: Literal["id", "name"] = "id", descending: bool = False
354
- ) -> list[Export]:
344
+ async def get_exports(self, sort_by: SortBy = None, descending: bool = False) -> list[Export]:
355
345
  """
356
346
  Lists all the Exports in the Model.
357
347
  :param sort_by: The field to sort the results by.
@@ -99,14 +99,15 @@ class _AsyncCloudWorksClient:
99
99
  logger.info(f"Deleted connection '{con_id}'.")
100
100
 
101
101
  async def get_integrations(
102
- self, sort_by_name: Literal["ascending", "descending"] = "ascending"
102
+ self, sort_by: Literal["name"] | None = None, descending: bool = False
103
103
  ) -> list[Integration]:
104
104
  """
105
105
  List all integrations in CloudWorks.
106
- :param sort_by_name: Sort the integrations by name in ascending or descending order.
106
+ :param sort_by: The field to sort the results by.
107
+ :param descending: If True, the results will be sorted in descending order.
107
108
  :return: A list of integrations.
108
109
  """
109
- params = {"sortBy": "name" if sort_by_name == "ascending" else "-name"}
110
+ params = {"sortBy": f"{'-' if descending else ''}{sort_by}"} if sort_by else None
110
111
  return [
111
112
  Integration.model_validate(e)
112
113
  for e in await self._http.get_paginated(f"{self._url}", "integrations", params=params)
@@ -30,6 +30,8 @@ from anaplan_sdk.models import (
30
30
  ViewInfo,
31
31
  )
32
32
 
33
+ SortBy = Literal["id", "name"] | None
34
+
33
35
  logger = logging.getLogger("anaplan_sdk")
34
36
 
35
37
 
@@ -69,9 +71,7 @@ class _AsyncTransactionalClient:
69
71
  )
70
72
  logger.info(f"Closed model '{self._model_id}'.")
71
73
 
72
- async def get_modules(
73
- self, sort_by: Literal["id", "name"] = "id", descending: bool = False
74
- ) -> list[Module]:
74
+ async def get_modules(self, sort_by: SortBy = None, descending: bool = False) -> list[Module]:
75
75
  """
76
76
  Lists all the Modules in the Model.
77
77
  :param sort_by: The field to sort the results by.
@@ -84,7 +84,7 @@ class _AsyncTransactionalClient:
84
84
  return [Module.model_validate(e) for e in res]
85
85
 
86
86
  async def get_views(
87
- self, sort_by: Literal["id", "module_id", "name"] = "id", descending: bool = False
87
+ self, sort_by: Literal["id", "module_id", "name"] | None = None, descending: bool = False
88
88
  ) -> list[View]:
89
89
  """
90
90
  Lists all the Views in the Model. This will include all Modules and potentially other saved
@@ -120,9 +120,7 @@ class _AsyncTransactionalClient:
120
120
  )
121
121
  return [LineItem.model_validate(e) for e in res.get("items", [])]
122
122
 
123
- async def get_lists(
124
- self, sort_by: Literal["id", "name"] = "id", descending: bool = False
125
- ) -> list[List]:
123
+ async def get_lists(self, sort_by: SortBy = None, descending: bool = False) -> list[List]:
126
124
  """
127
125
  Lists all the Lists in the Model.
128
126
  :param sort_by: The field to sort the results by.
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from typing import Literal, overload
3
3
 
4
- from anaplan_sdk._services import _HttpService
4
+ from anaplan_sdk._services import _HttpService, sort_params
5
5
  from anaplan_sdk.exceptions import AnaplanActionError
6
6
  from anaplan_sdk.models import (
7
7
  ModelRevision,
@@ -29,13 +29,21 @@ class _AlmClient:
29
29
  logger.info(f"Changed model status to '{status}' for model {self._model_id}.")
30
30
  self._http.put(f"{self._url}/onlineStatus", json={"status": status})
31
31
 
32
- def get_revisions(self) -> list[Revision]:
32
+ def get_revisions(
33
+ self,
34
+ sort_by: Literal["id", "name", "applied_on", "created_on"] | None = None,
35
+ descending: bool = False,
36
+ ) -> list[Revision]:
33
37
  """
34
38
  Use this call to return a list of revisions for a specific model.
39
+ :param sort_by: The field to sort the results by.
40
+ :param descending: If True, the results will be sorted in descending order.
35
41
  :return: A list of revisions for a specific model.
36
42
  """
37
- res = self._http.get(f"{self._url}/alm/revisions")
38
- return [Revision.model_validate(e) for e in res.get("revisions", [])]
43
+ res = self._http.get_paginated(
44
+ f"{self._url}/alm/revisions", "revisions", params=sort_params(sort_by, descending)
45
+ )
46
+ return [Revision.model_validate(e) for e in res]
39
47
 
40
48
  def get_latest_revision(self) -> Revision | None:
41
49
  """
@@ -1,9 +1,10 @@
1
1
  from typing import Any, Literal
2
2
 
3
- from anaplan_sdk._services import _HttpService
3
+ from anaplan_sdk._services import _HttpService, sort_params
4
4
  from anaplan_sdk.models import User
5
5
 
6
6
  Event = Literal["all", "byok", "user_activity"]
7
+ UserSortBy = Literal["first_name", "last_name", "email", "active", "last_login_date"] | None
7
8
 
8
9
 
9
10
  class _AuditClient:
@@ -12,20 +13,27 @@ class _AuditClient:
12
13
  self._limit = 10_000
13
14
  self._url = "https://audit.anaplan.com/audit/api/1/events"
14
15
 
15
- def get_users(self, search_pattern: str | None = None) -> list[User]:
16
+ def get_users(
17
+ self,
18
+ search_pattern: str | None = None,
19
+ sort_by: UserSortBy = None,
20
+ descending: bool = False,
21
+ ) -> list[User]:
16
22
  """
17
23
  Lists all the Users in the authenticated users default tenant.
18
- :param search_pattern: Optional filter for users. When provided, case-insensitive matches
19
- users with emails containing this string. When None (default), returns all users.
24
+ :param search_pattern: Optionally filter for specific users. When provided,
25
+ case-insensitive matches users with emails or names containing this string.
26
+ You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
27
+ When None (default), returns all users.
28
+ :param sort_by: The field to sort the results by.
29
+ :param descending: If True, the results will be sorted in descending order.
20
30
  :return: The List of Users.
21
31
  """
22
- params = {"s": search_pattern} if search_pattern else None
23
- return [
24
- User.model_validate(e)
25
- for e in self._http.get_paginated(
26
- "https://api.anaplan.com/2/0/users", "users", params=params
27
- )
28
- ]
32
+ params = sort_params(sort_by, descending)
33
+ if search_pattern:
34
+ params["s"] = search_pattern
35
+ res = self._http.get_paginated("https://api.anaplan.com/2/0/users", "users", params=params)
36
+ return [User.model_validate(e) for e in res]
29
37
 
30
38
  def get_user(self, user_id: str = "me") -> User:
31
39
  """
@@ -2,13 +2,13 @@ import logging
2
2
  import multiprocessing
3
3
  from concurrent.futures import ThreadPoolExecutor
4
4
  from copy import copy
5
- from typing import Iterator
5
+ from typing import Iterator, Literal
6
6
 
7
7
  import httpx
8
8
  from typing_extensions import Self
9
9
 
10
10
  from anaplan_sdk._auth import _create_auth
11
- from anaplan_sdk._services import _HttpService, action_url
11
+ from anaplan_sdk._services import _HttpService, action_url, sort_params
12
12
  from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
13
13
  from anaplan_sdk.models import (
14
14
  Action,
@@ -28,6 +28,8 @@ from ._audit import _AuditClient
28
28
  from ._cloud_works import _CloudWorksClient
29
29
  from ._transactional import _TransactionalClient
30
30
 
31
+ SortBy = Literal["id", "name"] | None
32
+
31
33
  logger = logging.getLogger("anaplan_sdk")
32
34
 
33
35
 
@@ -230,42 +232,53 @@ class Client:
230
232
  )
231
233
  return self._alm_client
232
234
 
233
- def get_workspaces(self, search_pattern: str | None = None) -> list[Workspace]:
235
+ def get_workspaces(
236
+ self,
237
+ search_pattern: str | None = None,
238
+ sort_by: Literal["size_allowance", "name"] | None = None,
239
+ descending: bool = False,
240
+ ) -> list[Workspace]:
234
241
  """
235
242
  Lists all the Workspaces the authenticated user has access to.
236
- :param search_pattern: Optional filter for workspaces. When provided, case-insensitive
237
- matches workspaces with names containing this string. When None (default),
238
- returns all workspaces.
243
+ :param search_pattern: Optionally filter for specific workspaces. When provided,
244
+ case-insensitive matches workspaces with names containing this string.
245
+ You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
246
+ When None (default), returns all users.
247
+ :param sort_by: The field to sort the results by.
248
+ :param descending: If True, the results will be sorted in descending order.
239
249
  :return: The List of Workspaces.
240
250
  """
241
- params = {"tenantDetails": "true"}
251
+ params = {"tenantDetails": "true"} | sort_params(sort_by, descending)
242
252
  if search_pattern:
243
253
  params["s"] = search_pattern
244
- return [
245
- Workspace.model_validate(e)
246
- for e in self._http.get_paginated(
247
- "https://api.anaplan.com/2/0/workspaces", "workspaces", params=params
248
- )
249
- ]
254
+ res = self._http.get_paginated(
255
+ "https://api.anaplan.com/2/0/workspaces", "workspaces", params=params
256
+ )
257
+ return [Workspace.model_validate(e) for e in res]
250
258
 
251
- def get_models(self, search_pattern: str | None = None) -> list[Model]:
259
+ def get_models(
260
+ self,
261
+ search_pattern: str | None = None,
262
+ sort_by: Literal["active_state", "name"] | None = None,
263
+ descending: bool = False,
264
+ ) -> list[Model]:
252
265
  """
253
266
  Lists all the Models the authenticated user has access to.
254
267
  :param search_pattern: Optionally filter for specific models. When provided,
255
268
  case-insensitive matches model names containing this string.
256
269
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
257
270
  When None (default), returns all models.
271
+ :param sort_by: The field to sort the results by.
272
+ :param descending: If True, the results will be sorted in descending order.
258
273
  :return: The List of Models.
259
274
  """
260
- params = {"modelDetails": "true"}
275
+ params = {"modelDetails": "true"} | sort_params(sort_by, descending)
261
276
  if search_pattern:
262
277
  params["s"] = search_pattern
263
- return [
264
- Model.model_validate(e)
265
- for e in self._http.get_paginated(
266
- "https://api.anaplan.com/2/0/models", "models", params=params
267
- )
268
- ]
278
+ res = self._http.get_paginated(
279
+ "https://api.anaplan.com/2/0/models", "models", params=params
280
+ )
281
+ return [Model.model_validate(e) for e in res]
269
282
 
270
283
  def delete_models(self, model_ids: list[str]) -> ModelDeletionResult:
271
284
  """
@@ -281,56 +294,69 @@ class Client:
281
294
  )
282
295
  return ModelDeletionResult.model_validate(res)
283
296
 
284
- def get_files(self) -> list[File]:
297
+ def get_files(
298
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
299
+ ) -> list[File]:
285
300
  """
286
301
  Lists all the Files in the Model.
302
+ :param sort_by: The field to sort the results by.
303
+ :param descending: If True, the results will be sorted in descending order.
287
304
  :return: The List of Files.
288
305
  """
289
- return [
290
- File.model_validate(e) for e in self._http.get_paginated(f"{self._url}/files", "files")
291
- ]
306
+ res = self._http.get_paginated(
307
+ f"{self._url}/files", "files", params=sort_params(sort_by, descending)
308
+ )
309
+ return [File.model_validate(e) for e in res]
292
310
 
293
- def get_actions(self) -> list[Action]:
311
+ def get_actions(self, sort_by: SortBy = None, descending: bool = False) -> list[Action]:
294
312
  """
295
313
  Lists all the Actions in the Model. This will only return the Actions listed under
296
314
  `Other Actions` in Anaplan. For Imports, exports, and processes, see their respective
297
315
  methods instead.
316
+ :param sort_by: The field to sort the results by.
317
+ :param descending: If True, the results will be sorted in descending order.
298
318
  :return: The List of Actions.
299
319
  """
300
- return [
301
- Action.model_validate(e)
302
- for e in self._http.get_paginated(f"{self._url}/actions", "actions")
303
- ]
320
+ res = self._http.get_paginated(
321
+ f"{self._url}/actions", "actions", params=sort_params(sort_by, descending)
322
+ )
323
+ return [Action.model_validate(e) for e in res]
304
324
 
305
- def get_processes(self) -> list[Process]:
325
+ def get_processes(self, sort_by: SortBy = None, descending: bool = False) -> list[Process]:
306
326
  """
307
327
  Lists all the Processes in the Model.
328
+ :param sort_by: The field to sort the results by.
329
+ :param descending: If True, the results will be sorted in descending order.
308
330
  :return: The List of Processes.
309
331
  """
310
- return [
311
- Process.model_validate(e)
312
- for e in self._http.get_paginated(f"{self._url}/processes", "processes")
313
- ]
332
+ res = self._http.get_paginated(
333
+ f"{self._url}/processes", "processes", params=sort_params(sort_by, descending)
334
+ )
335
+ return [Process.model_validate(e) for e in res]
314
336
 
315
- def get_imports(self) -> list[Import]:
337
+ def get_imports(self, sort_by: SortBy = None, descending: bool = False) -> list[Import]:
316
338
  """
317
339
  Lists all the Imports in the Model.
340
+ :param sort_by: The field to sort the results by.
341
+ :param descending: If True, the results will be sorted in descending order.
318
342
  :return: The List of Imports.
319
343
  """
320
- return [
321
- Import.model_validate(e)
322
- for e in self._http.get_paginated(f"{self._url}/imports", "imports")
323
- ]
344
+ res = self._http.get_paginated(
345
+ f"{self._url}/imports", "imports", params=sort_params(sort_by, descending)
346
+ )
347
+ return [Import.model_validate(e) for e in res]
324
348
 
325
- def get_exports(self) -> list[Export]:
349
+ def get_exports(self, sort_by: SortBy = None, descending: bool = False) -> list[Export]:
326
350
  """
327
351
  Lists all the Exports in the Model.
352
+ :param sort_by: The field to sort the results by.
353
+ :param descending: If True, the results will be sorted in descending order.
328
354
  :return: The List of Exports.
329
355
  """
330
- return [
331
- Export.model_validate(e)
332
- for e in (self._http.get(f"{self._url}/exports")).get("exports", [])
333
- ]
356
+ res = self._http.get_paginated(
357
+ f"{self._url}/exports", "exports", params=sort_params(sort_by, descending)
358
+ )
359
+ return [Export.model_validate(e) for e in res]
334
360
 
335
361
  def run_action(self, action_id: int, wait_for_completion: bool = True) -> TaskStatus:
336
362
  """
@@ -95,14 +95,15 @@ class _CloudWorksClient:
95
95
  logger.info(f"Deleted connection '{con_id}'.")
96
96
 
97
97
  def get_integrations(
98
- self, sort_by_name: Literal["ascending", "descending"] = "ascending"
98
+ self, sort_by: Literal["name"] | None = None, descending: bool = False
99
99
  ) -> list[Integration]:
100
100
  """
101
101
  List all integrations in CloudWorks.
102
- :param sort_by_name: Sort the integrations by name in ascending or descending order.
102
+ :param sort_by: The field to sort the results by.
103
+ :param descending: If True, the results will be sorted in descending order.
103
104
  :return: A list of integrations.
104
105
  """
105
- params = {"sortBy": "name" if sort_by_name == "ascending" else "-name"}
106
+ params = {"sortBy": f"{'-' if descending else ''}{sort_by}"} if sort_by else None
106
107
  return [
107
108
  Integration.model_validate(e)
108
109
  for e in self._http.get_paginated(f"{self._url}", "integrations", params=params)
@@ -7,6 +7,7 @@ from anaplan_sdk._services import (
7
7
  _HttpService,
8
8
  parse_calendar_response,
9
9
  parse_insertion_response,
10
+ sort_params,
10
11
  validate_dimension_id,
11
12
  )
12
13
  from anaplan_sdk.exceptions import InvalidIdentifierException
@@ -29,6 +30,8 @@ from anaplan_sdk.models import (
29
30
  ViewInfo,
30
31
  )
31
32
 
33
+ SortBy = Literal["id", "name"] | None
34
+
32
35
  logger = logging.getLogger("anaplan_sdk")
33
36
 
34
37
 
@@ -66,24 +69,32 @@ class _TransactionalClient:
66
69
  self._http.post_empty(f"{self._url}/close", headers={"Content-Type": "application/text"})
67
70
  logger.info(f"Closed model '{self._model_id}'.")
68
71
 
69
- def get_modules(self) -> list[Module]:
72
+ def get_modules(self, sort_by: SortBy = None, descending: bool = False) -> list[Module]:
70
73
  """
71
74
  Lists all the Modules in the Model.
75
+ :param sort_by: The field to sort the results by.
76
+ :param descending: If True, the results will be sorted in descending order.
72
77
  :return: The List of Modules.
73
78
  """
74
- return [
75
- Module.model_validate(e)
76
- for e in self._http.get_paginated(f"{self._url}/modules", "modules")
77
- ]
79
+ res = self._http.get_paginated(
80
+ f"{self._url}/modules", "modules", params=sort_params(sort_by, descending)
81
+ )
82
+ return [Module.model_validate(e) for e in res]
78
83
 
79
- def get_views(self) -> list[View]:
84
+ def get_views(
85
+ self, sort_by: Literal["id", "module_id", "name"] | None = None, descending: bool = False
86
+ ) -> list[View]:
80
87
  """
81
88
  Lists all the Views in the Model. This will include all Modules and potentially other saved
82
89
  views.
90
+ :param sort_by: The field to sort the results by.
91
+ :param descending: If True, the results will be sorted in descending order.
83
92
  :return: The List of Views.
84
93
  """
94
+ params = {"includesubsidiaryviews": True} | sort_params(sort_by, descending)
85
95
  return [
86
- View.model_validate(e) for e in self._http.get_paginated(f"{self._url}/views", "views")
96
+ View.model_validate(e)
97
+ for e in self._http.get_paginated(f"{self._url}/views", "views", params=params)
87
98
  ]
88
99
 
89
100
  def get_view_info(self, view_id: int) -> ViewInfo:
@@ -107,14 +118,17 @@ class _TransactionalClient:
107
118
  )
108
119
  return [LineItem.model_validate(e) for e in self._http.get(url).get("items", [])]
109
120
 
110
- def get_lists(self) -> list[List]:
121
+ def get_lists(self, sort_by: SortBy = None, descending: bool = False) -> list[List]:
111
122
  """
112
123
  Lists all the Lists in the Model.
124
+ :param sort_by: The field to sort the results by.
125
+ :param descending: If True, the results will be sorted in descending order.
113
126
  :return: All Lists on this model.
114
127
  """
115
- return [
116
- List.model_validate(e) for e in self._http.get_paginated(f"{self._url}/lists", "lists")
117
- ]
128
+ res = self._http.get_paginated(
129
+ f"{self._url}/lists", "lists", params=sort_params(sort_by, descending)
130
+ )
131
+ return [List.model_validate(e) for e in res]
118
132
 
119
133
  def get_list_metadata(self, list_id: int) -> ListMetadata:
120
134
  """
@@ -33,6 +33,15 @@ from .models.cloud_works import (
33
33
  ScheduleInput,
34
34
  )
35
35
 
36
+ SORT_WARNING = (
37
+ "If you are sorting by a field that is potentially ambiguous (e.g., name), the order of "
38
+ "results is not guaranteed to be internally consistent across multiple requests. This will "
39
+ "lead to wrong results when paginating through result sets where the ambiguous order can cause "
40
+ "records to slip between pages or be duplicated on multiple pages. The only way to ensure "
41
+ "correct results when sorting is to make sure the entire result set fits in one page, or to "
42
+ "sort by a field that is guaranteed to be unique (e.g., id)."
43
+ )
44
+
36
45
  logger = logging.getLogger("anaplan_sdk")
37
46
 
38
47
  _json_header = {"Content-Type": "application/json"}
@@ -104,7 +113,8 @@ class _HttpService:
104
113
  if total_items <= self._page_size:
105
114
  logger.debug("All items fit in first page, no additional requests needed.")
106
115
  return iter(first_page)
107
-
116
+ if kwargs and (kwargs.get("params") or {}).get("sort", None):
117
+ logger.warning(SORT_WARNING)
108
118
  pages_needed = ceil(total_items / actual_size)
109
119
  logger.debug(f"Fetching {pages_needed - 1} additional pages with {actual_size} items each.")
110
120
  with ThreadPoolExecutor() as executor:
@@ -222,6 +232,8 @@ class _AsyncHttpService:
222
232
  if total_items <= self._page_size:
223
233
  logger.debug("All items fit in first page, no additional requests needed.")
224
234
  return iter(first_page)
235
+ if kwargs and (kwargs.get("params") or {}).get("sort", None):
236
+ logger.warning(SORT_WARNING)
225
237
  pages = await gather(
226
238
  *(
227
239
  self._get_page(url, actual_size, n * actual_size, result_key, **kwargs)
@@ -274,13 +286,15 @@ class _AsyncHttpService:
274
286
  raise AnaplanException("Exhausted all retries without a successful response or Error.")
275
287
 
276
288
 
277
- def sort_params(sort_by: str, descending: bool) -> dict[str, str | bool]:
289
+ def sort_params(sort_by: str | None, descending: bool) -> dict[str, str | bool]:
278
290
  """
279
291
  Construct search parameters for sorting. This also converts snake_case to camelCase.
280
292
  :param sort_by: The field to sort by, optionally in snake_case.
281
293
  :param descending: Whether to sort in descending order.
282
294
  :return: A dictionary of search parameters in Anaplan's expected format.
283
295
  """
296
+ if not sort_by:
297
+ return {}
284
298
  return {"sort": f"{'-' if descending else '+'}{to_camel(sort_by)}"}
285
299
 
286
300
 
@@ -0,0 +1,41 @@
1
+ !!! danger "Anaplan Sorting is not consistent"
2
+ If you are sorting by a field that is potentially ambiguous (e.g., `name`), the order of results is not guaranteed
3
+ to be internally consistent across multiple requests. This will lead to wrong results when paginating through
4
+ result sets where the ambiguous order can cause records to slip between pages or be duplicated on multiple pages.
5
+ The only way to ensure correct results when sorting is to make sure the entire result set fits in one page,
6
+ or to sort by a field that is guaranteed to be unique (e.g., `id`).
7
+
8
+ Some endpoints support sorting results by a specified field in either ascending or descending order. The methods for
9
+ these endpoints include a `sort_by` parameter to specify the field to sort on, and a `descending` boolean parameter
10
+ to specify the sort order (default is ascending).
11
+
12
+ ## Syntax
13
+
14
+ These Methods support sorting. The Type Literals for the `sort_by` will tell you which fields are supported for sorting.
15
+
16
+ ```python
17
+ # Audit
18
+ anaplan.audit.get_users(sort_by="email")
19
+
20
+ # Workspaces & Models
21
+ anaplan.get_workspaces(sort_by="size_allowance", descending=True)
22
+ anaplan.get_models(sort_by="active_state")
23
+
24
+ # Model Objects
25
+ anaplan.get_files(sort_by="name")
26
+ anaplan.get_actions(sort_by="id")
27
+ anaplan.get_processes(sort_by="name")
28
+ anaplan.get_imports(sort_by="id")
29
+ anaplan.get_exports(sort_by="name")
30
+
31
+ # Transactional
32
+ anaplan.tr.get_modules(sort_by="name")
33
+ anaplan.tr.get_views(sort_by="module_id")
34
+ anaplan.tr.get_lists(sort_by="id")
35
+
36
+ # ALM
37
+ anaplan.alm.get_revisions(sort_by="created_on")
38
+
39
+ # CloudWorks
40
+ anaplan.cw.get_integrations(sort_by_name="descending")
41
+ ```
@@ -46,6 +46,7 @@ nav:
46
46
  - CloudWorks API: 'guides/cloud_works.md'
47
47
  - ALM API: 'guides/alm.md'
48
48
  - Audit API: 'guides/audit.md'
49
+ - Sorting: 'guides/sorting.md'
49
50
  - Multiple Models: 'guides/multiple_models.md'
50
51
  - Logging: 'guides/logging.md'
51
52
  - Concepts:
@@ -1,11 +1,9 @@
1
1
  [project]
2
2
  name = "anaplan-sdk"
3
- version = "0.5.0a3"
3
+ version = "0.5.0a4"
4
4
  description = "Streamlined Python Interface for the Anaplan API."
5
5
  license = "Apache-2.0"
6
- authors = [
7
- { name = "Vinzenz Klass", email = "vinzenz.klass@valantic.com" }
8
- ]
6
+ authors = [{ name = "Vinzenz Klass", email = "vinzenz.klass@valantic.com" }]
9
7
  readme = "README.md"
10
8
  keywords = [
11
9
  "anaplan",
@@ -51,7 +51,7 @@ async def test_list_integrations(client):
51
51
 
52
52
 
53
53
  async def test_list_integrations_desc(client):
54
- integrations_desc = await client.cw.get_integrations(sort_by_name="descending")
54
+ integrations_desc = await client.cw.get_integrations(sort_by="name", descending=True)
55
55
  assert isinstance(integrations_desc, list)
56
56
  assert all(isinstance(i, Integration) for i in integrations_desc)
57
57
 
@@ -49,7 +49,7 @@ def test_list_integrations(client):
49
49
 
50
50
 
51
51
  def test_list_integrations_desc(client):
52
- integrations_desc = client.cw.get_integrations(sort_by_name="descending")
52
+ integrations_desc = client.cw.get_integrations(sort_by="name", descending=True)
53
53
  assert isinstance(integrations_desc, list)
54
54
  assert all(isinstance(i, Integration) for i in integrations_desc)
55
55
 
File without changes
File without changes
File without changes
File without changes