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
@@ -0,0 +1,80 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
import httpx
|
4
|
+
|
5
|
+
from anaplan_sdk._base import _AsyncBaseClient, construct_payload
|
6
|
+
from anaplan_sdk.models.flows import Flow, FlowInput, FlowSummary
|
7
|
+
|
8
|
+
|
9
|
+
class _AsyncFlowClient(_AsyncBaseClient):
|
10
|
+
def __init__(self, client: httpx.AsyncClient, retry_count: int) -> None:
|
11
|
+
self._url = "https://api.cloudworks.anaplan.com/2/0/integrationflows"
|
12
|
+
super().__init__(retry_count, client)
|
13
|
+
|
14
|
+
async 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 await self._get_paginated(
|
24
|
+
self._url, "integrationFlows", page_size=25, params=params
|
25
|
+
)
|
26
|
+
]
|
27
|
+
|
28
|
+
async def get_flow(self, flow_id: str) -> Flow:
|
29
|
+
"""
|
30
|
+
Get a flow by its ID. This returns the full flow object, including the contained steps and
|
31
|
+
continuation behavior.
|
32
|
+
:param flow_id: The ID of the flow to get.
|
33
|
+
:return: The Flow object.
|
34
|
+
"""
|
35
|
+
return Flow.model_validate((await self._get(f"{self._url}/{flow_id}"))["integrationFlow"])
|
36
|
+
|
37
|
+
async def run_flow(self, flow_id: str, only_steps: list[str] = None) -> str:
|
38
|
+
"""
|
39
|
+
Run a flow by its ID. Make sure that neither the flow nor any of its contained are running.
|
40
|
+
If this is the case, the task will error. Anaplan neither schedules these tasks nor can it
|
41
|
+
handle concurrent executions.
|
42
|
+
:param flow_id: The ID of the flow to run.
|
43
|
+
:param only_steps: A list of step IDs to run. If not provided, only these will be run.
|
44
|
+
:return: The ID of the run.
|
45
|
+
"""
|
46
|
+
url = f"{self._url}/{flow_id}/run"
|
47
|
+
res = await (
|
48
|
+
self._post(url, json={"stepsToRun": only_steps})
|
49
|
+
if only_steps
|
50
|
+
else self._post_empty(url)
|
51
|
+
)
|
52
|
+
return res["run"]["id"]
|
53
|
+
|
54
|
+
async def create_flow(self, flow: FlowInput | dict[str, Any]) -> str:
|
55
|
+
"""
|
56
|
+
Create a new flow in CloudWorks. Be careful not to omit the `depends_on` field. Anaplan
|
57
|
+
will accept these values, but an invalid, corrupted flow will be created, as all Flows must
|
58
|
+
have at least 2 Steps, and they must always be sequential
|
59
|
+
:param flow: The flow to create. This can be a FlowInput object or a dictionary.
|
60
|
+
:return: The ID of the created flow.
|
61
|
+
"""
|
62
|
+
res = await self._post(self._url, json=construct_payload(FlowInput, flow))
|
63
|
+
return res["integrationFlow"]["integrationFlowId"]
|
64
|
+
|
65
|
+
async def update_flow(self, flow_id: str, flow: FlowInput | dict[str, Any]) -> None:
|
66
|
+
"""
|
67
|
+
Update a flow in CloudWorks. You must provide the full flow object, partial updates are not
|
68
|
+
supported.
|
69
|
+
:param flow_id: The ID of the flow to update.
|
70
|
+
:param flow: The flow to update. This can be a FlowInput object or a dictionary.
|
71
|
+
"""
|
72
|
+
await self._put(f"{self._url}/{flow_id}", json=construct_payload(FlowInput, flow))
|
73
|
+
|
74
|
+
async def delete_flow(self, flow_id: str) -> None:
|
75
|
+
"""
|
76
|
+
Delete a flow in CloudWorks. This will not delete its contained steps. This will fail if
|
77
|
+
the flow is running or if it has any running steps.
|
78
|
+
:param flow_id: The ID of the flow to delete.
|
79
|
+
"""
|
80
|
+
await self._delete(f"{self._url}/{flow_id}")
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import warnings
|
2
1
|
from asyncio import gather
|
3
2
|
from itertools import chain
|
4
3
|
from typing import Any
|
@@ -16,12 +15,9 @@ from anaplan_sdk.models import (
|
|
16
15
|
Module,
|
17
16
|
)
|
18
17
|
|
19
|
-
warnings.filterwarnings("always", category=DeprecationWarning)
|
20
|
-
|
21
18
|
|
22
19
|
class _AsyncTransactionalClient(_AsyncBaseClient):
|
23
20
|
def __init__(self, client: httpx.AsyncClient, model_id: str, retry_count: int) -> None:
|
24
|
-
self._client = client
|
25
21
|
self._url = f"https://api.anaplan.com/2/0/models/{model_id}"
|
26
22
|
super().__init__(retry_count, client)
|
27
23
|
|
@@ -170,25 +166,3 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
|
|
170
166
|
"""
|
171
167
|
res = await self._post(f"{self._url}/modules/{module_id}/data", json=data)
|
172
168
|
return res if "failures" in res else res["numberOfCellsChanged"]
|
173
|
-
|
174
|
-
async def write_to_module(
|
175
|
-
self, module_id: int, data: list[dict[str, Any]]
|
176
|
-
) -> int | dict[str, Any]:
|
177
|
-
warnings.warn(
|
178
|
-
"`write_to_module()` is deprecated and will be removed in a future version. "
|
179
|
-
"Use `update_module_data()` instead.",
|
180
|
-
DeprecationWarning,
|
181
|
-
stacklevel=1,
|
182
|
-
)
|
183
|
-
return await self.update_module_data(module_id, data)
|
184
|
-
|
185
|
-
async def add_items_to_list(
|
186
|
-
self, list_id: int, items: list[dict[str, str | int | dict]]
|
187
|
-
) -> InsertionResult:
|
188
|
-
warnings.warn(
|
189
|
-
"`add_items_to_list()` is deprecated and will be removed in a future version. "
|
190
|
-
"Use `insert_list_items()` instead.",
|
191
|
-
DeprecationWarning,
|
192
|
-
stacklevel=1,
|
193
|
-
)
|
194
|
-
return await self.insert_list_items(list_id, items)
|
anaplan_sdk/_auth.py
CHANGED
@@ -1,17 +1,9 @@
|
|
1
|
-
"""
|
2
|
-
Custom Authentication class to pass to httpx alongside some helper functions.
|
3
|
-
"""
|
4
|
-
|
5
1
|
import logging
|
6
2
|
import os
|
7
3
|
from base64 import b64encode
|
4
|
+
from typing import Callable
|
8
5
|
|
9
6
|
import httpx
|
10
|
-
from cryptography.exceptions import InvalidKey, UnsupportedAlgorithm
|
11
|
-
from cryptography.hazmat.backends import default_backend
|
12
|
-
from cryptography.hazmat.primitives import hashes, serialization
|
13
|
-
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
14
|
-
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
15
7
|
|
16
8
|
from .exceptions import AnaplanException, InvalidCredentialsException, InvalidPrivateKeyException
|
17
9
|
|
@@ -21,12 +13,12 @@ logger = logging.getLogger("anaplan_sdk")
|
|
21
13
|
class _AnaplanAuth(httpx.Auth):
|
22
14
|
requires_response_body = True
|
23
15
|
|
24
|
-
def __init__(self):
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
16
|
+
def __init__(self, pre_authed: bool = False):
|
17
|
+
if not pre_authed:
|
18
|
+
logger.info("Creating Authentication Token.")
|
19
|
+
with httpx.Client(timeout=15.0) as client:
|
20
|
+
res = client.send(self._build_auth_request())
|
21
|
+
self._parse_auth_response(res)
|
30
22
|
|
31
23
|
def _build_auth_request(self) -> httpx.Request:
|
32
24
|
raise NotImplementedError("Must be implemented in subclass.")
|
@@ -36,7 +28,7 @@ class _AnaplanAuth(httpx.Auth):
|
|
36
28
|
response = yield request
|
37
29
|
if response.status_code == 401:
|
38
30
|
logger.info("Token expired, refreshing.")
|
39
|
-
auth_res = yield self.
|
31
|
+
auth_res = yield self._build_auth_request()
|
40
32
|
self._parse_auth_response(auth_res)
|
41
33
|
request.headers["Authorization"] = f"AnaplanAuthToken {self._token}"
|
42
34
|
yield request
|
@@ -67,9 +59,14 @@ class AnaplanBasicAuth(_AnaplanAuth):
|
|
67
59
|
class AnaplanCertAuth(_AnaplanAuth):
|
68
60
|
requires_request_body = True
|
69
61
|
|
70
|
-
def __init__(
|
71
|
-
self
|
72
|
-
|
62
|
+
def __init__(
|
63
|
+
self,
|
64
|
+
certificate: str | bytes,
|
65
|
+
private_key: str | bytes,
|
66
|
+
private_key_password: str | bytes | None = None,
|
67
|
+
):
|
68
|
+
self.__set_certificate(certificate)
|
69
|
+
self.__set_private_key(private_key, private_key_password)
|
73
70
|
super().__init__()
|
74
71
|
|
75
72
|
def _build_auth_request(self) -> httpx.Request:
|
@@ -85,6 +82,9 @@ class AnaplanCertAuth(_AnaplanAuth):
|
|
85
82
|
)
|
86
83
|
|
87
84
|
def _prep_credentials(self) -> tuple[str, str, str]:
|
85
|
+
from cryptography.hazmat.primitives import hashes
|
86
|
+
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
87
|
+
|
88
88
|
message = os.urandom(150)
|
89
89
|
return (
|
90
90
|
b64encode(self._certificate).decode(),
|
@@ -92,44 +92,157 @@ class AnaplanCertAuth(_AnaplanAuth):
|
|
92
92
|
b64encode(self._private_key.sign(message, PKCS1v15(), hashes.SHA512())).decode(),
|
93
93
|
)
|
94
94
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
:return: The certificate as bytes.
|
101
|
-
"""
|
102
|
-
if isinstance(certificate, str):
|
103
|
-
if os.path.isfile(certificate):
|
104
|
-
with open(certificate, "rb") as f:
|
105
|
-
return f.read()
|
106
|
-
return certificate.encode()
|
107
|
-
return certificate
|
108
|
-
|
109
|
-
|
110
|
-
def get_private_key(private_key: str | bytes, private_key_password: str | bytes) -> RSAPrivateKey:
|
111
|
-
"""
|
112
|
-
Get the private key from a file or string.
|
113
|
-
:param private_key: The private key to use.
|
114
|
-
:param private_key_password: The password for the private key.
|
115
|
-
:return: The private key as an RSAPrivateKey object.
|
116
|
-
"""
|
117
|
-
try:
|
118
|
-
if isinstance(private_key, str):
|
119
|
-
if os.path.isfile(private_key):
|
120
|
-
with open(private_key, "rb") as f:
|
121
|
-
data = f.read()
|
95
|
+
def __set_certificate(self, certificate: str | bytes) -> None:
|
96
|
+
if isinstance(certificate, str):
|
97
|
+
if os.path.isfile(certificate):
|
98
|
+
with open(certificate, "rb") as f:
|
99
|
+
self._certificate = f.read()
|
122
100
|
else:
|
123
|
-
|
101
|
+
self._certificate = certificate.encode()
|
124
102
|
else:
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
103
|
+
self._certificate = certificate
|
104
|
+
|
105
|
+
def __set_private_key(
|
106
|
+
self, private_key: str | bytes, private_key_password: str | bytes
|
107
|
+
) -> None:
|
108
|
+
try:
|
109
|
+
from cryptography.exceptions import InvalidKey, UnsupportedAlgorithm
|
110
|
+
from cryptography.hazmat.backends import default_backend
|
111
|
+
from cryptography.hazmat.primitives import serialization
|
112
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
113
|
+
except ImportError as e:
|
114
|
+
raise AnaplanException(
|
115
|
+
"cryptography is not available. Please install anaplan-sdk with the cert extra "
|
116
|
+
"`pip install anaplan-sdk[cert]` or install cryptography separately."
|
117
|
+
) from e
|
118
|
+
try:
|
119
|
+
if isinstance(private_key, str):
|
120
|
+
if os.path.isfile(private_key):
|
121
|
+
with open(private_key, "rb") as f:
|
122
|
+
data = f.read()
|
123
|
+
else:
|
124
|
+
data = private_key.encode()
|
131
125
|
else:
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
126
|
+
data = private_key
|
127
|
+
password = (
|
128
|
+
private_key_password.encode()
|
129
|
+
if isinstance(private_key_password, str)
|
130
|
+
else private_key_password
|
131
|
+
)
|
132
|
+
self._private_key: RSAPrivateKey = serialization.load_pem_private_key(
|
133
|
+
data, password, backend=default_backend()
|
134
|
+
)
|
135
|
+
except (IOError, InvalidKey, UnsupportedAlgorithm) as error:
|
136
|
+
raise InvalidPrivateKeyException from error
|
137
|
+
|
138
|
+
|
139
|
+
class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
|
140
|
+
def __init__(
|
141
|
+
self,
|
142
|
+
client_id: str,
|
143
|
+
client_secret: str,
|
144
|
+
redirect_uri: str,
|
145
|
+
refresh_token: str | None = None,
|
146
|
+
scope: str = "openid profile email offline_access",
|
147
|
+
on_token_refresh: Callable[[dict[str, str]], None] | None = None,
|
148
|
+
):
|
149
|
+
try:
|
150
|
+
from oauthlib.oauth2 import WebApplicationClient
|
151
|
+
except ImportError as e:
|
152
|
+
raise AnaplanException(
|
153
|
+
"oauthlib is not available. Please install anaplan-sdk with the oauth extra "
|
154
|
+
"`pip install anaplan-sdk[oauth]` or install oauthlib separately."
|
155
|
+
) from e
|
156
|
+
self._oauth = WebApplicationClient(
|
157
|
+
client_id=client_id, client_secret=client_secret, refresh_token=refresh_token
|
158
|
+
)
|
159
|
+
self._token_url = "https://us1a.app.anaplan.com/oauth/token"
|
160
|
+
self._client_id = client_id
|
161
|
+
self._client_secret = client_secret
|
162
|
+
self._redirect_uri = redirect_uri
|
163
|
+
self._refresh_token = refresh_token
|
164
|
+
self._scope = scope
|
165
|
+
self._id_token = None
|
166
|
+
self._on_token_refresh = on_token_refresh
|
167
|
+
if not refresh_token:
|
168
|
+
self.__auth_code_flow()
|
169
|
+
super().__init__(pre_authed=not refresh_token)
|
170
|
+
|
171
|
+
def _build_auth_request(self) -> httpx.Request:
|
172
|
+
url, headers, body = self._oauth.prepare_refresh_token_request(
|
173
|
+
token_url=self._token_url,
|
174
|
+
refresh_token=self._refresh_token,
|
175
|
+
client_secret=self._client_secret,
|
176
|
+
client_id=self._client_id,
|
177
|
+
)
|
178
|
+
return httpx.Request(method="post", url=url, headers=headers, content=body)
|
179
|
+
|
180
|
+
def _parse_auth_response(self, response: httpx.Response) -> None:
|
181
|
+
if response.status_code == 401:
|
182
|
+
raise InvalidCredentialsException
|
183
|
+
if not response.is_success:
|
184
|
+
raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
|
185
|
+
token = response.json()
|
186
|
+
self._token = token["access_token"]
|
187
|
+
self._refresh_token = token["refresh_token"]
|
188
|
+
if self._on_token_refresh:
|
189
|
+
self._on_token_refresh(token)
|
190
|
+
self._id_token = token.get("id_token")
|
191
|
+
|
192
|
+
def __auth_code_flow(self):
|
193
|
+
from oauthlib.oauth2 import OAuth2Error
|
194
|
+
|
195
|
+
try:
|
196
|
+
logger.info("Creating Authentication Token with OAuth2 Authorization Code Flow.")
|
197
|
+
url, _, _ = self._oauth.prepare_authorization_request(
|
198
|
+
"https://us1a.app.anaplan.com/auth/authorize",
|
199
|
+
redirect_url=self._redirect_uri,
|
200
|
+
scope=self._scope,
|
201
|
+
)
|
202
|
+
authorization_response = input(
|
203
|
+
f"Please go to {url} and authorize the app.\n"
|
204
|
+
"Then paste the entire redirect URL here: "
|
205
|
+
)
|
206
|
+
url, headers, body = self._oauth.prepare_token_request(
|
207
|
+
token_url=self._token_url,
|
208
|
+
redirect_url=self._redirect_uri,
|
209
|
+
authorization_response=authorization_response,
|
210
|
+
client_secret=self._client_secret,
|
211
|
+
)
|
212
|
+
self._parse_auth_response(httpx.post(url=url, headers=headers, content=body))
|
213
|
+
except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
|
214
|
+
raise AnaplanException("Error during OAuth2 authorization flow.") from error
|
215
|
+
|
216
|
+
|
217
|
+
def create_auth(
|
218
|
+
user_email: str | None = None,
|
219
|
+
password: str | None = None,
|
220
|
+
certificate: str | bytes | None = None,
|
221
|
+
private_key: str | bytes | None = None,
|
222
|
+
private_key_password: str | bytes | None = None,
|
223
|
+
client_id: str | None = None,
|
224
|
+
client_secret: str | None = None,
|
225
|
+
redirect_uri: str | None = None,
|
226
|
+
refresh_token: str | None = None,
|
227
|
+
oauth2_scope: str = "openid profile email offline_access",
|
228
|
+
on_token_refresh: Callable[[dict[str, str]], None] | None = None,
|
229
|
+
) -> _AnaplanAuth:
|
230
|
+
if certificate and private_key:
|
231
|
+
return AnaplanCertAuth(certificate, private_key, private_key_password)
|
232
|
+
if user_email and password:
|
233
|
+
return AnaplanBasicAuth(user_email=user_email, password=password)
|
234
|
+
if client_id and client_secret and redirect_uri:
|
235
|
+
return AnaplanOauth2AuthCodeAuth(
|
236
|
+
client_id=client_id,
|
237
|
+
client_secret=client_secret,
|
238
|
+
redirect_uri=redirect_uri,
|
239
|
+
refresh_token=refresh_token,
|
240
|
+
scope=oauth2_scope,
|
241
|
+
on_token_refresh=on_token_refresh,
|
242
|
+
)
|
243
|
+
raise ValueError(
|
244
|
+
"No valid authentication parameters provided. Please provide either:\n"
|
245
|
+
"- user_email and password, or\n"
|
246
|
+
"- certificate and private_key, or\n"
|
247
|
+
"- client_id, client_secret, and redirect_uri"
|
248
|
+
)
|
anaplan_sdk/_base.py
CHANGED
@@ -1,7 +1,3 @@
|
|
1
|
-
"""
|
2
|
-
Provides Base Classes for this project.
|
3
|
-
"""
|
4
|
-
|
5
1
|
import asyncio
|
6
2
|
import logging
|
7
3
|
import random
|
@@ -11,19 +7,34 @@ from concurrent.futures import ThreadPoolExecutor
|
|
11
7
|
from gzip import compress
|
12
8
|
from itertools import chain
|
13
9
|
from math import ceil
|
14
|
-
from typing import Any, Callable, Coroutine, Iterator, Literal
|
10
|
+
from typing import Any, Callable, Coroutine, Iterator, Literal, Type, TypeVar
|
15
11
|
|
16
12
|
import httpx
|
17
13
|
from httpx import HTTPError, Response
|
18
14
|
|
19
|
-
from
|
15
|
+
from .exceptions import (
|
20
16
|
AnaplanException,
|
21
17
|
AnaplanTimeoutException,
|
22
18
|
InvalidIdentifierException,
|
23
19
|
)
|
20
|
+
from .models import AnaplanModel
|
21
|
+
from .models.cloud_works import (
|
22
|
+
AmazonS3ConnectionInput,
|
23
|
+
AzureBlobConnectionInput,
|
24
|
+
ConnectionBody,
|
25
|
+
GoogleBigQueryConnectionInput,
|
26
|
+
IntegrationInput,
|
27
|
+
IntegrationProcessInput,
|
28
|
+
ScheduleInput,
|
29
|
+
)
|
24
30
|
|
25
31
|
logger = logging.getLogger("anaplan_sdk")
|
26
32
|
|
33
|
+
_json_header = {"Content-Type": "application/json"}
|
34
|
+
_gzip_header = {"Content-Type": "application/x-gzip"}
|
35
|
+
|
36
|
+
T = TypeVar("T", bound=AnaplanModel)
|
37
|
+
|
27
38
|
|
28
39
|
class _BaseClient:
|
29
40
|
def __init__(self, retry_count: int, client: httpx.Client):
|
@@ -37,27 +48,34 @@ class _BaseClient:
|
|
37
48
|
return self._run_with_retry(self._client.get, url).content
|
38
49
|
|
39
50
|
def _post(self, url: str, json: dict | list) -> dict[str, Any]:
|
40
|
-
return self._run_with_retry(
|
41
|
-
|
51
|
+
return self._run_with_retry(self._client.post, url, headers=_json_header, json=json).json()
|
52
|
+
|
53
|
+
def _put(self, url: str, json: dict | list) -> dict[str, Any]:
|
54
|
+
return (self._run_with_retry(self._client.put, url, headers=_json_header, json=json)).json()
|
55
|
+
|
56
|
+
def _patch(self, url: str, json: dict | list) -> dict[str, Any]:
|
57
|
+
return (
|
58
|
+
self._run_with_retry(self._client.patch, url, headers=_json_header, json=json)
|
42
59
|
).json()
|
43
60
|
|
44
|
-
def
|
45
|
-
self._run_with_retry(self._client.
|
61
|
+
def _delete(self, url: str) -> dict[str, Any]:
|
62
|
+
return (self._run_with_retry(self._client.delete, url, headers=_json_header)).json()
|
63
|
+
|
64
|
+
def _post_empty(self, url: str) -> dict[str, Any]:
|
65
|
+
res = self._run_with_retry(self._client.post, url)
|
66
|
+
return res.json() if res.num_bytes_downloaded > 0 else {}
|
46
67
|
|
47
68
|
def _put_binary_gzip(self, url: str, content: bytes) -> Response:
|
48
69
|
return self._run_with_retry(
|
49
|
-
self._client.put,
|
50
|
-
url,
|
51
|
-
headers={"Content-Type": "application/x-gzip"},
|
52
|
-
content=compress(content),
|
70
|
+
self._client.put, url, headers=_gzip_header, content=compress(content)
|
53
71
|
)
|
54
72
|
|
55
73
|
def __get_page(self, url: str, limit: int, offset: int, result_key: str, **kwargs) -> list:
|
56
|
-
kwargs["params"] = kwargs.get("params"
|
74
|
+
kwargs["params"] = kwargs.get("params") or {} | {"limit": limit, "offset": offset}
|
57
75
|
return self._get(url, **kwargs).get(result_key, [])
|
58
76
|
|
59
77
|
def __get_first_page(self, url: str, limit: int, result_key: str, **kwargs) -> tuple[list, int]:
|
60
|
-
kwargs["params"] = kwargs.get("params"
|
78
|
+
kwargs["params"] = kwargs.get("params") or {} | {"limit": limit}
|
61
79
|
res = self._get(url, **kwargs)
|
62
80
|
return res.get(result_key, []), res["meta"]["paging"]["totalSize"]
|
63
81
|
|
@@ -111,32 +129,41 @@ class _AsyncBaseClient:
|
|
111
129
|
|
112
130
|
async def _post(self, url: str, json: dict | list) -> dict[str, Any]:
|
113
131
|
return (
|
114
|
-
await self._run_with_retry(
|
115
|
-
|
116
|
-
|
132
|
+
await self._run_with_retry(self._client.post, url, headers=_json_header, json=json)
|
133
|
+
).json()
|
134
|
+
|
135
|
+
async def _put(self, url: str, json: dict | list) -> dict[str, Any]:
|
136
|
+
return (
|
137
|
+
await self._run_with_retry(self._client.put, url, headers=_json_header, json=json)
|
138
|
+
).json()
|
139
|
+
|
140
|
+
async def _patch(self, url: str, json: dict | list) -> dict[str, Any]:
|
141
|
+
return (
|
142
|
+
await self._run_with_retry(self._client.patch, url, headers=_json_header, json=json)
|
117
143
|
).json()
|
118
144
|
|
119
|
-
async def
|
120
|
-
await self._run_with_retry(self._client.
|
145
|
+
async def _delete(self, url: str) -> dict[str, Any]:
|
146
|
+
return (await self._run_with_retry(self._client.delete, url, headers=_json_header)).json()
|
147
|
+
|
148
|
+
async def _post_empty(self, url: str) -> dict[str, Any]:
|
149
|
+
res = await self._run_with_retry(self._client.post, url)
|
150
|
+
return res.json() if res.num_bytes_downloaded > 0 else {}
|
121
151
|
|
122
152
|
async def _put_binary_gzip(self, url: str, content: bytes) -> Response:
|
123
153
|
return await self._run_with_retry(
|
124
|
-
self._client.put,
|
125
|
-
url,
|
126
|
-
headers={"Content-Type": "application/x-gzip"},
|
127
|
-
content=compress(content),
|
154
|
+
self._client.put, url, headers=_gzip_header, content=compress(content)
|
128
155
|
)
|
129
156
|
|
130
157
|
async def __get_page(
|
131
158
|
self, url: str, limit: int, offset: int, result_key: str, **kwargs
|
132
159
|
) -> list:
|
133
|
-
kwargs["params"] = kwargs.get("params"
|
160
|
+
kwargs["params"] = kwargs.get("params") or {} | {"limit": limit, "offset": offset}
|
134
161
|
return (await self._get(url, **kwargs)).get(result_key, [])
|
135
162
|
|
136
163
|
async def __get_first_page(
|
137
164
|
self, url: str, limit: int, result_key: str, **kwargs
|
138
165
|
) -> tuple[list, int]:
|
139
|
-
kwargs["params"] = kwargs.get("params"
|
166
|
+
kwargs["params"] = kwargs.get("params") or {} | {"limit": limit}
|
140
167
|
res = await self._get(url, **kwargs)
|
141
168
|
return res.get(result_key, []), res["meta"]["paging"]["totalSize"]
|
142
169
|
|
@@ -178,6 +205,68 @@ class _AsyncBaseClient:
|
|
178
205
|
raise AnaplanException("Exhausted all retries without a successful response or Error.")
|
179
206
|
|
180
207
|
|
208
|
+
def construct_payload(model: Type[T], body: T | dict[str, Any]) -> dict[str, Any]:
|
209
|
+
"""
|
210
|
+
Construct a payload for the given model and body.
|
211
|
+
:param model: The model class to use for validation.
|
212
|
+
:param body: The body to validate and optionally convert to a dictionary.
|
213
|
+
:return: A dictionary representation of the validated body.
|
214
|
+
"""
|
215
|
+
if isinstance(body, dict):
|
216
|
+
body = model.model_validate(body)
|
217
|
+
return body.model_dump(exclude_none=True, by_alias=True)
|
218
|
+
|
219
|
+
|
220
|
+
def connection_body_payload(body: ConnectionBody | dict[str, Any]) -> dict[str, Any]:
|
221
|
+
"""
|
222
|
+
Construct a payload for the given integration body.
|
223
|
+
:param body: The body to validate and optionally convert to a dictionary.
|
224
|
+
:return: A dictionary representation of the validated body.
|
225
|
+
"""
|
226
|
+
if isinstance(body, dict):
|
227
|
+
if "sasToken" in body:
|
228
|
+
body = AzureBlobConnectionInput.model_validate(body)
|
229
|
+
elif "secretAccessKey" in body:
|
230
|
+
body = AmazonS3ConnectionInput.model_validate(body)
|
231
|
+
else:
|
232
|
+
body = GoogleBigQueryConnectionInput.model_validate(body)
|
233
|
+
return body.model_dump(exclude_none=True, by_alias=True)
|
234
|
+
|
235
|
+
|
236
|
+
def integration_payload(
|
237
|
+
body: IntegrationInput | IntegrationProcessInput | dict[str, Any],
|
238
|
+
) -> dict[str, Any]:
|
239
|
+
"""
|
240
|
+
Construct a payload for the given integration body.
|
241
|
+
:param body: The body to validate and optionally convert to a dictionary.
|
242
|
+
:return: A dictionary representation of the validated body.
|
243
|
+
"""
|
244
|
+
if isinstance(body, dict):
|
245
|
+
body = (
|
246
|
+
IntegrationInput.model_validate(body)
|
247
|
+
if "jobs" in body
|
248
|
+
else IntegrationProcessInput.model_validate(body)
|
249
|
+
)
|
250
|
+
return body.model_dump(exclude_none=True, by_alias=True)
|
251
|
+
|
252
|
+
|
253
|
+
def schedule_payload(
|
254
|
+
integration_id: str, schedule: ScheduleInput | dict[str, Any]
|
255
|
+
) -> dict[str, Any]:
|
256
|
+
"""
|
257
|
+
Construct a payload for the given integration ID and schedule.
|
258
|
+
:param integration_id: The ID of the integration.
|
259
|
+
:param schedule: The schedule to validate and optionally convert to a dictionary.
|
260
|
+
:return: A dictionary representation of the validated schedule.
|
261
|
+
"""
|
262
|
+
if isinstance(schedule, dict):
|
263
|
+
schedule = ScheduleInput.model_validate(schedule)
|
264
|
+
return {
|
265
|
+
"integrationId": integration_id,
|
266
|
+
"schedule": schedule.model_dump(exclude_none=True, by_alias=True),
|
267
|
+
}
|
268
|
+
|
269
|
+
|
181
270
|
def action_url(action_id: int) -> Literal["imports", "exports", "actions", "processes"]:
|
182
271
|
"""
|
183
272
|
Determine the type of action based on its identifier.
|
anaplan_sdk/_clients/_alm.py
CHANGED
@@ -1,35 +1,14 @@
|
|
1
|
-
import warnings
|
2
|
-
|
3
1
|
import httpx
|
4
2
|
|
5
3
|
from anaplan_sdk._base import _BaseClient
|
6
|
-
from anaplan_sdk.models import ModelRevision, Revision, SyncTask
|
7
|
-
|
8
|
-
warnings.filterwarnings("always", category=DeprecationWarning)
|
4
|
+
from anaplan_sdk.models import ModelRevision, Revision, SyncTask
|
9
5
|
|
10
6
|
|
11
7
|
class _AlmClient(_BaseClient):
|
12
8
|
def __init__(self, client: httpx.Client, 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
|
-
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 self._get("https://api.anaplan.com/2/0/users").get("users")
|
31
|
-
]
|
32
|
-
|
33
12
|
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
|
anaplan_sdk/_clients/_audit.py
CHANGED
@@ -10,22 +10,35 @@ Event = Literal["all", "byok", "user_activity"]
|
|
10
10
|
|
11
11
|
class _AuditClient(_BaseClient):
|
12
12
|
def __init__(self, client: httpx.Client, retry_count: int, thread_count: int) -> None:
|
13
|
-
self._client = client
|
14
13
|
self._limit = 10_000
|
15
14
|
self._thread_count = thread_count
|
16
15
|
self._url = "https://audit.anaplan.com/audit/api/1/events"
|
17
16
|
super().__init__(retry_count, client)
|
18
17
|
|
19
|
-
def list_users(self) -> list[User]:
|
18
|
+
def list_users(self, search_pattern: str | None = None) -> list[User]:
|
20
19
|
"""
|
21
20
|
Lists all the Users in the authenticated users default tenant.
|
21
|
+
:param search_pattern: Optional filter for users. When provided, case-insensitive matches
|
22
|
+
users with emails containing this string. When None (default), returns all users.
|
22
23
|
:return: The List of Users.
|
23
24
|
"""
|
25
|
+
params = {"s": search_pattern} if search_pattern else None
|
24
26
|
return [
|
25
27
|
User.model_validate(e)
|
26
|
-
for e in self._get_paginated(
|
28
|
+
for e in self._get_paginated(
|
29
|
+
"https://api.anaplan.com/2/0/users", "users", params=params
|
30
|
+
)
|
27
31
|
]
|
28
32
|
|
33
|
+
def get_user(self, user_id: str = "me") -> User:
|
34
|
+
"""
|
35
|
+
Retrieves information about the specified user, or the authenticated user if none specified.
|
36
|
+
:return: The requested or currently authenticated User.
|
37
|
+
"""
|
38
|
+
return User.model_validate(
|
39
|
+
self._get(f"https://api.anaplan.com/2/0/users/{user_id}").get("user")
|
40
|
+
)
|
41
|
+
|
29
42
|
def get_events(self, days_into_past: int = 30, event_type: Event = "all") -> list:
|
30
43
|
"""
|
31
44
|
Get audit events from Anaplan Audit API.
|