anaplan-sdk 0.5.0a1__py3-none-any.whl → 0.5.0a3__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,13 +1,13 @@
1
1
  import logging
2
- from asyncio import gather, sleep
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._base import _AsyncBaseClient, 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,
@@ -30,7 +30,7 @@ from ._transactional import _AsyncTransactionalClient
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,13 @@ class AsyncClient(_AsyncBaseClient):
49
49
  auth: httpx.Auth | None = None,
50
50
  timeout: float | httpx.Timeout = 30,
51
51
  retry_count: int = 2,
52
+ backoff: float = 1.0,
53
+ backoff_factor: float = 2.0,
54
+ page_size: int = 5_000,
52
55
  status_poll_delay: int = 1,
53
56
  upload_chunk_size: int = 25_000_000,
54
57
  allow_file_creation: bool = False,
58
+ **httpx_kwargs,
55
59
  ) -> None:
56
60
  """
57
61
  Asynchronous Anaplan Client. For guides and examples
@@ -84,6 +88,14 @@ class AsyncClient(_AsyncBaseClient):
84
88
  :param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
85
89
  to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
86
90
  number of 2 times.
91
+ :param backoff: The initial backoff time in seconds for the retry mechanism. This is the
92
+ time to wait before the first retry.
93
+ :param backoff_factor: The factor by which the backoff time is multiplied after each retry.
94
+ For example, if the initial backoff is 1 second and the factor is 2, the second
95
+ retry will wait 2 seconds, the third retry will wait 4 seconds, and so on.
96
+ :param page_size: The number of items to return per page when paginating through results.
97
+ Defaults to 5000. This is the maximum number of items that can be returned per
98
+ request. If you pass a value greater than 5000, it will be capped to 5000.
87
99
  :param status_poll_delay: The delay between polling the status of a task.
88
100
  :param upload_chunk_size: The size of the chunks to upload. This is the maximum size of
89
101
  each chunk. Defaults to 25MB.
@@ -92,39 +104,41 @@ class AsyncClient(_AsyncBaseClient):
92
104
  altogether. A file that is created this way will not be referenced by any action in
93
105
  anaplan until manually assigned so there is typically no value in dynamically
94
106
  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,
107
+ :param httpx_kwargs: Additional keyword arguments to pass to the `httpx.AsyncClient`.
108
+ This can be used to set additional options such as proxies, headers, etc. See
109
+ https://www.python-httpx.org/api/#asyncclient for the full list of arguments.
110
+ """
111
+ _auth = auth or _create_auth(
112
+ token=token,
113
+ user_email=user_email,
114
+ password=password,
115
+ certificate=certificate,
116
+ private_key=private_key,
117
+ private_key_password=private_key_password,
118
+ )
119
+ _client = httpx.AsyncClient(auth=_auth, timeout=timeout, **httpx_kwargs)
120
+ self._http = _AsyncHttpService(
121
+ _client,
122
+ retry_count=retry_count,
123
+ backoff=backoff,
124
+ backoff_factor=backoff_factor,
125
+ page_size=page_size,
126
+ poll_delay=status_poll_delay,
109
127
  )
110
128
  self._workspace_id = workspace_id
111
129
  self._model_id = model_id
112
- self._retry_count = retry_count
113
130
  self._url = f"https://api.anaplan.com/2/0/workspaces/{workspace_id}/models/{model_id}"
114
131
  self._transactional_client = (
115
- _AsyncTransactionalClient(_client, model_id, retry_count) if model_id else None
116
- )
117
- self._alm_client = (
118
- _AsyncAlmClient(_client, model_id, self._retry_count, status_poll_delay)
119
- if model_id
120
- else None
132
+ _AsyncTransactionalClient(self._http, model_id) if model_id else None
121
133
  )
122
- self._audit = _AsyncAuditClient(_client, self._retry_count)
123
- self._cloud_works = _AsyncCloudWorksClient(_client, self._retry_count)
124
- self.status_poll_delay = status_poll_delay
134
+ self._alm_client = _AsyncAlmClient(self._http, model_id) if model_id else None
135
+ self._audit = _AsyncAuditClient(self._http)
136
+ self._cloud_works = _AsyncCloudWorksClient(self._http)
125
137
  self.upload_chunk_size = upload_chunk_size
126
138
  self.allow_file_creation = allow_file_creation
127
- super().__init__(retry_count, _client)
139
+ logger.debug(
140
+ f"Initialized AsyncClient with workspace_id={workspace_id}, model_id={model_id}"
141
+ )
128
142
 
129
143
  @classmethod
130
144
  def from_existing(
@@ -152,12 +166,8 @@ class AsyncClient(_AsyncBaseClient):
152
166
  f"with workspace_id={new_ws_id}, model_id={new_model_id}."
153
167
  )
154
168
  client._url = f"https://api.anaplan.com/2/0/workspaces/{new_ws_id}/models/{new_model_id}"
155
- client._transactional_client = _AsyncTransactionalClient(
156
- existing._client, new_model_id, existing._retry_count
157
- )
158
- client._alm_client = _AsyncAlmClient(
159
- existing._client, new_model_id, existing._retry_count, existing.status_poll_delay
160
- )
169
+ client._transactional_client = _AsyncTransactionalClient(existing._http, new_model_id)
170
+ client._alm_client = _AsyncAlmClient(existing._http, new_model_id)
161
171
  return client
162
172
 
163
173
  @property
@@ -177,14 +187,14 @@ class AsyncClient(_AsyncBaseClient):
177
187
  return self._cloud_works
178
188
 
179
189
  @property
180
- def transactional(self) -> _AsyncTransactionalClient:
190
+ def tr(self) -> _AsyncTransactionalClient:
181
191
  """
182
192
  The Transactional Client provides access to the Anaplan Transactional API. This is useful
183
193
  for more advanced use cases where you need to interact with the Anaplan Model in a more
184
194
  granular way.
185
195
 
186
196
  If you instantiated the client without the field `model_id`, this will raise a
187
- :py:class:`ValueError`, since none of the endpoints can be invoked without the model Id.
197
+ `ValueError`, since none of the endpoints can be invoked without the model Id.
188
198
  :return: The Transactional Client.
189
199
  """
190
200
  if not self._transactional_client:
@@ -215,40 +225,54 @@ class AsyncClient(_AsyncBaseClient):
215
225
  )
216
226
  return self._alm_client
217
227
 
218
- async def get_workspaces(self, search_pattern: str | None = None) -> list[Workspace]:
228
+ async def get_workspaces(
229
+ self,
230
+ search_pattern: str | None = None,
231
+ sort_by: Literal["size_allowance", "name"] = "name",
232
+ descending: bool = False,
233
+ ) -> list[Workspace]:
219
234
  """
220
235
  Lists all the Workspaces the authenticated user has access to.
221
236
  :param search_pattern: Optionally filter for specific workspaces. When provided,
222
237
  case-insensitive matches workspaces with names containing this string.
223
238
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
224
239
  When None (default), returns all users.
240
+ :param sort_by: The field to sort the results by.
241
+ :param descending: If True, the results will be sorted in descending order.
225
242
  :return: The List of Workspaces.
226
243
  """
227
- params = {"tenantDetails": "true"}
244
+ params = {"tenantDetails": "true"} | sort_params(sort_by, descending)
228
245
  if search_pattern:
229
246
  params["s"] = search_pattern
230
247
  return [
231
248
  Workspace.model_validate(e)
232
- for e in await self._get_paginated(
249
+ for e in await self._http.get_paginated(
233
250
  "https://api.anaplan.com/2/0/workspaces", "workspaces", params=params
234
251
  )
235
252
  ]
236
253
 
237
- async def get_models(self, search_pattern: str | None = None) -> list[Model]:
254
+ async def get_models(
255
+ self,
256
+ search_pattern: str | None = None,
257
+ sort_by: Literal["active_state", "name"] = "name",
258
+ descending: bool = False,
259
+ ) -> list[Model]:
238
260
  """
239
261
  Lists all the Models the authenticated user has access to.
240
262
  :param search_pattern: Optionally filter for specific models. When provided,
241
263
  case-insensitive matches model names containing this string.
242
264
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
243
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.
244
268
  :return: The List of Models.
245
269
  """
246
- params = {"modelDetails": "true"}
270
+ params = {"modelDetails": "true"} | sort_params(sort_by, descending)
247
271
  if search_pattern:
248
272
  params["s"] = search_pattern
249
273
  return [
250
274
  Model.model_validate(e)
251
- for e in await self._get_paginated(
275
+ for e in await self._http.get_paginated(
252
276
  "https://api.anaplan.com/2/0/models", "models", params=params
253
277
  )
254
278
  ]
@@ -261,62 +285,83 @@ class AsyncClient(_AsyncBaseClient):
261
285
  :return:
262
286
  """
263
287
  logger.info(f"Deleting Models: {', '.join(model_ids)}.")
264
- res = await self._post(
288
+ res = await self._http.post(
265
289
  f"https://api.anaplan.com/2/0/workspaces/{self._workspace_id}/bulkDeleteModels",
266
290
  json={"modelIdsToDelete": model_ids},
267
291
  )
268
292
  return ModelDeletionResult.model_validate(res)
269
293
 
270
- async def get_files(self) -> list[File]:
294
+ async def get_files(
295
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
296
+ ) -> list[File]:
271
297
  """
272
298
  Lists all the Files in the Model.
299
+ :param sort_by: The field to sort the results by.
300
+ :param descending: If True, the results will be sorted in descending order.
273
301
  :return: The List of Files.
274
302
  """
275
- return [
276
- File.model_validate(e) for e in await self._get_paginated(f"{self._url}/files", "files")
277
- ]
303
+ res = await self._http.get_paginated(
304
+ f"{self._url}/files", "files", params=sort_params(sort_by, descending)
305
+ )
306
+ return [File.model_validate(e) for e in res]
278
307
 
279
- async def get_actions(self) -> list[Action]:
308
+ async def get_actions(
309
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
310
+ ) -> list[Action]:
280
311
  """
281
312
  Lists all the Actions in the Model. This will only return the Actions listed under
282
313
  `Other Actions` in Anaplan. For Imports, exports, and processes, see their respective
283
314
  methods instead.
315
+ :param sort_by: The field to sort the results by.
316
+ :param descending: If True, the results will be sorted in descending order.
284
317
  :return: The List of Actions.
285
318
  """
286
- return [
287
- Action.model_validate(e)
288
- for e in await self._get_paginated(f"{self._url}/actions", "actions")
289
- ]
319
+ res = await self._http.get_paginated(
320
+ f"{self._url}/actions", "actions", params=sort_params(sort_by, descending)
321
+ )
322
+ return [Action.model_validate(e) for e in res]
290
323
 
291
- async def get_processes(self) -> list[Process]:
324
+ async def get_processes(
325
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
326
+ ) -> list[Process]:
292
327
  """
293
328
  Lists all the Processes in the Model.
329
+ :param sort_by: The field to sort the results by.
330
+ :param descending: If True, the results will be sorted in descending order.
294
331
  :return: The List of Processes.
295
332
  """
296
- return [
297
- Process.model_validate(e)
298
- for e in await self._get_paginated(f"{self._url}/processes", "processes")
299
- ]
333
+ res = await self._http.get_paginated(
334
+ f"{self._url}/processes", "processes", params=sort_params(sort_by, descending)
335
+ )
336
+ return [Process.model_validate(e) for e in res]
300
337
 
301
- async def get_imports(self) -> list[Import]:
338
+ async def get_imports(
339
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
340
+ ) -> list[Import]:
302
341
  """
303
342
  Lists all the Imports in the Model.
343
+ :param sort_by: The field to sort the results by.
344
+ :param descending: If True, the results will be sorted in descending order.
304
345
  :return: The List of Imports.
305
346
  """
306
- return [
307
- Import.model_validate(e)
308
- for e in await self._get_paginated(f"{self._url}/imports", "imports")
309
- ]
347
+ res = await self._http.get_paginated(
348
+ f"{self._url}/imports", "imports", params=sort_params(sort_by, descending)
349
+ )
350
+ return [Import.model_validate(e) for e in res]
310
351
 
311
- async def get_exports(self) -> list[Export]:
352
+ async def get_exports(
353
+ self, sort_by: Literal["id", "name"] = "id", descending: bool = False
354
+ ) -> list[Export]:
312
355
  """
313
356
  Lists all the Exports in the Model.
357
+ :param sort_by: The field to sort the results by.
358
+ :param descending: If True, the results will be sorted in descending order.
314
359
  :return: The List of Exports.
315
360
  """
316
- return [
317
- Export.model_validate(e)
318
- for e in await self._get_paginated(f"{self._url}/exports", "exports")
319
- ]
361
+ res = await self._http.get_paginated(
362
+ f"{self._url}/exports", "exports", params=sort_params(sort_by, descending)
363
+ )
364
+ return [Export.model_validate(e) for e in res]
320
365
 
321
366
  async def run_action(self, action_id: int, wait_for_completion: bool = True) -> TaskStatus:
322
367
  """
@@ -330,18 +375,17 @@ class AsyncClient(_AsyncBaseClient):
330
375
  until the task is complete. If False, it will spawn the task and return immediately.
331
376
  """
332
377
  body = {"localeName": "en_US"}
333
- res = await self._post(f"{self._url}/{action_url(action_id)}/{action_id}/tasks", json=body)
378
+ res = await self._http.post(
379
+ f"{self._url}/{action_url(action_id)}/{action_id}/tasks", json=body
380
+ )
334
381
  task_id = res["task"]["taskId"]
335
382
  logger.info(f"Invoked Action '{action_id}', spawned Task: '{task_id}'.")
336
383
 
337
384
  if not wait_for_completion:
338
385
  return TaskStatus.model_validate(await self.get_task_status(action_id, task_id))
339
-
340
- while (status := await self.get_task_status(action_id, task_id)).task_state != "COMPLETE":
341
- await sleep(self.status_poll_delay)
342
-
386
+ status = await self._http.poll_task(self.get_task_status, action_id, task_id)
343
387
  if status.task_state == "COMPLETE" and not status.result.successful:
344
- logger.error(f"Task '{task_id}' completed with errors: {status.result.error_message}")
388
+ logger.error(f"Task '{task_id}' completed with errors.")
345
389
  raise AnaplanActionError(f"Task '{task_id}' completed with errors.")
346
390
 
347
391
  logger.info(f"Task '{task_id}' of '{action_id}' completed successfully.")
@@ -356,11 +400,11 @@ class AsyncClient(_AsyncBaseClient):
356
400
  chunk_count = await self._file_pre_check(file_id)
357
401
  logger.info(f"File {file_id} has {chunk_count} chunks.")
358
402
  if chunk_count <= 1:
359
- return await self._get_binary(f"{self._url}/files/{file_id}")
403
+ return await self._http.get_binary(f"{self._url}/files/{file_id}")
360
404
  return b"".join(
361
405
  await gather(
362
406
  *(
363
- self._get_binary(f"{self._url}/files/{file_id}/chunks/{i}")
407
+ self._http.get_binary(f"{self._url}/files/{file_id}/chunks/{i}")
364
408
  for i in range(chunk_count)
365
409
  )
366
410
  )
@@ -380,13 +424,13 @@ class AsyncClient(_AsyncBaseClient):
380
424
  chunk_count = await self._file_pre_check(file_id)
381
425
  logger.info(f"File {file_id} has {chunk_count} chunks.")
382
426
  if chunk_count <= 1:
383
- yield await self._get_binary(f"{self._url}/files/{file_id}")
427
+ yield await self._http.get_binary(f"{self._url}/files/{file_id}")
384
428
  return
385
429
 
386
430
  for batch_start in range(0, chunk_count, batch_size):
387
431
  batch_chunks = await gather(
388
432
  *(
389
- self._get_binary(f"{self._url}/files/{file_id}/chunks/{i}")
433
+ self._http.get_binary(f"{self._url}/files/{file_id}/chunks/{i}")
390
434
  for i in range(batch_start, min(batch_start + batch_size, chunk_count))
391
435
  )
392
436
  )
@@ -462,21 +506,26 @@ class AsyncClient(_AsyncBaseClient):
462
506
  logger.info(
463
507
  f"Completed final upload stream batch of size {len(tasks)} for file {file_id}."
464
508
  )
465
- await self._post(f"{self._url}/files/{file_id}/complete", json={"id": file_id})
509
+ await self._http.post(f"{self._url}/files/{file_id}/complete", json={"id": file_id})
466
510
  logger.info(f"Completed upload stream for '{file_id}'.")
467
511
 
468
- async def upload_and_import(self, file_id: int, content: str | bytes, action_id: int) -> None:
512
+ async def upload_and_import(
513
+ self, file_id: int, content: str | bytes, action_id: int, wait_for_completion: bool = True
514
+ ) -> TaskStatus:
469
515
  """
470
516
  Convenience wrapper around `upload_file()` and `run_action()` to upload content to a file
471
517
  and run an import action in one call.
472
518
  :param file_id: The identifier of the file to upload to.
473
519
  :param content: The content to upload. **This Content will be compressed before uploading.
474
- If you are passing the Input as bytes, pass it uncompressed to avoid
475
- redundant work.**
520
+ If you are passing the Input as bytes, pass it uncompressed to avoid redundant
521
+ work.**
476
522
  :param action_id: The identifier of the action to run after uploading the content.
523
+ :param wait_for_completion: If True, the method will poll the import task status and not
524
+ return until the task is complete. If False, it will spawn the import task and
525
+ return immediately.
477
526
  """
478
527
  await self.upload_file(file_id, content)
479
- await self.run_action(action_id)
528
+ return await self.run_action(action_id, wait_for_completion)
480
529
 
481
530
  async def export_and_download(self, action_id: int) -> bytes:
482
531
  """
@@ -496,7 +545,7 @@ class AsyncClient(_AsyncBaseClient):
496
545
  """
497
546
  return [
498
547
  TaskSummary.model_validate(e)
499
- for e in await self._get_paginated(
548
+ for e in await self._http.get_paginated(
500
549
  f"{self._url}/{action_url(action_id)}/{action_id}/tasks", "tasks"
501
550
  )
502
551
  ]
@@ -510,7 +559,9 @@ class AsyncClient(_AsyncBaseClient):
510
559
  """
511
560
  return TaskStatus.model_validate(
512
561
  (
513
- await self._get(f"{self._url}/{action_url(action_id)}/{action_id}/tasks/{task_id}")
562
+ await self._http.get(
563
+ f"{self._url}/{action_url(action_id)}/{action_id}/tasks/{task_id}"
564
+ )
514
565
  ).get("task")
515
566
  )
516
567
 
@@ -521,7 +572,7 @@ class AsyncClient(_AsyncBaseClient):
521
572
  :param task_id: The Task identifier, sometimes also referred to as the Correlation Id.
522
573
  :return: The content of the solution logs.
523
574
  """
524
- return await self._get_binary(
575
+ return await self._http.get_binary(
525
576
  f"{self._url}/optimizeActions/{action_id}/tasks/{task_id}/solutionLogs"
526
577
  )
527
578
 
@@ -532,7 +583,7 @@ class AsyncClient(_AsyncBaseClient):
532
583
  return file.chunk_count
533
584
 
534
585
  async def _upload_chunk(self, file_id: int, index: int, chunk: str | bytes) -> None:
535
- await self._put_binary_gzip(f"{self._url}/files/{file_id}/chunks/{index}", chunk)
586
+ await self._http.put_binary_gzip(f"{self._url}/files/{file_id}/chunks/{index}", chunk)
536
587
  logger.debug(f"Chunk {index} loaded to file '{file_id}'.")
537
588
 
538
589
  async def _set_chunk_count(self, file_id: int, num_chunks: int) -> None:
@@ -542,7 +593,9 @@ class AsyncClient(_AsyncBaseClient):
542
593
  "to avoid this error, set `allow_file_creation=True` on the calling instance. "
543
594
  "Make sure you have understood the implications of this before doing so. "
544
595
  )
545
- response = await self._post(f"{self._url}/files/{file_id}", json={"chunkCount": num_chunks})
596
+ response = await self._http.post(
597
+ f"{self._url}/files/{file_id}", json={"chunkCount": num_chunks}
598
+ )
546
599
  optionally_new_file = int(response.get("file").get("id"))
547
600
  if optionally_new_file != file_id:
548
601
  if self.allow_file_creation: