anaplan-sdk 0.4.4a3__py3-none-any.whl → 0.4.5__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 CHANGED
@@ -1,12 +1,12 @@
1
1
  from ._async_clients import AsyncClient
2
- from ._auth import AnaplanOAuthCodeAuth, AnaplanRefreshTokenAuth
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
- "AnaplanOAuthCodeAuth",
9
+ "AnaplanLocalOAuth",
10
10
  "AnaplanRefreshTokenAuth",
11
11
  "AsyncOauth",
12
12
  "Oauth",
@@ -451,6 +451,17 @@ class AsyncClient(_AsyncBaseClient):
451
451
  ).get("task")
452
452
  )
453
453
 
454
+ async def get_optimizer_log(self, action_id: int, task_id: str) -> bytes:
455
+ """
456
+ Retrieves the solution logs of the specified optimization action task.
457
+ :param action_id: The identifier of the optimization action that was invoked.
458
+ :param task_id: The Task identifier, sometimes also referred to as the Correlation Id.
459
+ :return: The content of the solution logs.
460
+ """
461
+ return await self._get_binary(
462
+ f"{self._url}/optimizeActions/{action_id}/tasks/{task_id}/solutionLogs"
463
+ )
464
+
454
465
  async def invoke_action(self, action_id: int) -> str:
455
466
  """
456
467
  You may want to consider using `run_action()` instead.
anaplan_sdk/_auth.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import os
3
3
  from base64 import b64encode
4
- from typing import Awaitable, Callable
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 AnaplanOAuthCodeAuth(_AnaplanAuth):
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,7 +164,7 @@ class AnaplanOAuthCodeAuth(_AnaplanAuth):
168
164
  state_generator: Callable[[], str] | None = None,
169
165
  ):
170
166
  """
171
- Initializes the AnaplanOAuthCodeAuth class for OAuth2 authentication using 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.
@@ -177,6 +173,15 @@ class AnaplanOAuthCodeAuth(_AnaplanAuth):
177
173
  :param client_secret: The client secret of your Anaplan Oauth 2.0 application.
178
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,11 +190,27 @@ 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
- pass this if you need to customize the state generation logic. If not provided,
189
- the state will be generated by `oauthlib`.
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,
@@ -200,10 +221,20 @@ class AnaplanOAuthCodeAuth(_AnaplanAuth):
200
221
  validation_url=validation_url,
201
222
  state_generator=state_generator,
202
223
  )
203
- if not token:
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):
@@ -243,8 +280,20 @@ class AnaplanRefreshTokenAuth(_AnaplanAuth):
243
280
  ):
244
281
  """
245
282
  This class is a utility class for long-lived `Client` or `AsyncClient` instances that use
246
- OAuth. It expects that you have a valid OAuth token with a refresh token, which will be used
247
- to refresh the access token when it expires.
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.
248
297
 
249
298
  :param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
250
299
  must be an Authorization Code Grant application.
@@ -256,6 +305,12 @@ class AnaplanRefreshTokenAuth(_AnaplanAuth):
256
305
  :param token_url: The URL to post the refresh token request to in order to fetch the access
257
306
  token.
258
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
+ )
259
314
  self._oauth_token = token
260
315
  self._oauth = _OAuthRequestFactory(
261
316
  client_id=client_id,
@@ -265,6 +320,16 @@ class AnaplanRefreshTokenAuth(_AnaplanAuth):
265
320
  )
266
321
  super().__init__(self._oauth_token["access_token"])
267
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
+
268
333
  def _build_auth_request(self) -> httpx.Request:
269
334
  return self._oauth.refresh_token_request(self._oauth_token["refresh_token"])
270
335
 
@@ -438,6 +438,17 @@ class Client(_BaseClient):
438
438
  )
439
439
  )
440
440
 
441
+ def get_optimizer_log(self, action_id: int, task_id: str) -> bytes:
442
+ """
443
+ Retrieves the solution logs of the specified optimization action task.
444
+ :param action_id: The identifier of the optimization action that was invoked.
445
+ :param task_id: The Task identifier, sometimes also referred to as the Correlation Id.
446
+ :return: The content of the solution logs.
447
+ """
448
+ return self._get_binary(
449
+ f"{self._url}/optimizeActions/{action_id}/tasks/{task_id}/solutionLogs"
450
+ )
451
+
441
452
  def invoke_action(self, action_id: int) -> str:
442
453
  """
443
454
  You may want to consider using `run_action()` instead.
@@ -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.4a3
3
+ Version: 0.4.5
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.md) for basic usage instructions and examples.
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=lDFhs0IobOH7a34jqtAgcj9X1bR5hnFRkkkCl_vPHpo,357
2
- anaplan_sdk/_auth.py,sha256=wB6znPwnUQ3pG7dRqpZZMdxb0V-XsZELASY881t4ocE,12834
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
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=j0yMoM8NWQH9BsSQ4LRYt8djfd1d11vkjNfU8pUeGLU,23737
9
+ anaplan_sdk/_async_clients/_bulk.py,sha256=Bpq5HT8tT48YznN7gfycGb4LNR5WW5QBVXgo_kZsEj0,24295
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=nlsZHK8vjhvyC0auRuqyvJVvTISPqj9EIHBYLoqSpOc,23354
16
+ anaplan_sdk/_clients/_bulk.py,sha256=_-kb50yL-I-AjLtvhdvLpNyMVb4luRlfBEtxT_q3jO0,23900
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=dHP3kMvsKONCZS6mHB271-wp2S4P3rM874Ita8TzABU,8256
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.4a3.dist-info/METADATA,sha256=GTtQ-OiFOT9r5UWggC_25nGzrCDZMilYZeSOxUiwNBI,3545
28
- anaplan_sdk-0.4.4a3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- anaplan_sdk-0.4.4a3.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
30
- anaplan_sdk-0.4.4a3.dist-info/RECORD,,
27
+ anaplan_sdk-0.4.5.dist-info/METADATA,sha256=drUkXHMRjawxKTPMzL2bPTxCWx2XCwQCD9hrm1LNriw,3667
28
+ anaplan_sdk-0.4.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
+ anaplan_sdk-0.4.5.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
30
+ anaplan_sdk-0.4.5.dist-info/RECORD,,