anaplan-sdk 0.4.3a1__py3-none-any.whl → 0.4.4__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/__init__.py +11 -1
- anaplan_sdk/_async_clients/_bulk.py +35 -79
- anaplan_sdk/_auth.py +186 -147
- anaplan_sdk/_clients/_bulk.py +36 -70
- anaplan_sdk/_oauth.py +91 -32
- anaplan_sdk/models/_bulk.py +2 -2
- {anaplan_sdk-0.4.3a1.dist-info → anaplan_sdk-0.4.4.dist-info}/METADATA +5 -2
- {anaplan_sdk-0.4.3a1.dist-info → anaplan_sdk-0.4.4.dist-info}/RECORD +10 -10
- {anaplan_sdk-0.4.3a1.dist-info → anaplan_sdk-0.4.4.dist-info}/WHEEL +0 -0
- {anaplan_sdk-0.4.3a1.dist-info → anaplan_sdk-0.4.4.dist-info}/licenses/LICENSE +0 -0
anaplan_sdk/__init__.py
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
from ._async_clients import AsyncClient
|
2
|
+
from ._auth import AnaplanLocalOAuth, AnaplanRefreshTokenAuth
|
2
3
|
from ._clients import Client
|
3
4
|
from ._oauth import AsyncOauth, Oauth
|
4
5
|
|
5
|
-
__all__ = [
|
6
|
+
__all__ = [
|
7
|
+
"AsyncClient",
|
8
|
+
"Client",
|
9
|
+
"AnaplanLocalOAuth",
|
10
|
+
"AnaplanRefreshTokenAuth",
|
11
|
+
"AsyncOauth",
|
12
|
+
"Oauth",
|
13
|
+
"models",
|
14
|
+
"exceptions",
|
15
|
+
]
|
@@ -6,7 +6,7 @@ from typing import AsyncIterator, Iterator
|
|
6
6
|
import httpx
|
7
7
|
from typing_extensions import Self
|
8
8
|
|
9
|
-
from anaplan_sdk._auth import
|
9
|
+
from anaplan_sdk._auth import _create_auth
|
10
10
|
from anaplan_sdk._base import _AsyncBaseClient, action_url
|
11
11
|
from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
|
12
12
|
from anaplan_sdk.models import (
|
@@ -32,14 +32,8 @@ logger = logging.getLogger("anaplan_sdk")
|
|
32
32
|
|
33
33
|
class AsyncClient(_AsyncBaseClient):
|
34
34
|
"""
|
35
|
-
|
36
|
-
|
37
|
-
abstractions over the API, so you can deal with python objects and simple functions rather than
|
38
|
-
implementation details like http, json, compression, chunking etc.
|
39
|
-
|
40
|
-
|
41
|
-
For more information, quick start guides and detailed instructions refer to:
|
42
|
-
[Anaplan SDK](https://vinzenzklass.github.io/anaplan-sdk).
|
35
|
+
Asynchronous Anaplan Client. For guides and examples
|
36
|
+
refer to https://vinzenzklass.github.io/anaplan-sdk.
|
43
37
|
"""
|
44
38
|
|
45
39
|
def __init__(
|
@@ -51,15 +45,8 @@ class AsyncClient(_AsyncBaseClient):
|
|
51
45
|
certificate: str | bytes | None = None,
|
52
46
|
private_key: str | bytes | None = None,
|
53
47
|
private_key_password: str | bytes | None = None,
|
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_auth_code: AuthCodeCallback = None,
|
60
|
-
on_token_refresh: AuthTokenRefreshCallback = None,
|
61
|
-
oauth_token: dict[str, str] | None = None,
|
62
48
|
token: str | None = None,
|
49
|
+
auth: httpx.Auth | None = None,
|
63
50
|
timeout: float | httpx.Timeout = 30,
|
64
51
|
retry_count: int = 2,
|
65
52
|
status_poll_delay: int = 1,
|
@@ -67,86 +54,55 @@ class AsyncClient(_AsyncBaseClient):
|
|
67
54
|
allow_file_creation: bool = False,
|
68
55
|
) -> None:
|
69
56
|
"""
|
70
|
-
|
71
|
-
https://
|
72
|
-
so you can deal with python objects and simple functions rather than implementation details
|
73
|
-
like http, json, compression, chunking etc.
|
74
|
-
|
75
|
-
|
76
|
-
For more information, quick start guides and detailed instructions refer to:
|
77
|
-
https://vinzenzklass.github.io/anaplan-sdk.
|
57
|
+
Asynchronous Anaplan Client. For guides and examples
|
58
|
+
refer to https://vinzenzklass.github.io/anaplan-sdk.
|
78
59
|
|
79
60
|
:param workspace_id: The Anaplan workspace Id. You can copy this from the browser URL or
|
80
|
-
|
61
|
+
find them using an HTTP Client like Postman, Paw, Insomnia etc.
|
81
62
|
:param model_id: The identifier of the model.
|
82
63
|
:param user_email: A valid email registered with the Anaplan Workspace you are attempting
|
83
|
-
|
84
|
-
:param password: Password for the given `user_email
|
85
|
-
|
86
|
-
|
87
|
-
:param
|
88
|
-
|
89
|
-
:param
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
:param
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
handled appropriately regardless of the execution context (in a thread,
|
102
|
-
with or without an event loop, etc.).
|
103
|
-
**Note**: When using asynchronous callbacks in complex applications
|
104
|
-
with multiple event loops, be aware that callbacks may execute in a
|
105
|
-
separate event loop context from where they were defined, which can
|
106
|
-
make debugging challenging.
|
107
|
-
:param on_token_refresh: A callback function that is called whenever the token is refreshed.
|
108
|
-
This includes the initial token retrieval and any subsequent calls.
|
109
|
-
With this you can for example securely store the token in your
|
110
|
-
application or on your server for later reuse. The function
|
111
|
-
must accept a single argument, which is the token dictionary
|
112
|
-
returned by the Oauth2 token endpoint and does not return anything.
|
113
|
-
This can be either a synchronous function or an async coroutine
|
114
|
-
function. **Note**: When using asynchronous callbacks in complex
|
115
|
-
applications with multiple event loops, be aware that callbacks
|
116
|
-
may execute in a separate event loop context from where they were
|
117
|
-
defined, which can make debugging challenging.
|
64
|
+
to access.
|
65
|
+
:param password: Password for the given `user_email` for basic Authentication.
|
66
|
+
:param certificate: The certificate content or the absolute path to the certificate file.
|
67
|
+
:param private_key: The private key content or the absolute path to the private key file.
|
68
|
+
:param private_key_password: The password to access the private key file. This is only
|
69
|
+
considered if you provided a private key file and it password-protected.
|
70
|
+
:param token: An Anaplan API Token. This will be used to authenticate the client. If
|
71
|
+
sufficient other authentication parameters are provided, the token will be used
|
72
|
+
until it expires, after which a new one will be created. If you provide only this
|
73
|
+
parameter, the client will raise an error upon first authentication failure. For
|
74
|
+
short-lived instances, such as in web applications where user specific clients are
|
75
|
+
created, this is the recommended way to authenticate, since this has the least
|
76
|
+
overhead.
|
77
|
+
:param auth: You can provide a subclass of `httpx.Auth` to use for authentication. You can
|
78
|
+
provide an instance of one of the classes provided by the SDK, or an instance of
|
79
|
+
your own subclass of `httpx.Auth`. This will give you full control over the
|
80
|
+
authentication process, but you will need to implement the entire authentication
|
81
|
+
logic yourself.
|
118
82
|
:param timeout: The timeout in seconds for the HTTP requests. Alternatively, you can pass
|
119
|
-
|
83
|
+
an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
|
120
84
|
:param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
|
121
|
-
|
122
|
-
|
85
|
+
to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
|
86
|
+
number of 2 times.
|
123
87
|
:param status_poll_delay: The delay between polling the status of a task.
|
124
88
|
:param upload_chunk_size: The size of the chunks to upload. This is the maximum size of
|
125
|
-
|
89
|
+
each chunk. Defaults to 25MB.
|
126
90
|
:param allow_file_creation: Whether to allow the creation of new files. Defaults to False
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
creating new files and uploading content to them.
|
91
|
+
since this is typically unintentional and may well be unwanted behaviour in the API
|
92
|
+
altogether. A file that is created this way will not be referenced by any action in
|
93
|
+
anaplan until manually assigned so there is typically no value in dynamically
|
94
|
+
creating new files and uploading content to them.
|
132
95
|
"""
|
133
96
|
_client = httpx.AsyncClient(
|
134
97
|
auth=(
|
135
|
-
|
98
|
+
auth
|
99
|
+
or _create_auth(
|
136
100
|
token=token,
|
137
|
-
oauth_token=oauth_token,
|
138
101
|
user_email=user_email,
|
139
102
|
password=password,
|
140
103
|
certificate=certificate,
|
141
104
|
private_key=private_key,
|
142
105
|
private_key_password=private_key_password,
|
143
|
-
client_id=client_id,
|
144
|
-
client_secret=client_secret,
|
145
|
-
redirect_uri=redirect_uri,
|
146
|
-
refresh_token=refresh_token,
|
147
|
-
oauth2_scope=oauth2_scope,
|
148
|
-
on_auth_code=on_auth_code,
|
149
|
-
on_token_refresh=on_token_refresh,
|
150
106
|
)
|
151
107
|
),
|
152
108
|
timeout=timeout,
|
anaplan_sdk/_auth.py
CHANGED
@@ -1,33 +1,25 @@
|
|
1
|
-
import asyncio
|
2
|
-
import inspect
|
3
1
|
import logging
|
4
2
|
import os
|
5
|
-
import threading
|
6
3
|
from base64 import b64encode
|
7
|
-
from
|
8
|
-
from typing import Any, Awaitable, Callable, Coroutine
|
4
|
+
from typing import Callable
|
9
5
|
|
10
6
|
import httpx
|
11
7
|
|
8
|
+
from ._oauth import _OAuthRequestFactory
|
12
9
|
from .exceptions import AnaplanException, InvalidCredentialsException, InvalidPrivateKeyException
|
13
10
|
|
14
11
|
logger = logging.getLogger("anaplan_sdk")
|
15
12
|
|
16
|
-
AuthCodeCallback = (Callable[[str], str] | Callable[[str], Awaitable[str]]) | None
|
17
|
-
AuthTokenRefreshCallback = (
|
18
|
-
Callable[[dict[str, str]], None] | Callable[[dict[str, str]], Awaitable[None]]
|
19
|
-
) | None
|
20
|
-
|
21
13
|
|
22
14
|
class _AnaplanAuth(httpx.Auth):
|
23
15
|
requires_response_body = True
|
24
16
|
|
25
|
-
def __init__(self,
|
26
|
-
|
17
|
+
def __init__(self, token: str | None = None):
|
18
|
+
self._token: str = token or ""
|
19
|
+
if not token:
|
27
20
|
logger.info("Creating Authentication Token.")
|
28
21
|
with httpx.Client(timeout=15.0) as client:
|
29
|
-
|
30
|
-
self._parse_auth_response(res)
|
22
|
+
self._parse_auth_response(client.send(self._build_auth_request()))
|
31
23
|
|
32
24
|
def _build_auth_request(self) -> httpx.Request:
|
33
25
|
raise NotImplementedError("Must be implemented in subclass.")
|
@@ -47,26 +39,25 @@ class _AnaplanAuth(httpx.Auth):
|
|
47
39
|
raise InvalidCredentialsException
|
48
40
|
if not response.is_success:
|
49
41
|
raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
|
50
|
-
self._token
|
42
|
+
self._token = response.json()["tokenInfo"]["tokenValue"]
|
51
43
|
|
52
44
|
|
53
|
-
class
|
45
|
+
class _StaticTokenAuth(httpx.Auth):
|
54
46
|
def __init__(self, token: str):
|
55
|
-
logger.warning("Using static token authentication. Tokens will not be refreshed.")
|
56
47
|
self._token = token
|
57
48
|
|
58
49
|
def auth_flow(self, request):
|
59
50
|
request.headers["Authorization"] = f"AnaplanAuthToken {self._token}"
|
60
51
|
response = yield request
|
61
52
|
if response.status_code == 401:
|
62
|
-
raise InvalidCredentialsException("
|
53
|
+
raise InvalidCredentialsException("Token is invalid or expired.")
|
63
54
|
|
64
55
|
|
65
|
-
class
|
66
|
-
def __init__(self, user_email: str, password: str):
|
56
|
+
class _AnaplanBasicAuth(_AnaplanAuth):
|
57
|
+
def __init__(self, user_email: str, password: str, token: str | None = None):
|
67
58
|
self.user_email = user_email
|
68
59
|
self.password = password
|
69
|
-
super().__init__()
|
60
|
+
super().__init__(token)
|
70
61
|
|
71
62
|
def _build_auth_request(self) -> httpx.Request:
|
72
63
|
cred = b64encode(f"{self.user_email}:{self.password}".encode()).decode()
|
@@ -77,7 +68,7 @@ class AnaplanBasicAuth(_AnaplanAuth):
|
|
77
68
|
)
|
78
69
|
|
79
70
|
|
80
|
-
class
|
71
|
+
class _AnaplanCertAuth(_AnaplanAuth):
|
81
72
|
requires_request_body = True
|
82
73
|
|
83
74
|
def __init__(
|
@@ -85,10 +76,11 @@ class AnaplanCertAuth(_AnaplanAuth):
|
|
85
76
|
certificate: str | bytes,
|
86
77
|
private_key: str | bytes,
|
87
78
|
private_key_password: str | bytes | None = None,
|
79
|
+
token: str | None = None,
|
88
80
|
):
|
89
81
|
self.__set_certificate(certificate)
|
90
82
|
self.__set_private_key(private_key, private_key_password)
|
91
|
-
super().__init__()
|
83
|
+
super().__init__(token)
|
92
84
|
|
93
85
|
def _build_auth_request(self) -> httpx.Request:
|
94
86
|
encoded_cert, encoded_string, encoded_signed_string = self._prep_credentials()
|
@@ -157,168 +149,215 @@ class AnaplanCertAuth(_AnaplanAuth):
|
|
157
149
|
raise InvalidPrivateKeyException from error
|
158
150
|
|
159
151
|
|
160
|
-
class
|
152
|
+
class AnaplanLocalOAuth(_AnaplanAuth):
|
161
153
|
def __init__(
|
162
154
|
self,
|
163
155
|
client_id: str,
|
164
156
|
client_secret: str,
|
165
157
|
redirect_uri: str,
|
166
|
-
|
158
|
+
token: dict[str, str] | None = None,
|
159
|
+
persist_token: bool = False,
|
160
|
+
authorization_url: str = "https://us1a.app.anaplan.com/auth/prelogin",
|
161
|
+
token_url: str = "https://us1a.app.anaplan.com/oauth/token",
|
162
|
+
validation_url: str = "https://auth.anaplan.com/token/validate",
|
167
163
|
scope: str = "openid profile email offline_access",
|
168
|
-
|
169
|
-
on_token_refresh: AuthTokenRefreshCallback = None,
|
164
|
+
state_generator: Callable[[], str] | None = None,
|
170
165
|
):
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
166
|
+
"""
|
167
|
+
Initializes the AnaplanLocalOAuth class for OAuth2 authentication using the
|
168
|
+
Authorization Code Flow. This is a utility class for local development and requires user
|
169
|
+
interaction. For Web Applications and other scenarios, refer to `Oauth` or `AsyncOauth`.
|
170
|
+
This class will refresh the access token automatically when it expires.
|
171
|
+
:param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
|
172
|
+
must be an Authorization Code Grant application.
|
173
|
+
:param client_secret: The client secret of your Anaplan Oauth 2.0 application.
|
174
|
+
:param redirect_uri: The URL to which the user will be redirected after authorizing the
|
175
|
+
application.
|
176
|
+
:param token: The OAuth token dictionary containing at least the `access_token` and
|
177
|
+
`refresh_token`. If not provided, the user will be prompted to interactive
|
178
|
+
authorize the application, if `persist_token` is set to False or no valid refresh
|
179
|
+
token is found in the keyring.
|
180
|
+
:param persist_token: If set to True, the refresh token will be stored in the system's
|
181
|
+
keyring, allowing the application to use the same refresh token across multiple
|
182
|
+
runs. If set to False, the user will be prompted to authorize the application each
|
183
|
+
time. This requires the `keyring` extra to be installed. If a valid refresh token
|
184
|
+
is found in the keyring, it will be used instead of the given `token` parameter.
|
185
|
+
:param authorization_url: The URL to which the user will be redirected to authorize the
|
186
|
+
application. Defaults to the Anaplan Prelogin Page, where the user can select the
|
187
|
+
login method.
|
188
|
+
:param token_url: The URL to post the authorization code to in order to fetch the access
|
189
|
+
token.
|
190
|
+
:param validation_url: The URL to validate the access token.
|
191
|
+
:param scope: The scope of the access request.
|
192
|
+
:param state_generator: A callable that generates a random state string. You can optionally
|
193
|
+
pass this if you need to customize the state generation logic. If not provided,
|
194
|
+
the state will be generated by `oauthlib`.
|
195
|
+
"""
|
196
|
+
self._oauth_token = token or {}
|
197
|
+
self._service_name = "anaplan_sdk"
|
198
|
+
|
199
|
+
if persist_token:
|
200
|
+
try:
|
201
|
+
import keyring
|
202
|
+
|
203
|
+
stored = keyring.get_password(self._service_name, self._service_name)
|
204
|
+
if stored:
|
205
|
+
logger.info("Using persisted OAuth refresh token.")
|
206
|
+
self._oauth_token = {"refresh_token": stored}
|
207
|
+
self._token = "" # Set to blank to trigger the super().__init__ auth request.
|
208
|
+
except ImportError as e:
|
209
|
+
raise AnaplanException(
|
210
|
+
"keyring is not available. Please install anaplan-sdk with the keyring extra "
|
211
|
+
"`pip install anaplan-sdk[keyring]` or install keyring separately."
|
212
|
+
) from e
|
213
|
+
self._persist_token = persist_token
|
214
|
+
self._oauth = _OAuthRequestFactory(
|
215
|
+
client_id=client_id,
|
216
|
+
client_secret=client_secret,
|
217
|
+
redirect_uri=redirect_uri,
|
218
|
+
scope=scope,
|
219
|
+
authorization_url=authorization_url,
|
220
|
+
token_url=token_url,
|
221
|
+
validation_url=validation_url,
|
222
|
+
state_generator=state_generator,
|
180
223
|
)
|
181
|
-
|
182
|
-
self._client_id = client_id
|
183
|
-
self._client_secret = client_secret
|
184
|
-
self._redirect_uri = redirect_uri
|
185
|
-
self._refresh_token = refresh_token
|
186
|
-
self._scope = scope
|
187
|
-
self._id_token = None
|
188
|
-
self._on_auth_code = on_auth_code
|
189
|
-
self._on_token_refresh = on_token_refresh
|
190
|
-
if not refresh_token:
|
224
|
+
if not self._oauth_token:
|
191
225
|
self.__auth_code_flow()
|
192
|
-
super().__init__(
|
226
|
+
super().__init__(self._token)
|
227
|
+
|
228
|
+
@property
|
229
|
+
def token(self) -> dict[str, str]:
|
230
|
+
"""
|
231
|
+
Returns the current token dictionary. You can safely use the `access_token`, but if you
|
232
|
+
must not use the `refresh_token` outside of this class, if you expect to use this instance
|
233
|
+
further. If you do use the `refresh_token` outside of this class, this will error on the
|
234
|
+
next attempt to refresh the token, as the `refresh_token` can only be used once.
|
235
|
+
"""
|
236
|
+
return self._oauth_token
|
193
237
|
|
194
238
|
def _build_auth_request(self) -> httpx.Request:
|
195
|
-
|
196
|
-
token_url=self._token_url,
|
197
|
-
refresh_token=self._refresh_token,
|
198
|
-
client_secret=self._client_secret,
|
199
|
-
client_id=self._client_id,
|
200
|
-
)
|
201
|
-
return httpx.Request(method="post", url=url, headers=headers, content=body)
|
239
|
+
return self._oauth.refresh_token_request(self._oauth_token["refresh_token"])
|
202
240
|
|
203
241
|
def _parse_auth_response(self, response: httpx.Response) -> None:
|
204
242
|
if response.status_code == 401:
|
205
243
|
raise InvalidCredentialsException
|
206
244
|
if not response.is_success:
|
207
245
|
raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
|
208
|
-
|
209
|
-
self.
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
246
|
+
self._oauth_token = response.json()
|
247
|
+
if self._persist_token:
|
248
|
+
import keyring
|
249
|
+
|
250
|
+
keyring.set_password(
|
251
|
+
self._service_name, self._service_name, self._oauth_token["refresh_token"]
|
252
|
+
)
|
253
|
+
self._token: str = self._oauth_token["access_token"]
|
214
254
|
|
215
255
|
def __auth_code_flow(self):
|
216
256
|
from oauthlib.oauth2 import OAuth2Error
|
217
257
|
|
218
258
|
try:
|
219
259
|
logger.info("Creating Authentication Token with OAuth2 Authorization Code Flow.")
|
220
|
-
url, _
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
)
|
225
|
-
authorization_response = (
|
226
|
-
_run_callback(self._on_auth_code, url)
|
227
|
-
if self._on_auth_code
|
228
|
-
else input(
|
229
|
-
f"Please go to {url} and authorize the app.\n"
|
230
|
-
"Then paste the entire redirect URL here: "
|
231
|
-
)
|
232
|
-
)
|
233
|
-
url, headers, body = self._oauth.prepare_token_request(
|
234
|
-
token_url=self._token_url,
|
235
|
-
redirect_url=self._redirect_uri,
|
236
|
-
authorization_response=authorization_response,
|
237
|
-
client_secret=self._client_secret,
|
260
|
+
url, _ = self._oauth.authorization_url()
|
261
|
+
authorization_response = input(
|
262
|
+
f"Please go to {url} and authorize the app.\n"
|
263
|
+
"Then paste the entire redirect URL here: "
|
238
264
|
)
|
239
|
-
|
265
|
+
with httpx.Client() as client:
|
266
|
+
res = client.send(self._oauth.token_request(authorization_response))
|
267
|
+
self._parse_auth_response(res)
|
240
268
|
except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
|
241
269
|
raise InvalidCredentialsException("Error during OAuth2 authorization flow.") from error
|
242
270
|
|
243
271
|
|
272
|
+
class AnaplanRefreshTokenAuth(_AnaplanAuth):
|
273
|
+
def __init__(
|
274
|
+
self,
|
275
|
+
client_id: str,
|
276
|
+
client_secret: str,
|
277
|
+
redirect_uri: str,
|
278
|
+
token: dict[str, str],
|
279
|
+
token_url: str = "https://us1a.app.anaplan.com/oauth/token",
|
280
|
+
):
|
281
|
+
"""
|
282
|
+
This class is a utility class for long-lived `Client` or `AsyncClient` instances that use
|
283
|
+
OAuth. This class will use the `access_token` until the first request fails with a 401
|
284
|
+
Unauthorized error, at which point it will attempt to refresh the `access_token` using the
|
285
|
+
`refresh_token`. If the refresh fails, it will raise an `InvalidCredentialsException`. The
|
286
|
+
`expires_in` field in the token dictionary is not considered. Manipulating any of the
|
287
|
+
fields in the token dictionary is not recommended and will likely have no effect.
|
288
|
+
|
289
|
+
**For its entire lifetime, you are ceding control of the token to this class.**
|
290
|
+
You must not use the same token simultaneously in multiple instances of this class or
|
291
|
+
outside of it, as this may lead to unexpected behavior when e.g. the refresh token is
|
292
|
+
used, which can only happen once and will lead to errors when attempting to use the same
|
293
|
+
refresh token again elsewhere.
|
294
|
+
|
295
|
+
If you need the token back before this instance is destroyed, you can use the `token`
|
296
|
+
method.
|
297
|
+
|
298
|
+
:param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
|
299
|
+
must be an Authorization Code Grant application.
|
300
|
+
:param client_secret: The client secret of your Anaplan Oauth 2.0 application.
|
301
|
+
:param redirect_uri: The URL to which the user will be redirected after authorizing the
|
302
|
+
application.
|
303
|
+
:param token: The OAuth token dictionary containing at least the `access_token` and
|
304
|
+
`refresh_token`.
|
305
|
+
:param token_url: The URL to post the refresh token request to in order to fetch the access
|
306
|
+
token.
|
307
|
+
"""
|
308
|
+
if not isinstance(token, dict) or not all(
|
309
|
+
key in token for key in ("access_token", "refresh_token")
|
310
|
+
):
|
311
|
+
raise ValueError(
|
312
|
+
"The token must at least contain 'access_token' and 'refresh_token' keys."
|
313
|
+
)
|
314
|
+
self._oauth_token = token
|
315
|
+
self._oauth = _OAuthRequestFactory(
|
316
|
+
client_id=client_id,
|
317
|
+
client_secret=client_secret,
|
318
|
+
redirect_uri=redirect_uri,
|
319
|
+
token_url=token_url,
|
320
|
+
)
|
321
|
+
super().__init__(self._oauth_token["access_token"])
|
322
|
+
|
323
|
+
@property
|
324
|
+
def token(self) -> dict[str, str]:
|
325
|
+
"""
|
326
|
+
Returns the current OAuth token. You can safely use the `access_token`, but you
|
327
|
+
must not use the `refresh_token` outside of this class, if you expect to use this instance
|
328
|
+
further. If you do use the `refresh_token` outside of this class, this will error on the
|
329
|
+
next attempt to refresh the token, as the `refresh_token` can only be used once.
|
330
|
+
"""
|
331
|
+
return self._oauth_token
|
332
|
+
|
333
|
+
def _build_auth_request(self) -> httpx.Request:
|
334
|
+
return self._oauth.refresh_token_request(self._oauth_token["refresh_token"])
|
335
|
+
|
336
|
+
def _parse_auth_response(self, response: httpx.Response) -> None:
|
337
|
+
if response.status_code == 401:
|
338
|
+
raise InvalidCredentialsException
|
339
|
+
if not response.is_success:
|
340
|
+
raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
|
341
|
+
self._oauth_token = response.json()
|
342
|
+
self._token: str = self._oauth_token["access_token"]
|
343
|
+
|
344
|
+
|
244
345
|
def _create_auth(
|
245
346
|
user_email: str | None = None,
|
246
347
|
password: str | None = None,
|
247
348
|
certificate: str | bytes | None = None,
|
248
349
|
private_key: str | bytes | None = None,
|
249
350
|
private_key_password: str | bytes | None = None,
|
250
|
-
client_id: str | None = None,
|
251
|
-
client_secret: str | None = None,
|
252
|
-
redirect_uri: str | None = None,
|
253
|
-
refresh_token: str | None = None,
|
254
|
-
oauth2_scope: str = "openid profile email offline_access",
|
255
|
-
on_auth_code: AuthCodeCallback = None,
|
256
|
-
on_token_refresh: AuthTokenRefreshCallback = None,
|
257
351
|
token: str | None = None,
|
258
|
-
oauth_token: dict[str, str] | None = None,
|
259
352
|
) -> httpx.Auth:
|
260
|
-
if token:
|
261
|
-
# TODO: If other parameters are provided that allow refreshing the token, use them to create
|
262
|
-
# use them to create one of the other auth classes instead.
|
263
|
-
return StaticTokenAuth(token)
|
264
|
-
if oauth_token:
|
265
|
-
if not isinstance(oauth_token, dict) and all(
|
266
|
-
f in oauth_token for f in ("access_token", "refresh_token", "token_type", "scope")
|
267
|
-
):
|
268
|
-
raise ValueError(
|
269
|
-
"oauth_token must be a dictionary with at least 'access_token', 'refresh_token', "
|
270
|
-
"'token_type', and 'scope' keys."
|
271
|
-
)
|
272
|
-
# TODO: If client_id, client_secret, and redirect_uri are provided, use them to create an
|
273
|
-
# AnaplanOauth2AuthCodeAuth (extend to accept existing `access_token` instance. Else, use
|
274
|
-
# the StaticTokenAuth directly.
|
275
|
-
return StaticTokenAuth(oauth_token["access_token"])
|
276
353
|
if certificate and private_key:
|
277
|
-
return
|
354
|
+
return _AnaplanCertAuth(certificate, private_key, private_key_password, token)
|
278
355
|
if user_email and password:
|
279
|
-
return
|
280
|
-
if
|
281
|
-
return
|
282
|
-
client_id=client_id,
|
283
|
-
client_secret=client_secret,
|
284
|
-
redirect_uri=redirect_uri,
|
285
|
-
refresh_token=refresh_token,
|
286
|
-
scope=oauth2_scope,
|
287
|
-
on_auth_code=on_auth_code,
|
288
|
-
on_token_refresh=on_token_refresh,
|
289
|
-
)
|
356
|
+
return _AnaplanBasicAuth(user_email, password, token)
|
357
|
+
if token:
|
358
|
+
return _StaticTokenAuth(token)
|
290
359
|
raise ValueError(
|
291
360
|
"No valid authentication parameters provided. Please provide either:\n"
|
292
|
-
"- user_email and password
|
293
|
-
"- certificate and private_key
|
294
|
-
"- client_id, client_secret, and redirect_uri"
|
361
|
+
"- `user_email` and `password`, or\n"
|
362
|
+
"- `certificate` and `private_key`\n"
|
295
363
|
)
|
296
|
-
|
297
|
-
|
298
|
-
def _run_callback(func, *arg, **kwargs):
|
299
|
-
if not inspect.iscoroutinefunction(func):
|
300
|
-
return func(*arg, **kwargs)
|
301
|
-
coro = func(*arg, **kwargs)
|
302
|
-
try:
|
303
|
-
loop = asyncio.get_running_loop()
|
304
|
-
except RuntimeError:
|
305
|
-
return asyncio.run(coro)
|
306
|
-
|
307
|
-
if threading.current_thread() is threading.main_thread():
|
308
|
-
if not loop.is_running():
|
309
|
-
return loop.run_until_complete(coro)
|
310
|
-
else:
|
311
|
-
with ThreadPoolExecutor() as pool:
|
312
|
-
future = pool.submit(__run_in_new_loop, coro)
|
313
|
-
return future.result(timeout=30)
|
314
|
-
else:
|
315
|
-
return asyncio.run_coroutine_threadsafe(coro, loop).result()
|
316
|
-
|
317
|
-
|
318
|
-
def __run_in_new_loop(coroutine: Coroutine[Any, Any, Any]):
|
319
|
-
new_loop = asyncio.new_event_loop()
|
320
|
-
asyncio.set_event_loop(new_loop)
|
321
|
-
try:
|
322
|
-
return new_loop.run_until_complete(coroutine)
|
323
|
-
finally:
|
324
|
-
new_loop.close()
|
anaplan_sdk/_clients/_bulk.py
CHANGED
@@ -3,7 +3,7 @@ import multiprocessing
|
|
3
3
|
import time
|
4
4
|
from concurrent.futures import ThreadPoolExecutor
|
5
5
|
from copy import copy
|
6
|
-
from typing import
|
6
|
+
from typing import Iterator
|
7
7
|
|
8
8
|
import httpx
|
9
9
|
from typing_extensions import Self
|
@@ -34,14 +34,8 @@ logger = logging.getLogger("anaplan_sdk")
|
|
34
34
|
|
35
35
|
class Client(_BaseClient):
|
36
36
|
"""
|
37
|
-
|
38
|
-
|
39
|
-
abstractions over the API, so you can deal with python objects and simple functions rather
|
40
|
-
than implementation details like http, json, compression, chunking etc.
|
41
|
-
|
42
|
-
|
43
|
-
For more information, quick start guides and detailed instructions refer to:
|
44
|
-
[Anaplan SDK](https://vinzenzklass.github.io/anaplan-sdk).
|
37
|
+
Synchronous Anaplan Client. For guides and examples
|
38
|
+
refer to https://vinzenzklass.github.io/anaplan-sdk.
|
45
39
|
"""
|
46
40
|
|
47
41
|
def __init__(
|
@@ -53,15 +47,8 @@ class Client(_BaseClient):
|
|
53
47
|
certificate: str | bytes | None = None,
|
54
48
|
private_key: str | bytes | None = None,
|
55
49
|
private_key_password: str | bytes | None = None,
|
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_auth_code: Callable[[str], str] | None = None,
|
62
|
-
on_token_refresh: Callable[[dict[str, str]], None] | None = None,
|
63
|
-
oauth_token: dict[str, str] | None = None,
|
64
50
|
token: str | None = None,
|
51
|
+
auth: httpx.Auth | None = None,
|
65
52
|
timeout: float | httpx.Timeout = 30,
|
66
53
|
retry_count: int = 2,
|
67
54
|
status_poll_delay: int = 1,
|
@@ -70,77 +57,56 @@ class Client(_BaseClient):
|
|
70
57
|
allow_file_creation: bool = False,
|
71
58
|
) -> None:
|
72
59
|
"""
|
73
|
-
|
74
|
-
|
75
|
-
high-level abstractions over the API, so you can deal with python objects and simple
|
76
|
-
functions rather than implementation details like http, json, compression, chunking etc.
|
77
|
-
|
78
|
-
|
79
|
-
For more information, quick start guides and detailed instructions refer to:
|
80
|
-
[Anaplan SDK](https://vinzenzklass.github.io/anaplan-sdk).
|
60
|
+
Synchronous Anaplan Client. For guides and examples
|
61
|
+
refer to https://vinzenzklass.github.io/anaplan-sdk.
|
81
62
|
|
82
63
|
:param workspace_id: The Anaplan workspace Id. You can copy this from the browser URL or
|
83
|
-
|
64
|
+
find them using an HTTP Client like Postman, Paw, Insomnia etc.
|
84
65
|
:param model_id: The identifier of the model.
|
85
66
|
:param user_email: A valid email registered with the Anaplan Workspace you are attempting
|
86
|
-
|
87
|
-
:param password: Password for the given `user_email
|
88
|
-
|
89
|
-
|
90
|
-
:param
|
91
|
-
|
92
|
-
:param
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
:param
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
This includes the initial token retrieval and any subsequent calls.
|
105
|
-
With this you can for example securely store the token in your
|
106
|
-
application or on your server for later reuse. The function
|
107
|
-
must accept a single argument, which is the token dictionary
|
108
|
-
returned by the Oauth2 token endpoint.
|
67
|
+
to access.
|
68
|
+
:param password: Password for the given `user_email` for basic Authentication.
|
69
|
+
:param certificate: The certificate content or the absolute path to the certificate file.
|
70
|
+
:param private_key: The private key content or the absolute path to the private key file.
|
71
|
+
:param private_key_password: The password to access the private key file. This is only
|
72
|
+
considered if you provided a private key file and it password-protected.
|
73
|
+
:param token: An Anaplan API Token. This will be used to authenticate the client. If
|
74
|
+
sufficient other authentication parameters are provided, the token will be used
|
75
|
+
until it expires, after which a new one will be created. If you provide only this
|
76
|
+
parameter, the client will raise an error upon first authentication failure. For
|
77
|
+
short-lived instances, such as in web applications where user specific clients are
|
78
|
+
created, this is the recommended way to authenticate, since this has the least
|
79
|
+
overhead.
|
80
|
+
:param auth: You can provide a subclass of `httpx.Auth` to use for authentication. You can
|
81
|
+
provide an instance of one of the classes provided by the SDK, or an instance of
|
82
|
+
your own subclass of `httpx.Auth`. This will give you full control over the
|
83
|
+
authentication process, but you will need to implement the entire authentication
|
84
|
+
logic yourself.
|
109
85
|
:param timeout: The timeout in seconds for the HTTP requests. Alternatively, you can pass
|
110
|
-
|
86
|
+
an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
|
111
87
|
:param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
|
112
|
-
|
113
|
-
|
88
|
+
to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
|
89
|
+
number of 2 times.
|
114
90
|
:param status_poll_delay: The delay between polling the status of a task.
|
115
|
-
:param upload_parallel: Whether to upload
|
116
|
-
you are heavily network bound or are experiencing rate limiting
|
117
|
-
issues, set this to False.**
|
91
|
+
:param upload_parallel: Whether to upload chunks in parallel when uploading files.
|
118
92
|
:param upload_chunk_size: The size of the chunks to upload. This is the maximum size of
|
119
|
-
|
93
|
+
each chunk. Defaults to 25MB.
|
120
94
|
:param allow_file_creation: Whether to allow the creation of new files. Defaults to False
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
creating new files and uploading content to them.
|
95
|
+
since this is typically unintentional and may well be unwanted behaviour in the API
|
96
|
+
altogether. A file that is created this way will not be referenced by any action in
|
97
|
+
anaplan until manually assigned so there is typically no value in dynamically
|
98
|
+
creating new files and uploading content to them.
|
126
99
|
"""
|
127
100
|
_client = httpx.Client(
|
128
101
|
auth=(
|
129
|
-
|
102
|
+
auth
|
103
|
+
or _create_auth(
|
130
104
|
token=token,
|
131
|
-
oauth_token=oauth_token,
|
132
105
|
user_email=user_email,
|
133
106
|
password=password,
|
134
107
|
certificate=certificate,
|
135
108
|
private_key=private_key,
|
136
109
|
private_key_password=private_key_password,
|
137
|
-
client_id=client_id,
|
138
|
-
client_secret=client_secret,
|
139
|
-
redirect_uri=redirect_uri,
|
140
|
-
refresh_token=refresh_token,
|
141
|
-
oauth2_scope=oauth2_scope,
|
142
|
-
on_auth_code=on_auth_code,
|
143
|
-
on_token_refresh=on_token_refresh,
|
144
110
|
)
|
145
111
|
),
|
146
112
|
timeout=timeout,
|
anaplan_sdk/_oauth.py
CHANGED
@@ -13,7 +13,7 @@ class _BaseOauth:
|
|
13
13
|
self,
|
14
14
|
client_id: str,
|
15
15
|
client_secret: str,
|
16
|
-
|
16
|
+
redirect_uri: str,
|
17
17
|
authorization_url: str = "https://us1a.app.anaplan.com/auth/prelogin",
|
18
18
|
token_url: str = "https://us1a.app.anaplan.com/oauth/token",
|
19
19
|
validation_url: str = "https://auth.anaplan.com/token/validate",
|
@@ -38,7 +38,7 @@ class _BaseOauth:
|
|
38
38
|
:param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
|
39
39
|
must be an Authorization Code Grant application.
|
40
40
|
:param client_secret: The client secret of your Anaplan Oauth 2.0 application.
|
41
|
-
:param
|
41
|
+
:param redirect_uri: The URL to which the user will be redirected after authorizing the
|
42
42
|
application.
|
43
43
|
:param authorization_url: The URL to which the user will be redirected to authorize the
|
44
44
|
application. Defaults to the Anaplan Prelogin Page, where the user can select the
|
@@ -53,7 +53,7 @@ class _BaseOauth:
|
|
53
53
|
"""
|
54
54
|
self._client_id = client_id
|
55
55
|
self._client_secret = client_secret
|
56
|
-
self.
|
56
|
+
self._redirect_uri = redirect_uri
|
57
57
|
self._authorization_url = authorization_url
|
58
58
|
self._token_url = token_url
|
59
59
|
self._validation_url = validation_url
|
@@ -86,10 +86,46 @@ class _BaseOauth:
|
|
86
86
|
auth_url = authorization_url or self._authorization_url
|
87
87
|
state = state or self._state_generator()
|
88
88
|
url, _, _ = self._oauth.prepare_authorization_request(
|
89
|
-
auth_url, state, self.
|
89
|
+
auth_url, state, self._redirect_uri, self._scope
|
90
90
|
)
|
91
91
|
return url, state
|
92
92
|
|
93
|
+
def _token_request(self, authorization_response: str) -> httpx.Request:
|
94
|
+
url, headers, body = self._oauth.prepare_token_request(
|
95
|
+
authorization_response=authorization_response,
|
96
|
+
token_url=self._token_url,
|
97
|
+
redirect_url=self._redirect_uri,
|
98
|
+
client_secret=self._client_secret,
|
99
|
+
)
|
100
|
+
return httpx.Request(method="POST", url=url, headers=headers, content=body)
|
101
|
+
|
102
|
+
def _refresh_token_request(self, refresh_token: str) -> httpx.Request:
|
103
|
+
url, headers, body = self._oauth.prepare_refresh_token_request(
|
104
|
+
self._token_url,
|
105
|
+
refresh_token=refresh_token,
|
106
|
+
client_id=self._client_id,
|
107
|
+
client_secret=self._client_secret,
|
108
|
+
)
|
109
|
+
return httpx.Request(method="POST", url=url, headers=headers, content=body)
|
110
|
+
|
111
|
+
def _parse_response(self, response: httpx.Response) -> dict[str, str]:
|
112
|
+
if response.status_code == 401:
|
113
|
+
raise InvalidCredentialsException
|
114
|
+
if not response.is_success:
|
115
|
+
raise AnaplanException(
|
116
|
+
f"Token request for Client {self._client_id} failed: "
|
117
|
+
f"{response.status_code} {response.text}"
|
118
|
+
)
|
119
|
+
return response.json()
|
120
|
+
|
121
|
+
|
122
|
+
class _OAuthRequestFactory(_BaseOauth):
|
123
|
+
def token_request(self, authorization_response: str) -> httpx.Request:
|
124
|
+
return self._token_request(authorization_response)
|
125
|
+
|
126
|
+
def refresh_token_request(self, refresh_token: str) -> httpx.Request:
|
127
|
+
return self._refresh_token_request(refresh_token)
|
128
|
+
|
93
129
|
|
94
130
|
class AsyncOauth(_BaseOauth):
|
95
131
|
"""
|
@@ -97,7 +133,7 @@ class AsyncOauth(_BaseOauth):
|
|
97
133
|
Applications.
|
98
134
|
"""
|
99
135
|
|
100
|
-
async def fetch_token(self, authorization_response: str) -> dict[str, str]:
|
136
|
+
async def fetch_token(self, authorization_response: str) -> dict[str, str | int]:
|
101
137
|
"""
|
102
138
|
Fetches the token using the authorization response from the OAuth 2.0 flow.
|
103
139
|
:param authorization_response: The full URL that the user was redirected to after
|
@@ -108,24 +144,14 @@ class AsyncOauth(_BaseOauth):
|
|
108
144
|
from oauthlib.oauth2 import OAuth2Error
|
109
145
|
|
110
146
|
try:
|
111
|
-
url, headers, body = self._oauth.prepare_token_request(
|
112
|
-
authorization_response=authorization_response,
|
113
|
-
token_url=self._token_url,
|
114
|
-
redirect_url=self._redirect_url,
|
115
|
-
client_secret=self._client_secret,
|
116
|
-
)
|
117
147
|
async with httpx.AsyncClient() as client:
|
118
|
-
response = await client.
|
119
|
-
|
120
|
-
raise InvalidCredentialsException(
|
121
|
-
f"Token request failed: {response.status_code} {response.text}"
|
122
|
-
)
|
123
|
-
return response.json()
|
148
|
+
response = await client.send(self._token_request(authorization_response))
|
149
|
+
return self._parse_response(response)
|
124
150
|
except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
|
125
151
|
logger.error(error)
|
126
152
|
raise AnaplanException("Error during token creation.") from error
|
127
153
|
|
128
|
-
async def validate_token(self, token: str) -> dict[str, str | dict[str, str]]:
|
154
|
+
async def validate_token(self, token: str) -> dict[str, str | dict[str, str | int]]:
|
129
155
|
"""
|
130
156
|
Validates the provided token by checking its validity with the Anaplan Authentication API.
|
131
157
|
If the token is not valid, an `InvalidCredentialsException` is raised.
|
@@ -137,13 +163,28 @@ class AsyncOauth(_BaseOauth):
|
|
137
163
|
response = await client.get(
|
138
164
|
url=self._validation_url, headers={"Authorization": f"AnaplanAuthToken {token}"}
|
139
165
|
)
|
140
|
-
|
141
|
-
raise InvalidCredentialsException
|
142
|
-
return response.json()
|
166
|
+
return self._parse_response(response)
|
143
167
|
except httpx.HTTPError as error:
|
144
168
|
logger.error(error)
|
145
169
|
raise AnaplanException("Error during token validation.") from error
|
146
170
|
|
171
|
+
async def refresh_token(self, refresh_token: str) -> dict[str, str | int]:
|
172
|
+
"""
|
173
|
+
Refreshes the token using a refresh token.
|
174
|
+
:param refresh_token: The refresh token to use for refreshing the access token.
|
175
|
+
:return: The new token as a dictionary containing the access token, refresh token, scope,
|
176
|
+
expires_in, and type.
|
177
|
+
"""
|
178
|
+
from oauthlib.oauth2 import OAuth2Error
|
179
|
+
|
180
|
+
try:
|
181
|
+
async with httpx.AsyncClient() as client:
|
182
|
+
response = await client.send(self._refresh_token_request(refresh_token))
|
183
|
+
return self._parse_response(response)
|
184
|
+
except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
|
185
|
+
logger.error(error)
|
186
|
+
raise AnaplanException("Error during token refresh.") from error
|
187
|
+
|
147
188
|
|
148
189
|
class Oauth(_BaseOauth):
|
149
190
|
"""
|
@@ -151,7 +192,7 @@ class Oauth(_BaseOauth):
|
|
151
192
|
Applications.
|
152
193
|
"""
|
153
194
|
|
154
|
-
def fetch_token(self, authorization_response: str) -> dict[str, str]:
|
195
|
+
def fetch_token(self, authorization_response: str) -> dict[str, str | int]:
|
155
196
|
"""
|
156
197
|
Fetches the token using the authorization response from the OAuth 2.0 flow.
|
157
198
|
:param authorization_response: The full URL that the user was redirected to after
|
@@ -165,20 +206,17 @@ class Oauth(_BaseOauth):
|
|
165
206
|
url, headers, body = self._oauth.prepare_token_request(
|
166
207
|
authorization_response=authorization_response,
|
167
208
|
token_url=self._token_url,
|
168
|
-
redirect_url=self.
|
209
|
+
redirect_url=self._redirect_uri,
|
169
210
|
client_secret=self._client_secret,
|
170
211
|
)
|
171
212
|
with httpx.Client() as client:
|
172
213
|
response = client.post(url=url, headers=headers, content=body)
|
173
|
-
|
174
|
-
raise AnaplanException(
|
175
|
-
f"Token request failed: {response.status_code} {response.text}"
|
176
|
-
)
|
177
|
-
return response.json()
|
214
|
+
return self._parse_response(response)
|
178
215
|
except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
|
216
|
+
logger.error(error)
|
179
217
|
raise AnaplanException("Error during token creation.") from error
|
180
218
|
|
181
|
-
def validate_token(self, token: str) -> dict[str, str | dict[str, str]]:
|
219
|
+
def validate_token(self, token: str) -> dict[str, str | dict[str, str | int]]:
|
182
220
|
"""
|
183
221
|
Validates the provided token by checking its validity with the Anaplan Authentication API.
|
184
222
|
If the token is not valid, an `InvalidCredentialsException` is raised.
|
@@ -190,9 +228,30 @@ class Oauth(_BaseOauth):
|
|
190
228
|
response = client.get(
|
191
229
|
url=self._validation_url, headers={"Authorization": f"AnaplanAuthToken {token}"}
|
192
230
|
)
|
193
|
-
|
194
|
-
raise InvalidCredentialsException
|
195
|
-
return response.json()
|
231
|
+
return self._parse_response(response)
|
196
232
|
except httpx.HTTPError as error:
|
197
233
|
logger.error(error)
|
198
234
|
raise AnaplanException("Error during token validation.") from error
|
235
|
+
|
236
|
+
def refresh_token(self, refresh_token: str) -> dict[str, str | int]:
|
237
|
+
"""
|
238
|
+
Refreshes the token using a refresh token.
|
239
|
+
:param refresh_token: The refresh token to use for refreshing the access token.
|
240
|
+
:return: The new token as a dictionary containing the access token, refresh token, scope,
|
241
|
+
expires_in, and type.
|
242
|
+
"""
|
243
|
+
from oauthlib.oauth2 import OAuth2Error
|
244
|
+
|
245
|
+
try:
|
246
|
+
url, headers, body = self._oauth.prepare_refresh_token_request(
|
247
|
+
self._token_url,
|
248
|
+
refresh_token=refresh_token,
|
249
|
+
client_id=self._client_id,
|
250
|
+
client_secret=self._client_secret,
|
251
|
+
)
|
252
|
+
with httpx.Client() as client:
|
253
|
+
response = client.post(url=url, headers=headers, content=body)
|
254
|
+
return self._parse_response(response)
|
255
|
+
except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
|
256
|
+
logger.error(error)
|
257
|
+
raise AnaplanException("Error during token refresh.") from error
|
anaplan_sdk/models/_bulk.py
CHANGED
@@ -145,10 +145,10 @@ class TaskSummary(AnaplanModel):
|
|
145
145
|
|
146
146
|
|
147
147
|
class TaskResultDetail(AnaplanModel):
|
148
|
-
local_message_text: str = Field(description="Error message text.")
|
148
|
+
local_message_text: str | None = Field(None, description="Error message text.")
|
149
149
|
occurrences: int = Field(0, description="The number of occurrences of this error.")
|
150
150
|
type: str = Field(description="The type of this error.")
|
151
|
-
values: list[str] = Field([], description="Further error information if available.")
|
151
|
+
values: list[str | None] = Field([], description="Further error information if available.")
|
152
152
|
|
153
153
|
|
154
154
|
class TaskResult(AnaplanModel):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: anaplan-sdk
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.4
|
4
4
|
Summary: Streamlined Python Interface for Anaplan
|
5
5
|
Project-URL: Homepage, https://vinzenzklass.github.io/anaplan-sdk/
|
6
6
|
Project-URL: Repository, https://github.com/VinzenzKlass/anaplan-sdk
|
@@ -14,6 +14,8 @@ Requires-Dist: httpx<1.0.0,>=0.27.0
|
|
14
14
|
Requires-Dist: pydantic<3.0.0,>=2.7.2
|
15
15
|
Provides-Extra: cert
|
16
16
|
Requires-Dist: cryptography<46.0.0,>=42.0.7; extra == 'cert'
|
17
|
+
Provides-Extra: keyring
|
18
|
+
Requires-Dist: keyring<26.0.0,>=25.6.0; extra == 'keyring'
|
17
19
|
Provides-Extra: oauth
|
18
20
|
Requires-Dist: oauthlib<4.0.0,>=3.0.0; extra == 'oauth'
|
19
21
|
Description-Content-Type: text/markdown
|
@@ -58,7 +60,8 @@ abstractions over all Anaplan APIs, allowing you to focus on business requiremen
|
|
58
60
|
|
59
61
|
## Getting Started
|
60
62
|
|
61
|
-
Head over to the [Quick Start](quickstart
|
63
|
+
Head over to the [Quick Start](https://vinzenzklass.github.io/anaplan-sdk/quickstart/) for basic usage instructions and
|
64
|
+
examples.
|
62
65
|
|
63
66
|
## Contributing
|
64
67
|
|
@@ -1,30 +1,30 @@
|
|
1
|
-
anaplan_sdk/__init__.py,sha256=
|
2
|
-
anaplan_sdk/_auth.py,sha256=
|
1
|
+
anaplan_sdk/__init__.py,sha256=WScEKtXlnRLjCb-j3qW9W4kEACTyPsTLFs-L54et2TQ,351
|
2
|
+
anaplan_sdk/_auth.py,sha256=l5z2WCcfQ05OkuQ1dcmikp6dB87Rw1qy2zu8bbaAQTs,16620
|
3
3
|
anaplan_sdk/_base.py,sha256=9CdLshORWsLixOyoFa3A0Bka5lhLwlZrQI5sEdBcGFI,12298
|
4
|
-
anaplan_sdk/_oauth.py,sha256=
|
4
|
+
anaplan_sdk/_oauth.py,sha256=AynlJDrGIinQT0jwxI2RSvtU4D7Wasyw3H1uicdlLVI,12672
|
5
5
|
anaplan_sdk/exceptions.py,sha256=ALkA9fBF0NQ7dufFxV6AivjmHyuJk9DOQ9jtJV2n7f0,1809
|
6
6
|
anaplan_sdk/_async_clients/__init__.py,sha256=pZXgMMg4S9Aj_pxQCaSiPuNG-sePVGBtNJ0133VjqW4,364
|
7
7
|
anaplan_sdk/_async_clients/_alm.py,sha256=O1_r-O1tNDq7vXRwE2UEFE5S2bPmPh4IAQPQ8bmZfQE,3297
|
8
8
|
anaplan_sdk/_async_clients/_audit.py,sha256=a92RY0B3bWxp2CCAWjzqKfvBjG1LJGlai0Hn5qmwgF8,2312
|
9
|
-
anaplan_sdk/_async_clients/_bulk.py,sha256=
|
9
|
+
anaplan_sdk/_async_clients/_bulk.py,sha256=j0yMoM8NWQH9BsSQ4LRYt8djfd1d11vkjNfU8pUeGLU,23737
|
10
10
|
anaplan_sdk/_async_clients/_cloud_works.py,sha256=KPX9W55SF6h8fJd4Rx-HLq6eaRA-Vo3rFu343UiiaGQ,16642
|
11
11
|
anaplan_sdk/_async_clients/_cw_flow.py,sha256=ZTNAbKDwb59Wg3u68hbtt1kpd-LNz9K0sftT-gvYzJQ,3651
|
12
12
|
anaplan_sdk/_async_clients/_transactional.py,sha256=Mvr7OyBPjQRpBtzkJNfRzV4aNCzUiaYmm0zQubo62Wo,8035
|
13
13
|
anaplan_sdk/_clients/__init__.py,sha256=FsbwvZC1FHrxuRXwbPxUzbhz_lO1DpXIxEOjx6-3QuA,219
|
14
14
|
anaplan_sdk/_clients/_alm.py,sha256=UAdQxgHfax-VquC0YtbqrRBku2Rn35tVgwJdxYFScps,3202
|
15
15
|
anaplan_sdk/_clients/_audit.py,sha256=xQQiwWIb4QQefolPvxNwBFE-pkRzzi8fYPyewjF63lc,2181
|
16
|
-
anaplan_sdk/_clients/_bulk.py,sha256=
|
16
|
+
anaplan_sdk/_clients/_bulk.py,sha256=nlsZHK8vjhvyC0auRuqyvJVvTISPqj9EIHBYLoqSpOc,23354
|
17
17
|
anaplan_sdk/_clients/_cloud_works.py,sha256=KAMnLoeMJ2iwMXlDSbKynCE57BtkCfOgM5O8wT1kkSs,16291
|
18
18
|
anaplan_sdk/_clients/_cw_flow.py,sha256=5IFWFT-qbyGvaSOOtaFOjHnOlyYbj4Rj3xiavfTlm8c,3527
|
19
19
|
anaplan_sdk/_clients/_transactional.py,sha256=YUVbA54uhMloQcahwMtmZO3YooO6qQzwZN3ZRSu_z_c,7976
|
20
20
|
anaplan_sdk/models/__init__.py,sha256=nSplwPG_74CG9CKbv1PzP9bsA9v5-daS4azpTCvCQTI,925
|
21
21
|
anaplan_sdk/models/_alm.py,sha256=IqsTPvkx_ujLpaqZgIrTcr44KHJyKc4dyeRs9rkDjms,2307
|
22
22
|
anaplan_sdk/models/_base.py,sha256=6AZc9CfireUKgpZfMxYKu4MbwiyHQOsGLjKrxGXBLic,508
|
23
|
-
anaplan_sdk/models/_bulk.py,sha256=
|
23
|
+
anaplan_sdk/models/_bulk.py,sha256=_lHARGGjJgi-AmA7u5ZfCmGpLecPnr73LSAsZSX-a_A,8276
|
24
24
|
anaplan_sdk/models/_transactional.py,sha256=_0UbVR9D5QABI29yloYrJTSgL-K0EU7PzPeJu5LdhnY,4854
|
25
25
|
anaplan_sdk/models/cloud_works.py,sha256=nfn_LHPR-KmW7Tpvz-5qNCzmR8SYgvsVV-lx5iDlyqI,19425
|
26
26
|
anaplan_sdk/models/flows.py,sha256=SuLgNj5-2SeE3U1i8iY8cq2IkjuUgd_3M1n2ENructk,3625
|
27
|
-
anaplan_sdk-0.4.
|
28
|
-
anaplan_sdk-0.4.
|
29
|
-
anaplan_sdk-0.4.
|
30
|
-
anaplan_sdk-0.4.
|
27
|
+
anaplan_sdk-0.4.4.dist-info/METADATA,sha256=VU-hRC5mvfO5owSGRjfY7TTMFKL4z3QMRJM_ddiSPU4,3667
|
28
|
+
anaplan_sdk-0.4.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
29
|
+
anaplan_sdk-0.4.4.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
30
|
+
anaplan_sdk-0.4.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|