anaplan-sdk 0.4.0a3__tar.gz → 0.4.2__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.
Files changed (90) hide show
  1. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/PKG-INFO +3 -3
  2. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_bulk.py +21 -3
  3. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_auth.py +55 -9
  4. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_bulk.py +6 -0
  5. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/authentication.md +42 -1
  6. anaplan_sdk-0.4.2/docs/img/anaplan-sdk.webp +0 -0
  7. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/pyproject.toml +5 -5
  8. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/uv.lock +514 -514
  9. anaplan_sdk-0.4.0a3/docs/img/anaplan-sdk.webp +0 -0
  10. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/.github/dependabot.yml +0 -0
  11. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/.github/workflows/docs.yml +0 -0
  12. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/.github/workflows/lint.yml +0 -0
  13. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/.github/workflows/tests.yml +0 -0
  14. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/.gitignore +0 -0
  15. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/.pre-commit-config.yaml +0 -0
  16. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/LICENSE +0 -0
  17. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/README.md +0 -0
  18. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/__init__.py +0 -0
  19. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/__init__.py +0 -0
  20. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_alm.py +0 -0
  21. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_audit.py +0 -0
  22. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_cloud_works.py +0 -0
  23. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_cw_flow.py +0 -0
  24. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_transactional.py +0 -0
  25. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_base.py +0 -0
  26. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/__init__.py +0 -0
  27. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_alm.py +0 -0
  28. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_audit.py +0 -0
  29. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_cloud_works.py +0 -0
  30. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_cw_flow.py +0 -0
  31. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_transactional.py +0 -0
  32. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/exceptions.py +0 -0
  33. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/models/__init__.py +0 -0
  34. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/models/_alm.py +0 -0
  35. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/models/_base.py +0 -0
  36. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/models/_bulk.py +0 -0
  37. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/models/_transactional.py +0 -0
  38. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/models/cloud_works.py +0 -0
  39. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/anaplan_sdk/models/flows.py +0 -0
  40. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/anaplan_explained.md +0 -0
  41. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/async/async_alm_client.md +0 -0
  42. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/async/async_audit_client.md +0 -0
  43. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/async/async_client.md +0 -0
  44. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/async/async_cw_client.md +0 -0
  45. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/async/async_flows_client.md +0 -0
  46. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/async/async_transactional_client.md +0 -0
  47. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/exceptions.md +0 -0
  48. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/models.md +0 -0
  49. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/sync/sync_alm_client.md +0 -0
  50. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/sync/sync_audit_client.md +0 -0
  51. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/sync/sync_client.md +0 -0
  52. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/sync/sync_cw_client.md +0 -0
  53. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/sync/sync_flows_client.md +0 -0
  54. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/api/sync/sync_transactional_client.md +0 -0
  55. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/assets/overview.html +0 -0
  56. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/css/styles.css +0 -0
  57. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/alm.md +0 -0
  58. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/audit.md +0 -0
  59. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/bulk.md +0 -0
  60. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/bulk_vs_transactional.md +0 -0
  61. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/cloud_works.md +0 -0
  62. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/index.md +0 -0
  63. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/logging.md +0 -0
  64. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/multiple_models.md +0 -0
  65. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/guides/transactional.md +0 -0
  66. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/index.md +0 -0
  67. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/installation.md +0 -0
  68. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/js/assets/hljs.js +0 -0
  69. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/js/assets/hljs.min.js +0 -0
  70. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/js/assets/python.js +0 -0
  71. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/js/assets/python.min.js +0 -0
  72. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/js/highlight.js +0 -0
  73. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/js/highlight.min.js +0 -0
  74. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/docs/quickstart.md +0 -0
  75. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/mkdocs.yml +0 -0
  76. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/async/conftest.py +0 -0
  77. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/async/test_async_alm_client.py +0 -0
  78. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/async/test_async_audit_client.py +0 -0
  79. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/async/test_async_client.py +0 -0
  80. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/async/test_async_cloud_works_client.py +0 -0
  81. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/async/test_async_flows_client.py +0 -0
  82. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/async/test_async_transactional_client.py +0 -0
  83. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/conftest.py +0 -0
  84. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/sync/conftest.py +0 -0
  85. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/sync/test_alm_client.py +0 -0
  86. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/sync/test_audit_client.py +0 -0
  87. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/sync/test_client.py +0 -0
  88. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/sync/test_cloud_works_client.py +0 -0
  89. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/sync/test_flows_client.py +0 -0
  90. {anaplan_sdk-0.4.0a3 → anaplan_sdk-0.4.2}/tests/sync/test_transactional_client.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anaplan-sdk
3
- Version: 0.4.0a3
4
- Summary: Provides pythonic access to the Anaplan API
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<45.0.0,>=42.0.7; extra == 'cert'
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,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, Callable, Iterator
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
- on_token_refresh: Callable[[dict[str, str]], None] | None = None,
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
  ),
@@ -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 typing import Callable
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
- on_token_refresh: Callable[[dict[str, str]], None] | None = None,
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(token)
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/authorize",
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 = input(
203
- f"Please go to {url} and authorize the app.\n"
204
- "Then paste the entire redirect URL here: "
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 AnaplanException("Error during OAuth2 authorization flow.") from error
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
- on_token_refresh: Callable[[dict[str, str]], None] | None = None,
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()
@@ -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
  ),
@@ -142,6 +142,47 @@ will need to copy the entire redirect URI from your browser and paste it into th
142
142
  Unfortunately, registering localhost redirect URIs is not supported by Anaplan. This means we cannot intercept the
143
143
  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).
144
144
 
145
+ ### Authorization Code
146
+
147
+ When using OAuth authentication, the default behavior prompts you to manually open a URL, authorize the application, and paste the redirect URL back into your terminal. However, you can customize this flow by providing the `on_auth_code` callback.
148
+
149
+ The `on_auth_code` callback lets you hook into the Auth Flow to handle the authorization URL programmatically and return the authorization response. `on_auth_code` must be a callable that takes the authorization URL as a single argument of type `str` and returns the redirect URL as a `str`.
150
+
151
+
152
+ ???+ warning "Asynchronous Callbacks"
153
+ Both `on_auth_code` and `on_token_refresh` can be either synchronous or asynchronous. When using asynchronous
154
+ callbacks in complex applications with multiple event loops, be aware that callbacks may execute in a separate
155
+ event loop context from where they were defined, which can make debugging challenging.
156
+
157
+ === "Synchronous"
158
+ ```python
159
+ def on_auth_code(redirect_uri: str) -> str:
160
+ return input(f"Go fetch! {redirect_uri}\nPaste here: ")
161
+
162
+ anaplan = anaplan_sdk.Client(
163
+ workspace_id="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
164
+ model_id="11111111111111111111111111111111",
165
+ redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
166
+ client_id="my_anaplan_oauth_client_id",
167
+ client_secret="my_anaplan_oauth_client_secret",
168
+ on_auth_code=on_auth_code,
169
+ )
170
+ ```
171
+ === "Asynchronous"
172
+ ```python
173
+ async def on_auth_code(redirect_uri: str) -> str: # Can be sync or async
174
+ return input(f"Go fetch! {redirect_uri}\nPaste here: ")
175
+
176
+ anaplan = anaplan_sdk.AsyncClient(
177
+ workspace_id="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
178
+ model_id="11111111111111111111111111111111",
179
+ redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
180
+ client_id="my_anaplan_oauth_client_id",
181
+ client_secret="my_anaplan_oauth_client_secret",
182
+ on_auth_code=on_auth_code,
183
+ )
184
+ ```
185
+
145
186
  ### Refresh Tokens
146
187
 
147
188
  You can extend the above example to also pass a `refresh_token` to authenticate without any user interaction, and
@@ -186,7 +227,7 @@ a single argument of type `dict[str, str]` and returns `None`.
186
227
  kp = PyKeePass("db.kdbx", password="keepass")
187
228
  group = kp.add_group(kp.root_group, "Anaplan")
188
229
 
189
- def on_token_refresh(token: dict[str, str]) -> None:
230
+ def on_token_refresh(token: dict[str, str]) -> None: # Can also be async
190
231
  kp.add_entry(
191
232
  group, title="Anaplan Token", username=None, password=json.dumps(token)
192
233
  )
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "anaplan-sdk"
3
- version = "0.4.0a3"
4
- description = "Provides pythonic access to the Anaplan API"
3
+ version = "0.4.2"
4
+ description = "Streamlined Python Interface for Anaplan"
5
5
  license = "Apache-2.0"
6
6
  authors = [{ name = "Vinzenz Klass", email = "vinzenz.klass@valantic.com" }]
7
7
  readme = "README.md"
@@ -24,7 +24,7 @@ dependencies = [
24
24
  ]
25
25
 
26
26
  [project.optional-dependencies]
27
- cert = ["cryptography>=42.0.7,<45.0.0"]
27
+ cert = ["cryptography>=42.0.7,<46.0.0"]
28
28
  oauth = ["oauthlib>=3.0.0,<4.0.0"]
29
29
 
30
30
  [dependency-groups]
@@ -40,7 +40,7 @@ dev = [
40
40
  "polars>=1.29.0",
41
41
  "griffe-fieldz>=0.2.1",
42
42
  "oauthlib>=3.2.2",
43
- "cryptography>=42.0.7",
43
+ "cryptography>=45.0.0",
44
44
  ]
45
45
 
46
46
  [project.urls]
@@ -84,7 +84,7 @@ exclude = [
84
84
 
85
85
  line-length = 100
86
86
  fix = true
87
- target-version = "py312"
87
+ target-version = "py313"
88
88
 
89
89
  [tool.ruff.format]
90
90
  skip-magic-trailing-comma = true