anaplan-sdk 0.3.1b1__py3-none-any.whl → 0.4.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/__init__.py +8 -1
- anaplan_sdk/_async_clients/_alm.py +1 -22
- anaplan_sdk/_async_clients/_audit.py +18 -3
- anaplan_sdk/_async_clients/_bulk.py +75 -28
- anaplan_sdk/_async_clients/_cloud_works.py +344 -0
- anaplan_sdk/_async_clients/_cw_flow.py +80 -0
- anaplan_sdk/_async_clients/_transactional.py +0 -26
- anaplan_sdk/_auth.py +170 -57
- anaplan_sdk/_base.py +116 -27
- anaplan_sdk/_clients/_alm.py +1 -22
- anaplan_sdk/_clients/_audit.py +16 -3
- anaplan_sdk/_clients/_bulk.py +72 -30
- anaplan_sdk/_clients/_cloud_works.py +342 -0
- anaplan_sdk/_clients/_cw_flow.py +78 -0
- anaplan_sdk/_clients/_transactional.py +0 -22
- anaplan_sdk/models/__init__.py +49 -0
- anaplan_sdk/models/_alm.py +55 -0
- anaplan_sdk/models/_base.py +17 -0
- anaplan_sdk/models/_bulk.py +176 -0
- anaplan_sdk/models/_transactional.py +94 -0
- anaplan_sdk/models/cloud_works.py +478 -0
- anaplan_sdk/models/flows.py +86 -0
- anaplan_sdk-0.4.0a2.dist-info/METADATA +87 -0
- anaplan_sdk-0.4.0a2.dist-info/RECORD +29 -0
- anaplan_sdk/models.py +0 -329
- anaplan_sdk-0.3.1b1.dist-info/METADATA +0 -109
- anaplan_sdk-0.3.1b1.dist-info/RECORD +0 -19
- {anaplan_sdk-0.3.1b1.dist-info → anaplan_sdk-0.4.0a2.dist-info}/WHEEL +0 -0
- {anaplan_sdk-0.3.1b1.dist-info → anaplan_sdk-0.4.0a2.dist-info}/licenses/LICENSE +0 -0
anaplan_sdk/_clients/_bulk.py
CHANGED
@@ -1,18 +1,14 @@
|
|
1
|
-
"""
|
2
|
-
Synchronous Client.
|
3
|
-
"""
|
4
|
-
|
5
1
|
import logging
|
6
2
|
import multiprocessing
|
7
3
|
import time
|
8
4
|
from concurrent.futures import ThreadPoolExecutor
|
9
5
|
from copy import copy
|
10
|
-
from typing import Iterator
|
6
|
+
from typing import Callable, Iterator
|
11
7
|
|
12
8
|
import httpx
|
13
9
|
from typing_extensions import Self
|
14
10
|
|
15
|
-
from anaplan_sdk._auth import
|
11
|
+
from anaplan_sdk._auth import create_auth
|
16
12
|
from anaplan_sdk._base import _BaseClient, action_url
|
17
13
|
from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
|
18
14
|
from anaplan_sdk.models import (
|
@@ -29,6 +25,7 @@ from anaplan_sdk.models import (
|
|
29
25
|
|
30
26
|
from ._alm import _AlmClient
|
31
27
|
from ._audit import _AuditClient
|
28
|
+
from ._cloud_works import _CloudWorksClient
|
32
29
|
from ._transactional import _TransactionalClient
|
33
30
|
|
34
31
|
logging.getLogger("httpx").setLevel(logging.CRITICAL)
|
@@ -56,7 +53,13 @@ class Client(_BaseClient):
|
|
56
53
|
certificate: str | bytes | None = None,
|
57
54
|
private_key: str | bytes | None = None,
|
58
55
|
private_key_password: str | bytes | None = None,
|
59
|
-
|
56
|
+
client_id: str | None = None,
|
57
|
+
client_secret: str | None = None,
|
58
|
+
redirect_uri: str | None = None,
|
59
|
+
refresh_token: str | None = None,
|
60
|
+
oauth2_scope: str = "openid profile email offline_access",
|
61
|
+
on_token_refresh: Callable[[dict[str, str]], None] | None = None,
|
62
|
+
timeout: float | httpx.Timeout = 30,
|
60
63
|
retry_count: int = 2,
|
61
64
|
status_poll_delay: int = 1,
|
62
65
|
upload_parallel: bool = True,
|
@@ -85,7 +88,19 @@ class Client(_BaseClient):
|
|
85
88
|
itself.
|
86
89
|
:param private_key: The absolute path to the private key file or the private key itself.
|
87
90
|
:param private_key_password: The password to access the private key if there is one.
|
88
|
-
:param
|
91
|
+
:param client_id: The client Id of the Oauth2 Anaplan Client.
|
92
|
+
:param client_secret: The client secret for your Oauth2 Anaplan Client.
|
93
|
+
:param redirect_uri: The redirect URI for your Oauth2 Anaplan Client.
|
94
|
+
:param refresh_token: If you have a valid refresh token, you can pass it to skip the
|
95
|
+
interactive authentication code step.
|
96
|
+
:param oauth2_scope: The scope of the Oauth2 token, if you want to narrow it.
|
97
|
+
:param on_token_refresh: A callback function that is called whenever the token is refreshed.
|
98
|
+
With this you can for example securely store the token in your
|
99
|
+
application or on your server for later reuse. The function
|
100
|
+
must accept a single argument, which is the token dictionary
|
101
|
+
returned by the Oauth2 token endpoint.
|
102
|
+
:param timeout: The timeout in seconds for the HTTP requests. Alternatively, you can pass
|
103
|
+
an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
|
89
104
|
:param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
|
90
105
|
to never retry. Defaults to 2, meaning each HTTP Operation will be
|
91
106
|
tried a total number of 2 times.
|
@@ -102,36 +117,38 @@ class Client(_BaseClient):
|
|
102
117
|
manually assigned so there is typically no value in dynamically
|
103
118
|
creating new files and uploading content to them.
|
104
119
|
"""
|
105
|
-
|
106
|
-
raise ValueError(
|
107
|
-
"Either `certificate` and `private_key` or `user_email` and `password` must be "
|
108
|
-
"provided."
|
109
|
-
)
|
110
|
-
self._client = httpx.Client(
|
120
|
+
_client = httpx.Client(
|
111
121
|
auth=(
|
112
|
-
|
113
|
-
|
122
|
+
create_auth(
|
123
|
+
user_email=user_email,
|
124
|
+
password=password,
|
125
|
+
certificate=certificate,
|
126
|
+
private_key=private_key,
|
127
|
+
private_key_password=private_key_password,
|
128
|
+
client_id=client_id,
|
129
|
+
client_secret=client_secret,
|
130
|
+
redirect_uri=redirect_uri,
|
131
|
+
refresh_token=refresh_token,
|
132
|
+
oauth2_scope=oauth2_scope,
|
133
|
+
on_token_refresh=on_token_refresh,
|
114
134
|
)
|
115
|
-
if certificate
|
116
|
-
else AnaplanBasicAuth(user_email=user_email, password=password)
|
117
135
|
),
|
118
136
|
timeout=timeout,
|
119
137
|
)
|
120
138
|
self._retry_count = retry_count
|
121
139
|
self._url = f"https://api.anaplan.com/2/0/workspaces/{workspace_id}/models/{model_id}"
|
122
140
|
self._transactional_client = (
|
123
|
-
_TransactionalClient(
|
124
|
-
)
|
125
|
-
self._alm_client = (
|
126
|
-
_AlmClient(self._client, model_id, self._retry_count) if model_id else None
|
141
|
+
_TransactionalClient(_client, model_id, self._retry_count) if model_id else None
|
127
142
|
)
|
143
|
+
self._alm_client = _AlmClient(_client, model_id, self._retry_count) if model_id else None
|
144
|
+
self._cloud_works = _CloudWorksClient(_client, self._retry_count)
|
128
145
|
self._thread_count = multiprocessing.cpu_count()
|
129
|
-
self.
|
146
|
+
self._audit = _AuditClient(_client, self._retry_count, self._thread_count)
|
130
147
|
self.status_poll_delay = status_poll_delay
|
131
148
|
self.upload_parallel = upload_parallel
|
132
149
|
self.upload_chunk_size = upload_chunk_size
|
133
150
|
self.allow_file_creation = allow_file_creation
|
134
|
-
super().__init__(self._retry_count,
|
151
|
+
super().__init__(self._retry_count, _client)
|
135
152
|
|
136
153
|
@classmethod
|
137
154
|
def from_existing(cls, existing: Self, workspace_id: str, model_id: str) -> Self:
|
@@ -153,6 +170,22 @@ class Client(_BaseClient):
|
|
153
170
|
client._alm_client = _AlmClient(existing._client, model_id, existing._retry_count)
|
154
171
|
return client
|
155
172
|
|
173
|
+
@property
|
174
|
+
def audit(self) -> _AuditClient:
|
175
|
+
"""
|
176
|
+
The Audit Client provides access to the Anaplan Audit API.
|
177
|
+
For details, see https://vinzenzklass.github.io/anaplan-sdk/guides/audit/.
|
178
|
+
"""
|
179
|
+
return self._audit
|
180
|
+
|
181
|
+
@property
|
182
|
+
def cw(self) -> _CloudWorksClient:
|
183
|
+
"""
|
184
|
+
The Cloud Works Client provides access to the Anaplan Cloud Works API.
|
185
|
+
For details, see https://vinzenzklass.github.io/anaplan-sdk/guides/cloud_works/.
|
186
|
+
"""
|
187
|
+
return self._cloud_works
|
188
|
+
|
156
189
|
@property
|
157
190
|
def transactional(self) -> _TransactionalClient:
|
158
191
|
"""
|
@@ -192,29 +225,38 @@ class Client(_BaseClient):
|
|
192
225
|
)
|
193
226
|
return self._alm_client
|
194
227
|
|
195
|
-
def list_workspaces(self) -> list[Workspace]:
|
228
|
+
def list_workspaces(self, search_pattern: str | None = None) -> list[Workspace]:
|
196
229
|
"""
|
197
230
|
Lists all the Workspaces the authenticated user has access to.
|
231
|
+
:param search_pattern: Optional filter for workspaces. When provided, case-insensitive
|
232
|
+
matches workspaces with names containing this string. When None (default),
|
233
|
+
returns all workspaces.
|
198
234
|
:return: The List of Workspaces.
|
199
235
|
"""
|
236
|
+
params = {"tenantDetails": "true"}
|
237
|
+
if search_pattern:
|
238
|
+
params["s"] = search_pattern
|
200
239
|
return [
|
201
240
|
Workspace.model_validate(e)
|
202
241
|
for e in self._get_paginated(
|
203
|
-
"https://api.anaplan.com/2/0/workspaces",
|
204
|
-
"workspaces",
|
205
|
-
params={"tenantDetails": "true"},
|
242
|
+
"https://api.anaplan.com/2/0/workspaces", "workspaces", params=params
|
206
243
|
)
|
207
244
|
]
|
208
245
|
|
209
|
-
def list_models(self) -> list[Model]:
|
246
|
+
def list_models(self, search_pattern: str | None = None) -> list[Model]:
|
210
247
|
"""
|
211
248
|
Lists all the Models the authenticated user has access to.
|
249
|
+
:param search_pattern: Optional filter for models. When provided, case-insensitive matches
|
250
|
+
models with names containing this string. When None (default), returns all models.
|
212
251
|
:return: The List of Models.
|
213
252
|
"""
|
253
|
+
params = {"modelDetails": "true"}
|
254
|
+
if search_pattern:
|
255
|
+
params["s"] = search_pattern
|
214
256
|
return [
|
215
257
|
Model.model_validate(e)
|
216
258
|
for e in self._get_paginated(
|
217
|
-
"https://api.anaplan.com/2/0/models", "models", params=
|
259
|
+
"https://api.anaplan.com/2/0/models", "models", params=params
|
218
260
|
)
|
219
261
|
]
|
220
262
|
|
@@ -0,0 +1,342 @@
|
|
1
|
+
from typing import Any, Literal
|
2
|
+
|
3
|
+
import httpx
|
4
|
+
|
5
|
+
from anaplan_sdk._base import (
|
6
|
+
_BaseClient,
|
7
|
+
connection_body_payload,
|
8
|
+
construct_payload,
|
9
|
+
integration_payload,
|
10
|
+
schedule_payload,
|
11
|
+
)
|
12
|
+
from anaplan_sdk.models.cloud_works import (
|
13
|
+
Connection,
|
14
|
+
ConnectionBody,
|
15
|
+
ConnectionInput,
|
16
|
+
Integration,
|
17
|
+
IntegrationInput,
|
18
|
+
IntegrationProcessInput,
|
19
|
+
NotificationConfig,
|
20
|
+
NotificationInput,
|
21
|
+
RunError,
|
22
|
+
RunStatus,
|
23
|
+
RunSummary,
|
24
|
+
ScheduleInput,
|
25
|
+
SingleIntegration,
|
26
|
+
)
|
27
|
+
|
28
|
+
from ._cw_flow import _FlowClient
|
29
|
+
|
30
|
+
|
31
|
+
class _CloudWorksClient(_BaseClient):
|
32
|
+
def __init__(self, client: httpx.Client, retry_count: int) -> None:
|
33
|
+
self._url = "https://api.cloudworks.anaplan.com/2/0/integrations"
|
34
|
+
self._flow = _FlowClient(client, retry_count)
|
35
|
+
super().__init__(retry_count, client)
|
36
|
+
|
37
|
+
@property
|
38
|
+
def flows(self) -> _FlowClient:
|
39
|
+
"""
|
40
|
+
Access the Integration Flow APIs.
|
41
|
+
"""
|
42
|
+
return self._flow
|
43
|
+
|
44
|
+
def list_connections(self) -> list[Connection]:
|
45
|
+
"""
|
46
|
+
List all Connections available in CloudWorks.
|
47
|
+
:return: A list of connections.
|
48
|
+
"""
|
49
|
+
return [
|
50
|
+
Connection.model_validate(e)
|
51
|
+
for e in self._get_paginated(f"{self._url}/connections", "connections")
|
52
|
+
]
|
53
|
+
|
54
|
+
def create_connection(self, con_info: ConnectionInput | dict[str, Any]) -> str:
|
55
|
+
"""
|
56
|
+
Create a new connection in CloudWorks.
|
57
|
+
:param con_info: The connection information. This can be a ConnectionInput instance or a
|
58
|
+
dictionary as per the documentation. If a dictionary is passed, it will be validated
|
59
|
+
against the ConnectionInput model before sending the request.
|
60
|
+
:return: The ID of the new connection.
|
61
|
+
"""
|
62
|
+
res = self._post(
|
63
|
+
f"{self._url}/connections", json=construct_payload(ConnectionInput, con_info)
|
64
|
+
)
|
65
|
+
return res["connections"]["connectionId"]
|
66
|
+
|
67
|
+
def update_connection(self, con_id: str, con_info: ConnectionBody | dict[str, Any]) -> None:
|
68
|
+
"""
|
69
|
+
Update an existing connection in CloudWorks.
|
70
|
+
:param con_id: The ID of the connection to update.
|
71
|
+
:param con_info: The name and details of the connection. You must pass all the same details
|
72
|
+
as when initially creating the connection again. If you want to update only some of
|
73
|
+
the details, use the `patch_connection` method instead.
|
74
|
+
"""
|
75
|
+
self._put(f"{self._url}/connections/{con_id}", json=connection_body_payload(con_info))
|
76
|
+
|
77
|
+
def patch_connection(self, con_id: str, body: dict[str, Any]) -> None:
|
78
|
+
"""
|
79
|
+
Update an existing connection in CloudWorks.
|
80
|
+
:param con_id: The ID of the connection to update.
|
81
|
+
:param body: The name and details of the connection. You can pass all the same details as
|
82
|
+
when initially creating the connection again, or just any one of them.
|
83
|
+
"""
|
84
|
+
self._patch(f"{self._url}/connections/{con_id}", json=body)
|
85
|
+
|
86
|
+
def delete_connection(self, con_id: str) -> None:
|
87
|
+
"""
|
88
|
+
Delete an existing connection in CloudWorks.
|
89
|
+
:param con_id: The ID of the connection to delete.
|
90
|
+
"""
|
91
|
+
self._delete(f"{self._url}/connections/{con_id}")
|
92
|
+
|
93
|
+
def list_integrations(
|
94
|
+
self, sort_by_name: Literal["ascending", "descending"] = "ascending"
|
95
|
+
) -> list[Integration]:
|
96
|
+
"""
|
97
|
+
List all integrations in CloudWorks.
|
98
|
+
:param sort_by_name: Sort the integrations by name in ascending or descending order.
|
99
|
+
:return: A list of integrations.
|
100
|
+
"""
|
101
|
+
params = {"sortBy": "name" if sort_by_name == "ascending" else "-name"}
|
102
|
+
return [
|
103
|
+
Integration.model_validate(e)
|
104
|
+
for e in self._get_paginated(f"{self._url}", "integrations", params=params)
|
105
|
+
]
|
106
|
+
|
107
|
+
def get_integration(self, integration_id: str) -> SingleIntegration:
|
108
|
+
"""
|
109
|
+
Get the details of a specific integration in CloudWorks.
|
110
|
+
|
111
|
+
**Note: This will not include the integration type! While present when listing integrations,
|
112
|
+
the integration type is not included in the details of a single integration.**
|
113
|
+
:param integration_id: The ID of the integration to retrieve.
|
114
|
+
:return: The details of the integration, without the integration type.
|
115
|
+
"""
|
116
|
+
return SingleIntegration.model_validate(
|
117
|
+
(self._get(f"{self._url}/{integration_id}"))["integration"]
|
118
|
+
)
|
119
|
+
|
120
|
+
def create_integration(
|
121
|
+
self, body: IntegrationInput | IntegrationProcessInput | dict[str, Any]
|
122
|
+
) -> str:
|
123
|
+
"""
|
124
|
+
Create a new integration in CloudWorks. If not specified, the integration type will be
|
125
|
+
either "Import" or "Export" based on the source and target you provide.
|
126
|
+
|
127
|
+
If you want to instead create a process Integration, you can do so by specifying
|
128
|
+
the `process_id` parameter and passing several jobs. **Be careful to ensure, that all ids
|
129
|
+
specified in the job inputs match what is defined in your model and matches the process.**
|
130
|
+
If this is not the case, this will error, occasionally with a misleading error message,
|
131
|
+
i.e. `XYZ is not defined in your model` even though it is, Anaplan just does not know what
|
132
|
+
to do with it in the location you specified.
|
133
|
+
|
134
|
+
You can also use CloudWorks Integrations to simply schedule a process. To do this, you
|
135
|
+
can simply pass an IntegrationProcessInput instance with the process_id and no jobs. This
|
136
|
+
will create a process integration that will run the process you specified.
|
137
|
+
:param body: The integration information. This can be an
|
138
|
+
IntegrationInput | IntegrationProcessInput instance or a dictionary as per the
|
139
|
+
documentation. If a dictionary is passed, it will be validated against the
|
140
|
+
IntegrationInput model before sending the request.
|
141
|
+
:return: The ID of the new integration.
|
142
|
+
"""
|
143
|
+
json = integration_payload(body)
|
144
|
+
return (self._post(f"{self._url}", json=json))["integration"]["integrationId"]
|
145
|
+
|
146
|
+
def update_integration(
|
147
|
+
self, integration_id: str, body: IntegrationInput | IntegrationProcessInput | dict[str, Any]
|
148
|
+
) -> None:
|
149
|
+
"""
|
150
|
+
Update an existing integration in CloudWorks.
|
151
|
+
:param integration_id: The ID of the integration to update.
|
152
|
+
:param body: The name and details of the integration. You must pass all the same details
|
153
|
+
as when initially creating the integration again. If you want to update only some
|
154
|
+
of the details, use the `patch_integration` method instead.
|
155
|
+
"""
|
156
|
+
json = integration_payload(body)
|
157
|
+
self._put(f"{self._url}/{integration_id}", json=json)
|
158
|
+
|
159
|
+
def run_integration(self, integration_id: str) -> str:
|
160
|
+
"""
|
161
|
+
Run an integration in CloudWorks.
|
162
|
+
:param integration_id: The ID of the integration to run.
|
163
|
+
:return: The ID of the run instance.
|
164
|
+
"""
|
165
|
+
return (self._post_empty(f"{self._url}/{integration_id}/run"))["run"]["id"]
|
166
|
+
|
167
|
+
def delete_integration(self, integration_id: str) -> None:
|
168
|
+
"""
|
169
|
+
Delete an existing integration in CloudWorks.
|
170
|
+
:param integration_id: The ID of the integration to delete.
|
171
|
+
"""
|
172
|
+
self._delete(f"{self._url}/{integration_id}")
|
173
|
+
|
174
|
+
def get_run_history(self, integration_id: str) -> list[RunSummary]:
|
175
|
+
"""
|
176
|
+
Get the run history of a specific integration in CloudWorks.
|
177
|
+
:param integration_id: The ID of the integration to retrieve the run history for.
|
178
|
+
:return: A list of run statuses.
|
179
|
+
"""
|
180
|
+
return [
|
181
|
+
RunSummary.model_validate(e)
|
182
|
+
for e in (self._get(f"{self._url}/runs/{integration_id}"))["history_of_runs"].get(
|
183
|
+
"runs", []
|
184
|
+
)
|
185
|
+
]
|
186
|
+
|
187
|
+
def get_run_status(self, run_id: str) -> RunStatus:
|
188
|
+
"""
|
189
|
+
Get the status of a specific run in CloudWorks.
|
190
|
+
:param run_id: The ID of the run to retrieve.
|
191
|
+
:return: The details of the run.
|
192
|
+
"""
|
193
|
+
return RunStatus.model_validate((self._get(f"{self._url}/run/{run_id}"))["run"])
|
194
|
+
|
195
|
+
def get_run_error(self, run_id: str) -> RunError | None:
|
196
|
+
"""
|
197
|
+
Get the error details of a specific run in CloudWorks. This exposes potential underlying
|
198
|
+
errors like the error of the invoked action, failure dumps and other details.
|
199
|
+
:param run_id: The ID of the run to retrieve.
|
200
|
+
:return: The details of the run error.
|
201
|
+
"""
|
202
|
+
run = self._get(f"{self._url}/runerror/{run_id}")
|
203
|
+
return RunError.model_validate(run["runs"]) if run.get("runs") else None
|
204
|
+
|
205
|
+
def create_schedule(
|
206
|
+
self, integration_id: str, schedule: ScheduleInput | dict[str, Any]
|
207
|
+
) -> None:
|
208
|
+
"""
|
209
|
+
Schedule an integration in CloudWorks.
|
210
|
+
:param integration_id: The ID of the integration to schedule.
|
211
|
+
:param schedule: The schedule information. This can be a ScheduleInput instance or a
|
212
|
+
dictionary as per the documentation. If a dictionary is passed, it will be validated
|
213
|
+
against the ScheduleInput model before sending the request.
|
214
|
+
"""
|
215
|
+
self._post(
|
216
|
+
f"{self._url}/{integration_id}/schedule",
|
217
|
+
json=schedule_payload(integration_id, schedule),
|
218
|
+
)
|
219
|
+
|
220
|
+
def update_schedule(
|
221
|
+
self, integration_id: str, schedule: ScheduleInput | dict[str, Any]
|
222
|
+
) -> None:
|
223
|
+
"""
|
224
|
+
Update an integration Schedule in CloudWorks. A schedule must already exist.
|
225
|
+
:param integration_id: The ID of the integration to schedule.
|
226
|
+
:param schedule: The schedule information. This can be a ScheduleInput instance or a
|
227
|
+
dictionary as per the documentation. If a dictionary is passed, it will be validated
|
228
|
+
against the ScheduleInput model before sending the request.
|
229
|
+
"""
|
230
|
+
self._put(
|
231
|
+
f"{self._url}/{integration_id}/schedule",
|
232
|
+
json=schedule_payload(integration_id, schedule),
|
233
|
+
)
|
234
|
+
|
235
|
+
def set_schedule_status(
|
236
|
+
self, integration_id: str, status: Literal["enabled", "disabled"]
|
237
|
+
) -> None:
|
238
|
+
"""
|
239
|
+
Set the status of an integration schedule in CloudWorks. A schedule must already exist.
|
240
|
+
:param integration_id: The ID of the integration to schedule.
|
241
|
+
:param status: The status of the schedule. This can be either "enabled" or "disabled".
|
242
|
+
"""
|
243
|
+
self._post_empty(f"{self._url}/{integration_id}/schedule/status/{status}")
|
244
|
+
|
245
|
+
def delete_schedule(self, integration_id: str) -> None:
|
246
|
+
"""
|
247
|
+
Delete an integration schedule in CloudWorks. A schedule must already exist.
|
248
|
+
:param integration_id: The ID of the integration to schedule.
|
249
|
+
"""
|
250
|
+
self._delete(f"{self._url}/{integration_id}/schedule")
|
251
|
+
|
252
|
+
def get_notification_config(
|
253
|
+
self, notification_id: str | None = None, integration_id: str | None = None
|
254
|
+
) -> NotificationConfig:
|
255
|
+
"""
|
256
|
+
Get the notification configuration, either by its Id, or the notification configuration
|
257
|
+
for a specific integration. If the integration_id is specified, the notification_id
|
258
|
+
will be ignored.
|
259
|
+
:param notification_id: The ID of the notification configuration to retrieve.
|
260
|
+
:param integration_id: The ID of the integration to retrieve the notification
|
261
|
+
configuration for.
|
262
|
+
:return: The details of the notification configuration.
|
263
|
+
"""
|
264
|
+
if not (notification_id or integration_id):
|
265
|
+
raise ValueError("Either notification_id or integration_id must be specified.")
|
266
|
+
if integration_id:
|
267
|
+
notification_id = (self.get_integration(integration_id)).notification_id
|
268
|
+
return NotificationConfig.model_validate(
|
269
|
+
(self._get(f"{self._url}/notification/{notification_id}"))["notifications"]
|
270
|
+
)
|
271
|
+
|
272
|
+
def create_notification_config(self, config: NotificationInput | dict[str, Any]) -> str:
|
273
|
+
"""
|
274
|
+
Create a notification configuration for an integration in CloudWorks. This will error if
|
275
|
+
there is already a notification configuration for the integration, which is also the case
|
276
|
+
by default. In this case, you will want to use the `update_notification_config` method
|
277
|
+
instead, to partially update the existing configuration or overwrite it.
|
278
|
+
:param config: The notification configuration. This can be a NotificationInput instance or
|
279
|
+
a dictionary as per the documentation. If a dictionary is passed, it will be
|
280
|
+
validated against the NotificationConfig model before sending the request.
|
281
|
+
:return: The ID of the new notification configuration.
|
282
|
+
"""
|
283
|
+
res = self._post(
|
284
|
+
f"{self._url}/notification", json=construct_payload(NotificationInput, config)
|
285
|
+
)
|
286
|
+
return res["notification"]["notificationId"]
|
287
|
+
|
288
|
+
def update_notification_config(
|
289
|
+
self, notification_id: str, config: NotificationInput | dict[str, Any]
|
290
|
+
) -> None:
|
291
|
+
"""
|
292
|
+
Update a notification configuration for an integration in CloudWorks. You cannot pass empty
|
293
|
+
values or nulls to any of the fields If you want to for e.g. override an existing list of
|
294
|
+
users with an empty one, you must delete the notification configuration and create a new
|
295
|
+
one with only the values you want to keep.
|
296
|
+
:param notification_id: The ID of the notification configuration to update.
|
297
|
+
:param config: The notification configuration. This can be a NotificationInput instance or
|
298
|
+
a dictionary as per the documentation. If a dictionary is passed, it will be
|
299
|
+
validated against the NotificationConfig model before sending the request.
|
300
|
+
"""
|
301
|
+
self._put(
|
302
|
+
f"{self._url}/notification/{notification_id}",
|
303
|
+
json=construct_payload(NotificationInput, config),
|
304
|
+
)
|
305
|
+
|
306
|
+
def delete_notification_config(
|
307
|
+
self, notification_id: str | None = None, integration_id: str | None = None
|
308
|
+
) -> None:
|
309
|
+
"""
|
310
|
+
Delete a notification configuration for an integration in CloudWorks, either by its Id, or
|
311
|
+
the notification configuration for a specific integration. If the integration_id is
|
312
|
+
specified, the notification_id will be ignored.
|
313
|
+
:param notification_id: The ID of the notification configuration to delete.
|
314
|
+
:param integration_id: The ID of the integration to delete the notification config of.
|
315
|
+
"""
|
316
|
+
if not (notification_id or integration_id):
|
317
|
+
raise ValueError("Either notification_id or integration_id must be specified.")
|
318
|
+
if integration_id:
|
319
|
+
notification_id = (self.get_integration(integration_id)).notification_id
|
320
|
+
self._delete(f"{self._url}/notification/{notification_id}")
|
321
|
+
|
322
|
+
def get_import_error_dump(self, run_id: str) -> bytes:
|
323
|
+
"""
|
324
|
+
Get the error dump of a specific import run in CloudWorks. Calling this for a run_id that
|
325
|
+
did not generate any failure dumps will produce an error.
|
326
|
+
|
327
|
+
**Note that if you need the error dump of an action in a process, you must use the
|
328
|
+
`get_process_error_dump` method instead.**
|
329
|
+
:param run_id: The ID of the run to retrieve.
|
330
|
+
:return: The error dump.
|
331
|
+
"""
|
332
|
+
return self._get_binary(f"{self._url}/run/{run_id}/dump")
|
333
|
+
|
334
|
+
def get_process_error_dump(self, run_id: str, action_id: int | str) -> bytes:
|
335
|
+
"""
|
336
|
+
Get the error dump of a specific import run in CloudWorks, that is part of a process.
|
337
|
+
Calling this for a run_id that did not generate any failure dumps will produce an error.
|
338
|
+
:param run_id: The ID of the run to retrieve.
|
339
|
+
:param action_id: The ID of the action to retrieve. This can be found in the RunError.
|
340
|
+
:return: The error dump.
|
341
|
+
"""
|
342
|
+
return self._get_binary(f"{self._url}/run/{run_id}/process/import/{action_id}/dumps")
|
@@ -0,0 +1,78 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
import httpx
|
4
|
+
|
5
|
+
from anaplan_sdk._base import _BaseClient, construct_payload
|
6
|
+
from anaplan_sdk.models.flows import Flow, FlowInput, FlowSummary
|
7
|
+
|
8
|
+
|
9
|
+
class _FlowClient(_BaseClient):
|
10
|
+
def __init__(self, client: httpx.Client, retry_count: int) -> None:
|
11
|
+
self._url = "https://api.cloudworks.anaplan.com/2/0/integrationflows"
|
12
|
+
super().__init__(retry_count, client)
|
13
|
+
|
14
|
+
def list_flows(self, current_user_only: bool = False) -> list[FlowSummary]:
|
15
|
+
"""
|
16
|
+
List all flows in CloudWorks.
|
17
|
+
:param current_user_only: Filters the flows to only those created by the current user.
|
18
|
+
:return: A list of FlowSummaries.
|
19
|
+
"""
|
20
|
+
params = {"myIntegrations": 1 if current_user_only else 0}
|
21
|
+
return [
|
22
|
+
FlowSummary.model_validate(e)
|
23
|
+
for e in self._get_paginated(self._url, "integrationFlows", page_size=25, params=params)
|
24
|
+
]
|
25
|
+
|
26
|
+
def get_flow(self, flow_id: str) -> Flow:
|
27
|
+
"""
|
28
|
+
Get a flow by its ID. This returns the full flow object, including the contained steps and
|
29
|
+
continuation behavior.
|
30
|
+
:param flow_id: The ID of the flow to get.
|
31
|
+
:return: The Flow object.
|
32
|
+
"""
|
33
|
+
return Flow.model_validate((self._get(f"{self._url}/{flow_id}"))["integrationFlow"])
|
34
|
+
|
35
|
+
def run_flow(self, flow_id: str, only_steps: list[str] = None) -> str:
|
36
|
+
"""
|
37
|
+
Run a flow by its ID. Make sure that neither the flow nor any of its contained are running.
|
38
|
+
If this is the case, the task will error. Anaplan neither schedules these tasks nor can it
|
39
|
+
handle concurrent executions.
|
40
|
+
:param flow_id: The ID of the flow to run.
|
41
|
+
:param only_steps: A list of step IDs to run. If not provided, only these will be run.
|
42
|
+
:return: The ID of the run.
|
43
|
+
"""
|
44
|
+
url = f"{self._url}/{flow_id}/run"
|
45
|
+
res = (
|
46
|
+
self._post(url, json={"stepsToRun": only_steps})
|
47
|
+
if only_steps
|
48
|
+
else self._post_empty(url)
|
49
|
+
)
|
50
|
+
return res["run"]["id"]
|
51
|
+
|
52
|
+
def create_flow(self, flow: FlowInput | dict[str, Any]) -> str:
|
53
|
+
"""
|
54
|
+
Create a new flow in CloudWorks. Be careful not to omit the `depends_on` field. Anaplan
|
55
|
+
will accept these values, but an invalid, corrupted flow will be created, as all Flows must
|
56
|
+
have at least 2 Steps, and they must always be sequential
|
57
|
+
:param flow: The flow to create. This can be a FlowInput object or a dictionary.
|
58
|
+
:return: The ID of the created flow.
|
59
|
+
"""
|
60
|
+
res = self._post(self._url, json=construct_payload(FlowInput, flow))
|
61
|
+
return res["integrationFlow"]["integrationFlowId"]
|
62
|
+
|
63
|
+
def update_flow(self, flow_id: str, flow: FlowInput | dict[str, Any]) -> None:
|
64
|
+
"""
|
65
|
+
Update a flow in CloudWorks. You must provide the full flow object, partial updates are not
|
66
|
+
supported.
|
67
|
+
:param flow_id: The ID of the flow to update.
|
68
|
+
:param flow: The flow to update. This can be a FlowInput object or a dictionary.
|
69
|
+
"""
|
70
|
+
self._put(f"{self._url}/{flow_id}", json=construct_payload(FlowInput, flow))
|
71
|
+
|
72
|
+
def delete_flow(self, flow_id: str) -> None:
|
73
|
+
"""
|
74
|
+
Delete a flow in CloudWorks. This will not delete its contained steps. This will fail if
|
75
|
+
the flow is running or if it has any running steps.
|
76
|
+
:param flow_id: The ID of the flow to delete.
|
77
|
+
"""
|
78
|
+
self._delete(f"{self._url}/{flow_id}")
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import warnings
|
2
1
|
from concurrent.futures import ThreadPoolExecutor
|
3
2
|
from itertools import chain
|
4
3
|
from typing import Any
|
@@ -19,7 +18,6 @@ from anaplan_sdk.models import (
|
|
19
18
|
|
20
19
|
class _TransactionalClient(_BaseClient):
|
21
20
|
def __init__(self, client: httpx.Client, model_id: str, retry_count: int) -> None:
|
22
|
-
self._client = client
|
23
21
|
self._url = f"https://api.anaplan.com/2/0/models/{model_id}"
|
24
22
|
super().__init__(retry_count, client)
|
25
23
|
|
@@ -165,23 +163,3 @@ class _TransactionalClient(_BaseClient):
|
|
165
163
|
"""
|
166
164
|
res = self._post(f"{self._url}/modules/{module_id}/data", json=data)
|
167
165
|
return res if "failures" in res else res["numberOfCellsChanged"]
|
168
|
-
|
169
|
-
def write_to_module(self, module_id: int, data: list[dict[str, Any]]) -> int | dict[str, Any]:
|
170
|
-
warnings.warn(
|
171
|
-
"`write_to_module()` is deprecated and will be removed in a future version. "
|
172
|
-
"Use `update_module_data()` instead.",
|
173
|
-
DeprecationWarning,
|
174
|
-
stacklevel=1,
|
175
|
-
)
|
176
|
-
return self.update_module_data(module_id, data)
|
177
|
-
|
178
|
-
def add_items_to_list(
|
179
|
-
self, list_id: int, items: list[dict[str, str | int | dict]]
|
180
|
-
) -> InsertionResult:
|
181
|
-
warnings.warn(
|
182
|
-
"`add_items_to_list()` is deprecated and will be removed in a future version. "
|
183
|
-
"Use `insert_list_items()` instead.",
|
184
|
-
DeprecationWarning,
|
185
|
-
stacklevel=1,
|
186
|
-
)
|
187
|
-
return self.insert_list_items(list_id, items)
|