anaplan-sdk 0.4.1__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 (89) hide show
  1. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/PKG-INFO +1 -1
  2. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_bulk.py +18 -5
  3. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_auth.py +45 -7
  4. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_bulk.py +1 -0
  5. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/authentication.md +8 -2
  6. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/pyproject.toml +2 -2
  7. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/uv.lock +511 -511
  8. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/.github/dependabot.yml +0 -0
  9. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/.github/workflows/docs.yml +0 -0
  10. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/.github/workflows/lint.yml +0 -0
  11. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/.github/workflows/tests.yml +0 -0
  12. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/.gitignore +0 -0
  13. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/.pre-commit-config.yaml +0 -0
  14. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/LICENSE +0 -0
  15. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/README.md +0 -0
  16. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/__init__.py +0 -0
  17. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/__init__.py +0 -0
  18. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_alm.py +0 -0
  19. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_audit.py +0 -0
  20. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_cloud_works.py +0 -0
  21. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_cw_flow.py +0 -0
  22. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_async_clients/_transactional.py +0 -0
  23. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_base.py +0 -0
  24. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/__init__.py +0 -0
  25. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_alm.py +0 -0
  26. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_audit.py +0 -0
  27. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_cloud_works.py +0 -0
  28. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_cw_flow.py +0 -0
  29. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/_clients/_transactional.py +0 -0
  30. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/exceptions.py +0 -0
  31. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/models/__init__.py +0 -0
  32. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/models/_alm.py +0 -0
  33. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/models/_base.py +0 -0
  34. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/models/_bulk.py +0 -0
  35. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/models/_transactional.py +0 -0
  36. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/models/cloud_works.py +0 -0
  37. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/anaplan_sdk/models/flows.py +0 -0
  38. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/anaplan_explained.md +0 -0
  39. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/async/async_alm_client.md +0 -0
  40. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/async/async_audit_client.md +0 -0
  41. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/async/async_client.md +0 -0
  42. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/async/async_cw_client.md +0 -0
  43. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/async/async_flows_client.md +0 -0
  44. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/async/async_transactional_client.md +0 -0
  45. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/exceptions.md +0 -0
  46. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/models.md +0 -0
  47. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/sync/sync_alm_client.md +0 -0
  48. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/sync/sync_audit_client.md +0 -0
  49. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/sync/sync_client.md +0 -0
  50. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/sync/sync_cw_client.md +0 -0
  51. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/sync/sync_flows_client.md +0 -0
  52. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/api/sync/sync_transactional_client.md +0 -0
  53. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/assets/overview.html +0 -0
  54. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/css/styles.css +0 -0
  55. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/alm.md +0 -0
  56. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/audit.md +0 -0
  57. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/bulk.md +0 -0
  58. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/bulk_vs_transactional.md +0 -0
  59. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/cloud_works.md +0 -0
  60. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/index.md +0 -0
  61. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/logging.md +0 -0
  62. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/multiple_models.md +0 -0
  63. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/guides/transactional.md +0 -0
  64. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/img/anaplan-sdk.webp +0 -0
  65. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/index.md +0 -0
  66. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/installation.md +0 -0
  67. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/js/assets/hljs.js +0 -0
  68. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/js/assets/hljs.min.js +0 -0
  69. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/js/assets/python.js +0 -0
  70. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/js/assets/python.min.js +0 -0
  71. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/js/highlight.js +0 -0
  72. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/js/highlight.min.js +0 -0
  73. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/docs/quickstart.md +0 -0
  74. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/mkdocs.yml +0 -0
  75. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/async/conftest.py +0 -0
  76. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/async/test_async_alm_client.py +0 -0
  77. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/async/test_async_audit_client.py +0 -0
  78. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/async/test_async_client.py +0 -0
  79. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/async/test_async_cloud_works_client.py +0 -0
  80. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/async/test_async_flows_client.py +0 -0
  81. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/async/test_async_transactional_client.py +0 -0
  82. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/conftest.py +0 -0
  83. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/sync/conftest.py +0 -0
  84. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/sync/test_alm_client.py +0 -0
  85. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/sync/test_audit_client.py +0 -0
  86. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/sync/test_client.py +0 -0
  87. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/sync/test_cloud_works_client.py +0 -0
  88. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/tests/sync/test_flows_client.py +0 -0
  89. {anaplan_sdk-0.4.1 → anaplan_sdk-0.4.2}/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.1
3
+ Version: 0.4.2
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
@@ -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,8 +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_auth_code: Callable[[str], str] | None = None,
60
- on_token_refresh: Callable[[dict[str, str]], None] | None = None,
59
+ on_auth_code: AuthCodeCallback = None,
60
+ on_token_refresh: AuthTokenRefreshCallback = None,
61
61
  timeout: float | httpx.Timeout = 30,
62
62
  retry_count: int = 2,
63
63
  status_poll_delay: int = 1,
@@ -94,12 +94,25 @@ class AsyncClient(_AsyncBaseClient):
94
94
  :param oauth2_scope: The scope of the Oauth2 token, if you want to narrow it.
95
95
  :param on_auth_code: A callback that takes the redirect URI as a single argument and must
96
96
  return the entire response URI. This will substitute the interactive
97
- authentication code step in the terminal.
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.
98
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.
99
107
  With this you can for example securely store the token in your
100
108
  application or on your server for later reuse. The function
101
109
  must accept a single argument, which is the token dictionary
102
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.
103
116
  :param timeout: The timeout in seconds for the HTTP requests. Alternatively, you can pass
104
117
  an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
105
118
  :param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
@@ -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,8 +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_auth_code: Callable[[str], str] | None = None,
148
- on_token_refresh: Callable[[dict[str, str]], None] | None = None,
156
+ on_auth_code: AuthCodeCallback = None,
157
+ on_token_refresh: AuthTokenRefreshCallback = None,
149
158
  ):
150
159
  try:
151
160
  from oauthlib.oauth2 import WebApplicationClient
@@ -188,7 +197,7 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
188
197
  self._token = token["access_token"]
189
198
  self._refresh_token = token["refresh_token"]
190
199
  if self._on_token_refresh:
191
- self._on_token_refresh(token)
200
+ _run_callback(self._on_token_refresh, token)
192
201
  self._id_token = token.get("id_token")
193
202
 
194
203
  def __auth_code_flow(self):
@@ -202,7 +211,7 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
202
211
  scope=self._scope,
203
212
  )
204
213
  authorization_response = (
205
- self._on_auth_code(url)
214
+ _run_callback(self._on_auth_code, url)
206
215
  if self._on_auth_code
207
216
  else input(
208
217
  f"Please go to {url} and authorize the app.\n"
@@ -231,8 +240,8 @@ def create_auth(
231
240
  redirect_uri: str | None = None,
232
241
  refresh_token: str | None = None,
233
242
  oauth2_scope: str = "openid profile email offline_access",
234
- on_auth_code: Callable[[str], str] | None = None,
235
- on_token_refresh: Callable[[dict[str, str]], None] | None = None,
243
+ on_auth_code: AuthCodeCallback = None,
244
+ on_token_refresh: AuthTokenRefreshCallback = None,
236
245
  ) -> _AnaplanAuth:
237
246
  if certificate and private_key:
238
247
  return AnaplanCertAuth(certificate, private_key, private_key_password)
@@ -254,3 +263,32 @@ def create_auth(
254
263
  "- certificate and private_key, or\n"
255
264
  "- client_id, client_secret, and redirect_uri"
256
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()
@@ -99,6 +99,7 @@ class Client(_BaseClient):
99
99
  return the entire response URI. This will substitute the interactive
100
100
  authentication code step in the terminal.
101
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.
102
103
  With this you can for example securely store the token in your
103
104
  application or on your server for later reuse. The function
104
105
  must accept a single argument, which is the token dictionary
@@ -148,6 +148,12 @@ When using OAuth authentication, the default behavior prompts you to manually op
148
148
 
149
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
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
+
151
157
  === "Synchronous"
152
158
  ```python
153
159
  def on_auth_code(redirect_uri: str) -> str:
@@ -164,7 +170,7 @@ The `on_auth_code` callback lets you hook into the Auth Flow to handle the autho
164
170
  ```
165
171
  === "Asynchronous"
166
172
  ```python
167
- def on_auth_code(redirect_uri: str) -> str:
173
+ async def on_auth_code(redirect_uri: str) -> str: # Can be sync or async
168
174
  return input(f"Go fetch! {redirect_uri}\nPaste here: ")
169
175
 
170
176
  anaplan = anaplan_sdk.AsyncClient(
@@ -221,7 +227,7 @@ a single argument of type `dict[str, str]` and returns `None`.
221
227
  kp = PyKeePass("db.kdbx", password="keepass")
222
228
  group = kp.add_group(kp.root_group, "Anaplan")
223
229
 
224
- def on_token_refresh(token: dict[str, str]) -> None:
230
+ def on_token_refresh(token: dict[str, str]) -> None: # Can also be async
225
231
  kp.add_entry(
226
232
  group, title="Anaplan Token", username=None, password=json.dumps(token)
227
233
  )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anaplan-sdk"
3
- version = "0.4.1"
3
+ version = "0.4.2"
4
4
  description = "Streamlined Python Interface for Anaplan"
5
5
  license = "Apache-2.0"
6
6
  authors = [{ name = "Vinzenz Klass", email = "vinzenz.klass@valantic.com" }]
@@ -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