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.
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from asyncio import gather, sleep
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._base import _AsyncBaseClient, action_url
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(_AsyncBaseClient):
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
- _client = httpx.AsyncClient(
97
- auth=(
98
- auth
99
- or _create_auth(
100
- token=token,
101
- user_email=user_email,
102
- password=password,
103
- certificate=certificate,
104
- private_key=private_key,
105
- private_key_password=private_key_password,
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
- self._retry_count = retry_count
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(_client, model_id, retry_count) if model_id else None
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._audit = _AsyncAuditClient(_client, self._retry_count)
119
- self._cloud_works = _AsyncCloudWorksClient(_client, self._retry_count)
120
- self.status_poll_delay = status_poll_delay
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(cls, existing: Self, workspace_id: str, model_id: str) -> Self:
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
- update the relevant attributes to the new workspace and model.
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
- client._url = f"https://api.anaplan.com/2/0/workspaces/{workspace_id}/models/{model_id}"
139
- client._transactional_client = _AsyncTransactionalClient(
140
- existing._client, model_id, existing._retry_count
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._alm_client = _AsyncAlmClient(existing._client, model_id, existing._retry_count)
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 transactional(self) -> _AsyncTransactionalClient:
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
- :py:class:`ValueError`, since none of the endpoints can be invoked without the model Id.
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 list_workspaces(self, search_pattern: str | None = None) -> list[Workspace]:
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._get_paginated(
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 list_models(self, search_pattern: str | None = None) -> list[Model]:
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 models names containing this string.
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 users.
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._get_paginated(
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 list_files(self) -> list[File]:
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) for e in await self._get_paginated(f"{self._url}/files", "files")
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 list_actions(self) -> list[Action]:
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._get_paginated(f"{self._url}/actions", "actions")
282
+ for e in await self._http.get_paginated(f"{self._url}/actions", "actions")
257
283
  ]
258
284
 
259
- async def list_processes(self) -> list[Process]:
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._get_paginated(f"{self._url}/processes", "processes")
292
+ for e in await self._http.get_paginated(f"{self._url}/processes", "processes")
267
293
  ]
268
294
 
269
- async def list_imports(self) -> list[Import]:
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._get_paginated(f"{self._url}/imports", "imports")
302
+ for e in await self._http.get_paginated(f"{self._url}/imports", "imports")
277
303
  ]
278
304
 
279
- async def list_exports(self) -> list[Export]:
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._get_paginated(f"{self._url}/exports", "exports")
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 specified Anaplan Action and validates the spawned task. If the Action fails or
292
- completes with errors, will raise an :py:class:`AnaplanActionError`. Failed Tasks are
293
- usually not something you can recover from at runtime and often require manual changes in
294
- Anaplan, i.e. updating the mapping of an Import or similar. So, for convenience, this will
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
- Processes, Imports, Exports, Other Actions.
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
- task_id = await self.invoke_action(action_id)
305
- task_status = await self.get_task_status(action_id, task_id)
306
-
307
- while task_status.task_state != "COMPLETE":
308
- await sleep(self.status_poll_delay)
309
- task_status = await self.get_task_status(action_id, task_id)
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 task_status.task_state == "COMPLETE" and not task_status.result.successful:
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 task_status
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._get_binary(f"{self._url}/files/{file_id}/chunks/{i}")
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._get_binary(f"{self._url}/files/{file_id}")
376
+ yield await self._http.get_binary(f"{self._url}/files/{file_id}")
347
377
  return
348
- logger.info(f"File {file_id} has {chunk_count} chunks.")
349
- for i in range(chunk_count):
350
- yield await self._get_binary(f"{self._url}/files/{file_id}/chunks/{i}")
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 concurrent.
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
- If you are passing the Input as bytes, pass it uncompressed to avoid
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
- *[self._upload_chunk(file_id, index, chunk) for index, chunk in enumerate(chunks)]
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, file_id: int, content: AsyncIterator[bytes | str] | Iterator[str | bytes]
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
- (Most likely a generator).
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
- await self._upload_chunk(
392
- file_id, index, chunk.encode() if isinstance(chunk, str) else chunk
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
- await self._upload_chunk(
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
- await self._post(f"{self._url}/files/{file_id}/complete", json={"id": file_id})
403
- logger.info(f"Marked all chunks as complete for file '{file_id}'.")
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
- If you are passing the Input as bytes, pass it uncompressed to avoid
412
- redundant work.**
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 list_task_status(self, action_id: int) -> list[TaskSummary]:
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._get_paginated(
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._get(f"{self._url}/{action_url(action_id)}/{action_id}/tasks/{task_id}")
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._get_binary(
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(filter(lambda f: f.id == file_id, await self.list_files()), None)
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._run_with_retry(
490
- self._put_binary_gzip, f"{self._url}/files/{file_id}/chunks/{index}", content=chunk
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._post(f"{self._url}/files/{file_id}", json={"chunkCount": num_chunks})
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: