anaplan-sdk 0.5.0a2__py3-none-any.whl → 0.5.0a4__py3-none-any.whl
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/_async_clients/_alm.py +12 -4
- anaplan_sdk/_async_clients/_audit.py +17 -9
- anaplan_sdk/_async_clients/_bulk.py +85 -44
- anaplan_sdk/_async_clients/_cloud_works.py +4 -3
- anaplan_sdk/_async_clients/_cw_flow.py +1 -3
- anaplan_sdk/_async_clients/_transactional.py +23 -12
- anaplan_sdk/_clients/_alm.py +12 -4
- anaplan_sdk/_clients/_audit.py +19 -11
- anaplan_sdk/_clients/_bulk.py +87 -46
- anaplan_sdk/_clients/_cloud_works.py +5 -4
- anaplan_sdk/_clients/_cw_flow.py +1 -3
- anaplan_sdk/_clients/_transactional.py +25 -11
- anaplan_sdk/_services.py +103 -45
- anaplan_sdk/models/cloud_works.py +6 -2
- {anaplan_sdk-0.5.0a2.dist-info → anaplan_sdk-0.5.0a4.dist-info}/METADATA +2 -2
- anaplan_sdk-0.5.0a4.dist-info/RECORD +30 -0
- anaplan_sdk-0.5.0a2.dist-info/RECORD +0 -30
- {anaplan_sdk-0.5.0a2.dist-info → anaplan_sdk-0.5.0a4.dist-info}/WHEEL +0 -0
- {anaplan_sdk-0.5.0a2.dist-info → anaplan_sdk-0.5.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -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(
|
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.
|
38
|
-
|
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(
|
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 =
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
"""
|
@@ -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,
|
@@ -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
|
|
@@ -49,6 +51,8 @@ class AsyncClient:
|
|
49
51
|
auth: httpx.Auth | None = None,
|
50
52
|
timeout: float | httpx.Timeout = 30,
|
51
53
|
retry_count: int = 2,
|
54
|
+
backoff: float = 1.0,
|
55
|
+
backoff_factor: float = 2.0,
|
52
56
|
page_size: int = 5_000,
|
53
57
|
status_poll_delay: int = 1,
|
54
58
|
upload_chunk_size: int = 25_000_000,
|
@@ -86,6 +90,11 @@ class AsyncClient:
|
|
86
90
|
:param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
|
87
91
|
to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
|
88
92
|
number of 2 times.
|
93
|
+
:param backoff: The initial backoff time in seconds for the retry mechanism. This is the
|
94
|
+
time to wait before the first retry.
|
95
|
+
:param backoff_factor: The factor by which the backoff time is multiplied after each retry.
|
96
|
+
For example, if the initial backoff is 1 second and the factor is 2, the second
|
97
|
+
retry will wait 2 seconds, the third retry will wait 4 seconds, and so on.
|
89
98
|
:param page_size: The number of items to return per page when paginating through results.
|
90
99
|
Defaults to 5000. This is the maximum number of items that can be returned per
|
91
100
|
request. If you pass a value greater than 5000, it will be capped to 5000.
|
@@ -110,7 +119,14 @@ class AsyncClient:
|
|
110
119
|
private_key_password=private_key_password,
|
111
120
|
)
|
112
121
|
_client = httpx.AsyncClient(auth=_auth, timeout=timeout, **httpx_kwargs)
|
113
|
-
self._http = _AsyncHttpService(
|
122
|
+
self._http = _AsyncHttpService(
|
123
|
+
_client,
|
124
|
+
retry_count=retry_count,
|
125
|
+
backoff=backoff,
|
126
|
+
backoff_factor=backoff_factor,
|
127
|
+
page_size=page_size,
|
128
|
+
poll_delay=status_poll_delay,
|
129
|
+
)
|
114
130
|
self._workspace_id = workspace_id
|
115
131
|
self._model_id = model_id
|
116
132
|
self._url = f"https://api.anaplan.com/2/0/workspaces/{workspace_id}/models/{model_id}"
|
@@ -122,6 +138,9 @@ class AsyncClient:
|
|
122
138
|
self._cloud_works = _AsyncCloudWorksClient(self._http)
|
123
139
|
self.upload_chunk_size = upload_chunk_size
|
124
140
|
self.allow_file_creation = allow_file_creation
|
141
|
+
logger.debug(
|
142
|
+
f"Initialized AsyncClient with workspace_id={workspace_id}, model_id={model_id}"
|
143
|
+
)
|
125
144
|
|
126
145
|
@classmethod
|
127
146
|
def from_existing(
|
@@ -208,43 +227,53 @@ class AsyncClient:
|
|
208
227
|
)
|
209
228
|
return self._alm_client
|
210
229
|
|
211
|
-
async def get_workspaces(
|
230
|
+
async def get_workspaces(
|
231
|
+
self,
|
232
|
+
search_pattern: str | None = None,
|
233
|
+
sort_by: Literal["size_allowance", "name"] | None = None,
|
234
|
+
descending: bool = False,
|
235
|
+
) -> list[Workspace]:
|
212
236
|
"""
|
213
237
|
Lists all the Workspaces the authenticated user has access to.
|
214
238
|
:param search_pattern: Optionally filter for specific workspaces. When provided,
|
215
239
|
case-insensitive matches workspaces with names containing this string.
|
216
240
|
You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
|
217
241
|
When None (default), returns all users.
|
242
|
+
:param sort_by: The field to sort the results by.
|
243
|
+
:param descending: If True, the results will be sorted in descending order.
|
218
244
|
:return: The List of Workspaces.
|
219
245
|
"""
|
220
|
-
params = {"tenantDetails": "true"}
|
246
|
+
params = {"tenantDetails": "true"} | sort_params(sort_by, descending)
|
221
247
|
if search_pattern:
|
222
248
|
params["s"] = search_pattern
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
)
|
228
|
-
]
|
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]
|
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"] | None = None,
|
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
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
)
|
247
|
-
]
|
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]
|
248
277
|
|
249
278
|
async def delete_models(self, model_ids: list[str]) -> ModelDeletionResult:
|
250
279
|
"""
|
@@ -260,57 +289,69 @@ class AsyncClient:
|
|
260
289
|
)
|
261
290
|
return ModelDeletionResult.model_validate(res)
|
262
291
|
|
263
|
-
async def get_files(self) -> list[File]:
|
292
|
+
async def get_files(self, sort_by: SortBy = None, descending: bool = False) -> list[File]:
|
264
293
|
"""
|
265
294
|
Lists all the Files in the Model.
|
295
|
+
:param sort_by: The field to sort the results by.
|
296
|
+
:param descending: If True, the results will be sorted in descending order.
|
266
297
|
:return: The List of Files.
|
267
298
|
"""
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
]
|
299
|
+
res = await self._http.get_paginated(
|
300
|
+
f"{self._url}/files", "files", params=sort_params(sort_by, descending)
|
301
|
+
)
|
302
|
+
return [File.model_validate(e) for e in res]
|
272
303
|
|
273
|
-
async def get_actions(self) -> list[Action]:
|
304
|
+
async def get_actions(self, sort_by: SortBy = None, descending: bool = False) -> list[Action]:
|
274
305
|
"""
|
275
306
|
Lists all the Actions in the Model. This will only return the Actions listed under
|
276
307
|
`Other Actions` in Anaplan. For Imports, exports, and processes, see their respective
|
277
308
|
methods instead.
|
309
|
+
:param sort_by: The field to sort the results by.
|
310
|
+
:param descending: If True, the results will be sorted in descending order.
|
278
311
|
:return: The List of Actions.
|
279
312
|
"""
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
]
|
313
|
+
res = await self._http.get_paginated(
|
314
|
+
f"{self._url}/actions", "actions", params=sort_params(sort_by, descending)
|
315
|
+
)
|
316
|
+
return [Action.model_validate(e) for e in res]
|
284
317
|
|
285
|
-
async def get_processes(
|
318
|
+
async def get_processes(
|
319
|
+
self, sort_by: SortBy = None, descending: bool = False
|
320
|
+
) -> list[Process]:
|
286
321
|
"""
|
287
322
|
Lists all the Processes in the Model.
|
323
|
+
:param sort_by: The field to sort the results by.
|
324
|
+
:param descending: If True, the results will be sorted in descending order.
|
288
325
|
:return: The List of Processes.
|
289
326
|
"""
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
]
|
327
|
+
res = await self._http.get_paginated(
|
328
|
+
f"{self._url}/processes", "processes", params=sort_params(sort_by, descending)
|
329
|
+
)
|
330
|
+
return [Process.model_validate(e) for e in res]
|
294
331
|
|
295
|
-
async def get_imports(self) -> list[Import]:
|
332
|
+
async def get_imports(self, sort_by: SortBy = None, descending: bool = False) -> list[Import]:
|
296
333
|
"""
|
297
334
|
Lists all the Imports in the Model.
|
335
|
+
:param sort_by: The field to sort the results by.
|
336
|
+
:param descending: If True, the results will be sorted in descending order.
|
298
337
|
:return: The List of Imports.
|
299
338
|
"""
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
]
|
339
|
+
res = await self._http.get_paginated(
|
340
|
+
f"{self._url}/imports", "imports", params=sort_params(sort_by, descending)
|
341
|
+
)
|
342
|
+
return [Import.model_validate(e) for e in res]
|
304
343
|
|
305
|
-
async def get_exports(self) -> list[Export]:
|
344
|
+
async def get_exports(self, sort_by: SortBy = None, descending: bool = False) -> list[Export]:
|
306
345
|
"""
|
307
346
|
Lists all the Exports in the Model.
|
347
|
+
:param sort_by: The field to sort the results by.
|
348
|
+
:param descending: If True, the results will be sorted in descending order.
|
308
349
|
:return: The List of Exports.
|
309
350
|
"""
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
]
|
351
|
+
res = await self._http.get_paginated(
|
352
|
+
f"{self._url}/exports", "exports", params=sort_params(sort_by, descending)
|
353
|
+
)
|
354
|
+
return [Export.model_validate(e) for e in res]
|
314
355
|
|
315
356
|
async def run_action(self, action_id: int, wait_for_completion: bool = True) -> TaskStatus:
|
316
357
|
"""
|
@@ -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,
|
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
|
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": "
|
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)
|
@@ -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
|
@@ -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
|
|
@@ -68,23 +71,29 @@ class _AsyncTransactionalClient:
|
|
68
71
|
)
|
69
72
|
logger.info(f"Closed model '{self._model_id}'.")
|
70
73
|
|
71
|
-
async def get_modules(self) -> list[Module]:
|
74
|
+
async def get_modules(self, sort_by: SortBy = None, descending: bool = False) -> 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"] | None = None, 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,17 @@ 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(self, sort_by: SortBy = None, descending: bool = False) -> list[List]:
|
115
124
|
"""
|
116
125
|
Lists all the Lists in the Model.
|
126
|
+
:param sort_by: The field to sort the results by.
|
127
|
+
:param descending: If True, the results will be sorted in descending order.
|
117
128
|
:return: All Lists on this model.
|
118
129
|
"""
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
]
|
130
|
+
res = await self._http.get_paginated(
|
131
|
+
f"{self._url}/lists", "lists", params=sort_params(sort_by, descending)
|
132
|
+
)
|
133
|
+
return [List.model_validate(e) for e in res]
|
123
134
|
|
124
135
|
async def get_list_metadata(self, list_id: int) -> ListMetadata:
|
125
136
|
"""
|
anaplan_sdk/_clients/_alm.py
CHANGED
@@ -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(
|
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.
|
38
|
-
|
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
|
"""
|
anaplan_sdk/_clients/_audit.py
CHANGED
@@ -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(
|
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:
|
19
|
-
users with emails containing this string.
|
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 =
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
"""
|
anaplan_sdk/_clients/_bulk.py
CHANGED
@@ -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
|
|
@@ -50,6 +52,8 @@ class Client:
|
|
50
52
|
auth: httpx.Auth | None = None,
|
51
53
|
timeout: float | httpx.Timeout = 30,
|
52
54
|
retry_count: int = 2,
|
55
|
+
backoff: float = 1.0,
|
56
|
+
backoff_factor: float = 2.0,
|
53
57
|
page_size: int = 5_000,
|
54
58
|
status_poll_delay: int = 1,
|
55
59
|
upload_parallel: bool = True,
|
@@ -87,6 +91,11 @@ class Client:
|
|
87
91
|
an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
|
88
92
|
:param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
|
89
93
|
to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
|
94
|
+
:param backoff: The initial backoff time in seconds for the retry mechanism. This is the
|
95
|
+
time to wait before the first retry.
|
96
|
+
:param backoff_factor: The factor by which the backoff time is multiplied after each retry.
|
97
|
+
For example, if the initial backoff is 1 second and the factor is 2, the second
|
98
|
+
retry will wait 2 seconds, the third retry will wait 4 seconds, and so on.
|
90
99
|
number of 2 times.
|
91
100
|
:param page_size: The number of items to return per page when paginating through results.
|
92
101
|
Defaults to 5000. This is the maximum number of items that can be returned per
|
@@ -113,7 +122,14 @@ class Client:
|
|
113
122
|
private_key_password=private_key_password,
|
114
123
|
)
|
115
124
|
_client = httpx.Client(auth=auth, timeout=timeout, **httpx_kwargs)
|
116
|
-
self._http = _HttpService(
|
125
|
+
self._http = _HttpService(
|
126
|
+
_client,
|
127
|
+
retry_count=retry_count,
|
128
|
+
backoff=backoff,
|
129
|
+
backoff_factor=backoff_factor,
|
130
|
+
page_size=page_size,
|
131
|
+
poll_delay=status_poll_delay,
|
132
|
+
)
|
117
133
|
self._retry_count = retry_count
|
118
134
|
self._workspace_id = workspace_id
|
119
135
|
self._model_id = model_id
|
@@ -129,6 +145,7 @@ class Client:
|
|
129
145
|
self.upload_parallel = upload_parallel
|
130
146
|
self.upload_chunk_size = upload_chunk_size
|
131
147
|
self.allow_file_creation = allow_file_creation
|
148
|
+
logger.debug(f"Initialized Client with workspace_id={workspace_id}, model_id={model_id}")
|
132
149
|
|
133
150
|
@classmethod
|
134
151
|
def from_existing(
|
@@ -215,42 +232,53 @@ class Client:
|
|
215
232
|
)
|
216
233
|
return self._alm_client
|
217
234
|
|
218
|
-
def get_workspaces(
|
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]:
|
219
241
|
"""
|
220
242
|
Lists all the Workspaces the authenticated user has access to.
|
221
|
-
:param search_pattern:
|
222
|
-
matches workspaces with names containing this string.
|
223
|
-
|
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.
|
224
249
|
:return: The List of Workspaces.
|
225
250
|
"""
|
226
|
-
params = {"tenantDetails": "true"}
|
251
|
+
params = {"tenantDetails": "true"} | sort_params(sort_by, descending)
|
227
252
|
if search_pattern:
|
228
253
|
params["s"] = search_pattern
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
)
|
234
|
-
]
|
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]
|
235
258
|
|
236
|
-
def get_models(
|
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]:
|
237
265
|
"""
|
238
266
|
Lists all the Models the authenticated user has access to.
|
239
267
|
:param search_pattern: Optionally filter for specific models. When provided,
|
240
268
|
case-insensitive matches model names containing this string.
|
241
269
|
You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
|
242
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.
|
243
273
|
:return: The List of Models.
|
244
274
|
"""
|
245
|
-
params = {"modelDetails": "true"}
|
275
|
+
params = {"modelDetails": "true"} | sort_params(sort_by, descending)
|
246
276
|
if search_pattern:
|
247
277
|
params["s"] = search_pattern
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
)
|
253
|
-
]
|
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]
|
254
282
|
|
255
283
|
def delete_models(self, model_ids: list[str]) -> ModelDeletionResult:
|
256
284
|
"""
|
@@ -266,56 +294,69 @@ class Client:
|
|
266
294
|
)
|
267
295
|
return ModelDeletionResult.model_validate(res)
|
268
296
|
|
269
|
-
def get_files(
|
297
|
+
def get_files(
|
298
|
+
self, sort_by: Literal["id", "name"] = "id", descending: bool = False
|
299
|
+
) -> list[File]:
|
270
300
|
"""
|
271
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.
|
272
304
|
:return: The List of Files.
|
273
305
|
"""
|
274
|
-
|
275
|
-
|
276
|
-
|
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]
|
277
310
|
|
278
|
-
def get_actions(self) -> list[Action]:
|
311
|
+
def get_actions(self, sort_by: SortBy = None, descending: bool = False) -> list[Action]:
|
279
312
|
"""
|
280
313
|
Lists all the Actions in the Model. This will only return the Actions listed under
|
281
314
|
`Other Actions` in Anaplan. For Imports, exports, and processes, see their respective
|
282
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.
|
283
318
|
:return: The List of Actions.
|
284
319
|
"""
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
]
|
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]
|
289
324
|
|
290
|
-
def get_processes(self) -> list[Process]:
|
325
|
+
def get_processes(self, sort_by: SortBy = None, descending: bool = False) -> list[Process]:
|
291
326
|
"""
|
292
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.
|
293
330
|
:return: The List of Processes.
|
294
331
|
"""
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
]
|
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]
|
299
336
|
|
300
|
-
def get_imports(self) -> list[Import]:
|
337
|
+
def get_imports(self, sort_by: SortBy = None, descending: bool = False) -> list[Import]:
|
301
338
|
"""
|
302
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.
|
303
342
|
:return: The List of Imports.
|
304
343
|
"""
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
]
|
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]
|
309
348
|
|
310
|
-
def get_exports(self) -> list[Export]:
|
349
|
+
def get_exports(self, sort_by: SortBy = None, descending: bool = False) -> list[Export]:
|
311
350
|
"""
|
312
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.
|
313
354
|
:return: The List of Exports.
|
314
355
|
"""
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
]
|
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]
|
319
360
|
|
320
361
|
def run_action(self, action_id: int, wait_for_completion: bool = True) -> TaskStatus:
|
321
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,
|
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
|
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": "
|
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)
|
@@ -307,7 +308,7 @@ class _CloudWorksClient:
|
|
307
308
|
) -> None:
|
308
309
|
"""
|
309
310
|
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
|
311
|
+
values or nulls to any of the fields If you want to for e.g. override an existing list of
|
311
312
|
users with an empty one, you must delete the notification configuration and create a new
|
312
313
|
one with only the values you want to keep.
|
313
314
|
:param notification_id: The ID of the notification configuration to update.
|
anaplan_sdk/_clients/_cw_flow.py
CHANGED
@@ -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:
|
@@ -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
|
-
|
75
|
-
|
76
|
-
|
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(
|
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)
|
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
|
-
|
116
|
-
|
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
|
"""
|
anaplan_sdk/_services.py
CHANGED
@@ -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 (
|
@@ -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"}
|
@@ -43,9 +52,24 @@ Task = TypeVar("Task", bound=TaskSummary)
|
|
43
52
|
|
44
53
|
|
45
54
|
class _HttpService:
|
46
|
-
def __init__(
|
55
|
+
def __init__(
|
56
|
+
self,
|
57
|
+
client: httpx.Client,
|
58
|
+
*,
|
59
|
+
retry_count: int,
|
60
|
+
backoff: float,
|
61
|
+
backoff_factor: float,
|
62
|
+
page_size: int,
|
63
|
+
poll_delay: int,
|
64
|
+
):
|
65
|
+
logger.debug(
|
66
|
+
f"Initializing HttpService with retry_count={retry_count}, "
|
67
|
+
f"page_size={page_size}, poll_delay={poll_delay}."
|
68
|
+
)
|
47
69
|
self._client = client
|
48
70
|
self._retry_count = retry_count
|
71
|
+
self._backoff = backoff
|
72
|
+
self._backoff_factor = backoff_factor
|
49
73
|
self._poll_delay = poll_delay
|
50
74
|
self._page_size = min(page_size, 5_000)
|
51
75
|
|
@@ -78,42 +102,47 @@ class _HttpService:
|
|
78
102
|
content = compress(content.encode() if isinstance(content, str) else content)
|
79
103
|
return self.__run_with_retry(self._client.put, url, headers=_gzip_header, content=content)
|
80
104
|
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
105
|
+
def poll_task(self, func: Callable[..., Task], *args) -> Task:
|
106
|
+
while (result := func(*args)).task_state != "COMPLETE":
|
107
|
+
time.sleep(self._poll_delay)
|
108
|
+
return result
|
109
|
+
|
110
|
+
def get_paginated(self, url: str, result_key: str, **kwargs) -> Iterator[dict[str, Any]]:
|
111
|
+
logger.debug(f"Starting paginated fetch from {url} with page_size={self._page_size}.")
|
112
|
+
first_page, total_items, actual_size = self._get_first_page(url, result_key, **kwargs)
|
113
|
+
if total_items <= self._page_size:
|
87
114
|
logger.debug("All items fit in first page, no additional requests needed.")
|
88
115
|
return iter(first_page)
|
89
|
-
|
90
|
-
|
91
|
-
|
116
|
+
if kwargs and (kwargs.get("params") or {}).get("sort", None):
|
117
|
+
logger.warning(SORT_WARNING)
|
118
|
+
pages_needed = ceil(total_items / actual_size)
|
119
|
+
logger.debug(f"Fetching {pages_needed - 1} additional pages with {actual_size} items each.")
|
92
120
|
with ThreadPoolExecutor() as executor:
|
93
121
|
pages = executor.map(
|
94
|
-
lambda n: self._get_page(url,
|
122
|
+
lambda n: self._get_page(url, actual_size, n * actual_size, result_key, **kwargs),
|
95
123
|
range(1, pages_needed),
|
96
124
|
)
|
97
125
|
logger.debug(f"Completed paginated fetch of {total_items} total items.")
|
98
126
|
return chain(first_page, *pages)
|
99
127
|
|
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
128
|
def _get_page(self, url: str, limit: int, offset: int, result_key: str, **kwargs) -> list:
|
106
129
|
logger.debug(f"Fetching page: offset={offset}, limit={limit} from {url}.")
|
107
|
-
kwargs["params"] = kwargs.get("params") or {} | {"limit": limit, "offset": offset}
|
130
|
+
kwargs["params"] = (kwargs.get("params") or {}) | {"limit": limit, "offset": offset}
|
108
131
|
return self.get(url, **kwargs).get(result_key, [])
|
109
132
|
|
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":
|
133
|
+
def _get_first_page(self, url: str, result_key: str, **kwargs) -> tuple[list, int, int]:
|
134
|
+
logger.debug(f"Fetching first page with limit={self._page_size} from {url}.")
|
135
|
+
kwargs["params"] = (kwargs.get("params") or {}) | {"limit": self._page_size}
|
113
136
|
res = self.get(url, **kwargs)
|
114
137
|
total_items, first_page = res["meta"]["paging"]["totalSize"], res.get(result_key, [])
|
138
|
+
actual_page_size = res["meta"]["paging"]["currentPageSize"]
|
139
|
+
if actual_page_size < self._page_size and not actual_page_size == total_items:
|
140
|
+
logger.warning(
|
141
|
+
f"Page size {self._page_size} was silently truncated to {actual_page_size}."
|
142
|
+
f"Using the server-side enforced page size {actual_page_size} for further requests."
|
143
|
+
)
|
115
144
|
logger.debug(f"Found {total_items} total items, retrieved {len(first_page)} in first page.")
|
116
|
-
return first_page, total_items
|
145
|
+
return first_page, total_items, actual_page_size
|
117
146
|
|
118
147
|
def __run_with_retry(self, func: Callable[..., Response], *args, **kwargs) -> Response:
|
119
148
|
for i in range(max(self._retry_count, 1)):
|
@@ -122,7 +151,7 @@ class _HttpService:
|
|
122
151
|
if response.status_code == 429:
|
123
152
|
if i >= self._retry_count - 1:
|
124
153
|
raise AnaplanException("Rate limit exceeded.")
|
125
|
-
backoff_time =
|
154
|
+
backoff_time = self._backoff * (self._backoff_factor if i > 0 else 1)
|
126
155
|
logger.warning(f"Rate limited. Retrying in {backoff_time} seconds.")
|
127
156
|
time.sleep(backoff_time)
|
128
157
|
continue
|
@@ -139,10 +168,23 @@ class _HttpService:
|
|
139
168
|
|
140
169
|
class _AsyncHttpService:
|
141
170
|
def __init__(
|
142
|
-
self,
|
171
|
+
self,
|
172
|
+
client: httpx.AsyncClient,
|
173
|
+
*,
|
174
|
+
retry_count: int,
|
175
|
+
backoff: float,
|
176
|
+
backoff_factor: float,
|
177
|
+
page_size: int,
|
178
|
+
poll_delay: int,
|
143
179
|
):
|
180
|
+
logger.debug(
|
181
|
+
f"Initializing AsyncHttpService with retry_count={retry_count}, "
|
182
|
+
f"page_size={page_size}, poll_delay={poll_delay}."
|
183
|
+
)
|
144
184
|
self._client = client
|
145
185
|
self._retry_count = retry_count
|
186
|
+
self._backoff = backoff
|
187
|
+
self._backoff_factor = backoff_factor
|
146
188
|
self._poll_delay = poll_delay
|
147
189
|
self._page_size = min(page_size, 5_000)
|
148
190
|
|
@@ -179,42 +221,46 @@ class _AsyncHttpService:
|
|
179
221
|
self._client.put, url, headers=_gzip_header, content=content
|
180
222
|
)
|
181
223
|
|
182
|
-
async def
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
224
|
+
async def poll_task(self, func: Callable[..., Awaitable[Task]], *args) -> Task:
|
225
|
+
while (result := await func(*args)).task_state != "COMPLETE":
|
226
|
+
await sleep(self._poll_delay)
|
227
|
+
return result
|
228
|
+
|
229
|
+
async def get_paginated(self, url: str, result_key: str, **kwargs) -> Iterator[dict[str, Any]]:
|
230
|
+
logger.debug(f"Starting paginated fetch from {url} with page_size={self._page_size}.")
|
231
|
+
first_page, total_items, actual_size = await self._get_first_page(url, result_key, **kwargs)
|
232
|
+
if total_items <= self._page_size:
|
188
233
|
logger.debug("All items fit in first page, no additional requests needed.")
|
189
234
|
return iter(first_page)
|
235
|
+
if kwargs and (kwargs.get("params") or {}).get("sort", None):
|
236
|
+
logger.warning(SORT_WARNING)
|
190
237
|
pages = await gather(
|
191
238
|
*(
|
192
|
-
self._get_page(url,
|
193
|
-
for n in range(1, ceil(total_items /
|
239
|
+
self._get_page(url, actual_size, n * actual_size, result_key, **kwargs)
|
240
|
+
for n in range(1, ceil(total_items / actual_size))
|
194
241
|
)
|
195
242
|
)
|
196
243
|
logger.debug(f"Completed paginated fetch of {total_items} total items.")
|
197
244
|
return chain(first_page, *pages)
|
198
245
|
|
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
246
|
async def _get_page(self, url: str, limit: int, offset: int, result_key: str, **kwargs) -> list:
|
205
247
|
logger.debug(f"Fetching page: offset={offset}, limit={limit} from {url}.")
|
206
|
-
kwargs["params"] = kwargs.get("params") or {} | {"limit": limit, "offset": offset}
|
248
|
+
kwargs["params"] = (kwargs.get("params") or {}) | {"limit": limit, "offset": offset}
|
207
249
|
return (await self.get(url, **kwargs)).get(result_key, [])
|
208
250
|
|
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}
|
251
|
+
async def _get_first_page(self, url: str, result_key: str, **kwargs) -> tuple[list, int, int]:
|
252
|
+
logger.debug(f"Fetching first page with limit={self._page_size} from {url}.")
|
253
|
+
kwargs["params"] = (kwargs.get("params") or {}) | {"limit": self._page_size}
|
214
254
|
res = await self.get(url, **kwargs)
|
215
255
|
total_items, first_page = res["meta"]["paging"]["totalSize"], res.get(result_key, [])
|
256
|
+
actual_page_size = res["meta"]["paging"]["currentPageSize"]
|
257
|
+
if actual_page_size < self._page_size and not actual_page_size == total_items:
|
258
|
+
logger.warning(
|
259
|
+
f"Page size {self._page_size} was silently truncated to {actual_page_size}."
|
260
|
+
f"Using the server-side enforced page size {actual_page_size} for further requests."
|
261
|
+
)
|
216
262
|
logger.debug(f"Found {total_items} total items, retrieved {len(first_page)} in first page.")
|
217
|
-
return first_page, total_items
|
263
|
+
return first_page, total_items, actual_page_size
|
218
264
|
|
219
265
|
async def _run_with_retry(
|
220
266
|
self, func: Callable[..., Coroutine[Any, Any, Response]], *args, **kwargs
|
@@ -225,7 +271,7 @@ class _AsyncHttpService:
|
|
225
271
|
if response.status_code == 429:
|
226
272
|
if i >= self._retry_count - 1:
|
227
273
|
raise AnaplanException("Rate limit exceeded.")
|
228
|
-
backoff_time = (i
|
274
|
+
backoff_time = self._backoff * (self._backoff_factor if i > 0 else 1)
|
229
275
|
logger.warning(f"Rate limited. Retrying in {backoff_time} seconds.")
|
230
276
|
await asyncio.sleep(backoff_time)
|
231
277
|
continue
|
@@ -240,6 +286,18 @@ class _AsyncHttpService:
|
|
240
286
|
raise AnaplanException("Exhausted all retries without a successful response or Error.")
|
241
287
|
|
242
288
|
|
289
|
+
def sort_params(sort_by: str | None, descending: bool) -> dict[str, str | bool]:
|
290
|
+
"""
|
291
|
+
Construct search parameters for sorting. This also converts snake_case to camelCase.
|
292
|
+
:param sort_by: The field to sort by, optionally in snake_case.
|
293
|
+
:param descending: Whether to sort in descending order.
|
294
|
+
:return: A dictionary of search parameters in Anaplan's expected format.
|
295
|
+
"""
|
296
|
+
if not sort_by:
|
297
|
+
return {}
|
298
|
+
return {"sort": f"{'-' if descending else '+'}{to_camel(sort_by)}"}
|
299
|
+
|
300
|
+
|
243
301
|
def construct_payload(model: Type[T], body: T | dict[str, Any]) -> dict[str, Any]:
|
244
302
|
"""
|
245
303
|
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
|
)
|
@@ -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.0a4
|
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/
|
@@ -0,0 +1,30 @@
|
|
1
|
+
anaplan_sdk/__init__.py,sha256=WScEKtXlnRLjCb-j3qW9W4kEACTyPsTLFs-L54et2TQ,351
|
2
|
+
anaplan_sdk/_auth.py,sha256=l5z2WCcfQ05OkuQ1dcmikp6dB87Rw1qy2zu8bbaAQTs,16620
|
3
|
+
anaplan_sdk/_oauth.py,sha256=AynlJDrGIinQT0jwxI2RSvtU4D7Wasyw3H1uicdlLVI,12672
|
4
|
+
anaplan_sdk/_services.py,sha256=isBMmeoxgQi2Xoc5wdD7qtraIHBqYJNAp-CEKWyhqj4,20104
|
5
|
+
anaplan_sdk/exceptions.py,sha256=ALkA9fBF0NQ7dufFxV6AivjmHyuJk9DOQ9jtJV2n7f0,1809
|
6
|
+
anaplan_sdk/_async_clients/__init__.py,sha256=pZXgMMg4S9Aj_pxQCaSiPuNG-sePVGBtNJ0133VjqW4,364
|
7
|
+
anaplan_sdk/_async_clients/_alm.py,sha256=zvKEvXlxNkcQim_XvyZLCbDafFldljg8APHqhAAIfvw,13147
|
8
|
+
anaplan_sdk/_async_clients/_audit.py,sha256=j9CeWzIuGsZrVBbjS_T8w6le2cjieW7NW6fDiCY34TA,2718
|
9
|
+
anaplan_sdk/_async_clients/_bulk.py,sha256=TTdTMXu1-3iNlD9oxGAbh58JXSRHiFAxnMt70zLz8kk,29929
|
10
|
+
anaplan_sdk/_async_clients/_cloud_works.py,sha256=VB4l93426A0Xes5dZ6DsDu0go-BVNhs2RZn2zX5DSOc,17675
|
11
|
+
anaplan_sdk/_async_clients/_cw_flow.py,sha256=_allKIOP-qb33wrOj6GV5VAOvrCXOVJ1QXvck-jsocQ,3935
|
12
|
+
anaplan_sdk/_async_clients/_transactional.py,sha256=U6X5pW7By387JOgvHx-GmgVRi7MRJKALpx0lWI6xRMo,18024
|
13
|
+
anaplan_sdk/_clients/__init__.py,sha256=FsbwvZC1FHrxuRXwbPxUzbhz_lO1DpXIxEOjx6-3QuA,219
|
14
|
+
anaplan_sdk/_clients/_alm.py,sha256=3U7Cy5U5TsePF1YPogXvsOzNeQlQm_ezO5TlmD-Xbbs,12874
|
15
|
+
anaplan_sdk/_clients/_audit.py,sha256=b7l9xNbUGLceeNlS2No53RKJmksUeIICDTCuYPXgyV0,2641
|
16
|
+
anaplan_sdk/_clients/_bulk.py,sha256=lZhsbw-Zqtrwofgzds7Lct1HnUfN5M4QDxWP-PocotE,30085
|
17
|
+
anaplan_sdk/_clients/_cloud_works.py,sha256=FsCp2wPxIoArAN1vcIfOI6ANNkK2ZebQ4MWJZB-nFJU,17466
|
18
|
+
anaplan_sdk/_clients/_cw_flow.py,sha256=O6t4utbDZdSVXGC0PXUcPpQ4oXrPohU9_8SUBCpxTXw,3824
|
19
|
+
anaplan_sdk/_clients/_transactional.py,sha256=SaHAnaGLZrhXmM8d6JnWWkwf-sVCEDW0nL2a4_wvjfk,17849
|
20
|
+
anaplan_sdk/models/__init__.py,sha256=zfwDQJQrXuLEXSpbJcm08a_YK1P7a7u-kMhwtJiJFmA,1783
|
21
|
+
anaplan_sdk/models/_alm.py,sha256=oeENd0YM7-LoIRBq2uATIQTxVgIP9rXx3UZE2UnQAp0,4670
|
22
|
+
anaplan_sdk/models/_base.py,sha256=6AZc9CfireUKgpZfMxYKu4MbwiyHQOsGLjKrxGXBLic,508
|
23
|
+
anaplan_sdk/models/_bulk.py,sha256=S72qujNr5STdiyKaCEvrQjKYHik_aemiJFNKE7docpI,8405
|
24
|
+
anaplan_sdk/models/_transactional.py,sha256=2bH10zvtMb5Lfh6DC7iQk72aEwq6tyLQ-XnH_0wYSqI,14172
|
25
|
+
anaplan_sdk/models/cloud_works.py,sha256=APUGDt_e-JshtXkba5cQh5rZkXOZBz0Aix0qVNdEWgw,19501
|
26
|
+
anaplan_sdk/models/flows.py,sha256=SuLgNj5-2SeE3U1i8iY8cq2IkjuUgd_3M1n2ENructk,3625
|
27
|
+
anaplan_sdk-0.5.0a4.dist-info/METADATA,sha256=GQk0AT6_rIKYU7o79QBL1ur0DKfYGZ4fqxKv_hqajsg,3678
|
28
|
+
anaplan_sdk-0.5.0a4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
29
|
+
anaplan_sdk-0.5.0a4.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
30
|
+
anaplan_sdk-0.5.0a4.dist-info/RECORD,,
|
@@ -1,30 +0,0 @@
|
|
1
|
-
anaplan_sdk/__init__.py,sha256=WScEKtXlnRLjCb-j3qW9W4kEACTyPsTLFs-L54et2TQ,351
|
2
|
-
anaplan_sdk/_auth.py,sha256=l5z2WCcfQ05OkuQ1dcmikp6dB87Rw1qy2zu8bbaAQTs,16620
|
3
|
-
anaplan_sdk/_oauth.py,sha256=AynlJDrGIinQT0jwxI2RSvtU4D7Wasyw3H1uicdlLVI,12672
|
4
|
-
anaplan_sdk/_services.py,sha256=slsFFx_fCQSfKZo_G0HzEFGErvmEerSMyaVwLXUPCtU,17158
|
5
|
-
anaplan_sdk/exceptions.py,sha256=ALkA9fBF0NQ7dufFxV6AivjmHyuJk9DOQ9jtJV2n7f0,1809
|
6
|
-
anaplan_sdk/_async_clients/__init__.py,sha256=pZXgMMg4S9Aj_pxQCaSiPuNG-sePVGBtNJ0133VjqW4,364
|
7
|
-
anaplan_sdk/_async_clients/_alm.py,sha256=rhVhykUo6wIvA1SBQkpEAviSsVLURumi_3XQlxTf7z8,12788
|
8
|
-
anaplan_sdk/_async_clients/_audit.py,sha256=dipSzp4jMvRCHJAVMQfO854_wpmIcYEDinEPSGdoms4,2342
|
9
|
-
anaplan_sdk/_async_clients/_bulk.py,sha256=sUDWT01JcN-IWc5RY0thcAA45k54x6lO-lN2WMoCrZE,27278
|
10
|
-
anaplan_sdk/_async_clients/_cloud_works.py,sha256=ecm7DqT39J56xQwYxJMKd_ZVqxzXZdpmagwJUvqKBj4,17613
|
11
|
-
anaplan_sdk/_async_clients/_cw_flow.py,sha256=PTi-jKeRtYgibqrcE7SCsEgus18nE7ZCs7Awzk4u6Dk,3981
|
12
|
-
anaplan_sdk/_async_clients/_transactional.py,sha256=dRh4poYLWcV0kJyCG0MSFzJCzLW7fZ_1-j3c_Lx7zHY,17203
|
13
|
-
anaplan_sdk/_clients/__init__.py,sha256=FsbwvZC1FHrxuRXwbPxUzbhz_lO1DpXIxEOjx6-3QuA,219
|
14
|
-
anaplan_sdk/_clients/_alm.py,sha256=_LlZIRCE3HxZ4OzU13LOGnX4MQ26j2puSPTy9WGJa3o,12515
|
15
|
-
anaplan_sdk/_clients/_audit.py,sha256=9mq7VGYsl6wOdIU7G3GvzP3O7r1ZDCFg5eAu7k4RgxM,2154
|
16
|
-
anaplan_sdk/_clients/_bulk.py,sha256=Aw2cu8fAQ0lb4kc0kuYtVXr1BMuEry2yR7dlPR8CBEc,27330
|
17
|
-
anaplan_sdk/_clients/_cloud_works.py,sha256=b7LpFcRbUxcN7_9_5GgVIlGf1X1ZmQWFZgnCQKaU55s,17405
|
18
|
-
anaplan_sdk/_clients/_cw_flow.py,sha256=rwoQRdtxaigdBavr4LHtnrWsNpAgzt2zuIJOFV9ZE50,3870
|
19
|
-
anaplan_sdk/_clients/_transactional.py,sha256=avqww59ccM3FqYMeK1oE-8UH4jyk_pKSCETzhSGKyxA,16936
|
20
|
-
anaplan_sdk/models/__init__.py,sha256=zfwDQJQrXuLEXSpbJcm08a_YK1P7a7u-kMhwtJiJFmA,1783
|
21
|
-
anaplan_sdk/models/_alm.py,sha256=oeENd0YM7-LoIRBq2uATIQTxVgIP9rXx3UZE2UnQAp0,4670
|
22
|
-
anaplan_sdk/models/_base.py,sha256=6AZc9CfireUKgpZfMxYKu4MbwiyHQOsGLjKrxGXBLic,508
|
23
|
-
anaplan_sdk/models/_bulk.py,sha256=S72qujNr5STdiyKaCEvrQjKYHik_aemiJFNKE7docpI,8405
|
24
|
-
anaplan_sdk/models/_transactional.py,sha256=2bH10zvtMb5Lfh6DC7iQk72aEwq6tyLQ-XnH_0wYSqI,14172
|
25
|
-
anaplan_sdk/models/cloud_works.py,sha256=nfn_LHPR-KmW7Tpvz-5qNCzmR8SYgvsVV-lx5iDlyqI,19425
|
26
|
-
anaplan_sdk/models/flows.py,sha256=SuLgNj5-2SeE3U1i8iY8cq2IkjuUgd_3M1n2ENructk,3625
|
27
|
-
anaplan_sdk-0.5.0a2.dist-info/METADATA,sha256=82p3Dx0kw_5MNBdZYtbNzsCC8s7GM6hnFqpdpan-Ggo,3669
|
28
|
-
anaplan_sdk-0.5.0a2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
29
|
-
anaplan_sdk-0.5.0a2.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
30
|
-
anaplan_sdk-0.5.0a2.dist-info/RECORD,,
|
File without changes
|
File without changes
|