anaplan-sdk 0.4.3a1__py3-none-any.whl → 0.4.4__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,5 +1,15 @@
1
1
  from ._async_clients import AsyncClient
2
+ from ._auth import AnaplanLocalOAuth, AnaplanRefreshTokenAuth
2
3
  from ._clients import Client
3
4
  from ._oauth import AsyncOauth, Oauth
4
5
 
5
- __all__ = ["AsyncClient", "AsyncOauth", "Client", "Oauth", "models", "exceptions"]
6
+ __all__ = [
7
+ "AsyncClient",
8
+ "Client",
9
+ "AnaplanLocalOAuth",
10
+ "AnaplanRefreshTokenAuth",
11
+ "AsyncOauth",
12
+ "Oauth",
13
+ "models",
14
+ "exceptions",
15
+ ]
@@ -6,7 +6,7 @@ from typing import AsyncIterator, Iterator
6
6
  import httpx
7
7
  from typing_extensions import Self
8
8
 
9
- from anaplan_sdk._auth import AuthCodeCallback, AuthTokenRefreshCallback, _create_auth
9
+ from anaplan_sdk._auth import _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 (
@@ -32,14 +32,8 @@ logger = logging.getLogger("anaplan_sdk")
32
32
 
33
33
  class AsyncClient(_AsyncBaseClient):
34
34
  """
35
- An asynchronous Client for pythonic access to the
36
- [Anaplan Integration API v2](https://anaplan.docs.apiary.io/). This Client provides high-level
37
- abstractions over the API, so you can deal with python objects and simple functions rather than
38
- implementation details like http, json, compression, chunking etc.
39
-
40
-
41
- For more information, quick start guides and detailed instructions refer to:
42
- [Anaplan SDK](https://vinzenzklass.github.io/anaplan-sdk).
35
+ Asynchronous Anaplan Client. For guides and examples
36
+ refer to https://vinzenzklass.github.io/anaplan-sdk.
43
37
  """
44
38
 
45
39
  def __init__(
@@ -51,15 +45,8 @@ class AsyncClient(_AsyncBaseClient):
51
45
  certificate: str | bytes | None = None,
52
46
  private_key: str | bytes | None = None,
53
47
  private_key_password: str | bytes | None = None,
54
- client_id: str | None = None,
55
- client_secret: str | None = None,
56
- redirect_uri: str | None = None,
57
- refresh_token: str | None = None,
58
- oauth2_scope: str = "openid profile email offline_access",
59
- on_auth_code: AuthCodeCallback = None,
60
- on_token_refresh: AuthTokenRefreshCallback = None,
61
- oauth_token: dict[str, str] | None = None,
62
48
  token: str | None = None,
49
+ auth: httpx.Auth | None = None,
63
50
  timeout: float | httpx.Timeout = 30,
64
51
  retry_count: int = 2,
65
52
  status_poll_delay: int = 1,
@@ -67,86 +54,55 @@ class AsyncClient(_AsyncBaseClient):
67
54
  allow_file_creation: bool = False,
68
55
  ) -> None:
69
56
  """
70
- An asynchronous Client for pythonic access to the Anaplan Integration API v2:
71
- https://anaplan.docs.apiary.io/. This Client provides high-level abstractions over the API,
72
- so you can deal with python objects and simple functions rather than implementation details
73
- like http, json, compression, chunking etc.
74
-
75
-
76
- For more information, quick start guides and detailed instructions refer to:
77
- https://vinzenzklass.github.io/anaplan-sdk.
57
+ Asynchronous Anaplan Client. For guides and examples
58
+ refer to https://vinzenzklass.github.io/anaplan-sdk.
78
59
 
79
60
  :param workspace_id: The Anaplan workspace Id. You can copy this from the browser URL or
80
- find them using an HTTP Client like Postman, Paw, Insomnia etc.
61
+ find them using an HTTP Client like Postman, Paw, Insomnia etc.
81
62
  :param model_id: The identifier of the model.
82
63
  :param user_email: A valid email registered with the Anaplan Workspace you are attempting
83
- to access. **The associated user must have Workspace Admin privileges**
84
- :param password: Password for the given `user_email`. This is not suitable for production
85
- setups. If you intend to use this in production, acquire a client
86
- certificate as described under: https://help.anaplan.com/procure-ca-certificates-47842267-2cb3-4e38-90bf-13b1632bcd44
87
- :param certificate: The absolute path to the client certificate file or the certificate
88
- itself.
89
- :param private_key: The absolute path to the private key file or the private key itself.
90
- :param private_key_password: The password to access the private key if there is one.
91
- :param client_id: The client Id of the Oauth2 Anaplan Client.
92
- :param client_secret: The client secret for your Oauth2 Anaplan Client.
93
- :param redirect_uri: The redirect URI for your Oauth2 Anaplan Client.
94
- :param refresh_token: If you have a valid refresh token, you can pass it to skip the
95
- interactive authentication code step.
96
- :param oauth2_scope: The scope of the Oauth2 token, if you want to narrow it.
97
- :param on_auth_code: A callback that takes the redirect URI as a single argument and must
98
- return the entire response URI. This will substitute the interactive
99
- authentication code step in the terminal. The callback can be either
100
- a synchronous function or an async coroutine function - both will be
101
- handled appropriately regardless of the execution context (in a thread,
102
- with or without an event loop, etc.).
103
- **Note**: When using asynchronous callbacks in complex applications
104
- with multiple event loops, be aware that callbacks may execute in a
105
- separate event loop context from where they were defined, which can
106
- make debugging challenging.
107
- :param on_token_refresh: A callback function that is called whenever the token is refreshed.
108
- This includes the initial token retrieval and any subsequent calls.
109
- With this you can for example securely store the token in your
110
- application or on your server for later reuse. The function
111
- must accept a single argument, which is the token dictionary
112
- returned by the Oauth2 token endpoint and does not return anything.
113
- This can be either a synchronous function or an async coroutine
114
- function. **Note**: When using asynchronous callbacks in complex
115
- applications with multiple event loops, be aware that callbacks
116
- may execute in a separate event loop context from where they were
117
- defined, which can make debugging challenging.
64
+ to access.
65
+ :param password: Password for the given `user_email` for basic Authentication.
66
+ :param certificate: The certificate content or the absolute path to the certificate file.
67
+ :param private_key: The private key content or the absolute path to the private key file.
68
+ :param private_key_password: The password to access the private key file. This is only
69
+ considered if you provided a private key file and it password-protected.
70
+ :param token: An Anaplan API Token. This will be used to authenticate the client. If
71
+ sufficient other authentication parameters are provided, the token will be used
72
+ until it expires, after which a new one will be created. If you provide only this
73
+ parameter, the client will raise an error upon first authentication failure. For
74
+ short-lived instances, such as in web applications where user specific clients are
75
+ created, this is the recommended way to authenticate, since this has the least
76
+ overhead.
77
+ :param auth: You can provide a subclass of `httpx.Auth` to use for authentication. You can
78
+ provide an instance of one of the classes provided by the SDK, or an instance of
79
+ your own subclass of `httpx.Auth`. This will give you full control over the
80
+ authentication process, but you will need to implement the entire authentication
81
+ logic yourself.
118
82
  :param timeout: The timeout in seconds for the HTTP requests. Alternatively, you can pass
119
- an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
83
+ an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
120
84
  :param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
121
- to never retry. Defaults to 2, meaning each HTTP Operation will be
122
- tried a total number of 2 times.
85
+ to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
86
+ number of 2 times.
123
87
  :param status_poll_delay: The delay between polling the status of a task.
124
88
  :param upload_chunk_size: The size of the chunks to upload. This is the maximum size of
125
- each chunk. Defaults to 25MB.
89
+ each chunk. Defaults to 25MB.
126
90
  :param allow_file_creation: Whether to allow the creation of new files. Defaults to False
127
- since this is typically unintentional and may well be unwanted
128
- behaviour in the API altogether. A file that is created this
129
- way will not be referenced by any action in anaplan until
130
- manually assigned so there is typically no value in dynamically
131
- creating new files and uploading content to them.
91
+ since this is typically unintentional and may well be unwanted behaviour in the API
92
+ altogether. A file that is created this way will not be referenced by any action in
93
+ anaplan until manually assigned so there is typically no value in dynamically
94
+ creating new files and uploading content to them.
132
95
  """
133
96
  _client = httpx.AsyncClient(
134
97
  auth=(
135
- _create_auth(
98
+ auth
99
+ or _create_auth(
136
100
  token=token,
137
- oauth_token=oauth_token,
138
101
  user_email=user_email,
139
102
  password=password,
140
103
  certificate=certificate,
141
104
  private_key=private_key,
142
105
  private_key_password=private_key_password,
143
- client_id=client_id,
144
- client_secret=client_secret,
145
- redirect_uri=redirect_uri,
146
- refresh_token=refresh_token,
147
- oauth2_scope=oauth2_scope,
148
- on_auth_code=on_auth_code,
149
- on_token_refresh=on_token_refresh,
150
106
  )
151
107
  ),
152
108
  timeout=timeout,
anaplan_sdk/_auth.py CHANGED
@@ -1,33 +1,25 @@
1
- import asyncio
2
- import inspect
3
1
  import logging
4
2
  import os
5
- import threading
6
3
  from base64 import b64encode
7
- from concurrent.futures import ThreadPoolExecutor
8
- from typing import Any, Awaitable, Callable, Coroutine
4
+ from typing import Callable
9
5
 
10
6
  import httpx
11
7
 
8
+ from ._oauth import _OAuthRequestFactory
12
9
  from .exceptions import AnaplanException, InvalidCredentialsException, InvalidPrivateKeyException
13
10
 
14
11
  logger = logging.getLogger("anaplan_sdk")
15
12
 
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
-
21
13
 
22
14
  class _AnaplanAuth(httpx.Auth):
23
15
  requires_response_body = True
24
16
 
25
- def __init__(self, pre_authed: bool = False):
26
- if not pre_authed:
17
+ def __init__(self, token: str | None = None):
18
+ self._token: str = token or ""
19
+ if not token:
27
20
  logger.info("Creating Authentication Token.")
28
21
  with httpx.Client(timeout=15.0) as client:
29
- res = client.send(self._build_auth_request())
30
- self._parse_auth_response(res)
22
+ self._parse_auth_response(client.send(self._build_auth_request()))
31
23
 
32
24
  def _build_auth_request(self) -> httpx.Request:
33
25
  raise NotImplementedError("Must be implemented in subclass.")
@@ -47,26 +39,25 @@ class _AnaplanAuth(httpx.Auth):
47
39
  raise InvalidCredentialsException
48
40
  if not response.is_success:
49
41
  raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
50
- self._token: str = response.json()["tokenInfo"]["tokenValue"]
42
+ self._token = response.json()["tokenInfo"]["tokenValue"]
51
43
 
52
44
 
53
- class StaticTokenAuth(httpx.Auth):
45
+ class _StaticTokenAuth(httpx.Auth):
54
46
  def __init__(self, token: str):
55
- logger.warning("Using static token authentication. Tokens will not be refreshed.")
56
47
  self._token = token
57
48
 
58
49
  def auth_flow(self, request):
59
50
  request.headers["Authorization"] = f"AnaplanAuthToken {self._token}"
60
51
  response = yield request
61
52
  if response.status_code == 401:
62
- raise InvalidCredentialsException("Your token is invalid or expired.")
53
+ raise InvalidCredentialsException("Token is invalid or expired.")
63
54
 
64
55
 
65
- class AnaplanBasicAuth(_AnaplanAuth):
66
- def __init__(self, user_email: str, password: str):
56
+ class _AnaplanBasicAuth(_AnaplanAuth):
57
+ def __init__(self, user_email: str, password: str, token: str | None = None):
67
58
  self.user_email = user_email
68
59
  self.password = password
69
- super().__init__()
60
+ super().__init__(token)
70
61
 
71
62
  def _build_auth_request(self) -> httpx.Request:
72
63
  cred = b64encode(f"{self.user_email}:{self.password}".encode()).decode()
@@ -77,7 +68,7 @@ class AnaplanBasicAuth(_AnaplanAuth):
77
68
  )
78
69
 
79
70
 
80
- class AnaplanCertAuth(_AnaplanAuth):
71
+ class _AnaplanCertAuth(_AnaplanAuth):
81
72
  requires_request_body = True
82
73
 
83
74
  def __init__(
@@ -85,10 +76,11 @@ class AnaplanCertAuth(_AnaplanAuth):
85
76
  certificate: str | bytes,
86
77
  private_key: str | bytes,
87
78
  private_key_password: str | bytes | None = None,
79
+ token: str | None = None,
88
80
  ):
89
81
  self.__set_certificate(certificate)
90
82
  self.__set_private_key(private_key, private_key_password)
91
- super().__init__()
83
+ super().__init__(token)
92
84
 
93
85
  def _build_auth_request(self) -> httpx.Request:
94
86
  encoded_cert, encoded_string, encoded_signed_string = self._prep_credentials()
@@ -157,168 +149,215 @@ class AnaplanCertAuth(_AnaplanAuth):
157
149
  raise InvalidPrivateKeyException from error
158
150
 
159
151
 
160
- class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
152
+ class AnaplanLocalOAuth(_AnaplanAuth):
161
153
  def __init__(
162
154
  self,
163
155
  client_id: str,
164
156
  client_secret: str,
165
157
  redirect_uri: str,
166
- refresh_token: str | None = None,
158
+ token: dict[str, str] | None = None,
159
+ persist_token: bool = False,
160
+ authorization_url: str = "https://us1a.app.anaplan.com/auth/prelogin",
161
+ token_url: str = "https://us1a.app.anaplan.com/oauth/token",
162
+ validation_url: str = "https://auth.anaplan.com/token/validate",
167
163
  scope: str = "openid profile email offline_access",
168
- on_auth_code: AuthCodeCallback = None,
169
- on_token_refresh: AuthTokenRefreshCallback = None,
164
+ state_generator: Callable[[], str] | None = None,
170
165
  ):
171
- try:
172
- from oauthlib.oauth2 import WebApplicationClient
173
- except ImportError as e:
174
- raise AnaplanException(
175
- "oauthlib is not available. Please install anaplan-sdk with the oauth extra "
176
- "`pip install anaplan-sdk[oauth]` or install oauthlib separately."
177
- ) from e
178
- self._oauth = WebApplicationClient(
179
- client_id=client_id, client_secret=client_secret, refresh_token=refresh_token
166
+ """
167
+ Initializes the AnaplanLocalOAuth class for OAuth2 authentication using the
168
+ Authorization Code Flow. This is a utility class for local development and requires user
169
+ interaction. For Web Applications and other scenarios, refer to `Oauth` or `AsyncOauth`.
170
+ This class will refresh the access token automatically when it expires.
171
+ :param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
172
+ must be an Authorization Code Grant application.
173
+ :param client_secret: The client secret of your Anaplan Oauth 2.0 application.
174
+ :param redirect_uri: The URL to which the user will be redirected after authorizing the
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.
185
+ :param authorization_url: The URL to which the user will be redirected to authorize the
186
+ application. Defaults to the Anaplan Prelogin Page, where the user can select the
187
+ login method.
188
+ :param token_url: The URL to post the authorization code to in order to fetch the access
189
+ token.
190
+ :param validation_url: The URL to validate the access token.
191
+ :param scope: The scope of the access request.
192
+ :param state_generator: A callable that generates a random state string. You can optionally
193
+ pass this if you need to customize the state generation logic. If not provided,
194
+ the state will be generated by `oauthlib`.
195
+ """
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
214
+ self._oauth = _OAuthRequestFactory(
215
+ client_id=client_id,
216
+ client_secret=client_secret,
217
+ redirect_uri=redirect_uri,
218
+ scope=scope,
219
+ authorization_url=authorization_url,
220
+ token_url=token_url,
221
+ validation_url=validation_url,
222
+ state_generator=state_generator,
180
223
  )
181
- self._token_url = "https://us1a.app.anaplan.com/oauth/token"
182
- self._client_id = client_id
183
- self._client_secret = client_secret
184
- self._redirect_uri = redirect_uri
185
- self._refresh_token = refresh_token
186
- self._scope = scope
187
- self._id_token = None
188
- self._on_auth_code = on_auth_code
189
- self._on_token_refresh = on_token_refresh
190
- if not refresh_token:
224
+ if not self._oauth_token:
191
225
  self.__auth_code_flow()
192
- super().__init__(pre_authed=not refresh_token)
226
+ super().__init__(self._token)
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
193
237
 
194
238
  def _build_auth_request(self) -> httpx.Request:
195
- url, headers, body = self._oauth.prepare_refresh_token_request(
196
- token_url=self._token_url,
197
- refresh_token=self._refresh_token,
198
- client_secret=self._client_secret,
199
- client_id=self._client_id,
200
- )
201
- return httpx.Request(method="post", url=url, headers=headers, content=body)
239
+ return self._oauth.refresh_token_request(self._oauth_token["refresh_token"])
202
240
 
203
241
  def _parse_auth_response(self, response: httpx.Response) -> None:
204
242
  if response.status_code == 401:
205
243
  raise InvalidCredentialsException
206
244
  if not response.is_success:
207
245
  raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
208
- token = response.json()
209
- self._token = token["access_token"]
210
- self._refresh_token = token["refresh_token"]
211
- if self._on_token_refresh:
212
- _run_callback(self._on_token_refresh, token)
213
- self._id_token = token.get("id_token")
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
+ )
253
+ self._token: str = self._oauth_token["access_token"]
214
254
 
215
255
  def __auth_code_flow(self):
216
256
  from oauthlib.oauth2 import OAuth2Error
217
257
 
218
258
  try:
219
259
  logger.info("Creating Authentication Token with OAuth2 Authorization Code Flow.")
220
- url, _, _ = self._oauth.prepare_authorization_request(
221
- "https://us1a.app.anaplan.com/auth/prelogin",
222
- redirect_url=self._redirect_uri,
223
- scope=self._scope,
224
- )
225
- authorization_response = (
226
- _run_callback(self._on_auth_code, url)
227
- if self._on_auth_code
228
- else input(
229
- f"Please go to {url} and authorize the app.\n"
230
- "Then paste the entire redirect URL here: "
231
- )
232
- )
233
- url, headers, body = self._oauth.prepare_token_request(
234
- token_url=self._token_url,
235
- redirect_url=self._redirect_uri,
236
- authorization_response=authorization_response,
237
- client_secret=self._client_secret,
260
+ url, _ = self._oauth.authorization_url()
261
+ authorization_response = input(
262
+ f"Please go to {url} and authorize the app.\n"
263
+ "Then paste the entire redirect URL here: "
238
264
  )
239
- self._parse_auth_response(httpx.post(url=url, headers=headers, content=body))
265
+ with httpx.Client() as client:
266
+ res = client.send(self._oauth.token_request(authorization_response))
267
+ self._parse_auth_response(res)
240
268
  except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
241
269
  raise InvalidCredentialsException("Error during OAuth2 authorization flow.") from error
242
270
 
243
271
 
272
+ class AnaplanRefreshTokenAuth(_AnaplanAuth):
273
+ def __init__(
274
+ self,
275
+ client_id: str,
276
+ client_secret: str,
277
+ redirect_uri: str,
278
+ token: dict[str, str],
279
+ token_url: str = "https://us1a.app.anaplan.com/oauth/token",
280
+ ):
281
+ """
282
+ This class is a utility class for long-lived `Client` or `AsyncClient` instances that use
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
+
298
+ :param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
299
+ must be an Authorization Code Grant application.
300
+ :param client_secret: The client secret of your Anaplan Oauth 2.0 application.
301
+ :param redirect_uri: The URL to which the user will be redirected after authorizing the
302
+ application.
303
+ :param token: The OAuth token dictionary containing at least the `access_token` and
304
+ `refresh_token`.
305
+ :param token_url: The URL to post the refresh token request to in order to fetch the access
306
+ token.
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
+ )
314
+ self._oauth_token = token
315
+ self._oauth = _OAuthRequestFactory(
316
+ client_id=client_id,
317
+ client_secret=client_secret,
318
+ redirect_uri=redirect_uri,
319
+ token_url=token_url,
320
+ )
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
332
+
333
+ def _build_auth_request(self) -> httpx.Request:
334
+ return self._oauth.refresh_token_request(self._oauth_token["refresh_token"])
335
+
336
+ def _parse_auth_response(self, response: httpx.Response) -> None:
337
+ if response.status_code == 401:
338
+ raise InvalidCredentialsException
339
+ if not response.is_success:
340
+ raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
341
+ self._oauth_token = response.json()
342
+ self._token: str = self._oauth_token["access_token"]
343
+
344
+
244
345
  def _create_auth(
245
346
  user_email: str | None = None,
246
347
  password: str | None = None,
247
348
  certificate: str | bytes | None = None,
248
349
  private_key: str | bytes | None = None,
249
350
  private_key_password: str | bytes | None = None,
250
- client_id: str | None = None,
251
- client_secret: str | None = None,
252
- redirect_uri: str | None = None,
253
- refresh_token: str | None = None,
254
- oauth2_scope: str = "openid profile email offline_access",
255
- on_auth_code: AuthCodeCallback = None,
256
- on_token_refresh: AuthTokenRefreshCallback = None,
257
351
  token: str | None = None,
258
- oauth_token: dict[str, str] | None = None,
259
352
  ) -> httpx.Auth:
260
- if token:
261
- # TODO: If other parameters are provided that allow refreshing the token, use them to create
262
- # use them to create one of the other auth classes instead.
263
- return StaticTokenAuth(token)
264
- if oauth_token:
265
- if not isinstance(oauth_token, dict) and all(
266
- f in oauth_token for f in ("access_token", "refresh_token", "token_type", "scope")
267
- ):
268
- raise ValueError(
269
- "oauth_token must be a dictionary with at least 'access_token', 'refresh_token', "
270
- "'token_type', and 'scope' keys."
271
- )
272
- # TODO: If client_id, client_secret, and redirect_uri are provided, use them to create an
273
- # AnaplanOauth2AuthCodeAuth (extend to accept existing `access_token` instance. Else, use
274
- # the StaticTokenAuth directly.
275
- return StaticTokenAuth(oauth_token["access_token"])
276
353
  if certificate and private_key:
277
- return AnaplanCertAuth(certificate, private_key, private_key_password)
354
+ return _AnaplanCertAuth(certificate, private_key, private_key_password, token)
278
355
  if user_email and password:
279
- return AnaplanBasicAuth(user_email=user_email, password=password)
280
- if client_id and client_secret and redirect_uri:
281
- return AnaplanOauth2AuthCodeAuth(
282
- client_id=client_id,
283
- client_secret=client_secret,
284
- redirect_uri=redirect_uri,
285
- refresh_token=refresh_token,
286
- scope=oauth2_scope,
287
- on_auth_code=on_auth_code,
288
- on_token_refresh=on_token_refresh,
289
- )
356
+ return _AnaplanBasicAuth(user_email, password, token)
357
+ if token:
358
+ return _StaticTokenAuth(token)
290
359
  raise ValueError(
291
360
  "No valid authentication parameters provided. Please provide either:\n"
292
- "- user_email and password, or\n"
293
- "- certificate and private_key, or\n"
294
- "- client_id, client_secret, and redirect_uri"
361
+ "- `user_email` and `password`, or\n"
362
+ "- `certificate` and `private_key`\n"
295
363
  )
296
-
297
-
298
- def _run_callback(func, *arg, **kwargs):
299
- if not inspect.iscoroutinefunction(func):
300
- return func(*arg, **kwargs)
301
- coro = func(*arg, **kwargs)
302
- try:
303
- loop = asyncio.get_running_loop()
304
- except RuntimeError:
305
- return asyncio.run(coro)
306
-
307
- if threading.current_thread() is threading.main_thread():
308
- if not loop.is_running():
309
- return loop.run_until_complete(coro)
310
- else:
311
- with ThreadPoolExecutor() as pool:
312
- future = pool.submit(__run_in_new_loop, coro)
313
- return future.result(timeout=30)
314
- else:
315
- return asyncio.run_coroutine_threadsafe(coro, loop).result()
316
-
317
-
318
- def __run_in_new_loop(coroutine: Coroutine[Any, Any, Any]):
319
- new_loop = asyncio.new_event_loop()
320
- asyncio.set_event_loop(new_loop)
321
- try:
322
- return new_loop.run_until_complete(coroutine)
323
- finally:
324
- new_loop.close()
@@ -3,7 +3,7 @@ import multiprocessing
3
3
  import time
4
4
  from concurrent.futures import ThreadPoolExecutor
5
5
  from copy import copy
6
- from typing import Callable, Iterator
6
+ from typing import Iterator
7
7
 
8
8
  import httpx
9
9
  from typing_extensions import Self
@@ -34,14 +34,8 @@ logger = logging.getLogger("anaplan_sdk")
34
34
 
35
35
  class Client(_BaseClient):
36
36
  """
37
- A synchronous Client for pythonic access to the
38
- [Anaplan Integration API v2](https://anaplan.docs.apiary.io/). This Client provides high-level
39
- abstractions over the API, so you can deal with python objects and simple functions rather
40
- than implementation details like http, json, compression, chunking etc.
41
-
42
-
43
- For more information, quick start guides and detailed instructions refer to:
44
- [Anaplan SDK](https://vinzenzklass.github.io/anaplan-sdk).
37
+ Synchronous Anaplan Client. For guides and examples
38
+ refer to https://vinzenzklass.github.io/anaplan-sdk.
45
39
  """
46
40
 
47
41
  def __init__(
@@ -53,15 +47,8 @@ class Client(_BaseClient):
53
47
  certificate: str | bytes | None = None,
54
48
  private_key: str | bytes | None = None,
55
49
  private_key_password: str | bytes | None = None,
56
- client_id: str | None = None,
57
- client_secret: str | None = None,
58
- redirect_uri: str | None = None,
59
- refresh_token: str | None = None,
60
- oauth2_scope: str = "openid profile email offline_access",
61
- on_auth_code: Callable[[str], str] | None = None,
62
- on_token_refresh: Callable[[dict[str, str]], None] | None = None,
63
- oauth_token: dict[str, str] | None = None,
64
50
  token: str | None = None,
51
+ auth: httpx.Auth | None = None,
65
52
  timeout: float | httpx.Timeout = 30,
66
53
  retry_count: int = 2,
67
54
  status_poll_delay: int = 1,
@@ -70,77 +57,56 @@ class Client(_BaseClient):
70
57
  allow_file_creation: bool = False,
71
58
  ) -> None:
72
59
  """
73
- A synchronous Client for pythonic access to the
74
- [Anaplan Integration API v2](https://anaplan.docs.apiary.io/). This Client provides
75
- high-level abstractions over the API, so you can deal with python objects and simple
76
- functions rather than implementation details like http, json, compression, chunking etc.
77
-
78
-
79
- For more information, quick start guides and detailed instructions refer to:
80
- [Anaplan SDK](https://vinzenzklass.github.io/anaplan-sdk).
60
+ Synchronous Anaplan Client. For guides and examples
61
+ refer to https://vinzenzklass.github.io/anaplan-sdk.
81
62
 
82
63
  :param workspace_id: The Anaplan workspace Id. You can copy this from the browser URL or
83
- find them using an HTTP Client like Postman, Paw, Insomnia etc.
64
+ find them using an HTTP Client like Postman, Paw, Insomnia etc.
84
65
  :param model_id: The identifier of the model.
85
66
  :param user_email: A valid email registered with the Anaplan Workspace you are attempting
86
- to access. **The associated user must have Workspace Admin privileges**
87
- :param password: Password for the given `user_email`. This is not suitable for production
88
- setups. If you intend to use this in production, acquire a client
89
- certificate as described under: https://help.anaplan.com/procure-ca-certificates-47842267-2cb3-4e38-90bf-13b1632bcd44
90
- :param certificate: The absolute path to the client certificate file or the certificate
91
- itself.
92
- :param private_key: The absolute path to the private key file or the private key itself.
93
- :param private_key_password: The password to access the private key if there is one.
94
- :param client_id: The client Id of the Oauth2 Anaplan Client.
95
- :param client_secret: The client secret for your Oauth2 Anaplan Client.
96
- :param redirect_uri: The redirect URI for your Oauth2 Anaplan Client.
97
- :param refresh_token: If you have a valid refresh token, you can pass it to skip the
98
- interactive authentication code step.
99
- :param oauth2_scope: The scope of the Oauth2 token, if you want to narrow it.
100
- :param on_auth_code: A callback that takes the redirect URI as a single argument and must
101
- return the entire response URI. This will substitute the interactive
102
- authentication code step in the terminal.
103
- :param on_token_refresh: A callback function that is called whenever the token is refreshed.
104
- This includes the initial token retrieval and any subsequent calls.
105
- With this you can for example securely store the token in your
106
- application or on your server for later reuse. The function
107
- must accept a single argument, which is the token dictionary
108
- returned by the Oauth2 token endpoint.
67
+ to access.
68
+ :param password: Password for the given `user_email` for basic Authentication.
69
+ :param certificate: The certificate content or the absolute path to the certificate file.
70
+ :param private_key: The private key content or the absolute path to the private key file.
71
+ :param private_key_password: The password to access the private key file. This is only
72
+ considered if you provided a private key file and it password-protected.
73
+ :param token: An Anaplan API Token. This will be used to authenticate the client. If
74
+ sufficient other authentication parameters are provided, the token will be used
75
+ until it expires, after which a new one will be created. If you provide only this
76
+ parameter, the client will raise an error upon first authentication failure. For
77
+ short-lived instances, such as in web applications where user specific clients are
78
+ created, this is the recommended way to authenticate, since this has the least
79
+ overhead.
80
+ :param auth: You can provide a subclass of `httpx.Auth` to use for authentication. You can
81
+ provide an instance of one of the classes provided by the SDK, or an instance of
82
+ your own subclass of `httpx.Auth`. This will give you full control over the
83
+ authentication process, but you will need to implement the entire authentication
84
+ logic yourself.
109
85
  :param timeout: The timeout in seconds for the HTTP requests. Alternatively, you can pass
110
- an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
86
+ an instance of `httpx.Timeout` to set the timeout for the HTTP requests.
111
87
  :param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
112
- to never retry. Defaults to 2, meaning each HTTP Operation will be
113
- tried a total number of 2 times.
88
+ to never retry. Defaults to 2, meaning each HTTP Operation will be tried a total
89
+ number of 2 times.
114
90
  :param status_poll_delay: The delay between polling the status of a task.
115
- :param upload_parallel: Whether to upload the chunks in parallel. Defaults to True. **If
116
- you are heavily network bound or are experiencing rate limiting
117
- issues, set this to False.**
91
+ :param upload_parallel: Whether to upload chunks in parallel when uploading files.
118
92
  :param upload_chunk_size: The size of the chunks to upload. This is the maximum size of
119
- each chunk. Defaults to 25MB.
93
+ each chunk. Defaults to 25MB.
120
94
  :param allow_file_creation: Whether to allow the creation of new files. Defaults to False
121
- since this is typically unintentional and may well be unwanted
122
- behaviour in the API altogether. A file that is created this
123
- way will not be referenced by any action in anaplan until
124
- manually assigned so there is typically no value in dynamically
125
- creating new files and uploading content to them.
95
+ since this is typically unintentional and may well be unwanted behaviour in the API
96
+ altogether. A file that is created this way will not be referenced by any action in
97
+ anaplan until manually assigned so there is typically no value in dynamically
98
+ creating new files and uploading content to them.
126
99
  """
127
100
  _client = httpx.Client(
128
101
  auth=(
129
- _create_auth(
102
+ auth
103
+ or _create_auth(
130
104
  token=token,
131
- oauth_token=oauth_token,
132
105
  user_email=user_email,
133
106
  password=password,
134
107
  certificate=certificate,
135
108
  private_key=private_key,
136
109
  private_key_password=private_key_password,
137
- client_id=client_id,
138
- client_secret=client_secret,
139
- redirect_uri=redirect_uri,
140
- refresh_token=refresh_token,
141
- oauth2_scope=oauth2_scope,
142
- on_auth_code=on_auth_code,
143
- on_token_refresh=on_token_refresh,
144
110
  )
145
111
  ),
146
112
  timeout=timeout,
anaplan_sdk/_oauth.py CHANGED
@@ -13,7 +13,7 @@ class _BaseOauth:
13
13
  self,
14
14
  client_id: str,
15
15
  client_secret: str,
16
- redirect_url: str,
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 redirect_url: The URL to which the user will be redirected after authorizing the
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._redirect_url = redirect_url
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,10 +86,46 @@ 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._redirect_url, self._scope
89
+ auth_url, state, self._redirect_uri, self._scope
90
90
  )
91
91
  return url, state
92
92
 
93
+ def _token_request(self, authorization_response: str) -> httpx.Request:
94
+ url, headers, body = self._oauth.prepare_token_request(
95
+ authorization_response=authorization_response,
96
+ token_url=self._token_url,
97
+ redirect_url=self._redirect_uri,
98
+ client_secret=self._client_secret,
99
+ )
100
+ return httpx.Request(method="POST", url=url, headers=headers, content=body)
101
+
102
+ def _refresh_token_request(self, refresh_token: str) -> httpx.Request:
103
+ url, headers, body = self._oauth.prepare_refresh_token_request(
104
+ self._token_url,
105
+ refresh_token=refresh_token,
106
+ client_id=self._client_id,
107
+ client_secret=self._client_secret,
108
+ )
109
+ return httpx.Request(method="POST", url=url, headers=headers, content=body)
110
+
111
+ def _parse_response(self, response: httpx.Response) -> dict[str, str]:
112
+ if response.status_code == 401:
113
+ raise InvalidCredentialsException
114
+ if not response.is_success:
115
+ raise AnaplanException(
116
+ f"Token request for Client {self._client_id} failed: "
117
+ f"{response.status_code} {response.text}"
118
+ )
119
+ return response.json()
120
+
121
+
122
+ class _OAuthRequestFactory(_BaseOauth):
123
+ def token_request(self, authorization_response: str) -> httpx.Request:
124
+ return self._token_request(authorization_response)
125
+
126
+ def refresh_token_request(self, refresh_token: str) -> httpx.Request:
127
+ return self._refresh_token_request(refresh_token)
128
+
93
129
 
94
130
  class AsyncOauth(_BaseOauth):
95
131
  """
@@ -97,7 +133,7 @@ class AsyncOauth(_BaseOauth):
97
133
  Applications.
98
134
  """
99
135
 
100
- async def fetch_token(self, authorization_response: str) -> dict[str, str]:
136
+ async def fetch_token(self, authorization_response: str) -> dict[str, str | int]:
101
137
  """
102
138
  Fetches the token using the authorization response from the OAuth 2.0 flow.
103
139
  :param authorization_response: The full URL that the user was redirected to after
@@ -108,24 +144,14 @@ class AsyncOauth(_BaseOauth):
108
144
  from oauthlib.oauth2 import OAuth2Error
109
145
 
110
146
  try:
111
- url, headers, body = self._oauth.prepare_token_request(
112
- authorization_response=authorization_response,
113
- token_url=self._token_url,
114
- redirect_url=self._redirect_url,
115
- client_secret=self._client_secret,
116
- )
117
147
  async with httpx.AsyncClient() as client:
118
- response = await client.post(url=url, headers=headers, content=body)
119
- if not response.is_success:
120
- raise InvalidCredentialsException(
121
- f"Token request failed: {response.status_code} {response.text}"
122
- )
123
- return response.json()
148
+ response = await client.send(self._token_request(authorization_response))
149
+ return self._parse_response(response)
124
150
  except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
125
151
  logger.error(error)
126
152
  raise AnaplanException("Error during token creation.") from error
127
153
 
128
- 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]]:
129
155
  """
130
156
  Validates the provided token by checking its validity with the Anaplan Authentication API.
131
157
  If the token is not valid, an `InvalidCredentialsException` is raised.
@@ -137,13 +163,28 @@ class AsyncOauth(_BaseOauth):
137
163
  response = await client.get(
138
164
  url=self._validation_url, headers={"Authorization": f"AnaplanAuthToken {token}"}
139
165
  )
140
- if not response.is_success:
141
- raise InvalidCredentialsException
142
- return response.json()
166
+ return self._parse_response(response)
143
167
  except httpx.HTTPError as error:
144
168
  logger.error(error)
145
169
  raise AnaplanException("Error during token validation.") from error
146
170
 
171
+ async def refresh_token(self, refresh_token: str) -> dict[str, str | int]:
172
+ """
173
+ Refreshes the token using a refresh token.
174
+ :param refresh_token: The refresh token to use for refreshing the access token.
175
+ :return: The new token as a dictionary containing the access token, refresh token, scope,
176
+ expires_in, and type.
177
+ """
178
+ from oauthlib.oauth2 import OAuth2Error
179
+
180
+ try:
181
+ async with httpx.AsyncClient() as client:
182
+ response = await client.send(self._refresh_token_request(refresh_token))
183
+ return self._parse_response(response)
184
+ except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
185
+ logger.error(error)
186
+ raise AnaplanException("Error during token refresh.") from error
187
+
147
188
 
148
189
  class Oauth(_BaseOauth):
149
190
  """
@@ -151,7 +192,7 @@ class Oauth(_BaseOauth):
151
192
  Applications.
152
193
  """
153
194
 
154
- def fetch_token(self, authorization_response: str) -> dict[str, str]:
195
+ def fetch_token(self, authorization_response: str) -> dict[str, str | int]:
155
196
  """
156
197
  Fetches the token using the authorization response from the OAuth 2.0 flow.
157
198
  :param authorization_response: The full URL that the user was redirected to after
@@ -165,20 +206,17 @@ class Oauth(_BaseOauth):
165
206
  url, headers, body = self._oauth.prepare_token_request(
166
207
  authorization_response=authorization_response,
167
208
  token_url=self._token_url,
168
- redirect_url=self._redirect_url,
209
+ redirect_url=self._redirect_uri,
169
210
  client_secret=self._client_secret,
170
211
  )
171
212
  with httpx.Client() as client:
172
213
  response = client.post(url=url, headers=headers, content=body)
173
- if not response.is_success:
174
- raise AnaplanException(
175
- f"Token request failed: {response.status_code} {response.text}"
176
- )
177
- return response.json()
214
+ return self._parse_response(response)
178
215
  except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
216
+ logger.error(error)
179
217
  raise AnaplanException("Error during token creation.") from error
180
218
 
181
- 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]]:
182
220
  """
183
221
  Validates the provided token by checking its validity with the Anaplan Authentication API.
184
222
  If the token is not valid, an `InvalidCredentialsException` is raised.
@@ -190,9 +228,30 @@ class Oauth(_BaseOauth):
190
228
  response = client.get(
191
229
  url=self._validation_url, headers={"Authorization": f"AnaplanAuthToken {token}"}
192
230
  )
193
- if not response.is_success:
194
- raise InvalidCredentialsException
195
- return response.json()
231
+ return self._parse_response(response)
196
232
  except httpx.HTTPError as error:
197
233
  logger.error(error)
198
234
  raise AnaplanException("Error during token validation.") from error
235
+
236
+ def refresh_token(self, refresh_token: str) -> dict[str, str | int]:
237
+ """
238
+ Refreshes the token using a refresh token.
239
+ :param refresh_token: The refresh token to use for refreshing the access token.
240
+ :return: The new token as a dictionary containing the access token, refresh token, scope,
241
+ expires_in, and type.
242
+ """
243
+ from oauthlib.oauth2 import OAuth2Error
244
+
245
+ try:
246
+ url, headers, body = self._oauth.prepare_refresh_token_request(
247
+ self._token_url,
248
+ refresh_token=refresh_token,
249
+ client_id=self._client_id,
250
+ client_secret=self._client_secret,
251
+ )
252
+ with httpx.Client() as client:
253
+ response = client.post(url=url, headers=headers, content=body)
254
+ return self._parse_response(response)
255
+ except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
256
+ logger.error(error)
257
+ raise AnaplanException("Error during token refresh.") from error
@@ -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.3a1
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.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=4P-BZnoTxQdKRHOH0DUe-B4XUQKPrK45UfMDWeb1TtI,196
2
- anaplan_sdk/_auth.py,sha256=wo0iuQm1-uZiZ081hA2Y2knc-w1RIwhea_6CEqDxPto,13276
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
- anaplan_sdk/_oauth.py,sha256=TDv4d10htjhUIbcGrvwTf4Z7dYWoeh18Wl5CiMu93fE,9914
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=SYYqfiZ12FEETQP2lakEU3bGfy0mEenA34Nj3dwXkLc,27077
9
+ anaplan_sdk/_async_clients/_bulk.py,sha256=j0yMoM8NWQH9BsSQ4LRYt8djfd1d11vkjNfU8pUeGLU,23737
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=RCFczu_iePjn6O_1Oj4m8lMtaSw7bwNHIVHMz_ZsYPU,25737
16
+ anaplan_sdk/_clients/_bulk.py,sha256=nlsZHK8vjhvyC0auRuqyvJVvTISPqj9EIHBYLoqSpOc,23354
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.3a1.dist-info/METADATA,sha256=mxfCoUiocn3SPGKbUbUnHukS9eyRYj12EVMZIMFjI7k,3545
28
- anaplan_sdk-0.4.3a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- anaplan_sdk-0.4.3a1.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
30
- anaplan_sdk-0.4.3a1.dist-info/RECORD,,
27
+ anaplan_sdk-0.4.4.dist-info/METADATA,sha256=VU-hRC5mvfO5owSGRjfY7TTMFKL4z3QMRJM_ddiSPU4,3667
28
+ anaplan_sdk-0.4.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
+ anaplan_sdk-0.4.4.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
30
+ anaplan_sdk-0.4.4.dist-info/RECORD,,