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.
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/PKG-INFO +2 -2
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_bulk.py +83 -32
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_cw_flow.py +1 -3
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_transactional.py +25 -12
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_bulk.py +16 -1
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_cloud_works.py +1 -1
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_cw_flow.py +1 -3
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_services.py +88 -44
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/cloud_works.py +6 -2
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/pyproject.toml +110 -108
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/conftest.py +1 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_client.py +2 -1
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/conftest.py +1 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_client.py +2 -1
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/uv.lock +1469 -1469
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.github/dependabot.yml +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.github/workflows/docs.yml +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.github/workflows/lint.yml +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.github/workflows/tests.yml +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.gitignore +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/.pre-commit-config.yaml +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/LICENSE +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/README.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/__init__.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/__init__.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_alm.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_audit.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_async_clients/_cloud_works.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_auth.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/__init__.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_alm.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_audit.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_clients/_transactional.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/_oauth.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/exceptions.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/__init__.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/_alm.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/_base.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/_bulk.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/_transactional.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/anaplan_sdk/models/flows.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_alm_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_audit_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_cw_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_flows_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_oauth_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/async/async_transactional_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/exceptions.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/alm.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/bulk.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/cloud_works.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/flows.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/models/transactional.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_alm_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_audit_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_cw_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_flows_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_oauth_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/api/sync/sync_transactional_client.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/assets/overview.html +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/concepts.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/css/styles.css +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/alm.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/audit.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/authentication.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/bulk.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/bulk_vs_transactional.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/cloud_works.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/index.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/logging.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/multiple_models.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/guides/transactional.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/img/anaplan-sdk.webp +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/index.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/installation.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/assets/hljs.js +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/assets/hljs.min.js +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/assets/python.js +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/assets/python.min.js +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/highlight.js +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/js/highlight.min.js +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/docs/quickstart.md +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/mkdocs.yml +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_alm_client.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_audit_client.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_cloud_works_client.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_flows_client.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/async/test_async_transactional_client.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/conftest.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_alm_client.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_audit_client.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_cloud_works_client.py +0 -0
- {anaplan_sdk-0.5.0a2 → anaplan_sdk-0.5.0a3}/tests/sync/test_flows_client.py +0 -0
- {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.
|
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(
|
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(
|
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(
|
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(
|
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
|
-
|
269
|
-
|
270
|
-
|
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(
|
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
|
-
|
281
|
-
|
282
|
-
|
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(
|
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
|
-
|
291
|
-
|
292
|
-
|
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(
|
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
|
-
|
301
|
-
|
302
|
-
|
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(
|
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
|
-
|
311
|
-
|
312
|
-
|
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(
|
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
|
-
|
77
|
-
|
78
|
-
|
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(
|
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(
|
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
|
-
|
120
|
-
|
121
|
-
|
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(
|
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
|
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__(
|
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
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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 /
|
91
|
-
logger.debug(f"Fetching {pages_needed - 1} additional pages with {
|
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,
|
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,
|
111
|
-
logger.debug(f"Fetching first page with limit={
|
112
|
-
kwargs["params"] = kwargs.get("params") or {} | {"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 =
|
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,
|
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
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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,
|
193
|
-
for n in range(1, ceil(total_items /
|
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
|
-
|
211
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
)
|