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.
- anaplan_sdk/_async_clients/_alm.py +24 -31
- anaplan_sdk/_async_clients/_audit.py +7 -9
- anaplan_sdk/_async_clients/_bulk.py +141 -88
- anaplan_sdk/_async_clients/_cloud_works.py +36 -34
- anaplan_sdk/_async_clients/_cw_flow.py +13 -15
- anaplan_sdk/_async_clients/_transactional.py +62 -42
- anaplan_sdk/_clients/_alm.py +22 -31
- anaplan_sdk/_clients/_audit.py +7 -10
- anaplan_sdk/_clients/_bulk.py +78 -62
- anaplan_sdk/_clients/_cloud_works.py +33 -33
- anaplan_sdk/_clients/_cw_flow.py +11 -13
- anaplan_sdk/_clients/_transactional.py +42 -33
- anaplan_sdk/{_base.py → _services.py} +145 -87
- anaplan_sdk/models/__init__.py +2 -0
- anaplan_sdk/models/_bulk.py +2 -9
- anaplan_sdk/models/cloud_works.py +6 -2
- {anaplan_sdk-0.5.0a1.dist-info → anaplan_sdk-0.5.0a3.dist-info}/METADATA +2 -2
- anaplan_sdk-0.5.0a3.dist-info/RECORD +30 -0
- anaplan_sdk-0.5.0a1.dist-info/RECORD +0 -30
- {anaplan_sdk-0.5.0a1.dist-info → anaplan_sdk-0.5.0a3.dist-info}/WHEEL +0 -0
- {anaplan_sdk-0.5.0a1.dist-info → anaplan_sdk-0.5.0a3.dist-info}/licenses/LICENSE +0 -0
anaplan_sdk/_clients/_alm.py
CHANGED
@@ -1,10 +1,7 @@
|
|
1
1
|
import logging
|
2
|
-
from time import sleep
|
3
2
|
from typing import Literal, overload
|
4
3
|
|
5
|
-
import
|
6
|
-
|
7
|
-
from anaplan_sdk._base import _BaseClient
|
4
|
+
from anaplan_sdk._services import _HttpService
|
8
5
|
from anaplan_sdk.exceptions import AnaplanActionError
|
9
6
|
from anaplan_sdk.models import (
|
10
7
|
ModelRevision,
|
@@ -18,14 +15,11 @@ from anaplan_sdk.models import (
|
|
18
15
|
logger = logging.getLogger("anaplan_sdk")
|
19
16
|
|
20
17
|
|
21
|
-
class _AlmClient
|
22
|
-
def __init__(
|
23
|
-
self
|
24
|
-
) -> None:
|
25
|
-
self.status_poll_delay = status_poll_delay
|
18
|
+
class _AlmClient:
|
19
|
+
def __init__(self, http: _HttpService, model_id: str) -> None:
|
20
|
+
self._http = http
|
26
21
|
self._model_id = model_id
|
27
22
|
self._url = f"https://api.anaplan.com/2/0/models/{model_id}"
|
28
|
-
super().__init__(retry_count, client)
|
29
23
|
|
30
24
|
def change_model_status(self, status: Literal["online", "offline"]) -> None:
|
31
25
|
"""
|
@@ -33,14 +27,14 @@ class _AlmClient(_BaseClient):
|
|
33
27
|
:param status: The status of the model. Can be either "online" or "offline".
|
34
28
|
"""
|
35
29
|
logger.info(f"Changed model status to '{status}' for model {self._model_id}.")
|
36
|
-
self.
|
30
|
+
self._http.put(f"{self._url}/onlineStatus", json={"status": status})
|
37
31
|
|
38
32
|
def get_revisions(self) -> list[Revision]:
|
39
33
|
"""
|
40
34
|
Use this call to return a list of revisions for a specific model.
|
41
35
|
:return: A list of revisions for a specific model.
|
42
36
|
"""
|
43
|
-
res = self.
|
37
|
+
res = self._http.get(f"{self._url}/alm/revisions")
|
44
38
|
return [Revision.model_validate(e) for e in res.get("revisions", [])]
|
45
39
|
|
46
40
|
def get_latest_revision(self) -> Revision | None:
|
@@ -52,7 +46,7 @@ class _AlmClient(_BaseClient):
|
|
52
46
|
latest revision.
|
53
47
|
:return: The latest revision for a specific model, or None if no revisions exist.
|
54
48
|
"""
|
55
|
-
res = (self.
|
49
|
+
res = (self._http.get(f"{self._url}/alm/latestRevision")).get("revisions")
|
56
50
|
return Revision.model_validate(res[0]) if res else None
|
57
51
|
|
58
52
|
def get_syncable_revisions(self, source_model_id: str) -> list[Revision]:
|
@@ -65,7 +59,7 @@ class _AlmClient(_BaseClient):
|
|
65
59
|
:param source_model_id: The ID of the source model.
|
66
60
|
:return: A list of revisions that can be synchronized to the target model.
|
67
61
|
"""
|
68
|
-
res = self.
|
62
|
+
res = self._http.get(f"{self._url}/alm/syncableRevisions?sourceModelId={source_model_id}")
|
69
63
|
return [Revision.model_validate(e) for e in res.get("revisions", [])]
|
70
64
|
|
71
65
|
def create_revision(self, name: str, description: str) -> Revision:
|
@@ -75,7 +69,7 @@ class _AlmClient(_BaseClient):
|
|
75
69
|
:param description: The description of the revision.
|
76
70
|
:return: The created Revision Info.
|
77
71
|
"""
|
78
|
-
res = self.
|
72
|
+
res = self._http.post(
|
79
73
|
f"{self._url}/alm/revisions", json={"name": name, "description": description}
|
80
74
|
)
|
81
75
|
rev = Revision.model_validate(res["revision"])
|
@@ -88,7 +82,7 @@ class _AlmClient(_BaseClient):
|
|
88
82
|
they completed within the last 48 hours.
|
89
83
|
:return: A list of sync tasks in descending order of creation time.
|
90
84
|
"""
|
91
|
-
res = self.
|
85
|
+
res = self._http.get(f"{self._url}/alm/syncTasks")
|
92
86
|
return [TaskSummary.model_validate(e) for e in res.get("tasks", [])]
|
93
87
|
|
94
88
|
def get_sync_task(self, task_id: str) -> SyncTask:
|
@@ -97,7 +91,7 @@ class _AlmClient(_BaseClient):
|
|
97
91
|
:param task_id: The ID of the sync task.
|
98
92
|
:return: The sync task information.
|
99
93
|
"""
|
100
|
-
res = self.
|
94
|
+
res = self._http.get(f"{self._url}/alm/syncTasks/{task_id}")
|
101
95
|
return SyncTask.model_validate(res["task"])
|
102
96
|
|
103
97
|
def sync_models(
|
@@ -123,7 +117,7 @@ class _AlmClient(_BaseClient):
|
|
123
117
|
"sourceModelId": source_model_id,
|
124
118
|
"targetRevisionId": target_revision_id,
|
125
119
|
}
|
126
|
-
res = self.
|
120
|
+
res = self._http.post(f"{self._url}/alm/syncTasks", json=payload)
|
127
121
|
task = self.get_sync_task(res["task"]["taskId"])
|
128
122
|
logger.info(
|
129
123
|
f"Started sync task '{task.id}' from Model '{source_model_id}' "
|
@@ -131,8 +125,7 @@ class _AlmClient(_BaseClient):
|
|
131
125
|
)
|
132
126
|
if not wait_for_completion:
|
133
127
|
return task
|
134
|
-
|
135
|
-
sleep(self.status_poll_delay)
|
128
|
+
task = self._http.poll_task(self.get_sync_task, task.id)
|
136
129
|
if not task.result.successful:
|
137
130
|
msg = f"Sync task {task.id} completed with errors: {task.result.error}."
|
138
131
|
logger.error(msg)
|
@@ -147,7 +140,7 @@ class _AlmClient(_BaseClient):
|
|
147
140
|
:param revision_id: The ID of the revision.
|
148
141
|
:return: A list of models that had a specific revision applied to them.
|
149
142
|
"""
|
150
|
-
res = self.
|
143
|
+
res = self._http.get(f"{self._url}/alm/revisions/{revision_id}/appliedToModels")
|
151
144
|
return [ModelRevision.model_validate(e) for e in res.get("appliedToModels", [])]
|
152
145
|
|
153
146
|
def create_comparison_report(
|
@@ -172,7 +165,7 @@ class _AlmClient(_BaseClient):
|
|
172
165
|
"sourceModelId": source_model_id,
|
173
166
|
"targetRevisionId": target_revision_id,
|
174
167
|
}
|
175
|
-
res = self.
|
168
|
+
res = self._http.post(f"{self._url}/alm/comparisonReportTasks", json=payload)
|
176
169
|
task = self.get_comparison_report_task(res["task"]["taskId"])
|
177
170
|
logger.info(
|
178
171
|
f"Started Comparison Report task '{task.id}' between Model '{source_model_id}' "
|
@@ -180,8 +173,7 @@ class _AlmClient(_BaseClient):
|
|
180
173
|
)
|
181
174
|
if not wait_for_completion:
|
182
175
|
return task
|
183
|
-
|
184
|
-
sleep(self.status_poll_delay)
|
176
|
+
task = self._http.poll_task(self.get_comparison_report_task, task.id)
|
185
177
|
if not task.result.successful:
|
186
178
|
msg = f"Comparison Report task {task.id} completed with errors: {task.result.error}."
|
187
179
|
logger.error(msg)
|
@@ -195,7 +187,7 @@ class _AlmClient(_BaseClient):
|
|
195
187
|
:param task_id: The ID of the comparison report task.
|
196
188
|
:return: The report task information.
|
197
189
|
"""
|
198
|
-
res = self.
|
190
|
+
res = self._http.get(f"{self._url}/alm/comparisonReportTasks/{task_id}")
|
199
191
|
return ReportTask.model_validate(res["task"])
|
200
192
|
|
201
193
|
def get_comparison_report(self, task: ReportTask) -> bytes:
|
@@ -204,7 +196,7 @@ class _AlmClient(_BaseClient):
|
|
204
196
|
:param task: The report task object containing the task ID.
|
205
197
|
:return: The binary content of the comparison report.
|
206
198
|
"""
|
207
|
-
return self.
|
199
|
+
return self._http.get_binary(
|
208
200
|
f"{self._url}/alm/comparisonReports/"
|
209
201
|
f"{task.result.target_revision_id}/{task.result.source_revision_id}"
|
210
202
|
)
|
@@ -248,7 +240,7 @@ class _AlmClient(_BaseClient):
|
|
248
240
|
"sourceModelId": source_model_id,
|
249
241
|
"targetRevisionId": target_revision_id,
|
250
242
|
}
|
251
|
-
res = self.
|
243
|
+
res = self._http.post(f"{self._url}/alm/summaryReportTasks", json=payload)
|
252
244
|
task = self.get_comparison_summary_task(res["task"]["taskId"])
|
253
245
|
logger.info(
|
254
246
|
f"Started Comparison Summary task '{task.id}' between Model '{source_model_id}' "
|
@@ -256,8 +248,7 @@ class _AlmClient(_BaseClient):
|
|
256
248
|
)
|
257
249
|
if not wait_for_completion:
|
258
250
|
return task
|
259
|
-
|
260
|
-
sleep(self.status_poll_delay)
|
251
|
+
task = self._http.poll_task(self.get_comparison_summary_task, task.id)
|
261
252
|
if not task.result.successful:
|
262
253
|
msg = f"Comparison Summary task {task.id} completed with errors: {task.result.error}."
|
263
254
|
logger.error(msg)
|
@@ -271,7 +262,7 @@ class _AlmClient(_BaseClient):
|
|
271
262
|
:param task_id: The ID of the comparison summary task.
|
272
263
|
:return: The report task information.
|
273
264
|
"""
|
274
|
-
res = self.
|
265
|
+
res = self._http.get(f"{self._url}/alm/summaryReportTasks/{task_id}")
|
275
266
|
return ReportTask.model_validate(res["task"])
|
276
267
|
|
277
268
|
def get_comparison_summary(self, task: ReportTask) -> SummaryReport:
|
@@ -280,7 +271,7 @@ class _AlmClient(_BaseClient):
|
|
280
271
|
:param task: The summary task object containing the task ID.
|
281
272
|
:return: The binary content of the comparison summary.
|
282
273
|
"""
|
283
|
-
res = self.
|
274
|
+
res = self._http.get(
|
284
275
|
f"{self._url}/alm/summaryReports/"
|
285
276
|
f"{task.result.target_revision_id}/{task.result.source_revision_id}"
|
286
277
|
)
|
anaplan_sdk/_clients/_audit.py
CHANGED
@@ -1,19 +1,16 @@
|
|
1
1
|
from typing import Any, Literal
|
2
2
|
|
3
|
-
import
|
4
|
-
|
5
|
-
from anaplan_sdk._base import _BaseClient
|
3
|
+
from anaplan_sdk._services import _HttpService
|
6
4
|
from anaplan_sdk.models import User
|
7
5
|
|
8
6
|
Event = Literal["all", "byok", "user_activity"]
|
9
7
|
|
10
8
|
|
11
|
-
class _AuditClient
|
12
|
-
def __init__(self,
|
9
|
+
class _AuditClient:
|
10
|
+
def __init__(self, http: _HttpService) -> None:
|
11
|
+
self._http = http
|
13
12
|
self._limit = 10_000
|
14
|
-
self._thread_count = thread_count
|
15
13
|
self._url = "https://audit.anaplan.com/audit/api/1/events"
|
16
|
-
super().__init__(retry_count, client)
|
17
14
|
|
18
15
|
def get_users(self, search_pattern: str | None = None) -> list[User]:
|
19
16
|
"""
|
@@ -25,7 +22,7 @@ class _AuditClient(_BaseClient):
|
|
25
22
|
params = {"s": search_pattern} if search_pattern else None
|
26
23
|
return [
|
27
24
|
User.model_validate(e)
|
28
|
-
for e in self.
|
25
|
+
for e in self._http.get_paginated(
|
29
26
|
"https://api.anaplan.com/2/0/users", "users", params=params
|
30
27
|
)
|
31
28
|
]
|
@@ -36,7 +33,7 @@ class _AuditClient(_BaseClient):
|
|
36
33
|
:return: The requested or currently authenticated User.
|
37
34
|
"""
|
38
35
|
return User.model_validate(
|
39
|
-
self.
|
36
|
+
self._http.get(f"https://api.anaplan.com/2/0/users/{user_id}").get("user")
|
40
37
|
)
|
41
38
|
|
42
39
|
def get_events(
|
@@ -50,7 +47,7 @@ class _AuditClient(_BaseClient):
|
|
50
47
|
:return: A list of log entries, each containing a dictionary with event details.
|
51
48
|
"""
|
52
49
|
return list(
|
53
|
-
self.
|
50
|
+
self._http.get_paginated(
|
54
51
|
self._url,
|
55
52
|
"response",
|
56
53
|
params={"type": event_type, "intervalInHours": days_into_past * 24},
|
anaplan_sdk/_clients/_bulk.py
CHANGED
@@ -2,14 +2,13 @@ import logging
|
|
2
2
|
import multiprocessing
|
3
3
|
from concurrent.futures import ThreadPoolExecutor
|
4
4
|
from copy import copy
|
5
|
-
from time import sleep
|
6
5
|
from typing import Iterator
|
7
6
|
|
8
7
|
import httpx
|
9
8
|
from typing_extensions import Self
|
10
9
|
|
11
10
|
from anaplan_sdk._auth import _create_auth
|
12
|
-
from anaplan_sdk.
|
11
|
+
from anaplan_sdk._services import _HttpService, action_url
|
13
12
|
from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
|
14
13
|
from anaplan_sdk.models import (
|
15
14
|
Action,
|
@@ -32,7 +31,7 @@ from ._transactional import _TransactionalClient
|
|
32
31
|
logger = logging.getLogger("anaplan_sdk")
|
33
32
|
|
34
33
|
|
35
|
-
class Client
|
34
|
+
class Client:
|
36
35
|
"""
|
37
36
|
Synchronous Anaplan Client. For guides and examples
|
38
37
|
refer to https://vinzenzklass.github.io/anaplan-sdk.
|
@@ -51,10 +50,14 @@ class Client(_BaseClient):
|
|
51
50
|
auth: httpx.Auth | None = None,
|
52
51
|
timeout: float | httpx.Timeout = 30,
|
53
52
|
retry_count: int = 2,
|
53
|
+
backoff: float = 1.0,
|
54
|
+
backoff_factor: float = 2.0,
|
55
|
+
page_size: int = 5_000,
|
54
56
|
status_poll_delay: int = 1,
|
55
57
|
upload_parallel: bool = True,
|
56
58
|
upload_chunk_size: int = 25_000_000,
|
57
59
|
allow_file_creation: bool = False,
|
60
|
+
**httpx_kwargs,
|
58
61
|
) -> None:
|
59
62
|
"""
|
60
63
|
Synchronous Anaplan Client. For guides and examples
|
@@ -86,7 +89,15 @@ class Client(_BaseClient):
|
|
86
89
|
an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
|
87
90
|
:param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
|
88
91
|
to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
|
92
|
+
:param backoff: The initial backoff time in seconds for the retry mechanism. This is the
|
93
|
+
time to wait before the first retry.
|
94
|
+
:param backoff_factor: The factor by which the backoff time is multiplied after each retry.
|
95
|
+
For example, if the initial backoff is 1 second and the factor is 2, the second
|
96
|
+
retry will wait 2 seconds, the third retry will wait 4 seconds, and so on.
|
89
97
|
number of 2 times.
|
98
|
+
:param page_size: The number of items to return per page when paginating through results.
|
99
|
+
Defaults to 5000. This is the maximum number of items that can be returned per
|
100
|
+
request. If you pass a value greater than 5000, it will be capped to 5000.
|
90
101
|
:param status_poll_delay: The delay between polling the status of a task.
|
91
102
|
:param upload_parallel: Whether to upload chunks in parallel when uploading files.
|
92
103
|
:param upload_chunk_size: The size of the chunks to upload. This is the maximum size of
|
@@ -96,41 +107,43 @@ class Client(_BaseClient):
|
|
96
107
|
altogether. A file that is created this way will not be referenced by any action in
|
97
108
|
anaplan until manually assigned so there is typically no value in dynamically
|
98
109
|
creating new files and uploading content to them.
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
110
|
+
:param httpx_kwargs: Additional keyword arguments to pass to the `httpx.Client`.
|
111
|
+
This can be used to set additional options such as proxies, headers, etc. See
|
112
|
+
https://www.python-httpx.org/api/#client for the full list of arguments.
|
113
|
+
"""
|
114
|
+
auth = auth or _create_auth(
|
115
|
+
token=token,
|
116
|
+
user_email=user_email,
|
117
|
+
password=password,
|
118
|
+
certificate=certificate,
|
119
|
+
private_key=private_key,
|
120
|
+
private_key_password=private_key_password,
|
121
|
+
)
|
122
|
+
_client = httpx.Client(auth=auth, timeout=timeout, **httpx_kwargs)
|
123
|
+
self._http = _HttpService(
|
124
|
+
_client,
|
125
|
+
retry_count=retry_count,
|
126
|
+
backoff=backoff,
|
127
|
+
backoff_factor=backoff_factor,
|
128
|
+
page_size=page_size,
|
129
|
+
poll_delay=status_poll_delay,
|
113
130
|
)
|
114
131
|
self._retry_count = retry_count
|
115
132
|
self._workspace_id = workspace_id
|
116
133
|
self._model_id = model_id
|
117
134
|
self._url = f"https://api.anaplan.com/2/0/workspaces/{workspace_id}/models/{model_id}"
|
118
135
|
self._transactional_client = (
|
119
|
-
_TransactionalClient(
|
136
|
+
_TransactionalClient(self._http, model_id) if model_id else None
|
120
137
|
)
|
121
|
-
self._alm_client = (
|
122
|
-
|
123
|
-
if model_id
|
124
|
-
else None
|
125
|
-
)
|
126
|
-
self._cloud_works = _CloudWorksClient(_client, self._retry_count)
|
138
|
+
self._alm_client = _AlmClient(self._http, model_id) if model_id else None
|
139
|
+
self._cloud_works = _CloudWorksClient(self._http)
|
127
140
|
self._thread_count = multiprocessing.cpu_count()
|
128
|
-
self._audit = _AuditClient(
|
141
|
+
self._audit = _AuditClient(self._http)
|
129
142
|
self.status_poll_delay = status_poll_delay
|
130
143
|
self.upload_parallel = upload_parallel
|
131
144
|
self.upload_chunk_size = upload_chunk_size
|
132
145
|
self.allow_file_creation = allow_file_creation
|
133
|
-
|
146
|
+
logger.debug(f"Initialized Client with workspace_id={workspace_id}, model_id={model_id}")
|
134
147
|
|
135
148
|
@classmethod
|
136
149
|
def from_existing(
|
@@ -158,12 +171,8 @@ class Client(_BaseClient):
|
|
158
171
|
f"with workspace_id={new_ws_id}, model_id={new_model_id}."
|
159
172
|
)
|
160
173
|
client._url = f"https://api.anaplan.com/2/0/workspaces/{new_ws_id}/models/{new_model_id}"
|
161
|
-
client._transactional_client = _TransactionalClient(
|
162
|
-
|
163
|
-
)
|
164
|
-
client._alm_client = _AlmClient(
|
165
|
-
existing._client, new_model_id, existing._retry_count, existing.status_poll_delay
|
166
|
-
)
|
174
|
+
client._transactional_client = _TransactionalClient(existing._http, new_model_id)
|
175
|
+
client._alm_client = _AlmClient(existing._http, new_model_id)
|
167
176
|
return client
|
168
177
|
|
169
178
|
@property
|
@@ -183,14 +192,14 @@ class Client(_BaseClient):
|
|
183
192
|
return self._cloud_works
|
184
193
|
|
185
194
|
@property
|
186
|
-
def
|
195
|
+
def tr(self) -> _TransactionalClient:
|
187
196
|
"""
|
188
197
|
The Transactional Client provides access to the Anaplan Transactional API. This is useful
|
189
198
|
for more advanced use cases where you need to interact with the Anaplan Model in a more
|
190
199
|
granular way.
|
191
200
|
|
192
201
|
If you instantiated the client without the field `model_id`, this will raise a
|
193
|
-
|
202
|
+
`ValueError`, since none of the endpoints can be invoked without the model Id.
|
194
203
|
:return: The Transactional Client.
|
195
204
|
"""
|
196
205
|
if not self._transactional_client:
|
@@ -234,7 +243,7 @@ class Client(_BaseClient):
|
|
234
243
|
params["s"] = search_pattern
|
235
244
|
return [
|
236
245
|
Workspace.model_validate(e)
|
237
|
-
for e in self.
|
246
|
+
for e in self._http.get_paginated(
|
238
247
|
"https://api.anaplan.com/2/0/workspaces", "workspaces", params=params
|
239
248
|
)
|
240
249
|
]
|
@@ -253,7 +262,7 @@ class Client(_BaseClient):
|
|
253
262
|
params["s"] = search_pattern
|
254
263
|
return [
|
255
264
|
Model.model_validate(e)
|
256
|
-
for e in self.
|
265
|
+
for e in self._http.get_paginated(
|
257
266
|
"https://api.anaplan.com/2/0/models", "models", params=params
|
258
267
|
)
|
259
268
|
]
|
@@ -266,7 +275,7 @@ class Client(_BaseClient):
|
|
266
275
|
:return:
|
267
276
|
"""
|
268
277
|
logger.info(f"Deleting Models: {', '.join(model_ids)}.")
|
269
|
-
res = self.
|
278
|
+
res = self._http.post(
|
270
279
|
f"https://api.anaplan.com/2/0/workspaces/{self._workspace_id}/bulkDeleteModels",
|
271
280
|
json={"modelIdsToDelete": model_ids},
|
272
281
|
)
|
@@ -277,7 +286,9 @@ class Client(_BaseClient):
|
|
277
286
|
Lists all the Files in the Model.
|
278
287
|
:return: The List of Files.
|
279
288
|
"""
|
280
|
-
return [
|
289
|
+
return [
|
290
|
+
File.model_validate(e) for e in self._http.get_paginated(f"{self._url}/files", "files")
|
291
|
+
]
|
281
292
|
|
282
293
|
def get_actions(self) -> list[Action]:
|
283
294
|
"""
|
@@ -287,7 +298,8 @@ class Client(_BaseClient):
|
|
287
298
|
:return: The List of Actions.
|
288
299
|
"""
|
289
300
|
return [
|
290
|
-
Action.model_validate(e)
|
301
|
+
Action.model_validate(e)
|
302
|
+
for e in self._http.get_paginated(f"{self._url}/actions", "actions")
|
291
303
|
]
|
292
304
|
|
293
305
|
def get_processes(self) -> list[Process]:
|
@@ -297,7 +309,7 @@ class Client(_BaseClient):
|
|
297
309
|
"""
|
298
310
|
return [
|
299
311
|
Process.model_validate(e)
|
300
|
-
for e in self.
|
312
|
+
for e in self._http.get_paginated(f"{self._url}/processes", "processes")
|
301
313
|
]
|
302
314
|
|
303
315
|
def get_imports(self) -> list[Import]:
|
@@ -306,7 +318,8 @@ class Client(_BaseClient):
|
|
306
318
|
:return: The List of Imports.
|
307
319
|
"""
|
308
320
|
return [
|
309
|
-
Import.model_validate(e)
|
321
|
+
Import.model_validate(e)
|
322
|
+
for e in self._http.get_paginated(f"{self._url}/imports", "imports")
|
310
323
|
]
|
311
324
|
|
312
325
|
def get_exports(self) -> list[Export]:
|
@@ -315,7 +328,8 @@ class Client(_BaseClient):
|
|
315
328
|
:return: The List of Exports.
|
316
329
|
"""
|
317
330
|
return [
|
318
|
-
Export.model_validate(e)
|
331
|
+
Export.model_validate(e)
|
332
|
+
for e in (self._http.get(f"{self._url}/exports")).get("exports", [])
|
319
333
|
]
|
320
334
|
|
321
335
|
def run_action(self, action_id: int, wait_for_completion: bool = True) -> TaskStatus:
|
@@ -330,18 +344,15 @@ class Client(_BaseClient):
|
|
330
344
|
until the task is complete. If False, it will spawn the task and return immediately.
|
331
345
|
"""
|
332
346
|
body = {"localeName": "en_US"}
|
333
|
-
res = self.
|
347
|
+
res = self._http.post(f"{self._url}/{action_url(action_id)}/{action_id}/tasks", json=body)
|
334
348
|
task_id = res["task"]["taskId"]
|
335
349
|
logger.info(f"Invoked Action '{action_id}', spawned Task: '{task_id}'.")
|
336
350
|
|
337
351
|
if not wait_for_completion:
|
338
352
|
return TaskStatus.model_validate(self.get_task_status(action_id, task_id))
|
339
|
-
|
340
|
-
while (status := self.get_task_status(action_id, task_id)).task_state != "COMPLETE":
|
341
|
-
sleep(self.status_poll_delay)
|
342
|
-
|
353
|
+
status = self._http.poll_task(self.get_task_status, action_id, task_id)
|
343
354
|
if status.task_state == "COMPLETE" and not status.result.successful:
|
344
|
-
logger.error(f"Task '{task_id}' completed with errors
|
355
|
+
logger.error(f"Task '{task_id}' completed with errors.")
|
345
356
|
raise AnaplanActionError(f"Task '{task_id}' completed with errors.")
|
346
357
|
|
347
358
|
logger.info(f"Task '{task_id}' of Action '{action_id}' completed successfully.")
|
@@ -356,10 +367,10 @@ class Client(_BaseClient):
|
|
356
367
|
chunk_count = self._file_pre_check(file_id)
|
357
368
|
logger.info(f"File {file_id} has {chunk_count} chunks.")
|
358
369
|
if chunk_count <= 1:
|
359
|
-
return self.
|
370
|
+
return self._http.get_binary(f"{self._url}/files/{file_id}")
|
360
371
|
with ThreadPoolExecutor(max_workers=self._thread_count) as executor:
|
361
372
|
chunks = executor.map(
|
362
|
-
self.
|
373
|
+
self._http.get_binary,
|
363
374
|
(f"{self._url}/files/{file_id}/chunks/{i}" for i in range(chunk_count)),
|
364
375
|
)
|
365
376
|
return b"".join(chunks)
|
@@ -378,13 +389,13 @@ class Client(_BaseClient):
|
|
378
389
|
chunk_count = self._file_pre_check(file_id)
|
379
390
|
logger.info(f"File {file_id} has {chunk_count} chunks.")
|
380
391
|
if chunk_count <= 1:
|
381
|
-
yield self.
|
392
|
+
yield self._http.get_binary(f"{self._url}/files/{file_id}")
|
382
393
|
return
|
383
394
|
|
384
395
|
with ThreadPoolExecutor(max_workers=batch_size) as executor:
|
385
396
|
for batch_start in range(0, chunk_count, batch_size):
|
386
397
|
batch_chunks = executor.map(
|
387
|
-
self.
|
398
|
+
self._http.get_binary,
|
388
399
|
(
|
389
400
|
f"{self._url}/files/{file_id}/chunks/{i}"
|
390
401
|
for i in range(batch_start, min(batch_start + batch_size, chunk_count))
|
@@ -458,21 +469,26 @@ class Client(_BaseClient):
|
|
458
469
|
logger.info(
|
459
470
|
f"Completed final upload stream batch of size {len(indices)} for file {file_id}."
|
460
471
|
)
|
461
|
-
self.
|
472
|
+
self._http.post(f"{self._url}/files/{file_id}/complete", json={"id": file_id})
|
462
473
|
logger.info(f"Completed upload stream for '{file_id}'.")
|
463
474
|
|
464
|
-
def upload_and_import(
|
475
|
+
def upload_and_import(
|
476
|
+
self, file_id: int, content: str | bytes, action_id: int, wait_for_completion: bool = True
|
477
|
+
) -> TaskStatus:
|
465
478
|
"""
|
466
479
|
Convenience wrapper around `upload_file()` and `run_action()` to upload content to a file
|
467
480
|
and run an import action in one call.
|
468
481
|
:param file_id: The identifier of the file to upload to.
|
469
482
|
:param content: The content to upload. **This Content will be compressed before uploading.
|
470
|
-
|
471
|
-
|
483
|
+
If you are passing the Input as bytes, pass it uncompressed to avoid redundant
|
484
|
+
work.**
|
472
485
|
:param action_id: The identifier of the action to run after uploading the content.
|
486
|
+
:param wait_for_completion: If True, the method will poll the import task status and not
|
487
|
+
return until the task is complete. If False, it will spawn the import task and
|
488
|
+
return immediately.
|
473
489
|
"""
|
474
490
|
self.upload_file(file_id, content)
|
475
|
-
self.run_action(action_id)
|
491
|
+
return self.run_action(action_id, wait_for_completion)
|
476
492
|
|
477
493
|
def export_and_download(self, action_id: int) -> bytes:
|
478
494
|
"""
|
@@ -492,7 +508,7 @@ class Client(_BaseClient):
|
|
492
508
|
"""
|
493
509
|
return [
|
494
510
|
TaskSummary.model_validate(e)
|
495
|
-
for e in self.
|
511
|
+
for e in self._http.get_paginated(
|
496
512
|
f"{self._url}/{action_url(action_id)}/{action_id}/tasks", "tasks"
|
497
513
|
)
|
498
514
|
]
|
@@ -505,7 +521,7 @@ class Client(_BaseClient):
|
|
505
521
|
:return: The status of the task.
|
506
522
|
"""
|
507
523
|
return TaskStatus.model_validate(
|
508
|
-
self.
|
524
|
+
self._http.get(f"{self._url}/{action_url(action_id)}/{action_id}/tasks/{task_id}").get(
|
509
525
|
"task"
|
510
526
|
)
|
511
527
|
)
|
@@ -517,7 +533,7 @@ class Client(_BaseClient):
|
|
517
533
|
:param task_id: The Task identifier, sometimes also referred to as the Correlation Id.
|
518
534
|
:return: The content of the solution logs.
|
519
535
|
"""
|
520
|
-
return self.
|
536
|
+
return self._http.get_binary(
|
521
537
|
f"{self._url}/optimizeActions/{action_id}/tasks/{task_id}/solutionLogs"
|
522
538
|
)
|
523
539
|
|
@@ -528,7 +544,7 @@ class Client(_BaseClient):
|
|
528
544
|
return file.chunk_count
|
529
545
|
|
530
546
|
def _upload_chunk(self, file_id: int, index: int, chunk: str | bytes) -> None:
|
531
|
-
self.
|
547
|
+
self._http.put_binary_gzip(f"{self._url}/files/{file_id}/chunks/{index}", chunk)
|
532
548
|
logger.debug(f"Chunk {index} loaded to file '{file_id}'.")
|
533
549
|
|
534
550
|
def _set_chunk_count(self, file_id: int, num_chunks: int) -> None:
|
@@ -539,7 +555,7 @@ class Client(_BaseClient):
|
|
539
555
|
"to avoid this error, set `allow_file_creation=True` on the calling instance. "
|
540
556
|
"Make sure you have understood the implications of this before doing so. "
|
541
557
|
)
|
542
|
-
response = self.
|
558
|
+
response = self._http.post(f"{self._url}/files/{file_id}", json={"chunkCount": num_chunks})
|
543
559
|
optionally_new_file = int(response.get("file").get("id"))
|
544
560
|
if optionally_new_file != file_id:
|
545
561
|
if self.allow_file_creation:
|