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