anaplan-sdk 0.5.0a2__tar.gz → 0.5.0a3__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 (96) hide show
  1. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/PKG-INFO +2 -2
  2. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_bulk.py +83 -32
  3. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_cw_flow.py +1 -3
  4. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_transactional.py +25 -12
  5. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_bulk.py +16 -1
  6. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_cloud_works.py +1 -1
  7. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_cw_flow.py +1 -3
  8. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_services.py +88 -44
  9. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/cloud_works.py +6 -2
  10. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/pyproject.toml +110 -108
  11. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/conftest.py +1 -0
  12. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_client.py +2 -1
  13. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/conftest.py +1 -0
  14. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_client.py +2 -1
  15. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/uv.lock +1469 -1469
  16. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.github/dependabot.yml +0 -0
  17. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.github/workflows/docs.yml +0 -0
  18. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.github/workflows/lint.yml +0 -0
  19. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.github/workflows/tests.yml +0 -0
  20. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.gitignore +0 -0
  21. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.pre-commit-config.yaml +0 -0
  22. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/LICENSE +0 -0
  23. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/README.md +0 -0
  24. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/__init__.py +0 -0
  25. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/__init__.py +0 -0
  26. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_alm.py +0 -0
  27. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_audit.py +0 -0
  28. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_cloud_works.py +0 -0
  29. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_auth.py +0 -0
  30. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/__init__.py +0 -0
  31. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_alm.py +0 -0
  32. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_audit.py +0 -0
  33. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_transactional.py +0 -0
  34. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_oauth.py +0 -0
  35. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/exceptions.py +0 -0
  36. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/__init__.py +0 -0
  37. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/_alm.py +0 -0
  38. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/_base.py +0 -0
  39. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/_bulk.py +0 -0
  40. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/_transactional.py +0 -0
  41. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/flows.py +0 -0
  42. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_alm_client.md +0 -0
  43. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_audit_client.md +0 -0
  44. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_client.md +0 -0
  45. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_cw_client.md +0 -0
  46. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_flows_client.md +0 -0
  47. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_oauth_client.md +0 -0
  48. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_transactional_client.md +0 -0
  49. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/exceptions.md +0 -0
  50. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/alm.md +0 -0
  51. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/bulk.md +0 -0
  52. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/cloud_works.md +0 -0
  53. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/flows.md +0 -0
  54. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/transactional.md +0 -0
  55. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_alm_client.md +0 -0
  56. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_audit_client.md +0 -0
  57. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_client.md +0 -0
  58. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_cw_client.md +0 -0
  59. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_flows_client.md +0 -0
  60. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_oauth_client.md +0 -0
  61. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_transactional_client.md +0 -0
  62. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/assets/overview.html +0 -0
  63. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/concepts.md +0 -0
  64. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/css/styles.css +0 -0
  65. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/alm.md +0 -0
  66. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/audit.md +0 -0
  67. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/authentication.md +0 -0
  68. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/bulk.md +0 -0
  69. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/bulk_vs_transactional.md +0 -0
  70. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/cloud_works.md +0 -0
  71. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/index.md +0 -0
  72. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/logging.md +0 -0
  73. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/multiple_models.md +0 -0
  74. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/transactional.md +0 -0
  75. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/img/anaplan-sdk.webp +0 -0
  76. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/index.md +0 -0
  77. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/installation.md +0 -0
  78. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/assets/hljs.js +0 -0
  79. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/assets/hljs.min.js +0 -0
  80. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/assets/python.js +0 -0
  81. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/assets/python.min.js +0 -0
  82. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/highlight.js +0 -0
  83. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/highlight.min.js +0 -0
  84. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/quickstart.md +0 -0
  85. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/mkdocs.yml +0 -0
  86. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_alm_client.py +0 -0
  87. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_audit_client.py +0 -0
  88. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_cloud_works_client.py +0 -0
  89. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_flows_client.py +0 -0
  90. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_transactional_client.py +0 -0
  91. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/conftest.py +0 -0
  92. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_alm_client.py +0 -0
  93. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_audit_client.py +0 -0
  94. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_cloud_works_client.py +0 -0
  95. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_flows_client.py +0 -0
  96. {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_transactional_client.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anaplan-sdk
3
- Version: 0.5.0a2
4
- Summary: Streamlined Python Interface for Anaplan
3
+ Version: 0.5.0a3
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
7
7
  Project-URL: Documentation, https://vinzenzklass.github.io/anaplan-sdk/
@@ -1,13 +1,13 @@
1
1
  import logging
2
2
  from asyncio import gather
3
3
  from copy import copy
4
- from typing import AsyncIterator, Iterator
4
+ from typing import AsyncIterator, Iterator, Literal
5
5
 
6
6
  import httpx
7
7
  from typing_extensions import Self
8
8
 
9
9
  from anaplan_sdk._auth import _create_auth
10
- from anaplan_sdk._services import _AsyncHttpService, action_url
10
+ from anaplan_sdk._services import _AsyncHttpService, action_url, sort_params
11
11
  from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
12
12
  from anaplan_sdk.models import (
13
13
  Action,
@@ -49,6 +49,8 @@ class AsyncClient:
49
49
  auth: httpx.Auth | None = None,
50
50
  timeout: float | httpx.Timeout = 30,
51
51
  retry_count: int = 2,
52
+ backoff: float = 1.0,
53
+ backoff_factor: float = 2.0,
52
54
  page_size: int = 5_000,
53
55
  status_poll_delay: int = 1,
54
56
  upload_chunk_size: int = 25_000_000,
@@ -86,6 +88,11 @@ class AsyncClient:
86
88
  :param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
87
89
  to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
88
90
  number of 2 times.
91
+ :param backoff: The initial backoff time in seconds for the retry mechanism. This is the
92
+ time to wait before the first retry.
93
+ :param backoff_factor: The factor by which the backoff time is multiplied after each retry.
94
+ For example, if the initial backoff is 1 second and the factor is 2, the second
95
+ retry will wait 2 seconds, the third retry will wait 4 seconds, and so on.
89
96
  :param page_size: The number of items to return per page when paginating through results.
90
97
  Defaults to 5000. This is the maximum number of items that can be returned per
91
98
  request. If you pass a value greater than 5000, it will be capped to 5000.
@@ -110,7 +117,14 @@ class AsyncClient:
110
117
  private_key_password=private_key_password,
111
118
  )
112
119
  _client = httpx.AsyncClient(auth=_auth, timeout=timeout, **httpx_kwargs)
113
- self._http = _AsyncHttpService(_client, retry_count, page_size, status_poll_delay)
120
+ self._http = _AsyncHttpService(
121
+ _client,
122
+ retry_count=retry_count,
123
+ backoff=backoff,
124
+ backoff_factor=backoff_factor,
125
+ page_size=page_size,
126
+ poll_delay=status_poll_delay,
127
+ )
114
128
  self._workspace_id = workspace_id
115
129
  self._model_id = model_id
116
130
  self._url = f"https://api.anaplan.com/2/0/workspaces/{workspace_id}/models/{model_id}"
@@ -122,6 +136,9 @@ class AsyncClient:
122
136
  self._cloud_works = _AsyncCloudWorksClient(self._http)
123
137
  self.upload_chunk_size = upload_chunk_size
124
138
  self.allow_file_creation = allow_file_creation
139
+ logger.debug(
140
+ f"Initialized AsyncClient with workspace_id={workspace_id}, model_id={model_id}"
141
+ )
125
142
 
126
143
  @classmethod
127
144
  def from_existing(
@@ -208,16 +225,23 @@ class AsyncClient:
208
225
  )
209
226
  return self._alm_client
210
227
 
211
- async def get_workspaces(self, search_pattern: str | None = None) -> list[Workspace]:
228
+ async def get_workspaces(
229
+ self,
230
+ search_pattern: str | None = None,
231
+ sort_by: Literal["size_allowance", "name"] = "name",
232
+ descending: bool = False,
233
+ ) -> list[Workspace]:
212
234
  """
213
235
  Lists all the Workspaces the authenticated user has access to.
214
236
  :param search_pattern: Optionally filter for specific workspaces. When provided,
215
237
  case-insensitive matches workspaces with names containing this string.
216
238
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
217
239
  When None (default), returns all users.
240
+ :param sort_by: The field to sort the results by.
241
+ :param descending: If True, the results will be sorted in descending order.
218
242
  :return: The List of Workspaces.
219
243
  """
220
- params = {"tenantDetails": "true"}
244
+ params = {"tenantDetails": "true"} | sort_params(sort_by, descending)
221
245
  if search_pattern:
222
246
  params["s"] = search_pattern
223
247
  return [
@@ -227,16 +251,23 @@ class AsyncClient:
227
251
  )
228
252
  ]
229
253
 
230
- async def get_models(self, search_pattern: str | None = None) -> list[Model]:
254
+ async def get_models(
255
+ self,
256
+ search_pattern: str | None = None,
257
+ sort_by: Literal["active_state", "name"] = "name",
258
+ descending: bool = False,
259
+ ) -> list[Model]:
231
260
  """
232
261
  Lists all the Models the authenticated user has access to.
233
262
  :param search_pattern: Optionally filter for specific models. When provided,
234
263
  case-insensitive matches model names containing this string.
235
264
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
236
265
  When None (default), returns all models.
266
+ :param sort_by: The field to sort the results by.
267
+ :param descending: If True, the results will be sorted in descending order.
237
268
  :return: The List of Models.
238
269
  """
239
- params = {"modelDetails": "true"}
270
+ params = {"modelDetails": "true"} | sort_params(sort_by, descending)
240
271
  if search_pattern:
241
272
  params["s"] = search_pattern
242
273
  return [
@@ -260,57 +291,77 @@ class AsyncClient:
260
291
  )
261
292
  return ModelDeletionResult.model_validate(res)
262
293
 
263
- async def get_files(self) -> list[File]:
294
+ async def get_files(
295
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
296
+ ) -> list[File]:
264
297
  """
265
298
  Lists all the Files in the Model.
299
+ :param sort_by: The field to sort the results by.
300
+ :param descending: If True, the results will be sorted in descending order.
266
301
  :return: The List of Files.
267
302
  """
268
- return [
269
- File.model_validate(e)
270
- for e in await self._http.get_paginated(f"{self._url}/files", "files")
271
- ]
303
+ res = await self._http.get_paginated(
304
+ f"{self._url}/files", "files", params=sort_params(sort_by, descending)
305
+ )
306
+ return [File.model_validate(e) for e in res]
272
307
 
273
- async def get_actions(self) -> list[Action]:
308
+ async def get_actions(
309
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
310
+ ) -> list[Action]:
274
311
  """
275
312
  Lists all the Actions in the Model. This will only return the Actions listed under
276
313
  `Other Actions` in Anaplan. For Imports, exports, and processes, see their respective
277
314
  methods instead.
315
+ :param sort_by: The field to sort the results by.
316
+ :param descending: If True, the results will be sorted in descending order.
278
317
  :return: The List of Actions.
279
318
  """
280
- return [
281
- Action.model_validate(e)
282
- for e in await self._http.get_paginated(f"{self._url}/actions", "actions")
283
- ]
319
+ res = await self._http.get_paginated(
320
+ f"{self._url}/actions", "actions", params=sort_params(sort_by, descending)
321
+ )
322
+ return [Action.model_validate(e) for e in res]
284
323
 
285
- async def get_processes(self) -> list[Process]:
324
+ async def get_processes(
325
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
326
+ ) -> list[Process]:
286
327
  """
287
328
  Lists all the Processes in the Model.
329
+ :param sort_by: The field to sort the results by.
330
+ :param descending: If True, the results will be sorted in descending order.
288
331
  :return: The List of Processes.
289
332
  """
290
- return [
291
- Process.model_validate(e)
292
- for e in await self._http.get_paginated(f"{self._url}/processes", "processes")
293
- ]
333
+ res = await self._http.get_paginated(
334
+ f"{self._url}/processes", "processes", params=sort_params(sort_by, descending)
335
+ )
336
+ return [Process.model_validate(e) for e in res]
294
337
 
295
- async def get_imports(self) -> list[Import]:
338
+ async def get_imports(
339
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
340
+ ) -> list[Import]:
296
341
  """
297
342
  Lists all the Imports in the Model.
343
+ :param sort_by: The field to sort the results by.
344
+ :param descending: If True, the results will be sorted in descending order.
298
345
  :return: The List of Imports.
299
346
  """
300
- return [
301
- Import.model_validate(e)
302
- for e in await self._http.get_paginated(f"{self._url}/imports", "imports")
303
- ]
347
+ res = await self._http.get_paginated(
348
+ f"{self._url}/imports", "imports", params=sort_params(sort_by, descending)
349
+ )
350
+ return [Import.model_validate(e) for e in res]
304
351
 
305
- async def get_exports(self) -> list[Export]:
352
+ async def get_exports(
353
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
354
+ ) -> list[Export]:
306
355
  """
307
356
  Lists all the Exports in the Model.
357
+ :param sort_by: The field to sort the results by.
358
+ :param descending: If True, the results will be sorted in descending order.
308
359
  :return: The List of Exports.
309
360
  """
310
- return [
311
- Export.model_validate(e)
312
- for e in await self._http.get_paginated(f"{self._url}/exports", "exports")
313
- ]
361
+ res = await self._http.get_paginated(
362
+ f"{self._url}/exports", "exports", params=sort_params(sort_by, descending)
363
+ )
364
+ return [Export.model_validate(e) for e in res]
314
365
 
315
366
  async def run_action(self, action_id: int, wait_for_completion: bool = True) -> TaskStatus:
316
367
  """
@@ -21,9 +21,7 @@ class _AsyncFlowClient:
21
21
  params = {"myIntegrations": 1 if current_user_only else 0}
22
22
  return [
23
23
  FlowSummary.model_validate(e)
24
- for e in await self._http.get_paginated(
25
- self._url, "integrationFlows", page_size=25, params=params
26
- )
24
+ for e in await self._http.get_paginated(self._url, "integrationFlows", params=params)
27
25
  ]
28
26
 
29
27
  async def get_flow(self, flow_id: str) -> Flow:
@@ -7,6 +7,7 @@ from anaplan_sdk._services import (
7
7
  _AsyncHttpService,
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
@@ -68,23 +69,31 @@ class _AsyncTransactionalClient:
68
69
  )
69
70
  logger.info(f"Closed model '{self._model_id}'.")
70
71
 
71
- async def get_modules(self) -> list[Module]:
72
+ async def get_modules(
73
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
74
+ ) -> list[Module]:
72
75
  """
73
76
  Lists all the Modules in the Model.
77
+ :param sort_by: The field to sort the results by.
78
+ :param descending: If True, the results will be sorted in descending order.
74
79
  :return: The List of Modules.
75
80
  """
76
- return [
77
- Module.model_validate(e)
78
- for e in await self._http.get_paginated(f"{self._url}/modules", "modules")
79
- ]
81
+ res = await self._http.get_paginated(
82
+ f"{self._url}/modules", "modules", params=sort_params(sort_by, descending)
83
+ )
84
+ return [Module.model_validate(e) for e in res]
80
85
 
81
- async def get_views(self) -> list[View]:
86
+ async def get_views(
87
+ self, sort_by: Literal["id", "module_id", "name"] = "id", descending: bool = False
88
+ ) -> list[View]:
82
89
  """
83
90
  Lists all the Views in the Model. This will include all Modules and potentially other saved
84
91
  views.
92
+ :param sort_by: The field to sort the results by.
93
+ :param descending: If True, the results will be sorted in descending order.
85
94
  :return: The List of Views.
86
95
  """
87
- params = {"includesubsidiaryviews": True}
96
+ params = {"includesubsidiaryviews": True} | sort_params(sort_by, descending)
88
97
  return [
89
98
  View.model_validate(e)
90
99
  for e in await self._http.get_paginated(f"{self._url}/views", "views", params=params)
@@ -111,15 +120,19 @@ class _AsyncTransactionalClient:
111
120
  )
112
121
  return [LineItem.model_validate(e) for e in res.get("items", [])]
113
122
 
114
- async def get_lists(self) -> list[List]:
123
+ async def get_lists(
124
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
125
+ ) -> list[List]:
115
126
  """
116
127
  Lists all the Lists in the Model.
128
+ :param sort_by: The field to sort the results by.
129
+ :param descending: If True, the results will be sorted in descending order.
117
130
  :return: All Lists on this model.
118
131
  """
119
- return [
120
- List.model_validate(e)
121
- for e in await self._http.get_paginated(f"{self._url}/lists", "lists")
122
- ]
132
+ res = await self._http.get_paginated(
133
+ f"{self._url}/lists", "lists", params=sort_params(sort_by, descending)
134
+ )
135
+ return [List.model_validate(e) for e in res]
123
136
 
124
137
  async def get_list_metadata(self, list_id: int) -> ListMetadata:
125
138
  """
@@ -50,6 +50,8 @@ class Client:
50
50
  auth: httpx.Auth | None = None,
51
51
  timeout: float | httpx.Timeout = 30,
52
52
  retry_count: int = 2,
53
+ backoff: float = 1.0,
54
+ backoff_factor: float = 2.0,
53
55
  page_size: int = 5_000,
54
56
  status_poll_delay: int = 1,
55
57
  upload_parallel: bool = True,
@@ -87,6 +89,11 @@ class Client:
87
89
  an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
88
90
  :param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
89
91
  to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
92
+ :param backoff: The initial backoff time in seconds for the retry mechanism. This is the
93
+ time to wait before the first retry.
94
+ :param backoff_factor: The factor by which the backoff time is multiplied after each retry.
95
+ For example, if the initial backoff is 1 second and the factor is 2, the second
96
+ retry will wait 2 seconds, the third retry will wait 4 seconds, and so on.
90
97
  number of 2 times.
91
98
  :param page_size: The number of items to return per page when paginating through results.
92
99
  Defaults to 5000. This is the maximum number of items that can be returned per
@@ -113,7 +120,14 @@ class Client:
113
120
  private_key_password=private_key_password,
114
121
  )
115
122
  _client = httpx.Client(auth=auth, timeout=timeout, **httpx_kwargs)
116
- self._http = _HttpService(_client, retry_count, page_size, status_poll_delay)
123
+ self._http = _HttpService(
124
+ _client,
125
+ retry_count=retry_count,
126
+ backoff=backoff,
127
+ backoff_factor=backoff_factor,
128
+ page_size=page_size,
129
+ poll_delay=status_poll_delay,
130
+ )
117
131
  self._retry_count = retry_count
118
132
  self._workspace_id = workspace_id
119
133
  self._model_id = model_id
@@ -129,6 +143,7 @@ class Client:
129
143
  self.upload_parallel = upload_parallel
130
144
  self.upload_chunk_size = upload_chunk_size
131
145
  self.allow_file_creation = allow_file_creation
146
+ logger.debug(f"Initialized Client with workspace_id={workspace_id}, model_id={model_id}")
132
147
 
133
148
  @classmethod
134
149
  def from_existing(
@@ -307,7 +307,7 @@ class _CloudWorksClient:
307
307
  ) -> None:
308
308
  """
309
309
  Update a notification configuration for an integration in CloudWorks. You cannot pass empty
310
- values or nulls to any of the fields If you want to for e.g. override an existing list of
310
+ values or nulls to any of the fields If you want to for e.g. override an existing list of
311
311
  users with an empty one, you must delete the notification configuration and create a new
312
312
  one with only the values you want to keep.
313
313
  :param notification_id: The ID of the notification configuration to update.
@@ -21,9 +21,7 @@ class _FlowClient:
21
21
  params = {"myIntegrations": 1 if current_user_only else 0}
22
22
  return [
23
23
  FlowSummary.model_validate(e)
24
- for e in self._http.get_paginated(
25
- self._url, "integrationFlows", page_size=25, params=params
26
- )
24
+ for e in self._http.get_paginated(self._url, "integrationFlows", params=params)
27
25
  ]
28
26
 
29
27
  def get_flow(self, flow_id: str) -> Flow:
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
2
  import logging
3
- import random
4
3
  import time
5
4
  from asyncio import gather, sleep
6
5
  from concurrent.futures import ThreadPoolExecutor
@@ -11,6 +10,7 @@ from typing import Any, Awaitable, Callable, Coroutine, Iterator, Literal, Type,
11
10
 
12
11
  import httpx
13
12
  from httpx import HTTPError, Response
13
+ from pydantic.alias_generators import to_camel
14
14
 
15
15
  from .exceptions import AnaplanException, AnaplanTimeoutException, InvalidIdentifierException
16
16
  from .models import (
@@ -43,9 +43,24 @@ Task = TypeVar("Task", bound=TaskSummary)
43
43
 
44
44
 
45
45
  class _HttpService:
46
- def __init__(self, client: httpx.Client, retry_count: int, page_size: int, poll_delay: int):
46
+ def __init__(
47
+ self,
48
+ client: httpx.Client,
49
+ *,
50
+ retry_count: int,
51
+ backoff: float,
52
+ backoff_factor: float,
53
+ page_size: int,
54
+ poll_delay: int,
55
+ ):
56
+ logger.debug(
57
+ f"Initializing HttpService with retry_count={retry_count}, "
58
+ f"page_size={page_size}, poll_delay={poll_delay}."
59
+ )
47
60
  self._client = client
48
61
  self._retry_count = retry_count
62
+ self._backoff = backoff
63
+ self._backoff_factor = backoff_factor
49
64
  self._poll_delay = poll_delay
50
65
  self._page_size = min(page_size, 5_000)
51
66
 
@@ -78,42 +93,46 @@ class _HttpService:
78
93
  content = compress(content.encode() if isinstance(content, str) else content)
79
94
  return self.__run_with_retry(self._client.put, url, headers=_gzip_header, content=content)
80
95
 
81
- def get_paginated(
82
- self, url: str, result_key: str, page_size: int = 5_000, **kwargs
83
- ) -> Iterator[dict[str, Any]]:
84
- logger.debug(f"Starting paginated fetch from {url} with page_size={page_size}.")
85
- first_page, total_items = self._get_first_page(url, page_size, result_key, **kwargs)
86
- if total_items <= page_size:
96
+ def poll_task(self, func: Callable[..., Task], *args) -> Task:
97
+ while (result := func(*args)).task_state != "COMPLETE":
98
+ time.sleep(self._poll_delay)
99
+ return result
100
+
101
+ def get_paginated(self, url: str, result_key: str, **kwargs) -> Iterator[dict[str, Any]]:
102
+ logger.debug(f"Starting paginated fetch from {url} with page_size={self._page_size}.")
103
+ first_page, total_items, actual_size = self._get_first_page(url, result_key, **kwargs)
104
+ if total_items <= self._page_size:
87
105
  logger.debug("All items fit in first page, no additional requests needed.")
88
106
  return iter(first_page)
89
107
 
90
- pages_needed = ceil(total_items / page_size)
91
- logger.debug(f"Fetching {pages_needed - 1} additional pages with {page_size} items each.")
108
+ pages_needed = ceil(total_items / actual_size)
109
+ logger.debug(f"Fetching {pages_needed - 1} additional pages with {actual_size} items each.")
92
110
  with ThreadPoolExecutor() as executor:
93
111
  pages = executor.map(
94
- lambda n: self._get_page(url, page_size, n * page_size, result_key, **kwargs),
112
+ lambda n: self._get_page(url, actual_size, n * actual_size, result_key, **kwargs),
95
113
  range(1, pages_needed),
96
114
  )
97
115
  logger.debug(f"Completed paginated fetch of {total_items} total items.")
98
116
  return chain(first_page, *pages)
99
117
 
100
- def poll_task(self, func: Callable[..., Task], *args) -> Task:
101
- while (result := func(*args)).task_state != "COMPLETE":
102
- time.sleep(self._poll_delay)
103
- return result
104
-
105
118
  def _get_page(self, url: str, limit: int, offset: int, result_key: str, **kwargs) -> list:
106
119
  logger.debug(f"Fetching page: offset={offset}, limit={limit} from {url}.")
107
- kwargs["params"] = kwargs.get("params") or {} | {"limit": limit, "offset": offset}
120
+ kwargs["params"] = (kwargs.get("params") or {}) | {"limit": limit, "offset": offset}
108
121
  return self.get(url, **kwargs).get(result_key, [])
109
122
 
110
- def _get_first_page(self, url: str, limit: int, result_key: str, **kwargs) -> tuple[list, int]:
111
- logger.debug(f"Fetching first page with limit={limit} from {url}.")
112
- kwargs["params"] = kwargs.get("params") or {} | {"limit": limit}
123
+ def _get_first_page(self, url: str, result_key: str, **kwargs) -> tuple[list, int, int]:
124
+ logger.debug(f"Fetching first page with limit={self._page_size} from {url}.")
125
+ kwargs["params"] = (kwargs.get("params") or {}) | {"limit": self._page_size}
113
126
  res = self.get(url, **kwargs)
114
127
  total_items, first_page = res["meta"]["paging"]["totalSize"], res.get(result_key, [])
128
+ actual_page_size = res["meta"]["paging"]["currentPageSize"]
129
+ if actual_page_size < self._page_size and not actual_page_size == total_items:
130
+ logger.warning(
131
+ f"Page size {self._page_size} was silently truncated to {actual_page_size}."
132
+ f"Using the server-side enforced page size {actual_page_size} for further requests."
133
+ )
115
134
  logger.debug(f"Found {total_items} total items, retrieved {len(first_page)} in first page.")
116
- return first_page, total_items
135
+ return first_page, total_items, actual_page_size
117
136
 
118
137
  def __run_with_retry(self, func: Callable[..., Response], *args, **kwargs) -> Response:
119
138
  for i in range(max(self._retry_count, 1)):
@@ -122,7 +141,7 @@ class _HttpService:
122
141
  if response.status_code == 429:
123
142
  if i >= self._retry_count - 1:
124
143
  raise AnaplanException("Rate limit exceeded.")
125
- backoff_time = max(i, 1) * random.randint(2, 5)
144
+ backoff_time = self._backoff * (self._backoff_factor if i > 0 else 1)
126
145
  logger.warning(f"Rate limited. Retrying in {backoff_time} seconds.")
127
146
  time.sleep(backoff_time)
128
147
  continue
@@ -139,10 +158,23 @@ class _HttpService:
139
158
 
140
159
  class _AsyncHttpService:
141
160
  def __init__(
142
- self, client: httpx.AsyncClient, retry_count: int, page_size: int, poll_delay: int
161
+ self,
162
+ client: httpx.AsyncClient,
163
+ *,
164
+ retry_count: int,
165
+ backoff: float,
166
+ backoff_factor: float,
167
+ page_size: int,
168
+ poll_delay: int,
143
169
  ):
170
+ logger.debug(
171
+ f"Initializing AsyncHttpService with retry_count={retry_count}, "
172
+ f"page_size={page_size}, poll_delay={poll_delay}."
173
+ )
144
174
  self._client = client
145
175
  self._retry_count = retry_count
176
+ self._backoff = backoff
177
+ self._backoff_factor = backoff_factor
146
178
  self._poll_delay = poll_delay
147
179
  self._page_size = min(page_size, 5_000)
148
180
 
@@ -179,42 +211,44 @@ class _AsyncHttpService:
179
211
  self._client.put, url, headers=_gzip_header, content=content
180
212
  )
181
213
 
182
- async def get_paginated(
183
- self, url: str, result_key: str, page_size: int = 5_000, **kwargs
184
- ) -> Iterator[dict[str, Any]]:
185
- logger.debug(f"Starting paginated fetch from {url} with page_size={page_size}.")
186
- first_page, total_items = await self._get_first_page(url, page_size, result_key, **kwargs)
187
- if total_items <= page_size:
214
+ async def poll_task(self, func: Callable[..., Awaitable[Task]], *args) -> Task:
215
+ while (result := await func(*args)).task_state != "COMPLETE":
216
+ await sleep(self._poll_delay)
217
+ return result
218
+
219
+ async def get_paginated(self, url: str, result_key: str, **kwargs) -> Iterator[dict[str, Any]]:
220
+ logger.debug(f"Starting paginated fetch from {url} with page_size={self._page_size}.")
221
+ first_page, total_items, actual_size = await self._get_first_page(url, result_key, **kwargs)
222
+ if total_items <= self._page_size:
188
223
  logger.debug("All items fit in first page, no additional requests needed.")
189
224
  return iter(first_page)
190
225
  pages = await gather(
191
226
  *(
192
- self._get_page(url, page_size, n * page_size, result_key, **kwargs)
193
- for n in range(1, ceil(total_items / page_size))
227
+ self._get_page(url, actual_size, n * actual_size, result_key, **kwargs)
228
+ for n in range(1, ceil(total_items / actual_size))
194
229
  )
195
230
  )
196
231
  logger.debug(f"Completed paginated fetch of {total_items} total items.")
197
232
  return chain(first_page, *pages)
198
233
 
199
- async def poll_task(self, func: Callable[..., Awaitable[Task]], *args) -> Task:
200
- while (result := await func(*args)).task_state != "COMPLETE":
201
- await sleep(self._poll_delay)
202
- return result
203
-
204
234
  async def _get_page(self, url: str, limit: int, offset: int, result_key: str, **kwargs) -> list:
205
235
  logger.debug(f"Fetching page: offset={offset}, limit={limit} from {url}.")
206
- kwargs["params"] = kwargs.get("params") or {} | {"limit": limit, "offset": offset}
236
+ kwargs["params"] = (kwargs.get("params") or {}) | {"limit": limit, "offset": offset}
207
237
  return (await self.get(url, **kwargs)).get(result_key, [])
208
238
 
209
- async def _get_first_page(
210
- self, url: str, limit: int, result_key: str, **kwargs
211
- ) -> tuple[list, int]:
212
- logger.debug(f"Fetching first page with limit={limit} from {url}.")
213
- kwargs["params"] = kwargs.get("params") or {} | {"limit": limit}
239
+ async def _get_first_page(self, url: str, result_key: str, **kwargs) -> tuple[list, int, int]:
240
+ logger.debug(f"Fetching first page with limit={self._page_size} from {url}.")
241
+ kwargs["params"] = (kwargs.get("params") or {}) | {"limit": self._page_size}
214
242
  res = await self.get(url, **kwargs)
215
243
  total_items, first_page = res["meta"]["paging"]["totalSize"], res.get(result_key, [])
244
+ actual_page_size = res["meta"]["paging"]["currentPageSize"]
245
+ if actual_page_size < self._page_size and not actual_page_size == total_items:
246
+ logger.warning(
247
+ f"Page size {self._page_size} was silently truncated to {actual_page_size}."
248
+ f"Using the server-side enforced page size {actual_page_size} for further requests."
249
+ )
216
250
  logger.debug(f"Found {total_items} total items, retrieved {len(first_page)} in first page.")
217
- return first_page, total_items
251
+ return first_page, total_items, actual_page_size
218
252
 
219
253
  async def _run_with_retry(
220
254
  self, func: Callable[..., Coroutine[Any, Any, Response]], *args, **kwargs
@@ -225,7 +259,7 @@ class _AsyncHttpService:
225
259
  if response.status_code == 429:
226
260
  if i >= self._retry_count - 1:
227
261
  raise AnaplanException("Rate limit exceeded.")
228
- backoff_time = (i + 1) * random.randint(3, 5)
262
+ backoff_time = self._backoff * (self._backoff_factor if i > 0 else 1)
229
263
  logger.warning(f"Rate limited. Retrying in {backoff_time} seconds.")
230
264
  await asyncio.sleep(backoff_time)
231
265
  continue
@@ -240,6 +274,16 @@ class _AsyncHttpService:
240
274
  raise AnaplanException("Exhausted all retries without a successful response or Error.")
241
275
 
242
276
 
277
+ def sort_params(sort_by: str, descending: bool) -> dict[str, str | bool]:
278
+ """
279
+ Construct search parameters for sorting. This also converts snake_case to camelCase.
280
+ :param sort_by: The field to sort by, optionally in snake_case.
281
+ :param descending: Whether to sort in descending order.
282
+ :return: A dictionary of search parameters in Anaplan's expected format.
283
+ """
284
+ return {"sort": f"{'-' if descending else '+'}{to_camel(sort_by)}"}
285
+
286
+
243
287
  def construct_payload(model: Type[T], body: T | dict[str, Any]) -> dict[str, Any]:
244
288
  """
245
289
  Construct a payload for the given model and body.
@@ -141,7 +141,9 @@ class ConnectionInput(AnaplanModel):
141
141
 
142
142
 
143
143
  class Connection(_VersionedBaseModel):
144
- connection_id: str = Field(description="The unique identifier of this connection.")
144
+ id: str = Field(
145
+ validation_alias="connectionId", description="The unique identifier of this connection."
146
+ )
145
147
  connection_type: ConnectionType = Field(description="The type of this connection.")
146
148
  body: AzureBlobConnectionInfo | AmazonS3ConnectionInfo | GoogleBigQueryConnectionInfo = Field(
147
149
  description="Connection information."
@@ -237,7 +239,9 @@ class _BaseIntegration(_VersionedBaseModel):
237
239
 
238
240
 
239
241
  class Integration(_BaseIntegration):
240
- integration_id: str = Field(description="The unique identifier of this integration.")
242
+ id: str = Field(
243
+ validation_alias="integrationId", description="The unique identifier of this integration."
244
+ )
241
245
  integration_type: Literal["Import", "Export", "Process"] = Field(
242
246
  description="The type of this integration."
243
247
  )