anaplan-sdk 0.3.1__py3-none-any.whl → 0.4.0a1__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 +0 -1
- anaplan_sdk/_async_clients/_audit.py +9 -1
- anaplan_sdk/_async_clients/_bulk.py +57 -22
- 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 -1
- anaplan_sdk/_auth.py +170 -57
- anaplan_sdk/_base.py +116 -27
- anaplan_sdk/_clients/_alm.py +0 -1
- anaplan_sdk/_clients/_audit.py +9 -1
- anaplan_sdk/_clients/_bulk.py +57 -24
- anaplan_sdk/_clients/_cloud_works.py +342 -0
- anaplan_sdk/_clients/_cw_flow.py +78 -0
- anaplan_sdk/_clients/_transactional.py +0 -1
- 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.3.1.dist-info → anaplan_sdk-0.4.0a1.dist-info}/METADATA +33 -2
- anaplan_sdk-0.4.0a1.dist-info/RECORD +29 -0
- anaplan_sdk/models.py +0 -329
- anaplan_sdk-0.3.1.dist-info/RECORD +0 -19
- {anaplan_sdk-0.3.1.dist-info → anaplan_sdk-0.4.0a1.dist-info}/WHEEL +0 -0
- {anaplan_sdk-0.3.1.dist-info → anaplan_sdk-0.4.0a1.dist-info}/licenses/LICENSE +0 -0
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
@@ -10,7 +10,6 @@ warnings.filterwarnings("always", category=DeprecationWarning)
|
|
10
10
|
|
11
11
|
class _AlmClient(_BaseClient):
|
12
12
|
def __init__(self, client: httpx.Client, model_id: str, retry_count: int) -> None:
|
13
|
-
self._client = client
|
14
13
|
self._url = f"https://api.anaplan.com/2/0/models/{model_id}/alm"
|
15
14
|
super().__init__(retry_count, client)
|
16
15
|
|
anaplan_sdk/_clients/_audit.py
CHANGED
@@ -10,7 +10,6 @@ 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"
|
@@ -26,6 +25,15 @@ class _AuditClient(_BaseClient):
|
|
26
25
|
for e in self._get_paginated("https://api.anaplan.com/2/0/users", "users")
|
27
26
|
]
|
28
27
|
|
28
|
+
def get_user(self, user_id: str = "me") -> User:
|
29
|
+
"""
|
30
|
+
Retrieves information about the specified user, or the authenticated user if none specified.
|
31
|
+
:return: The requested or currently authenticated User.
|
32
|
+
"""
|
33
|
+
return User.model_validate(
|
34
|
+
self._get(f"https://api.anaplan.com/2/0/users/{user_id}").get("user")
|
35
|
+
)
|
36
|
+
|
29
37
|
def get_events(self, days_into_past: int = 30, event_type: Event = "all") -> list:
|
30
38
|
"""
|
31
39
|
Get audit events from Anaplan Audit API.
|
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
|
"""
|