anaplan-sdk 0.4.0a3__py3-none-any.whl → 0.4.2__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/_bulk.py +21 -3
- anaplan_sdk/_auth.py +55 -9
- anaplan_sdk/_clients/_bulk.py +6 -0
- {anaplan_sdk-0.4.0a3.dist-info → anaplan_sdk-0.4.2.dist-info}/METADATA +3 -3
- {anaplan_sdk-0.4.0a3.dist-info → anaplan_sdk-0.4.2.dist-info}/RECORD +7 -7
- {anaplan_sdk-0.4.0a3.dist-info → anaplan_sdk-0.4.2.dist-info}/WHEEL +0 -0
- {anaplan_sdk-0.4.0a3.dist-info → anaplan_sdk-0.4.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,12 +1,12 @@
|
|
1
1
|
import logging
|
2
2
|
from asyncio import gather, sleep
|
3
3
|
from copy import copy
|
4
|
-
from typing import AsyncIterator,
|
4
|
+
from typing import AsyncIterator, Iterator
|
5
5
|
|
6
6
|
import httpx
|
7
7
|
from typing_extensions import Self
|
8
8
|
|
9
|
-
from anaplan_sdk._auth import create_auth
|
9
|
+
from anaplan_sdk._auth import AuthCodeCallback, AuthTokenRefreshCallback, 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 (
|
@@ -56,7 +56,8 @@ class AsyncClient(_AsyncBaseClient):
|
|
56
56
|
redirect_uri: str | None = None,
|
57
57
|
refresh_token: str | None = None,
|
58
58
|
oauth2_scope: str = "openid profile email offline_access",
|
59
|
-
|
59
|
+
on_auth_code: AuthCodeCallback = None,
|
60
|
+
on_token_refresh: AuthTokenRefreshCallback = None,
|
60
61
|
timeout: float | httpx.Timeout = 30,
|
61
62
|
retry_count: int = 2,
|
62
63
|
status_poll_delay: int = 1,
|
@@ -91,11 +92,27 @@ class AsyncClient(_AsyncBaseClient):
|
|
91
92
|
:param refresh_token: If you have a valid refresh token, you can pass it to skip the
|
92
93
|
interactive authentication code step.
|
93
94
|
:param oauth2_scope: The scope of the Oauth2 token, if you want to narrow it.
|
95
|
+
:param on_auth_code: A callback that takes the redirect URI as a single argument and must
|
96
|
+
return the entire response URI. This will substitute the interactive
|
97
|
+
authentication code step in the terminal. The callback can be either
|
98
|
+
a synchronous function or an async coroutine function - both will be
|
99
|
+
handled appropriately regardless of the execution context (in a thread,
|
100
|
+
with or without an event loop, etc.).
|
101
|
+
**Note**: When using asynchronous callbacks in complex applications
|
102
|
+
with multiple event loops, be aware that callbacks may execute in a
|
103
|
+
separate event loop context from where they were defined, which can
|
104
|
+
make debugging challenging.
|
94
105
|
:param on_token_refresh: A callback function that is called whenever the token is refreshed.
|
106
|
+
This includes the initial token retrieval and any subsequent calls.
|
95
107
|
With this you can for example securely store the token in your
|
96
108
|
application or on your server for later reuse. The function
|
97
109
|
must accept a single argument, which is the token dictionary
|
98
110
|
returned by the Oauth2 token endpoint and does not return anything.
|
111
|
+
This can be either a synchronous function or an async coroutine
|
112
|
+
function. **Note**: When using asynchronous callbacks in complex
|
113
|
+
applications with multiple event loops, be aware that callbacks
|
114
|
+
may execute in a separate event loop context from where they were
|
115
|
+
defined, which can make debugging challenging.
|
99
116
|
:param timeout: The timeout in seconds for the HTTP requests. Alternatively, you can pass
|
100
117
|
an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
|
101
118
|
:param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
|
@@ -124,6 +141,7 @@ class AsyncClient(_AsyncBaseClient):
|
|
124
141
|
redirect_uri=redirect_uri,
|
125
142
|
refresh_token=refresh_token,
|
126
143
|
oauth2_scope=oauth2_scope,
|
144
|
+
on_auth_code=on_auth_code,
|
127
145
|
on_token_refresh=on_token_refresh,
|
128
146
|
)
|
129
147
|
),
|
anaplan_sdk/_auth.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
+
import asyncio
|
2
|
+
import inspect
|
1
3
|
import logging
|
2
4
|
import os
|
5
|
+
import threading
|
3
6
|
from base64 import b64encode
|
4
|
-
from
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
8
|
+
from typing import Any, Awaitable, Callable, Coroutine
|
5
9
|
|
6
10
|
import httpx
|
7
11
|
|
@@ -9,6 +13,11 @@ from .exceptions import AnaplanException, InvalidCredentialsException, InvalidPr
|
|
9
13
|
|
10
14
|
logger = logging.getLogger("anaplan_sdk")
|
11
15
|
|
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
|
+
|
12
21
|
|
13
22
|
class _AnaplanAuth(httpx.Auth):
|
14
23
|
requires_response_body = True
|
@@ -144,7 +153,8 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
|
|
144
153
|
redirect_uri: str,
|
145
154
|
refresh_token: str | None = None,
|
146
155
|
scope: str = "openid profile email offline_access",
|
147
|
-
|
156
|
+
on_auth_code: AuthCodeCallback = None,
|
157
|
+
on_token_refresh: AuthTokenRefreshCallback = None,
|
148
158
|
):
|
149
159
|
try:
|
150
160
|
from oauthlib.oauth2 import WebApplicationClient
|
@@ -163,6 +173,7 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
|
|
163
173
|
self._refresh_token = refresh_token
|
164
174
|
self._scope = scope
|
165
175
|
self._id_token = None
|
176
|
+
self._on_auth_code = on_auth_code
|
166
177
|
self._on_token_refresh = on_token_refresh
|
167
178
|
if not refresh_token:
|
168
179
|
self.__auth_code_flow()
|
@@ -186,7 +197,7 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
|
|
186
197
|
self._token = token["access_token"]
|
187
198
|
self._refresh_token = token["refresh_token"]
|
188
199
|
if self._on_token_refresh:
|
189
|
-
self._on_token_refresh
|
200
|
+
_run_callback(self._on_token_refresh, token)
|
190
201
|
self._id_token = token.get("id_token")
|
191
202
|
|
192
203
|
def __auth_code_flow(self):
|
@@ -195,13 +206,17 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
|
|
195
206
|
try:
|
196
207
|
logger.info("Creating Authentication Token with OAuth2 Authorization Code Flow.")
|
197
208
|
url, _, _ = self._oauth.prepare_authorization_request(
|
198
|
-
"https://us1a.app.anaplan.com/auth/
|
209
|
+
"https://us1a.app.anaplan.com/auth/prelogin",
|
199
210
|
redirect_url=self._redirect_uri,
|
200
211
|
scope=self._scope,
|
201
212
|
)
|
202
|
-
authorization_response =
|
203
|
-
|
204
|
-
|
213
|
+
authorization_response = (
|
214
|
+
_run_callback(self._on_auth_code, url)
|
215
|
+
if self._on_auth_code
|
216
|
+
else input(
|
217
|
+
f"Please go to {url} and authorize the app.\n"
|
218
|
+
"Then paste the entire redirect URL here: "
|
219
|
+
)
|
205
220
|
)
|
206
221
|
url, headers, body = self._oauth.prepare_token_request(
|
207
222
|
token_url=self._token_url,
|
@@ -211,7 +226,7 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
|
|
211
226
|
)
|
212
227
|
self._parse_auth_response(httpx.post(url=url, headers=headers, content=body))
|
213
228
|
except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
|
214
|
-
raise
|
229
|
+
raise InvalidCredentialsException("Error during OAuth2 authorization flow.") from error
|
215
230
|
|
216
231
|
|
217
232
|
def create_auth(
|
@@ -225,7 +240,8 @@ def create_auth(
|
|
225
240
|
redirect_uri: str | None = None,
|
226
241
|
refresh_token: str | None = None,
|
227
242
|
oauth2_scope: str = "openid profile email offline_access",
|
228
|
-
|
243
|
+
on_auth_code: AuthCodeCallback = None,
|
244
|
+
on_token_refresh: AuthTokenRefreshCallback = None,
|
229
245
|
) -> _AnaplanAuth:
|
230
246
|
if certificate and private_key:
|
231
247
|
return AnaplanCertAuth(certificate, private_key, private_key_password)
|
@@ -238,6 +254,7 @@ def create_auth(
|
|
238
254
|
redirect_uri=redirect_uri,
|
239
255
|
refresh_token=refresh_token,
|
240
256
|
scope=oauth2_scope,
|
257
|
+
on_auth_code=on_auth_code,
|
241
258
|
on_token_refresh=on_token_refresh,
|
242
259
|
)
|
243
260
|
raise ValueError(
|
@@ -246,3 +263,32 @@ def create_auth(
|
|
246
263
|
"- certificate and private_key, or\n"
|
247
264
|
"- client_id, client_secret, and redirect_uri"
|
248
265
|
)
|
266
|
+
|
267
|
+
|
268
|
+
def _run_callback(func, *arg, **kwargs):
|
269
|
+
if not inspect.iscoroutinefunction(func):
|
270
|
+
return func(*arg, **kwargs)
|
271
|
+
coro = func(*arg, **kwargs)
|
272
|
+
try:
|
273
|
+
loop = asyncio.get_running_loop()
|
274
|
+
except RuntimeError:
|
275
|
+
return asyncio.run(coro)
|
276
|
+
|
277
|
+
if threading.current_thread() is threading.main_thread():
|
278
|
+
if not loop.is_running():
|
279
|
+
return loop.run_until_complete(coro)
|
280
|
+
else:
|
281
|
+
with ThreadPoolExecutor() as pool:
|
282
|
+
future = pool.submit(__run_in_new_loop, coro)
|
283
|
+
return future.result(timeout=30)
|
284
|
+
else:
|
285
|
+
return asyncio.run_coroutine_threadsafe(coro, loop).result()
|
286
|
+
|
287
|
+
|
288
|
+
def __run_in_new_loop(coroutine: Coroutine[Any, Any, Any]):
|
289
|
+
new_loop = asyncio.new_event_loop()
|
290
|
+
asyncio.set_event_loop(new_loop)
|
291
|
+
try:
|
292
|
+
return new_loop.run_until_complete(coroutine)
|
293
|
+
finally:
|
294
|
+
new_loop.close()
|
anaplan_sdk/_clients/_bulk.py
CHANGED
@@ -58,6 +58,7 @@ class Client(_BaseClient):
|
|
58
58
|
redirect_uri: str | None = None,
|
59
59
|
refresh_token: str | None = None,
|
60
60
|
oauth2_scope: str = "openid profile email offline_access",
|
61
|
+
on_auth_code: Callable[[str], str] | None = None,
|
61
62
|
on_token_refresh: Callable[[dict[str, str]], None] | None = None,
|
62
63
|
timeout: float | httpx.Timeout = 30,
|
63
64
|
retry_count: int = 2,
|
@@ -94,7 +95,11 @@ class Client(_BaseClient):
|
|
94
95
|
:param refresh_token: If you have a valid refresh token, you can pass it to skip the
|
95
96
|
interactive authentication code step.
|
96
97
|
:param oauth2_scope: The scope of the Oauth2 token, if you want to narrow it.
|
98
|
+
:param on_auth_code: A callback that takes the redirect URI as a single argument and must
|
99
|
+
return the entire response URI. This will substitute the interactive
|
100
|
+
authentication code step in the terminal.
|
97
101
|
:param on_token_refresh: A callback function that is called whenever the token is refreshed.
|
102
|
+
This includes the initial token retrieval and any subsequent calls.
|
98
103
|
With this you can for example securely store the token in your
|
99
104
|
application or on your server for later reuse. The function
|
100
105
|
must accept a single argument, which is the token dictionary
|
@@ -130,6 +135,7 @@ class Client(_BaseClient):
|
|
130
135
|
redirect_uri=redirect_uri,
|
131
136
|
refresh_token=refresh_token,
|
132
137
|
oauth2_scope=oauth2_scope,
|
138
|
+
on_auth_code=on_auth_code,
|
133
139
|
on_token_refresh=on_token_refresh,
|
134
140
|
)
|
135
141
|
),
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: anaplan-sdk
|
3
|
-
Version: 0.4.
|
4
|
-
Summary:
|
3
|
+
Version: 0.4.2
|
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
|
7
7
|
Project-URL: Documentation, https://vinzenzklass.github.io/anaplan-sdk/
|
@@ -13,7 +13,7 @@ Requires-Python: >=3.10.4
|
|
13
13
|
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
|
-
Requires-Dist: cryptography<
|
16
|
+
Requires-Dist: cryptography<46.0.0,>=42.0.7; extra == 'cert'
|
17
17
|
Provides-Extra: oauth
|
18
18
|
Requires-Dist: oauthlib<4.0.0,>=3.0.0; extra == 'oauth'
|
19
19
|
Description-Content-Type: text/markdown
|
@@ -1,18 +1,18 @@
|
|
1
1
|
anaplan_sdk/__init__.py,sha256=5fr-SZSsH6f3vkRUTDoK6xdAN31cCpe9Mwz2VNu47Uw,134
|
2
|
-
anaplan_sdk/_auth.py,sha256=
|
2
|
+
anaplan_sdk/_auth.py,sha256=0htPrOYXDb2CCm4ZkwKQ4Zi26fsK6D0OIBiQdR6ESm8,11817
|
3
3
|
anaplan_sdk/_base.py,sha256=9CdLshORWsLixOyoFa3A0Bka5lhLwlZrQI5sEdBcGFI,12298
|
4
4
|
anaplan_sdk/exceptions.py,sha256=ALkA9fBF0NQ7dufFxV6AivjmHyuJk9DOQ9jtJV2n7f0,1809
|
5
5
|
anaplan_sdk/_async_clients/__init__.py,sha256=pZXgMMg4S9Aj_pxQCaSiPuNG-sePVGBtNJ0133VjqW4,364
|
6
6
|
anaplan_sdk/_async_clients/_alm.py,sha256=O1_r-O1tNDq7vXRwE2UEFE5S2bPmPh4IAQPQ8bmZfQE,3297
|
7
7
|
anaplan_sdk/_async_clients/_audit.py,sha256=a92RY0B3bWxp2CCAWjzqKfvBjG1LJGlai0Hn5qmwgF8,2312
|
8
|
-
anaplan_sdk/_async_clients/_bulk.py,sha256=
|
8
|
+
anaplan_sdk/_async_clients/_bulk.py,sha256=APhgKE4Deh90lm8rcCJMyQTJNMHAXFCKkqnGV_lAtgY,26908
|
9
9
|
anaplan_sdk/_async_clients/_cloud_works.py,sha256=KPX9W55SF6h8fJd4Rx-HLq6eaRA-Vo3rFu343UiiaGQ,16642
|
10
10
|
anaplan_sdk/_async_clients/_cw_flow.py,sha256=ZTNAbKDwb59Wg3u68hbtt1kpd-LNz9K0sftT-gvYzJQ,3651
|
11
11
|
anaplan_sdk/_async_clients/_transactional.py,sha256=Mvr7OyBPjQRpBtzkJNfRzV4aNCzUiaYmm0zQubo62Wo,8035
|
12
12
|
anaplan_sdk/_clients/__init__.py,sha256=FsbwvZC1FHrxuRXwbPxUzbhz_lO1DpXIxEOjx6-3QuA,219
|
13
13
|
anaplan_sdk/_clients/_alm.py,sha256=UAdQxgHfax-VquC0YtbqrRBku2Rn35tVgwJdxYFScps,3202
|
14
14
|
anaplan_sdk/_clients/_audit.py,sha256=xQQiwWIb4QQefolPvxNwBFE-pkRzzi8fYPyewjF63lc,2181
|
15
|
-
anaplan_sdk/_clients/_bulk.py,sha256=
|
15
|
+
anaplan_sdk/_clients/_bulk.py,sha256=4JkuutqCo7yt3Ik2f90ixkfPw1r-7TOq9xg1MUZ5es8,25568
|
16
16
|
anaplan_sdk/_clients/_cloud_works.py,sha256=KAMnLoeMJ2iwMXlDSbKynCE57BtkCfOgM5O8wT1kkSs,16291
|
17
17
|
anaplan_sdk/_clients/_cw_flow.py,sha256=5IFWFT-qbyGvaSOOtaFOjHnOlyYbj4Rj3xiavfTlm8c,3527
|
18
18
|
anaplan_sdk/_clients/_transactional.py,sha256=YUVbA54uhMloQcahwMtmZO3YooO6qQzwZN3ZRSu_z_c,7976
|
@@ -23,7 +23,7 @@ anaplan_sdk/models/_bulk.py,sha256=dHP3kMvsKONCZS6mHB271-wp2S4P3rM874Ita8TzABU,8
|
|
23
23
|
anaplan_sdk/models/_transactional.py,sha256=_0UbVR9D5QABI29yloYrJTSgL-K0EU7PzPeJu5LdhnY,4854
|
24
24
|
anaplan_sdk/models/cloud_works.py,sha256=nfn_LHPR-KmW7Tpvz-5qNCzmR8SYgvsVV-lx5iDlyqI,19425
|
25
25
|
anaplan_sdk/models/flows.py,sha256=SuLgNj5-2SeE3U1i8iY8cq2IkjuUgd_3M1n2ENructk,3625
|
26
|
-
anaplan_sdk-0.4.
|
27
|
-
anaplan_sdk-0.4.
|
28
|
-
anaplan_sdk-0.4.
|
29
|
-
anaplan_sdk-0.4.
|
26
|
+
anaplan_sdk-0.4.2.dist-info/METADATA,sha256=oyd3terF5C2LV55DARCix6LSpCJ6ch7jlub7nKE6wDo,3543
|
27
|
+
anaplan_sdk-0.4.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
28
|
+
anaplan_sdk-0.4.2.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
29
|
+
anaplan_sdk-0.4.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|