anaplan-sdk 0.4.3a2__tar.gz → 0.4.4__tar.gz
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-0.4.3a2 → anaplan_sdk-0.4.4}/PKG-INFO +5 -2
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/README.md +2 -1
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/__init__.py +2 -2
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_auth.py +89 -21
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_oauth.py +12 -12
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/models/_bulk.py +2 -2
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/authentication.md +98 -15
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/transactional.md +2 -2
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/pyproject.toml +103 -101
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/uv.lock +1294 -1164
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/.github/dependabot.yml +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/.github/workflows/docs.yml +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/.github/workflows/lint.yml +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/.github/workflows/tests.yml +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/.gitignore +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/.pre-commit-config.yaml +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/LICENSE +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_async_clients/__init__.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_async_clients/_alm.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_async_clients/_audit.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_async_clients/_bulk.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_async_clients/_cloud_works.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_async_clients/_cw_flow.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_async_clients/_transactional.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_base.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_clients/__init__.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_clients/_alm.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_clients/_audit.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_clients/_bulk.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_clients/_cloud_works.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_clients/_cw_flow.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/_clients/_transactional.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/exceptions.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/models/__init__.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/models/_alm.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/models/_base.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/models/_transactional.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/models/cloud_works.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/anaplan_sdk/models/flows.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/anaplan_explained.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/async/async_alm_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/async/async_audit_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/async/async_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/async/async_cw_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/async/async_flows_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/async/async_oauth_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/async/async_transactional_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/exceptions.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/models/alm.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/models/bulk.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/models/cloud_works.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/models/flows.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/models/transactional.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/sync/sync_alm_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/sync/sync_audit_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/sync/sync_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/sync/sync_cw_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/sync/sync_flows_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/sync/sync_oauth_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/api/sync/sync_transactional_client.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/assets/overview.html +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/css/styles.css +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/alm.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/audit.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/bulk.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/bulk_vs_transactional.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/cloud_works.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/index.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/logging.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/guides/multiple_models.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/img/anaplan-sdk.webp +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/index.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/installation.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/js/assets/hljs.js +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/js/assets/hljs.min.js +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/js/assets/python.js +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/js/assets/python.min.js +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/js/highlight.js +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/js/highlight.min.js +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/docs/quickstart.md +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/mkdocs.yml +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/async/conftest.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/async/test_async_alm_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/async/test_async_audit_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/async/test_async_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/async/test_async_cloud_works_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/async/test_async_flows_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/async/test_async_transactional_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/conftest.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/sync/conftest.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/sync/test_alm_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/sync/test_audit_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/sync/test_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/sync/test_cloud_works_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/sync/test_flows_client.py +0 -0
- {anaplan_sdk-0.4.3a2 → anaplan_sdk-0.4.4}/tests/sync/test_transactional_client.py +0 -0
@@ -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
|
|
@@ -38,7 +38,8 @@ abstractions over all Anaplan APIs, allowing you to focus on business requiremen
|
|
38
38
|
|
39
39
|
## Getting Started
|
40
40
|
|
41
|
-
Head over to the [Quick Start](quickstart
|
41
|
+
Head over to the [Quick Start](https://vinzenzklass.github.io/anaplan-sdk/quickstart/) for basic usage instructions and
|
42
|
+
examples.
|
42
43
|
|
43
44
|
## Contributing
|
44
45
|
|
@@ -1,12 +1,12 @@
|
|
1
1
|
from ._async_clients import AsyncClient
|
2
|
-
from ._auth import
|
2
|
+
from ._auth import AnaplanLocalOAuth, AnaplanRefreshTokenAuth
|
3
3
|
from ._clients import Client
|
4
4
|
from ._oauth import AsyncOauth, Oauth
|
5
5
|
|
6
6
|
__all__ = [
|
7
7
|
"AsyncClient",
|
8
8
|
"Client",
|
9
|
-
"
|
9
|
+
"AnaplanLocalOAuth",
|
10
10
|
"AnaplanRefreshTokenAuth",
|
11
11
|
"AsyncOauth",
|
12
12
|
"Oauth",
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
from base64 import b64encode
|
4
|
-
from typing import
|
4
|
+
from typing import Callable
|
5
5
|
|
6
6
|
import httpx
|
7
7
|
|
@@ -10,11 +10,6 @@ from .exceptions import AnaplanException, InvalidCredentialsException, InvalidPr
|
|
10
10
|
|
11
11
|
logger = logging.getLogger("anaplan_sdk")
|
12
12
|
|
13
|
-
AuthCodeCallback = (Callable[[str], str] | Callable[[str], Awaitable[str]]) | None
|
14
|
-
AuthTokenRefreshCallback = (
|
15
|
-
Callable[[dict[str, str]], None] | Callable[[dict[str, str]], Awaitable[None]]
|
16
|
-
) | None
|
17
|
-
|
18
13
|
|
19
14
|
class _AnaplanAuth(httpx.Auth):
|
20
15
|
requires_response_body = True
|
@@ -154,13 +149,14 @@ class _AnaplanCertAuth(_AnaplanAuth):
|
|
154
149
|
raise InvalidPrivateKeyException from error
|
155
150
|
|
156
151
|
|
157
|
-
class
|
152
|
+
class AnaplanLocalOAuth(_AnaplanAuth):
|
158
153
|
def __init__(
|
159
154
|
self,
|
160
155
|
client_id: str,
|
161
156
|
client_secret: str,
|
162
|
-
|
157
|
+
redirect_uri: str,
|
163
158
|
token: dict[str, str] | None = None,
|
159
|
+
persist_token: bool = False,
|
164
160
|
authorization_url: str = "https://us1a.app.anaplan.com/auth/prelogin",
|
165
161
|
token_url: str = "https://us1a.app.anaplan.com/oauth/token",
|
166
162
|
validation_url: str = "https://auth.anaplan.com/token/validate",
|
@@ -168,15 +164,24 @@ class AnaplanOAuthCodeAuth(_AnaplanAuth):
|
|
168
164
|
state_generator: Callable[[], str] | None = None,
|
169
165
|
):
|
170
166
|
"""
|
171
|
-
Initializes the
|
167
|
+
Initializes the AnaplanLocalOAuth class for OAuth2 authentication using the
|
172
168
|
Authorization Code Flow. This is a utility class for local development and requires user
|
173
169
|
interaction. For Web Applications and other scenarios, refer to `Oauth` or `AsyncOauth`.
|
174
170
|
This class will refresh the access token automatically when it expires.
|
175
171
|
:param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
|
176
172
|
must be an Authorization Code Grant application.
|
177
173
|
:param client_secret: The client secret of your Anaplan Oauth 2.0 application.
|
178
|
-
:param
|
174
|
+
:param redirect_uri: The URL to which the user will be redirected after authorizing the
|
179
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.
|
180
185
|
:param authorization_url: The URL to which the user will be redirected to authorize the
|
181
186
|
application. Defaults to the Anaplan Prelogin Page, where the user can select the
|
182
187
|
login method.
|
@@ -185,25 +190,51 @@ class AnaplanOAuthCodeAuth(_AnaplanAuth):
|
|
185
190
|
:param validation_url: The URL to validate the access token.
|
186
191
|
:param scope: The scope of the access request.
|
187
192
|
:param state_generator: A callable that generates a random state string. You can optionally
|
188
|
-
|
189
|
-
|
193
|
+
pass this if you need to customize the state generation logic. If not provided,
|
194
|
+
the state will be generated by `oauthlib`.
|
190
195
|
"""
|
191
|
-
|
192
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
|
193
214
|
self._oauth = _OAuthRequestFactory(
|
194
215
|
client_id=client_id,
|
195
216
|
client_secret=client_secret,
|
196
|
-
|
217
|
+
redirect_uri=redirect_uri,
|
197
218
|
scope=scope,
|
198
219
|
authorization_url=authorization_url,
|
199
220
|
token_url=token_url,
|
200
221
|
validation_url=validation_url,
|
201
222
|
state_generator=state_generator,
|
202
223
|
)
|
203
|
-
if not
|
224
|
+
if not self._oauth_token:
|
204
225
|
self.__auth_code_flow()
|
205
226
|
super().__init__(self._token)
|
206
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
|
237
|
+
|
207
238
|
def _build_auth_request(self) -> httpx.Request:
|
208
239
|
return self._oauth.refresh_token_request(self._oauth_token["refresh_token"])
|
209
240
|
|
@@ -213,6 +244,12 @@ class AnaplanOAuthCodeAuth(_AnaplanAuth):
|
|
213
244
|
if not response.is_success:
|
214
245
|
raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
|
215
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
|
+
)
|
216
253
|
self._token: str = self._oauth_token["access_token"]
|
217
254
|
|
218
255
|
def __auth_code_flow(self):
|
@@ -237,30 +274,61 @@ class AnaplanRefreshTokenAuth(_AnaplanAuth):
|
|
237
274
|
self,
|
238
275
|
client_id: str,
|
239
276
|
client_secret: str,
|
240
|
-
|
277
|
+
redirect_uri: str,
|
241
278
|
token: dict[str, str],
|
242
279
|
token_url: str = "https://us1a.app.anaplan.com/oauth/token",
|
243
280
|
):
|
244
281
|
"""
|
245
282
|
This class is a utility class for long-lived `Client` or `AsyncClient` instances that use
|
246
|
-
OAuth.
|
247
|
-
to refresh the
|
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
|
+
|
248
298
|
:param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
|
249
299
|
must be an Authorization Code Grant application.
|
250
300
|
:param client_secret: The client secret of your Anaplan Oauth 2.0 application.
|
251
|
-
:param
|
301
|
+
:param redirect_uri: The URL to which the user will be redirected after authorizing the
|
252
302
|
application.
|
303
|
+
:param token: The OAuth token dictionary containing at least the `access_token` and
|
304
|
+
`refresh_token`.
|
253
305
|
:param token_url: The URL to post the refresh token request to in order to fetch the access
|
254
306
|
token.
|
255
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
|
+
)
|
256
314
|
self._oauth_token = token
|
257
315
|
self._oauth = _OAuthRequestFactory(
|
258
316
|
client_id=client_id,
|
259
317
|
client_secret=client_secret,
|
260
|
-
|
318
|
+
redirect_uri=redirect_uri,
|
261
319
|
token_url=token_url,
|
262
320
|
)
|
263
|
-
super().__init__(self.
|
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
|
264
332
|
|
265
333
|
def _build_auth_request(self) -> httpx.Request:
|
266
334
|
return self._oauth.refresh_token_request(self._oauth_token["refresh_token"])
|
@@ -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,7 +86,7 @@ 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
|
|
@@ -94,7 +94,7 @@ class _BaseOauth:
|
|
94
94
|
url, headers, body = self._oauth.prepare_token_request(
|
95
95
|
authorization_response=authorization_response,
|
96
96
|
token_url=self._token_url,
|
97
|
-
redirect_url=self.
|
97
|
+
redirect_url=self._redirect_uri,
|
98
98
|
client_secret=self._client_secret,
|
99
99
|
)
|
100
100
|
return httpx.Request(method="POST", url=url, headers=headers, content=body)
|
@@ -133,7 +133,7 @@ class AsyncOauth(_BaseOauth):
|
|
133
133
|
Applications.
|
134
134
|
"""
|
135
135
|
|
136
|
-
async def fetch_token(self, authorization_response: str) -> dict[str, str]:
|
136
|
+
async def fetch_token(self, authorization_response: str) -> dict[str, str | int]:
|
137
137
|
"""
|
138
138
|
Fetches the token using the authorization response from the OAuth 2.0 flow.
|
139
139
|
:param authorization_response: The full URL that the user was redirected to after
|
@@ -151,7 +151,7 @@ class AsyncOauth(_BaseOauth):
|
|
151
151
|
logger.error(error)
|
152
152
|
raise AnaplanException("Error during token creation.") from error
|
153
153
|
|
154
|
-
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]]:
|
155
155
|
"""
|
156
156
|
Validates the provided token by checking its validity with the Anaplan Authentication API.
|
157
157
|
If the token is not valid, an `InvalidCredentialsException` is raised.
|
@@ -168,7 +168,7 @@ class AsyncOauth(_BaseOauth):
|
|
168
168
|
logger.error(error)
|
169
169
|
raise AnaplanException("Error during token validation.") from error
|
170
170
|
|
171
|
-
async def refresh_token(self, refresh_token: str) -> dict[str, str]:
|
171
|
+
async def refresh_token(self, refresh_token: str) -> dict[str, str | int]:
|
172
172
|
"""
|
173
173
|
Refreshes the token using a refresh token.
|
174
174
|
:param refresh_token: The refresh token to use for refreshing the access token.
|
@@ -192,7 +192,7 @@ class Oauth(_BaseOauth):
|
|
192
192
|
Applications.
|
193
193
|
"""
|
194
194
|
|
195
|
-
def fetch_token(self, authorization_response: str) -> dict[str, str]:
|
195
|
+
def fetch_token(self, authorization_response: str) -> dict[str, str | int]:
|
196
196
|
"""
|
197
197
|
Fetches the token using the authorization response from the OAuth 2.0 flow.
|
198
198
|
:param authorization_response: The full URL that the user was redirected to after
|
@@ -206,7 +206,7 @@ class Oauth(_BaseOauth):
|
|
206
206
|
url, headers, body = self._oauth.prepare_token_request(
|
207
207
|
authorization_response=authorization_response,
|
208
208
|
token_url=self._token_url,
|
209
|
-
redirect_url=self.
|
209
|
+
redirect_url=self._redirect_uri,
|
210
210
|
client_secret=self._client_secret,
|
211
211
|
)
|
212
212
|
with httpx.Client() as client:
|
@@ -216,7 +216,7 @@ class Oauth(_BaseOauth):
|
|
216
216
|
logger.error(error)
|
217
217
|
raise AnaplanException("Error during token creation.") from error
|
218
218
|
|
219
|
-
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]]:
|
220
220
|
"""
|
221
221
|
Validates the provided token by checking its validity with the Anaplan Authentication API.
|
222
222
|
If the token is not valid, an `InvalidCredentialsException` is raised.
|
@@ -233,7 +233,7 @@ class Oauth(_BaseOauth):
|
|
233
233
|
logger.error(error)
|
234
234
|
raise AnaplanException("Error during token validation.") from error
|
235
235
|
|
236
|
-
def refresh_token(self, refresh_token: str) -> dict[str, str]:
|
236
|
+
def refresh_token(self, refresh_token: str) -> dict[str, str | int]:
|
237
237
|
"""
|
238
238
|
Refreshes the token using a refresh token.
|
239
239
|
:param refresh_token: The refresh token to use for refreshing the access token.
|
@@ -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):
|
@@ -7,9 +7,9 @@ There are three main ways to authenticate with Anaplan.
|
|
7
7
|
- OAuth2
|
8
8
|
|
9
9
|
Anaplan SDK supports all of them, though Basic Authentication is strictly not recommended for production use.
|
10
|
-
Certificate
|
11
|
-
|
12
|
-
|
10
|
+
Certificate Authentication is currently the most suitable for production use, since the Anaplan OAuth 2.0
|
11
|
+
implementation does not support the `client_credentials` grant type. This means you will have to manually manage the
|
12
|
+
refresh Token if you choose to use OAuth2.
|
13
13
|
|
14
14
|
## Basic Authentication
|
15
15
|
|
@@ -45,7 +45,7 @@ maintain and error-prone.
|
|
45
45
|
|
46
46
|
Certificate Authentication is the most suitable for production use. It uses an X.509 S/MIME Certificate (aka. Client Certificate or HTTPS-Certificate) and Private Key. The Process of acquiring such a certificate is well [documented](https://help.anaplan.com/procure-ca-certificates-47842267-2cb3-4e38-90bf-13b1632bcd44). Anaplan does not support self-signed certificates, so you will need to procure a certificate from a trusted Certificate Authority (CA).
|
47
47
|
|
48
|
-
|
48
|
+
???+ tip "Requires Extra"
|
49
49
|
If you want to use certificate authentication, you need to install the `cert` extra:
|
50
50
|
=== "pip"
|
51
51
|
```shell
|
@@ -96,7 +96,8 @@ in which the authentication flow must occur outside the SDK for the user to log
|
|
96
96
|
|
97
97
|
These Classes exist for convenience only, and you can use any other Library to handle the Oauth2 flow.
|
98
98
|
|
99
|
-
|
99
|
+
A minimal, illustrative example for FastAPI is shown below, but you can use any other Web Framework. This will not run
|
100
|
+
until you implement the TODOs in a suitable way for your purpose.
|
100
101
|
|
101
102
|
??? tip "Requires Extra"
|
102
103
|
If you want to use OAuth2 authentication, you need to install the `oauth` extra:
|
@@ -114,7 +115,6 @@ An example for FastAPI is shown below, but you can use any other Web Framework.
|
|
114
115
|
```
|
115
116
|
This will install [OAuthLib](https://oauthlib.readthedocs.io/en/latest/index.html) to securely construct the authentication request.
|
116
117
|
|
117
|
-
|
118
118
|
```python
|
119
119
|
import os
|
120
120
|
from typing import Annotated
|
@@ -127,7 +127,7 @@ from anaplan_sdk import AsyncClient, AsyncOauth, exceptions
|
|
127
127
|
_oauth = AsyncOauth(
|
128
128
|
client_id=os.environ["OAUTH_CLIENT_ID"],
|
129
129
|
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
|
130
|
-
|
130
|
+
redirect_uri="https://vinzenzklass.github.io/anaplan-sdk/oauth/callback",
|
131
131
|
)
|
132
132
|
|
133
133
|
app = FastAPI()
|
@@ -182,7 +182,7 @@ when it expires.
|
|
182
182
|
token=token,
|
183
183
|
client_id=os.environ["OAUTH_CLIENT_ID"],
|
184
184
|
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
|
185
|
-
|
185
|
+
redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
|
186
186
|
)
|
187
187
|
)
|
188
188
|
```
|
@@ -193,7 +193,7 @@ when it expires.
|
|
193
193
|
token=token,
|
194
194
|
client_id=os.environ["OAUTH_CLIENT_ID"],
|
195
195
|
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
|
196
|
-
|
196
|
+
redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
|
197
197
|
)
|
198
198
|
)
|
199
199
|
```
|
@@ -201,10 +201,10 @@ when it expires.
|
|
201
201
|
|
202
202
|
## OAuth for Local Applications
|
203
203
|
|
204
|
-
For local applications, you can use `
|
204
|
+
For local applications, you can use `AnaplanLocalOAuth` Class to handle the initial Oauth2 `authorization_code` flow
|
205
205
|
and the subsequent token refreshes.
|
206
206
|
|
207
|
-
|
207
|
+
???+ tip "Requires Extra"
|
208
208
|
If you want to use OAuth2 authentication, you need to install the `oauth` extra:
|
209
209
|
=== "pip"
|
210
210
|
```shell
|
@@ -223,20 +223,20 @@ and the subsequent token refreshes.
|
|
223
223
|
=== "Synchronous"
|
224
224
|
```python
|
225
225
|
anaplan = Client(
|
226
|
-
auth=
|
226
|
+
auth=AnaplanLocalOAuth(
|
227
227
|
client_id=os.environ["OAUTH_CLIENT_ID"],
|
228
228
|
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
|
229
|
-
|
229
|
+
redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
|
230
230
|
)
|
231
231
|
)
|
232
232
|
```
|
233
233
|
=== "Asynchronous"
|
234
234
|
```python
|
235
235
|
anaplan = AsyncClient(
|
236
|
-
auth=
|
236
|
+
auth=AnaplanLocalOAuth(
|
237
237
|
client_id=os.environ["OAUTH_CLIENT_ID"],
|
238
238
|
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
|
239
|
-
|
239
|
+
redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
|
240
240
|
)
|
241
241
|
)
|
242
242
|
```
|
@@ -248,6 +248,89 @@ will need to copy the entire redirect URI from your browser and paste it into th
|
|
248
248
|
Unfortunately, registering localhost redirect URIs is not supported by Anaplan. This means we cannot intercept the
|
249
249
|
redirect URI and extract the `authorization_code` automatically. This is a limitation of Anaplan's OAuth2 implementation. See [this Community Note](https://community.anaplan.com/discussion/156599/oauth-rediredt-url-port-for-desktop-apps).
|
250
250
|
|
251
|
+
## Persisting OAuth Tokens
|
252
|
+
|
253
|
+
The SDK provides the ability to persist OAuth refresh tokens between sessions using the system's secure keyring for
|
254
|
+
local applications. This allows you to avoid having to re-authenticate every time you run your application while using
|
255
|
+
OAuth2.
|
256
|
+
|
257
|
+
???+ tip "Requires Extras"
|
258
|
+
If you want to use persisting Tokens, you need to additionally install the `keyring` extra:
|
259
|
+
=== "pip"
|
260
|
+
```shell
|
261
|
+
pip install anaplan-sdk[oauth,keyring]
|
262
|
+
```
|
263
|
+
===+ "uv"
|
264
|
+
```shell
|
265
|
+
uv add anaplan-sdk[oauth,keyring]
|
266
|
+
```
|
267
|
+
=== "Poetry"
|
268
|
+
```shell
|
269
|
+
poetry add anaplan-sdk[oauth,keyring]
|
270
|
+
```
|
271
|
+
|
272
|
+
This will install [Keyring](https://github.com/jaraco/keyring) to securely store refresh tokens.
|
273
|
+
|
274
|
+
To enable token persistence, set the `persist_token=True` parameter when creating an `AnaplanLocalOAuth` instance:
|
275
|
+
|
276
|
+
=== "Synchronous"
|
277
|
+
```python
|
278
|
+
anaplan = Client(
|
279
|
+
auth=AnaplanLocalOAuth(
|
280
|
+
client_id=os.environ["OAUTH_CLIENT_ID"],
|
281
|
+
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
|
282
|
+
redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
|
283
|
+
persist_token=True,
|
284
|
+
)
|
285
|
+
)
|
286
|
+
```
|
287
|
+
=== "Asynchronous"
|
288
|
+
```python
|
289
|
+
anaplan = AsyncClient(
|
290
|
+
auth=AnaplanLocalOAuth(
|
291
|
+
client_id=os.environ["OAUTH_CLIENT_ID"],
|
292
|
+
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
|
293
|
+
redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
|
294
|
+
persist_token=True,
|
295
|
+
)
|
296
|
+
)
|
297
|
+
```
|
298
|
+
When `persist_token` is set to True, the SDK will:
|
299
|
+
|
300
|
+
- Look for a stored refresh token in the system's keyring
|
301
|
+
- If found, use it to obtain a new access token. If also given, this will overwrite the passed `token` parameter.
|
302
|
+
- If not found or if the token is invalid, prompt the user for authentication
|
303
|
+
- After authentication, store the new refresh token in the keyring
|
304
|
+
|
305
|
+
??? note "Keyring Configuration"
|
306
|
+
The keyring library may require additional configuration depending on your environment:
|
307
|
+
|
308
|
+
- In headless environments, you may need to explicitely configure a different keyring backend
|
309
|
+
- Some Linux distributions may require additional packages or configuration
|
310
|
+
|
311
|
+
Configuring the keyring backend is your responsibility as it depends on your specific environment.
|
312
|
+
|
313
|
+
For example, to use the libsecret file backend:
|
314
|
+
|
315
|
+
```python
|
316
|
+
import keyring
|
317
|
+
from keyring.backends import libsecret
|
318
|
+
|
319
|
+
keyring.set_keyring(libsecret.Keyring())
|
320
|
+
```
|
321
|
+
|
322
|
+
For more information, refer to the [keyring documentation](https://github.com/jaraco/keyring).
|
323
|
+
|
324
|
+
## OAuth Token Ownership
|
325
|
+
|
326
|
+
Instances of both `AnaplanLocalOAuth` and `AnaplanRefreshTokenAuth` assert ownership of the token you pass to them
|
327
|
+
for their entire lifetime. This means that you should not use the token outside of these classes, as it may lead to
|
328
|
+
errors when attempting to use the same refresh token in multiple places. You can access the current token by using the
|
329
|
+
`token` property, but you should not use anything other than the `access_token`. You can use this property to
|
330
|
+
reassert control of the OAuth token when the instance is nor longer needed. If you do need to use the token in several
|
331
|
+
places simultaneously, you should use a [custom scheme](#custom-authentication-schemes) to do so and handle all
|
332
|
+
potential conflicts appropriately.
|
333
|
+
|
251
334
|
|
252
335
|
## Custom Authentication Schemes
|
253
336
|
|
@@ -175,7 +175,7 @@ Warning. To automate this tedious task without losing any data, we can perform f
|
|
175
175
|
)
|
176
176
|
anaplan.transactional.reset_list_index(101000000000)
|
177
177
|
result = anaplan.transactional.insert_list_items(
|
178
|
-
|
178
|
+
101000000000, [e.model_dump() for e in items] # Reimport all fields.
|
179
179
|
)
|
180
180
|
```
|
181
181
|
=== "Asynchronous"
|
@@ -186,6 +186,6 @@ Warning. To automate this tedious task without losing any data, we can perform f
|
|
186
186
|
)
|
187
187
|
await anaplan.transactional.reset_list_index(101000000000)
|
188
188
|
result = await anaplan.transactional.insert_list_items(
|
189
|
-
|
189
|
+
101000000000, [e.model_dump() for e in items] # Reimport all fields.
|
190
190
|
)
|
191
191
|
```
|