anaplan-sdk 0.4.5__py3-none-any.whl → 0.5.0a2__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 +248 -44
- anaplan_sdk/_async_clients/_audit.py +13 -13
- anaplan_sdk/_async_clients/_bulk.py +181 -135
- anaplan_sdk/_async_clients/_cloud_works.py +57 -38
- anaplan_sdk/_async_clients/_cw_flow.py +25 -16
- anaplan_sdk/_async_clients/_transactional.py +251 -53
- anaplan_sdk/_clients/_alm.py +246 -45
- anaplan_sdk/_clients/_audit.py +13 -14
- anaplan_sdk/_clients/_bulk.py +180 -123
- anaplan_sdk/_clients/_cloud_works.py +54 -36
- anaplan_sdk/_clients/_cw_flow.py +25 -16
- anaplan_sdk/_clients/_transactional.py +246 -50
- anaplan_sdk/_services.py +392 -0
- anaplan_sdk/models/__init__.py +49 -2
- anaplan_sdk/models/_alm.py +64 -6
- anaplan_sdk/models/_bulk.py +16 -9
- anaplan_sdk/models/_transactional.py +221 -4
- {anaplan_sdk-0.4.5.dist-info → anaplan_sdk-0.5.0a2.dist-info}/METADATA +1 -1
- anaplan_sdk-0.5.0a2.dist-info/RECORD +30 -0
- anaplan_sdk/_base.py +0 -297
- anaplan_sdk-0.4.5.dist-info/RECORD +0 -30
- {anaplan_sdk-0.4.5.dist-info → anaplan_sdk-0.5.0a2.dist-info}/WHEEL +0 -0
- {anaplan_sdk-0.4.5.dist-info → anaplan_sdk-0.5.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,5 @@
|
|
1
1
|
import logging
|
2
|
-
from asyncio import gather
|
2
|
+
from asyncio import gather
|
3
3
|
from copy import copy
|
4
4
|
from typing import AsyncIterator, Iterator
|
5
5
|
|
@@ -7,7 +7,7 @@ import httpx
|
|
7
7
|
from typing_extensions import Self
|
8
8
|
|
9
9
|
from anaplan_sdk._auth import _create_auth
|
10
|
-
from anaplan_sdk.
|
10
|
+
from anaplan_sdk._services import _AsyncHttpService, action_url
|
11
11
|
from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
|
12
12
|
from anaplan_sdk.models import (
|
13
13
|
Action,
|
@@ -15,6 +15,7 @@ from anaplan_sdk.models import (
|
|
15
15
|
File,
|
16
16
|
Import,
|
17
17
|
Model,
|
18
|
+
ModelDeletionResult,
|
18
19
|
Process,
|
19
20
|
TaskStatus,
|
20
21
|
TaskSummary,
|
@@ -26,11 +27,10 @@ from ._audit import _AsyncAuditClient
|
|
26
27
|
from ._cloud_works import _AsyncCloudWorksClient
|
27
28
|
from ._transactional import _AsyncTransactionalClient
|
28
29
|
|
29
|
-
logging.getLogger("httpx").setLevel(logging.CRITICAL)
|
30
30
|
logger = logging.getLogger("anaplan_sdk")
|
31
31
|
|
32
32
|
|
33
|
-
class AsyncClient
|
33
|
+
class AsyncClient:
|
34
34
|
"""
|
35
35
|
Asynchronous Anaplan Client. For guides and examples
|
36
36
|
refer to https://vinzenzklass.github.io/anaplan-sdk.
|
@@ -49,9 +49,11 @@ class AsyncClient(_AsyncBaseClient):
|
|
49
49
|
auth: httpx.Auth | None = None,
|
50
50
|
timeout: float | httpx.Timeout = 30,
|
51
51
|
retry_count: int = 2,
|
52
|
+
page_size: int = 5_000,
|
52
53
|
status_poll_delay: int = 1,
|
53
54
|
upload_chunk_size: int = 25_000_000,
|
54
55
|
allow_file_creation: bool = False,
|
56
|
+
**httpx_kwargs,
|
55
57
|
) -> None:
|
56
58
|
"""
|
57
59
|
Asynchronous Anaplan Client. For guides and examples
|
@@ -84,6 +86,9 @@ class AsyncClient(_AsyncBaseClient):
|
|
84
86
|
:param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
|
85
87
|
to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
|
86
88
|
number of 2 times.
|
89
|
+
:param page_size: The number of items to return per page when paginating through results.
|
90
|
+
Defaults to 5000. This is the maximum number of items that can be returned per
|
91
|
+
request. If you pass a value greater than 5000, it will be capped to 5000.
|
87
92
|
:param status_poll_delay: The delay between polling the status of a task.
|
88
93
|
:param upload_chunk_size: The size of the chunks to upload. This is the maximum size of
|
89
94
|
each chunk. Defaults to 25MB.
|
@@ -92,54 +97,60 @@ class AsyncClient(_AsyncBaseClient):
|
|
92
97
|
altogether. A file that is created this way will not be referenced by any action in
|
93
98
|
anaplan until manually assigned so there is typically no value in dynamically
|
94
99
|
creating new files and uploading content to them.
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
)
|
107
|
-
),
|
108
|
-
timeout=timeout,
|
100
|
+
:param httpx_kwargs: Additional keyword arguments to pass to the `httpx.AsyncClient`.
|
101
|
+
This can be used to set additional options such as proxies, headers, etc. See
|
102
|
+
https://www.python-httpx.org/api/#asyncclient for the full list of arguments.
|
103
|
+
"""
|
104
|
+
_auth = auth or _create_auth(
|
105
|
+
token=token,
|
106
|
+
user_email=user_email,
|
107
|
+
password=password,
|
108
|
+
certificate=certificate,
|
109
|
+
private_key=private_key,
|
110
|
+
private_key_password=private_key_password,
|
109
111
|
)
|
110
|
-
|
112
|
+
_client = httpx.AsyncClient(auth=_auth, timeout=timeout, **httpx_kwargs)
|
113
|
+
self._http = _AsyncHttpService(_client, retry_count, page_size, status_poll_delay)
|
114
|
+
self._workspace_id = workspace_id
|
115
|
+
self._model_id = model_id
|
111
116
|
self._url = f"https://api.anaplan.com/2/0/workspaces/{workspace_id}/models/{model_id}"
|
112
117
|
self._transactional_client = (
|
113
|
-
_AsyncTransactionalClient(
|
114
|
-
)
|
115
|
-
self._alm_client = (
|
116
|
-
_AsyncAlmClient(_client, model_id, self._retry_count) if model_id else None
|
118
|
+
_AsyncTransactionalClient(self._http, model_id) if model_id else None
|
117
119
|
)
|
118
|
-
self.
|
119
|
-
self.
|
120
|
-
self.
|
120
|
+
self._alm_client = _AsyncAlmClient(self._http, model_id) if model_id else None
|
121
|
+
self._audit = _AsyncAuditClient(self._http)
|
122
|
+
self._cloud_works = _AsyncCloudWorksClient(self._http)
|
121
123
|
self.upload_chunk_size = upload_chunk_size
|
122
124
|
self.allow_file_creation = allow_file_creation
|
123
|
-
super().__init__(retry_count, _client)
|
124
125
|
|
125
126
|
@classmethod
|
126
|
-
def from_existing(
|
127
|
+
def from_existing(
|
128
|
+
cls, existing: Self, *, workspace_id: str | None = None, model_id: str | None = None
|
129
|
+
) -> Self:
|
127
130
|
"""
|
128
131
|
Create a new instance of the Client from an existing instance. This is useful if you want
|
129
132
|
to interact with multiple models or workspaces in the same script but share the same
|
130
133
|
authentication and configuration. This creates a shallow copy of the existing client and
|
131
|
-
|
134
|
+
optionally updates the relevant attributes to the new workspace and model. You can provide
|
135
|
+
either a new workspace Id or a new model Id, or both. If you do not provide one of them,
|
136
|
+
the existing value will be used. If you omit both, the new instance will be an identical
|
137
|
+
copy of the existing instance.
|
138
|
+
|
132
139
|
:param existing: The existing instance to copy.
|
133
|
-
:param workspace_id: The workspace Id to use.
|
134
|
-
:param model_id: The model Id to use.
|
140
|
+
:param workspace_id: The workspace Id to use or None to use the existing workspace Id.
|
141
|
+
:param model_id: The model Id to use or None to use the existing model Id.
|
135
142
|
:return: A new instance of the Client.
|
136
143
|
"""
|
137
144
|
client = copy(existing)
|
138
|
-
|
139
|
-
|
140
|
-
|
145
|
+
new_ws_id = workspace_id or existing._workspace_id
|
146
|
+
new_model_id = model_id or existing._model_id
|
147
|
+
logger.debug(
|
148
|
+
f"Creating a new AsyncClient from existing instance "
|
149
|
+
f"with workspace_id={new_ws_id}, model_id={new_model_id}."
|
141
150
|
)
|
142
|
-
client.
|
151
|
+
client._url = f"https://api.anaplan.com/2/0/workspaces/{new_ws_id}/models/{new_model_id}"
|
152
|
+
client._transactional_client = _AsyncTransactionalClient(existing._http, new_model_id)
|
153
|
+
client._alm_client = _AsyncAlmClient(existing._http, new_model_id)
|
143
154
|
return client
|
144
155
|
|
145
156
|
@property
|
@@ -159,14 +170,14 @@ class AsyncClient(_AsyncBaseClient):
|
|
159
170
|
return self._cloud_works
|
160
171
|
|
161
172
|
@property
|
162
|
-
def
|
173
|
+
def tr(self) -> _AsyncTransactionalClient:
|
163
174
|
"""
|
164
175
|
The Transactional Client provides access to the Anaplan Transactional API. This is useful
|
165
176
|
for more advanced use cases where you need to interact with the Anaplan Model in a more
|
166
177
|
granular way.
|
167
178
|
|
168
179
|
If you instantiated the client without the field `model_id`, this will raise a
|
169
|
-
|
180
|
+
`ValueError`, since none of the endpoints can be invoked without the model Id.
|
170
181
|
:return: The Transactional Client.
|
171
182
|
"""
|
172
183
|
if not self._transactional_client:
|
@@ -197,7 +208,7 @@ class AsyncClient(_AsyncBaseClient):
|
|
197
208
|
)
|
198
209
|
return self._alm_client
|
199
210
|
|
200
|
-
async def
|
211
|
+
async def get_workspaces(self, search_pattern: str | None = None) -> list[Workspace]:
|
201
212
|
"""
|
202
213
|
Lists all the Workspaces the authenticated user has access to.
|
203
214
|
:param search_pattern: Optionally filter for specific workspaces. When provided,
|
@@ -211,18 +222,18 @@ class AsyncClient(_AsyncBaseClient):
|
|
211
222
|
params["s"] = search_pattern
|
212
223
|
return [
|
213
224
|
Workspace.model_validate(e)
|
214
|
-
for e in await self.
|
225
|
+
for e in await self._http.get_paginated(
|
215
226
|
"https://api.anaplan.com/2/0/workspaces", "workspaces", params=params
|
216
227
|
)
|
217
228
|
]
|
218
229
|
|
219
|
-
async def
|
230
|
+
async def get_models(self, search_pattern: str | None = None) -> list[Model]:
|
220
231
|
"""
|
221
232
|
Lists all the Models the authenticated user has access to.
|
222
233
|
:param search_pattern: Optionally filter for specific models. When provided,
|
223
|
-
case-insensitive matches
|
234
|
+
case-insensitive matches model names containing this string.
|
224
235
|
You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
|
225
|
-
When None (default), returns all
|
236
|
+
When None (default), returns all models.
|
226
237
|
:return: The List of Models.
|
227
238
|
"""
|
228
239
|
params = {"modelDetails": "true"}
|
@@ -230,21 +241,36 @@ class AsyncClient(_AsyncBaseClient):
|
|
230
241
|
params["s"] = search_pattern
|
231
242
|
return [
|
232
243
|
Model.model_validate(e)
|
233
|
-
for e in await self.
|
244
|
+
for e in await self._http.get_paginated(
|
234
245
|
"https://api.anaplan.com/2/0/models", "models", params=params
|
235
246
|
)
|
236
247
|
]
|
237
248
|
|
238
|
-
async def
|
249
|
+
async def delete_models(self, model_ids: list[str]) -> ModelDeletionResult:
|
250
|
+
"""
|
251
|
+
Delete the given Models. Models need to be closed before they can be deleted. If one of the
|
252
|
+
deletions fails, the other deletions will still be attempted and may complete.
|
253
|
+
:param model_ids: The list of Model identifiers to delete.
|
254
|
+
:return:
|
255
|
+
"""
|
256
|
+
logger.info(f"Deleting Models: {', '.join(model_ids)}.")
|
257
|
+
res = await self._http.post(
|
258
|
+
f"https://api.anaplan.com/2/0/workspaces/{self._workspace_id}/bulkDeleteModels",
|
259
|
+
json={"modelIdsToDelete": model_ids},
|
260
|
+
)
|
261
|
+
return ModelDeletionResult.model_validate(res)
|
262
|
+
|
263
|
+
async def get_files(self) -> list[File]:
|
239
264
|
"""
|
240
265
|
Lists all the Files in the Model.
|
241
266
|
:return: The List of Files.
|
242
267
|
"""
|
243
268
|
return [
|
244
|
-
File.model_validate(e)
|
269
|
+
File.model_validate(e)
|
270
|
+
for e in await self._http.get_paginated(f"{self._url}/files", "files")
|
245
271
|
]
|
246
272
|
|
247
|
-
async def
|
273
|
+
async def get_actions(self) -> list[Action]:
|
248
274
|
"""
|
249
275
|
Lists all the Actions in the Model. This will only return the Actions listed under
|
250
276
|
`Other Actions` in Anaplan. For Imports, exports, and processes, see their respective
|
@@ -253,66 +279,66 @@ class AsyncClient(_AsyncBaseClient):
|
|
253
279
|
"""
|
254
280
|
return [
|
255
281
|
Action.model_validate(e)
|
256
|
-
for e in await self.
|
282
|
+
for e in await self._http.get_paginated(f"{self._url}/actions", "actions")
|
257
283
|
]
|
258
284
|
|
259
|
-
async def
|
285
|
+
async def get_processes(self) -> list[Process]:
|
260
286
|
"""
|
261
287
|
Lists all the Processes in the Model.
|
262
288
|
:return: The List of Processes.
|
263
289
|
"""
|
264
290
|
return [
|
265
291
|
Process.model_validate(e)
|
266
|
-
for e in await self.
|
292
|
+
for e in await self._http.get_paginated(f"{self._url}/processes", "processes")
|
267
293
|
]
|
268
294
|
|
269
|
-
async def
|
295
|
+
async def get_imports(self) -> list[Import]:
|
270
296
|
"""
|
271
297
|
Lists all the Imports in the Model.
|
272
298
|
:return: The List of Imports.
|
273
299
|
"""
|
274
300
|
return [
|
275
301
|
Import.model_validate(e)
|
276
|
-
for e in await self.
|
302
|
+
for e in await self._http.get_paginated(f"{self._url}/imports", "imports")
|
277
303
|
]
|
278
304
|
|
279
|
-
async def
|
305
|
+
async def get_exports(self) -> list[Export]:
|
280
306
|
"""
|
281
307
|
Lists all the Exports in the Model.
|
282
308
|
:return: The List of Exports.
|
283
309
|
"""
|
284
310
|
return [
|
285
311
|
Export.model_validate(e)
|
286
|
-
for e in await self.
|
312
|
+
for e in await self._http.get_paginated(f"{self._url}/exports", "exports")
|
287
313
|
]
|
288
314
|
|
289
|
-
async def run_action(self, action_id: int) -> TaskStatus:
|
315
|
+
async def run_action(self, action_id: int, wait_for_completion: bool = True) -> TaskStatus:
|
290
316
|
"""
|
291
|
-
Runs the
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
raise an Exception to handle - if you for e.g. think that one of the uploaded chunks may
|
296
|
-
have been dropped and simply retrying with new data may help - and not return the task
|
297
|
-
status information that needs to be handled by the caller.
|
298
|
-
|
299
|
-
If you need more information or control, you can use `invoke_action()` and
|
300
|
-
`get_task_status()`.
|
317
|
+
Runs the Action and validates the spawned task. If the Action fails or completes with
|
318
|
+
errors, this will raise an AnaplanActionError. Failed Tasks are often not something you
|
319
|
+
can recover from at runtime and often require manual changes in Anaplan, i.e. updating the
|
320
|
+
mapping of an Import or similar.
|
301
321
|
:param action_id: The identifier of the Action to run. Can be any Anaplan Invokable;
|
302
|
-
|
322
|
+
Processes, Imports, Exports, Other Actions.
|
323
|
+
:param wait_for_completion: If True, the method will poll the task status and not return
|
324
|
+
until the task is complete. If False, it will spawn the task and return immediately.
|
303
325
|
"""
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
326
|
+
body = {"localeName": "en_US"}
|
327
|
+
res = await self._http.post(
|
328
|
+
f"{self._url}/{action_url(action_id)}/{action_id}/tasks", json=body
|
329
|
+
)
|
330
|
+
task_id = res["task"]["taskId"]
|
331
|
+
logger.info(f"Invoked Action '{action_id}', spawned Task: '{task_id}'.")
|
310
332
|
|
311
|
-
if
|
333
|
+
if not wait_for_completion:
|
334
|
+
return TaskStatus.model_validate(await self.get_task_status(action_id, task_id))
|
335
|
+
status = await self._http.poll_task(self.get_task_status, action_id, task_id)
|
336
|
+
if status.task_state == "COMPLETE" and not status.result.successful:
|
337
|
+
logger.error(f"Task '{task_id}' completed with errors.")
|
312
338
|
raise AnaplanActionError(f"Task '{task_id}' completed with errors.")
|
313
339
|
|
314
|
-
logger.info(f"Task '{task_id}' completed successfully.")
|
315
|
-
return
|
340
|
+
logger.info(f"Task '{task_id}' of '{action_id}' completed successfully.")
|
341
|
+
return status
|
316
342
|
|
317
343
|
async def get_file(self, file_id: int) -> bytes:
|
318
344
|
"""
|
@@ -321,58 +347,71 @@ class AsyncClient(_AsyncBaseClient):
|
|
321
347
|
:return: The content of the file.
|
322
348
|
"""
|
323
349
|
chunk_count = await self._file_pre_check(file_id)
|
324
|
-
if chunk_count <= 1:
|
325
|
-
return await self._get_binary(f"{self._url}/files/{file_id}")
|
326
350
|
logger.info(f"File {file_id} has {chunk_count} chunks.")
|
351
|
+
if chunk_count <= 1:
|
352
|
+
return await self._http.get_binary(f"{self._url}/files/{file_id}")
|
327
353
|
return b"".join(
|
328
354
|
await gather(
|
329
|
-
*
|
330
|
-
self.
|
355
|
+
*(
|
356
|
+
self._http.get_binary(f"{self._url}/files/{file_id}/chunks/{i}")
|
331
357
|
for i in range(chunk_count)
|
332
|
-
|
358
|
+
)
|
333
359
|
)
|
334
360
|
)
|
335
361
|
|
336
|
-
async def get_file_stream(self, file_id: int) -> AsyncIterator[bytes]:
|
362
|
+
async def get_file_stream(self, file_id: int, batch_size: int = 1) -> AsyncIterator[bytes]:
|
337
363
|
"""
|
338
364
|
Retrieves the content of the specified file as a stream of chunks. The chunks are yielded
|
339
365
|
one by one, so you can process them as they arrive. This is useful for large files where
|
340
366
|
you don't want to or cannot load the entire file into memory at once.
|
341
367
|
:param file_id: The identifier of the file to retrieve.
|
368
|
+
:param batch_size: Number of chunks to fetch concurrently. If > 1, n chunks will be fetched
|
369
|
+
concurrently. This still yields each chunk individually, only the requests are
|
370
|
+
batched. If 1 (default), each chunk is fetched sequentially.
|
342
371
|
:return: A generator yielding the chunks of the file.
|
343
372
|
"""
|
344
373
|
chunk_count = await self._file_pre_check(file_id)
|
374
|
+
logger.info(f"File {file_id} has {chunk_count} chunks.")
|
345
375
|
if chunk_count <= 1:
|
346
|
-
yield await self.
|
376
|
+
yield await self._http.get_binary(f"{self._url}/files/{file_id}")
|
347
377
|
return
|
348
|
-
|
349
|
-
for
|
350
|
-
|
378
|
+
|
379
|
+
for batch_start in range(0, chunk_count, batch_size):
|
380
|
+
batch_chunks = await gather(
|
381
|
+
*(
|
382
|
+
self._http.get_binary(f"{self._url}/files/{file_id}/chunks/{i}")
|
383
|
+
for i in range(batch_start, min(batch_start + batch_size, chunk_count))
|
384
|
+
)
|
385
|
+
)
|
386
|
+
for chunk in batch_chunks:
|
387
|
+
yield chunk
|
351
388
|
|
352
389
|
async def upload_file(self, file_id: int, content: str | bytes) -> None:
|
353
390
|
"""
|
354
391
|
Uploads the content to the specified file. If there are several chunks, upload of
|
355
|
-
individual chunks are
|
392
|
+
individual chunks are uploaded concurrently.
|
356
393
|
|
357
394
|
:param file_id: The identifier of the file to upload to.
|
358
395
|
:param content: The content to upload. **This Content will be compressed before uploading.
|
359
|
-
|
360
|
-
redundant work.**
|
396
|
+
If you are passing the Input as bytes, pass it uncompressed.**
|
361
397
|
"""
|
362
|
-
if isinstance(content, str):
|
363
|
-
content = content.encode()
|
364
398
|
chunks = [
|
365
399
|
content[i : i + self.upload_chunk_size]
|
366
400
|
for i in range(0, len(content), self.upload_chunk_size)
|
367
401
|
]
|
368
|
-
logger.info(f"Content will be uploaded in {len(chunks)} chunks.")
|
402
|
+
logger.info(f"Content for file '{file_id}' will be uploaded in {len(chunks)} chunks.")
|
369
403
|
await self._set_chunk_count(file_id, len(chunks))
|
370
404
|
await gather(
|
371
|
-
*
|
405
|
+
*(self._upload_chunk(file_id, index, chunk) for index, chunk in enumerate(chunks))
|
372
406
|
)
|
373
407
|
|
408
|
+
logger.info(f"Completed upload for file '{file_id}'.")
|
409
|
+
|
374
410
|
async def upload_file_stream(
|
375
|
-
self,
|
411
|
+
self,
|
412
|
+
file_id: int,
|
413
|
+
content: AsyncIterator[bytes | str] | Iterator[str | bytes],
|
414
|
+
batch_size: int = 1,
|
376
415
|
) -> None:
|
377
416
|
"""
|
378
417
|
Uploads the content to the specified file as a stream of chunks. This is useful either for
|
@@ -382,38 +421,60 @@ class AsyncClient(_AsyncBaseClient):
|
|
382
421
|
generator that yields the chunks of the file one by one to this method.
|
383
422
|
|
384
423
|
:param file_id: The identifier of the file to upload to.
|
385
|
-
:param content: An Iterator or AsyncIterator yielding the chunks of the file.
|
386
|
-
|
424
|
+
:param content: An Iterator or AsyncIterator yielding the chunks of the file. You can pass
|
425
|
+
any Iterator, but you will most likely want to pass a Generator.
|
426
|
+
:param batch_size: Number of chunks to upload concurrently. If > 1, n chunks will be
|
427
|
+
uploaded concurrently. This can be useful if you either do not control the chunk
|
428
|
+
size, or if you want to keep the chunk size small but still want some concurrency.
|
387
429
|
"""
|
430
|
+
logger.info(f"Starting upload stream for file '{file_id}' with batch size {batch_size}.")
|
388
431
|
await self._set_chunk_count(file_id, -1)
|
432
|
+
tasks = []
|
389
433
|
if isinstance(content, Iterator):
|
390
434
|
for index, chunk in enumerate(content):
|
391
|
-
|
392
|
-
|
393
|
-
|
435
|
+
tasks.append(self._upload_chunk(file_id, index, chunk))
|
436
|
+
if len(tasks) == max(batch_size, 1):
|
437
|
+
await gather(*tasks)
|
438
|
+
logger.info(
|
439
|
+
f"Completed upload stream batch of size {batch_size} for file {file_id}."
|
440
|
+
)
|
441
|
+
tasks = []
|
394
442
|
else:
|
395
443
|
index = 0
|
396
444
|
async for chunk in content:
|
397
|
-
|
398
|
-
file_id, index, chunk.encode() if isinstance(chunk, str) else chunk
|
399
|
-
)
|
445
|
+
tasks.append(self._upload_chunk(file_id, index, chunk))
|
400
446
|
index += 1
|
447
|
+
if len(tasks) == max(batch_size, 1):
|
448
|
+
await gather(*tasks)
|
449
|
+
logger.info(
|
450
|
+
f"Completed upload stream batch of size {batch_size} for file {file_id}."
|
451
|
+
)
|
452
|
+
tasks = []
|
453
|
+
if tasks:
|
454
|
+
await gather(*tasks)
|
455
|
+
logger.info(
|
456
|
+
f"Completed final upload stream batch of size {len(tasks)} for file {file_id}."
|
457
|
+
)
|
458
|
+
await self._http.post(f"{self._url}/files/{file_id}/complete", json={"id": file_id})
|
459
|
+
logger.info(f"Completed upload stream for '{file_id}'.")
|
401
460
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
async def upload_and_import(self, file_id: int, content: str | bytes, action_id: int) -> None:
|
461
|
+
async def upload_and_import(
|
462
|
+
self, file_id: int, content: str | bytes, action_id: int, wait_for_completion: bool = True
|
463
|
+
) -> TaskStatus:
|
406
464
|
"""
|
407
465
|
Convenience wrapper around `upload_file()` and `run_action()` to upload content to a file
|
408
466
|
and run an import action in one call.
|
409
467
|
:param file_id: The identifier of the file to upload to.
|
410
468
|
:param content: The content to upload. **This Content will be compressed before uploading.
|
411
|
-
|
412
|
-
|
469
|
+
If you are passing the Input as bytes, pass it uncompressed to avoid redundant
|
470
|
+
work.**
|
413
471
|
:param action_id: The identifier of the action to run after uploading the content.
|
472
|
+
:param wait_for_completion: If True, the method will poll the import task status and not
|
473
|
+
return until the task is complete. If False, it will spawn the import task and
|
474
|
+
return immediately.
|
414
475
|
"""
|
415
476
|
await self.upload_file(file_id, content)
|
416
|
-
await self.run_action(action_id)
|
477
|
+
return await self.run_action(action_id, wait_for_completion)
|
417
478
|
|
418
479
|
async def export_and_download(self, action_id: int) -> bytes:
|
419
480
|
"""
|
@@ -425,7 +486,7 @@ class AsyncClient(_AsyncBaseClient):
|
|
425
486
|
await self.run_action(action_id)
|
426
487
|
return await self.get_file(action_id)
|
427
488
|
|
428
|
-
async def
|
489
|
+
async def get_task_summaries(self, action_id: int) -> list[TaskSummary]:
|
429
490
|
"""
|
430
491
|
Retrieves the status of all tasks spawned by the specified action.
|
431
492
|
:param action_id: The identifier of the action that was invoked.
|
@@ -433,7 +494,7 @@ class AsyncClient(_AsyncBaseClient):
|
|
433
494
|
"""
|
434
495
|
return [
|
435
496
|
TaskSummary.model_validate(e)
|
436
|
-
for e in await self.
|
497
|
+
for e in await self._http.get_paginated(
|
437
498
|
f"{self._url}/{action_url(action_id)}/{action_id}/tasks", "tasks"
|
438
499
|
)
|
439
500
|
]
|
@@ -447,7 +508,9 @@ class AsyncClient(_AsyncBaseClient):
|
|
447
508
|
"""
|
448
509
|
return TaskStatus.model_validate(
|
449
510
|
(
|
450
|
-
await self.
|
511
|
+
await self._http.get(
|
512
|
+
f"{self._url}/{action_url(action_id)}/{action_id}/tasks/{task_id}"
|
513
|
+
)
|
451
514
|
).get("task")
|
452
515
|
)
|
453
516
|
|
@@ -458,38 +521,19 @@ class AsyncClient(_AsyncBaseClient):
|
|
458
521
|
:param task_id: The Task identifier, sometimes also referred to as the Correlation Id.
|
459
522
|
:return: The content of the solution logs.
|
460
523
|
"""
|
461
|
-
return await self.
|
524
|
+
return await self._http.get_binary(
|
462
525
|
f"{self._url}/optimizeActions/{action_id}/tasks/{task_id}/solutionLogs"
|
463
526
|
)
|
464
527
|
|
465
|
-
async def invoke_action(self, action_id: int) -> str:
|
466
|
-
"""
|
467
|
-
You may want to consider using `run_action()` instead.
|
468
|
-
|
469
|
-
Invokes the specified Anaplan Action and returns the spawned Task identifier. This is
|
470
|
-
useful if you want to handle the Task status yourself or if you want to run multiple
|
471
|
-
Actions in parallel.
|
472
|
-
:param action_id: The identifier of the Action to run. Can be any Anaplan Invokable.
|
473
|
-
:return: The identifier of the spawned Task.
|
474
|
-
"""
|
475
|
-
response = await self._post(
|
476
|
-
f"{self._url}/{action_url(action_id)}/{action_id}/tasks", json={"localeName": "en_US"}
|
477
|
-
)
|
478
|
-
task_id = response.get("task").get("taskId")
|
479
|
-
logger.info(f"Invoked Action '{action_id}', spawned Task: '{task_id}'.")
|
480
|
-
return task_id
|
481
|
-
|
482
528
|
async def _file_pre_check(self, file_id: int) -> int:
|
483
|
-
file = next(
|
529
|
+
file = next((f for f in await self.get_files() if f.id == file_id), None)
|
484
530
|
if not file:
|
485
531
|
raise InvalidIdentifierException(f"File {file_id} not found.")
|
486
532
|
return file.chunk_count
|
487
533
|
|
488
|
-
async def _upload_chunk(self, file_id: int, index: int, chunk: bytes) -> None:
|
489
|
-
await self.
|
490
|
-
|
491
|
-
)
|
492
|
-
logger.info(f"Chunk {index} loaded to file '{file_id}'.")
|
534
|
+
async def _upload_chunk(self, file_id: int, index: int, chunk: str | bytes) -> None:
|
535
|
+
await self._http.put_binary_gzip(f"{self._url}/files/{file_id}/chunks/{index}", chunk)
|
536
|
+
logger.debug(f"Chunk {index} loaded to file '{file_id}'.")
|
493
537
|
|
494
538
|
async def _set_chunk_count(self, file_id: int, num_chunks: int) -> None:
|
495
539
|
if not self.allow_file_creation and not (113000000000 <= file_id <= 113999999999):
|
@@ -498,7 +542,9 @@ class AsyncClient(_AsyncBaseClient):
|
|
498
542
|
"to avoid this error, set `allow_file_creation=True` on the calling instance. "
|
499
543
|
"Make sure you have understood the implications of this before doing so. "
|
500
544
|
)
|
501
|
-
response = await self.
|
545
|
+
response = await self._http.post(
|
546
|
+
f"{self._url}/files/{file_id}", json={"chunkCount": num_chunks}
|
547
|
+
)
|
502
548
|
optionally_new_file = int(response.get("file").get("id"))
|
503
549
|
if optionally_new_file != file_id:
|
504
550
|
if self.allow_file_creation:
|