anaplan-sdk 0.4.2__py3-none-any.whl → 0.4.3a2__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,4 +1,15 @@
1
1
  from ._async_clients import AsyncClient
2
+ from ._auth import AnaplanOAuthCodeAuth, AnaplanRefreshTokenAuth
2
3
  from ._clients import Client
4
+ from ._oauth import AsyncOauth, Oauth
3
5
 
4
- __all__ = ["AsyncClient", "Client", "models", "exceptions"]
6
+ __all__ = [
7
+ "AsyncClient",
8
+ "Client",
9
+ "AnaplanOAuthCodeAuth",
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,13 +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,
48
+ token: str | None = None,
49
+ auth: httpx.Auth | None = None,
61
50
  timeout: float | httpx.Timeout = 30,
62
51
  retry_count: int = 2,
63
52
  status_poll_delay: int = 1,
@@ -65,84 +54,55 @@ class AsyncClient(_AsyncBaseClient):
65
54
  allow_file_creation: bool = False,
66
55
  ) -> None:
67
56
  """
68
- An asynchronous Client for pythonic access to the Anaplan Integration API v2:
69
- https://anaplan.docs.apiary.io/. This Client provides high-level abstractions over the API,
70
- so you can deal with python objects and simple functions rather than implementation details
71
- like http, json, compression, chunking etc.
72
-
73
-
74
- For more information, quick start guides and detailed instructions refer to:
75
- https://vinzenzklass.github.io/anaplan-sdk.
57
+ Asynchronous Anaplan Client. For guides and examples
58
+ refer to https://vinzenzklass.github.io/anaplan-sdk.
76
59
 
77
60
  :param workspace_id: The Anaplan workspace Id. You can copy this from the browser URL or
78
- find them using an HTTP Client like Postman, Paw, Insomnia etc.
61
+ find them using an HTTP Client like Postman, Paw, Insomnia etc.
79
62
  :param model_id: The identifier of the model.
80
63
  :param user_email: A valid email registered with the Anaplan Workspace you are attempting
81
- to access. **The associated user must have Workspace Admin privileges**
82
- :param password: Password for the given `user_email`. This is not suitable for production
83
- setups. If you intend to use this in production, acquire a client
84
- certificate as described under: https://help.anaplan.com/procure-ca-certificates-47842267-2cb3-4e38-90bf-13b1632bcd44
85
- :param certificate: The absolute path to the client certificate file or the certificate
86
- itself.
87
- :param private_key: The absolute path to the private key file or the private key itself.
88
- :param private_key_password: The password to access the private key if there is one.
89
- :param client_id: The client Id of the Oauth2 Anaplan Client.
90
- :param client_secret: The client secret for your Oauth2 Anaplan Client.
91
- :param redirect_uri: The redirect URI for your Oauth2 Anaplan Client.
92
- :param refresh_token: If you have a valid refresh token, you can pass it to skip the
93
- interactive authentication code step.
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.
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.
107
- With this you can for example securely store the token in your
108
- application or on your server for later reuse. The function
109
- must accept a single argument, which is the token dictionary
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.
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.
116
82
  :param timeout: The timeout in seconds for the HTTP requests. Alternatively, you can pass
117
- 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.
118
84
  :param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
119
- to never retry. Defaults to 2, meaning each HTTP Operation will be
120
- 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.
121
87
  :param status_poll_delay: The delay between polling the status of a task.
122
88
  :param upload_chunk_size: The size of the chunks to upload. This is the maximum size of
123
- each chunk. Defaults to 25MB.
89
+ each chunk. Defaults to 25MB.
124
90
  :param allow_file_creation: Whether to allow the creation of new files. Defaults to False
125
- since this is typically unintentional and may well be unwanted
126
- behaviour in the API altogether. A file that is created this
127
- way will not be referenced by any action in anaplan until
128
- manually assigned so there is typically no value in dynamically
129
- 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.
130
95
  """
131
96
  _client = httpx.AsyncClient(
132
97
  auth=(
133
- create_auth(
98
+ auth
99
+ or _create_auth(
100
+ token=token,
134
101
  user_email=user_email,
135
102
  password=password,
136
103
  certificate=certificate,
137
104
  private_key=private_key,
138
105
  private_key_password=private_key_password,
139
- client_id=client_id,
140
- client_secret=client_secret,
141
- redirect_uri=redirect_uri,
142
- refresh_token=refresh_token,
143
- oauth2_scope=oauth2_scope,
144
- on_auth_code=on_auth_code,
145
- on_token_refresh=on_token_refresh,
146
106
  )
147
107
  ),
148
108
  timeout=timeout,
anaplan_sdk/_auth.py CHANGED
@@ -1,14 +1,11 @@
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 Awaitable, 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")
@@ -22,12 +19,12 @@ AuthTokenRefreshCallback = (
22
19
  class _AnaplanAuth(httpx.Auth):
23
20
  requires_response_body = True
24
21
 
25
- def __init__(self, pre_authed: bool = False):
26
- if not pre_authed:
22
+ def __init__(self, token: str | None = None):
23
+ self._token: str = token or ""
24
+ if not token:
27
25
  logger.info("Creating Authentication Token.")
28
26
  with httpx.Client(timeout=15.0) as client:
29
- res = client.send(self._build_auth_request())
30
- self._parse_auth_response(res)
27
+ self._parse_auth_response(client.send(self._build_auth_request()))
31
28
 
32
29
  def _build_auth_request(self) -> httpx.Request:
33
30
  raise NotImplementedError("Must be implemented in subclass.")
@@ -47,14 +44,25 @@ class _AnaplanAuth(httpx.Auth):
47
44
  raise InvalidCredentialsException
48
45
  if not response.is_success:
49
46
  raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
50
- self._token: str = response.json()["tokenInfo"]["tokenValue"]
47
+ self._token = response.json()["tokenInfo"]["tokenValue"]
51
48
 
52
49
 
53
- class AnaplanBasicAuth(_AnaplanAuth):
54
- def __init__(self, user_email: str, password: str):
50
+ class _StaticTokenAuth(httpx.Auth):
51
+ def __init__(self, token: str):
52
+ self._token = token
53
+
54
+ def auth_flow(self, request):
55
+ request.headers["Authorization"] = f"AnaplanAuthToken {self._token}"
56
+ response = yield request
57
+ if response.status_code == 401:
58
+ raise InvalidCredentialsException("Token is invalid or expired.")
59
+
60
+
61
+ class _AnaplanBasicAuth(_AnaplanAuth):
62
+ def __init__(self, user_email: str, password: str, token: str | None = None):
55
63
  self.user_email = user_email
56
64
  self.password = password
57
- super().__init__()
65
+ super().__init__(token)
58
66
 
59
67
  def _build_auth_request(self) -> httpx.Request:
60
68
  cred = b64encode(f"{self.user_email}:{self.password}".encode()).decode()
@@ -65,7 +73,7 @@ class AnaplanBasicAuth(_AnaplanAuth):
65
73
  )
66
74
 
67
75
 
68
- class AnaplanCertAuth(_AnaplanAuth):
76
+ class _AnaplanCertAuth(_AnaplanAuth):
69
77
  requires_request_body = True
70
78
 
71
79
  def __init__(
@@ -73,10 +81,11 @@ class AnaplanCertAuth(_AnaplanAuth):
73
81
  certificate: str | bytes,
74
82
  private_key: str | bytes,
75
83
  private_key_password: str | bytes | None = None,
84
+ token: str | None = None,
76
85
  ):
77
86
  self.__set_certificate(certificate)
78
87
  self.__set_private_key(private_key, private_key_password)
79
- super().__init__()
88
+ super().__init__(token)
80
89
 
81
90
  def _build_auth_request(self) -> httpx.Request:
82
91
  encoded_cert, encoded_string, encoded_signed_string = self._prep_credentials()
@@ -145,150 +154,142 @@ class AnaplanCertAuth(_AnaplanAuth):
145
154
  raise InvalidPrivateKeyException from error
146
155
 
147
156
 
148
- class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
157
+ class AnaplanOAuthCodeAuth(_AnaplanAuth):
149
158
  def __init__(
150
159
  self,
151
160
  client_id: str,
152
161
  client_secret: str,
153
- redirect_uri: str,
154
- refresh_token: str | None = None,
162
+ redirect_url: str,
163
+ token: dict[str, str] | None = None,
164
+ authorization_url: str = "https://us1a.app.anaplan.com/auth/prelogin",
165
+ token_url: str = "https://us1a.app.anaplan.com/oauth/token",
166
+ validation_url: str = "https://auth.anaplan.com/token/validate",
155
167
  scope: str = "openid profile email offline_access",
156
- on_auth_code: AuthCodeCallback = None,
157
- on_token_refresh: AuthTokenRefreshCallback = None,
168
+ state_generator: Callable[[], str] | None = None,
158
169
  ):
159
- try:
160
- from oauthlib.oauth2 import WebApplicationClient
161
- except ImportError as e:
162
- raise AnaplanException(
163
- "oauthlib is not available. Please install anaplan-sdk with the oauth extra "
164
- "`pip install anaplan-sdk[oauth]` or install oauthlib separately."
165
- ) from e
166
- self._oauth = WebApplicationClient(
167
- client_id=client_id, client_secret=client_secret, refresh_token=refresh_token
170
+ """
171
+ Initializes the AnaplanOAuthCodeAuth class for OAuth2 authentication using the
172
+ Authorization Code Flow. This is a utility class for local development and requires user
173
+ interaction. For Web Applications and other scenarios, refer to `Oauth` or `AsyncOauth`.
174
+ This class will refresh the access token automatically when it expires.
175
+ :param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
176
+ must be an Authorization Code Grant application.
177
+ :param client_secret: The client secret of your Anaplan Oauth 2.0 application.
178
+ :param redirect_url: The URL to which the user will be redirected after authorizing the
179
+ application.
180
+ :param authorization_url: The URL to which the user will be redirected to authorize the
181
+ application. Defaults to the Anaplan Prelogin Page, where the user can select the
182
+ login method.
183
+ :param token_url: The URL to post the authorization code to in order to fetch the access
184
+ token.
185
+ :param validation_url: The URL to validate the access token.
186
+ :param scope: The scope of the access request.
187
+ :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`.
190
+ """
191
+
192
+ self._oauth_token = token or {}
193
+ self._oauth = _OAuthRequestFactory(
194
+ client_id=client_id,
195
+ client_secret=client_secret,
196
+ redirect_url=redirect_url,
197
+ scope=scope,
198
+ authorization_url=authorization_url,
199
+ token_url=token_url,
200
+ validation_url=validation_url,
201
+ state_generator=state_generator,
168
202
  )
169
- self._token_url = "https://us1a.app.anaplan.com/oauth/token"
170
- self._client_id = client_id
171
- self._client_secret = client_secret
172
- self._redirect_uri = redirect_uri
173
- self._refresh_token = refresh_token
174
- self._scope = scope
175
- self._id_token = None
176
- self._on_auth_code = on_auth_code
177
- self._on_token_refresh = on_token_refresh
178
- if not refresh_token:
203
+ if not token:
179
204
  self.__auth_code_flow()
180
- super().__init__(pre_authed=not refresh_token)
205
+ super().__init__(self._token)
181
206
 
182
207
  def _build_auth_request(self) -> httpx.Request:
183
- url, headers, body = self._oauth.prepare_refresh_token_request(
184
- token_url=self._token_url,
185
- refresh_token=self._refresh_token,
186
- client_secret=self._client_secret,
187
- client_id=self._client_id,
188
- )
189
- return httpx.Request(method="post", url=url, headers=headers, content=body)
208
+ return self._oauth.refresh_token_request(self._oauth_token["refresh_token"])
190
209
 
191
210
  def _parse_auth_response(self, response: httpx.Response) -> None:
192
211
  if response.status_code == 401:
193
212
  raise InvalidCredentialsException
194
213
  if not response.is_success:
195
214
  raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
196
- token = response.json()
197
- self._token = token["access_token"]
198
- self._refresh_token = token["refresh_token"]
199
- if self._on_token_refresh:
200
- _run_callback(self._on_token_refresh, token)
201
- self._id_token = token.get("id_token")
215
+ self._oauth_token = response.json()
216
+ self._token: str = self._oauth_token["access_token"]
202
217
 
203
218
  def __auth_code_flow(self):
204
219
  from oauthlib.oauth2 import OAuth2Error
205
220
 
206
221
  try:
207
222
  logger.info("Creating Authentication Token with OAuth2 Authorization Code Flow.")
208
- url, _, _ = self._oauth.prepare_authorization_request(
209
- "https://us1a.app.anaplan.com/auth/prelogin",
210
- redirect_url=self._redirect_uri,
211
- scope=self._scope,
212
- )
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
- )
220
- )
221
- url, headers, body = self._oauth.prepare_token_request(
222
- token_url=self._token_url,
223
- redirect_url=self._redirect_uri,
224
- authorization_response=authorization_response,
225
- client_secret=self._client_secret,
223
+ url, _ = self._oauth.authorization_url()
224
+ authorization_response = input(
225
+ f"Please go to {url} and authorize the app.\n"
226
+ "Then paste the entire redirect URL here: "
226
227
  )
227
- self._parse_auth_response(httpx.post(url=url, headers=headers, content=body))
228
+ with httpx.Client() as client:
229
+ res = client.send(self._oauth.token_request(authorization_response))
230
+ self._parse_auth_response(res)
228
231
  except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
229
232
  raise InvalidCredentialsException("Error during OAuth2 authorization flow.") from error
230
233
 
231
234
 
232
- def create_auth(
235
+ class AnaplanRefreshTokenAuth(_AnaplanAuth):
236
+ def __init__(
237
+ self,
238
+ client_id: str,
239
+ client_secret: str,
240
+ redirect_url: str,
241
+ token: dict[str, str],
242
+ token_url: str = "https://us1a.app.anaplan.com/oauth/token",
243
+ ):
244
+ """
245
+ 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.
248
+ :param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
249
+ must be an Authorization Code Grant application.
250
+ :param client_secret: The client secret of your Anaplan Oauth 2.0 application.
251
+ :param redirect_url: The URL to which the user will be redirected after authorizing the
252
+ application.
253
+ :param token_url: The URL to post the refresh token request to in order to fetch the access
254
+ token.
255
+ """
256
+ self._oauth_token = token
257
+ self._oauth = _OAuthRequestFactory(
258
+ client_id=client_id,
259
+ client_secret=client_secret,
260
+ redirect_url=redirect_url,
261
+ token_url=token_url,
262
+ )
263
+ super().__init__(self._token)
264
+
265
+ def _build_auth_request(self) -> httpx.Request:
266
+ return self._oauth.refresh_token_request(self._oauth_token["refresh_token"])
267
+
268
+ def _parse_auth_response(self, response: httpx.Response) -> None:
269
+ if response.status_code == 401:
270
+ raise InvalidCredentialsException
271
+ if not response.is_success:
272
+ raise AnaplanException(f"Authentication failed: {response.status_code} {response.text}")
273
+ self._oauth_token = response.json()
274
+ self._token: str = self._oauth_token["access_token"]
275
+
276
+
277
+ def _create_auth(
233
278
  user_email: str | None = None,
234
279
  password: str | None = None,
235
280
  certificate: str | bytes | None = None,
236
281
  private_key: str | bytes | None = None,
237
282
  private_key_password: str | bytes | None = None,
238
- client_id: str | None = None,
239
- client_secret: str | None = None,
240
- redirect_uri: str | None = None,
241
- refresh_token: str | None = None,
242
- oauth2_scope: str = "openid profile email offline_access",
243
- on_auth_code: AuthCodeCallback = None,
244
- on_token_refresh: AuthTokenRefreshCallback = None,
245
- ) -> _AnaplanAuth:
283
+ token: str | None = None,
284
+ ) -> httpx.Auth:
246
285
  if certificate and private_key:
247
- return AnaplanCertAuth(certificate, private_key, private_key_password)
286
+ return _AnaplanCertAuth(certificate, private_key, private_key_password, token)
248
287
  if user_email and password:
249
- return AnaplanBasicAuth(user_email=user_email, password=password)
250
- if client_id and client_secret and redirect_uri:
251
- return AnaplanOauth2AuthCodeAuth(
252
- client_id=client_id,
253
- client_secret=client_secret,
254
- redirect_uri=redirect_uri,
255
- refresh_token=refresh_token,
256
- scope=oauth2_scope,
257
- on_auth_code=on_auth_code,
258
- on_token_refresh=on_token_refresh,
259
- )
288
+ return _AnaplanBasicAuth(user_email, password, token)
289
+ if token:
290
+ return _StaticTokenAuth(token)
260
291
  raise ValueError(
261
292
  "No valid authentication parameters provided. Please provide either:\n"
262
- "- user_email and password, or\n"
263
- "- certificate and private_key, or\n"
264
- "- client_id, client_secret, and redirect_uri"
293
+ "- `user_email` and `password`, or\n"
294
+ "- `certificate` and `private_key`\n"
265
295
  )
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()
@@ -3,12 +3,12 @@ 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
10
10
 
11
- from anaplan_sdk._auth import create_auth
11
+ from anaplan_sdk._auth import _create_auth
12
12
  from anaplan_sdk._base import _BaseClient, action_url
13
13
  from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
14
14
  from anaplan_sdk.models import (
@@ -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,13 +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,
50
+ token: str | None = None,
51
+ auth: httpx.Auth | None = None,
63
52
  timeout: float | httpx.Timeout = 30,
64
53
  retry_count: int = 2,
65
54
  status_poll_delay: int = 1,
@@ -68,75 +57,56 @@ class Client(_BaseClient):
68
57
  allow_file_creation: bool = False,
69
58
  ) -> None:
70
59
  """
71
- A synchronous Client for pythonic access to the
72
- [Anaplan Integration API v2](https://anaplan.docs.apiary.io/). This Client provides
73
- high-level abstractions over the API, so you can deal with python objects and simple
74
- functions rather than implementation details like http, json, compression, chunking etc.
75
-
76
-
77
- For more information, quick start guides and detailed instructions refer to:
78
- [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.
79
62
 
80
63
  :param workspace_id: The Anaplan workspace Id. You can copy this from the browser URL or
81
- find them using an HTTP Client like Postman, Paw, Insomnia etc.
64
+ find them using an HTTP Client like Postman, Paw, Insomnia etc.
82
65
  :param model_id: The identifier of the model.
83
66
  :param user_email: A valid email registered with the Anaplan Workspace you are attempting
84
- to access. **The associated user must have Workspace Admin privileges**
85
- :param password: Password for the given `user_email`. This is not suitable for production
86
- setups. If you intend to use this in production, acquire a client
87
- certificate as described under: https://help.anaplan.com/procure-ca-certificates-47842267-2cb3-4e38-90bf-13b1632bcd44
88
- :param certificate: The absolute path to the client certificate file or the certificate
89
- itself.
90
- :param private_key: The absolute path to the private key file or the private key itself.
91
- :param private_key_password: The password to access the private key if there is one.
92
- :param client_id: The client Id of the Oauth2 Anaplan Client.
93
- :param client_secret: The client secret for your Oauth2 Anaplan Client.
94
- :param redirect_uri: The redirect URI for your Oauth2 Anaplan Client.
95
- :param refresh_token: If you have a valid refresh token, you can pass it to skip the
96
- interactive authentication code step.
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.
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.
103
- With this you can for example securely store the token in your
104
- application or on your server for later reuse. The function
105
- must accept a single argument, which is the token dictionary
106
- 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.
107
85
  :param timeout: The timeout in seconds for the HTTP requests. Alternatively, you can pass
108
- 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.
109
87
  :param retry_count: The number of times to retry an HTTP request if it fails. Set this to 0
110
- to never retry. Defaults to 2, meaning each HTTP Operation will be
111
- 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.
112
90
  :param status_poll_delay: The delay between polling the status of a task.
113
- :param upload_parallel: Whether to upload the chunks in parallel. Defaults to True. **If
114
- you are heavily network bound or are experiencing rate limiting
115
- issues, set this to False.**
91
+ :param upload_parallel: Whether to upload chunks in parallel when uploading files.
116
92
  :param upload_chunk_size: The size of the chunks to upload. This is the maximum size of
117
- each chunk. Defaults to 25MB.
93
+ each chunk. Defaults to 25MB.
118
94
  :param allow_file_creation: Whether to allow the creation of new files. Defaults to False
119
- since this is typically unintentional and may well be unwanted
120
- behaviour in the API altogether. A file that is created this
121
- way will not be referenced by any action in anaplan until
122
- manually assigned so there is typically no value in dynamically
123
- 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.
124
99
  """
125
100
  _client = httpx.Client(
126
101
  auth=(
127
- create_auth(
102
+ auth
103
+ or _create_auth(
104
+ token=token,
128
105
  user_email=user_email,
129
106
  password=password,
130
107
  certificate=certificate,
131
108
  private_key=private_key,
132
109
  private_key_password=private_key_password,
133
- client_id=client_id,
134
- client_secret=client_secret,
135
- redirect_uri=redirect_uri,
136
- refresh_token=refresh_token,
137
- oauth2_scope=oauth2_scope,
138
- on_auth_code=on_auth_code,
139
- on_token_refresh=on_token_refresh,
140
110
  )
141
111
  ),
142
112
  timeout=timeout,
anaplan_sdk/_oauth.py ADDED
@@ -0,0 +1,257 @@
1
+ import logging
2
+ from typing import Callable
3
+
4
+ import httpx
5
+
6
+ from .exceptions import AnaplanException, InvalidCredentialsException
7
+
8
+ logger = logging.getLogger("anaplan_sdk")
9
+
10
+
11
+ class _BaseOauth:
12
+ def __init__(
13
+ self,
14
+ client_id: str,
15
+ client_secret: str,
16
+ redirect_url: str,
17
+ authorization_url: str = "https://us1a.app.anaplan.com/auth/prelogin",
18
+ token_url: str = "https://us1a.app.anaplan.com/oauth/token",
19
+ validation_url: str = "https://auth.anaplan.com/token/validate",
20
+ scope: str = "openid profile email offline_access",
21
+ state_generator: Callable[[], str] | None = None,
22
+ ):
23
+ """
24
+ Initializes the OAuth Client. This class provides the two utilities needed to implement
25
+ the OAuth 2.0 authorization code flow for user-facing Web Applications. It differs from the
26
+ other Authentication Strategies in this SDK in two main ways:
27
+
28
+ 1. You must implement the actual authentication flow in your application. You cannot pass
29
+ the credentials directly to the `Client` or `AsyncClient`, and this class does not
30
+ implement the SDK internal authentication flow, i.e. it does not subclass `httpx.Auth`.
31
+
32
+ 2. You then simply pass the resulting token to the `Client` or `AsyncClient`, rather than
33
+ passing the credentials directly, which will internally construct an `httpx.Auth` instance
34
+
35
+ Note that this class exist for convenience only, and you can implement the OAuth 2.0 Flow
36
+ yourself in your preferred library, or bring an existing implementation. For details on the
37
+ Anaplan OAuth 2.0 Flow, see the [the Docs](https://anaplanoauth2service.docs.apiary.io/#reference/overview-of-the-authorization-code-grant).
38
+ :param client_id: The client ID of your Anaplan Oauth 2.0 application. This Application
39
+ must be an Authorization Code Grant application.
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
42
+ application.
43
+ :param authorization_url: The URL to which the user will be redirected to authorize the
44
+ application. Defaults to the Anaplan Prelogin Page, where the user can select the
45
+ login method.
46
+ :param token_url: The URL to post the authorization code to in order to fetch the access
47
+ token.
48
+ :param validation_url: The URL to validate the access token.
49
+ :param scope: The scope of the access request.
50
+ :param state_generator: A callable that generates a random state string. You can optionally
51
+ pass this if you need to customize the state generation logic. If not provided,
52
+ the state will be generated by `oauthlib`.
53
+ """
54
+ self._client_id = client_id
55
+ self._client_secret = client_secret
56
+ self._redirect_url = redirect_url
57
+ self._authorization_url = authorization_url
58
+ self._token_url = token_url
59
+ self._validation_url = validation_url
60
+ self._scope = scope
61
+
62
+ try:
63
+ from oauthlib.oauth2 import WebApplicationClient
64
+ except ImportError as e:
65
+ raise AnaplanException(
66
+ "oauthlib is not available. Please install anaplan-sdk with the oauth extra "
67
+ "`pip install anaplan-sdk[oauth]` or install oauthlib separately."
68
+ ) from e
69
+ self._oauth = WebApplicationClient(client_id=client_id, client_secret=client_secret)
70
+ self._state_generator = state_generator if state_generator else self._oauth.state_generator
71
+
72
+ def authorization_url(
73
+ self, authorization_url: str | None = None, state: str | None = None
74
+ ) -> tuple[str, str]:
75
+ """
76
+ Generates the authorization URL for the OAuth 2.0 flow.
77
+ :param authorization_url: You can optionally pass a custom authorization URL. This is
78
+ useful if you want to redirect i.e. redirect the user directly to the Anaplan login
79
+ page rather than the Prelogin page in only one scenario, while still reusing the
80
+ Client.
81
+ :param state: You can optionally pass a custom state string. If not provided, a random
82
+ state string will be generated by the `oauthlib` library, or by the
83
+ `state_generator` callable if provided.
84
+ :return: A tuple containing the authorization URL and the state string.
85
+ """
86
+ auth_url = authorization_url or self._authorization_url
87
+ state = state or self._state_generator()
88
+ url, _, _ = self._oauth.prepare_authorization_request(
89
+ auth_url, state, self._redirect_url, self._scope
90
+ )
91
+ return url, state
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_url,
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
+
129
+
130
+ class AsyncOauth(_BaseOauth):
131
+ """
132
+ Asynchronous Variant of the Anaplan OAuth client for interactive OAuth Flows in Web
133
+ Applications.
134
+ """
135
+
136
+ async def fetch_token(self, authorization_response: str) -> dict[str, str]:
137
+ """
138
+ Fetches the token using the authorization response from the OAuth 2.0 flow.
139
+ :param authorization_response: The full URL that the user was redirected to after
140
+ authorizing the application. This URL will contain the authorization code and state.
141
+ :return: The token as a dictionary containing the access token, refresh token, scope,
142
+ expires_in, and type.
143
+ """
144
+ from oauthlib.oauth2 import OAuth2Error
145
+
146
+ try:
147
+ async with httpx.AsyncClient() as client:
148
+ response = await client.send(self._token_request(authorization_response))
149
+ return self._parse_response(response)
150
+ except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
151
+ logger.error(error)
152
+ raise AnaplanException("Error during token creation.") from error
153
+
154
+ async def validate_token(self, token: str) -> dict[str, str | dict[str, str]]:
155
+ """
156
+ Validates the provided token by checking its validity with the Anaplan Authentication API.
157
+ If the token is not valid, an `InvalidCredentialsException` is raised.
158
+ :param token: The access token to validate.
159
+ :return: The Token information as a dictionary containing the token's details.
160
+ """
161
+ try:
162
+ async with httpx.AsyncClient() as client:
163
+ response = await client.get(
164
+ url=self._validation_url, headers={"Authorization": f"AnaplanAuthToken {token}"}
165
+ )
166
+ return self._parse_response(response)
167
+ except httpx.HTTPError as error:
168
+ logger.error(error)
169
+ raise AnaplanException("Error during token validation.") from error
170
+
171
+ async def refresh_token(self, refresh_token: str) -> dict[str, str]:
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
+
188
+
189
+ class Oauth(_BaseOauth):
190
+ """
191
+ Synchronous Variant of the Anaplan OAuth client for interactive OAuth Flows in Web
192
+ Applications.
193
+ """
194
+
195
+ def fetch_token(self, authorization_response: str) -> dict[str, str]:
196
+ """
197
+ Fetches the token using the authorization response from the OAuth 2.0 flow.
198
+ :param authorization_response: The full URL that the user was redirected to after
199
+ authorizing the application. This URL will contain the authorization code and state.
200
+ :return: The token as a dictionary containing the access token, refresh token, scope,
201
+ expires_in, and type.
202
+ """
203
+ from oauthlib.oauth2 import OAuth2Error
204
+
205
+ try:
206
+ url, headers, body = self._oauth.prepare_token_request(
207
+ authorization_response=authorization_response,
208
+ token_url=self._token_url,
209
+ redirect_url=self._redirect_url,
210
+ client_secret=self._client_secret,
211
+ )
212
+ with httpx.Client() as client:
213
+ response = client.post(url=url, headers=headers, content=body)
214
+ return self._parse_response(response)
215
+ except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
216
+ logger.error(error)
217
+ raise AnaplanException("Error during token creation.") from error
218
+
219
+ def validate_token(self, token: str) -> dict[str, str | dict[str, str]]:
220
+ """
221
+ Validates the provided token by checking its validity with the Anaplan Authentication API.
222
+ If the token is not valid, an `InvalidCredentialsException` is raised.
223
+ :param token: The access token to validate.
224
+ :return: The Token information as a dictionary containing the token's details.
225
+ """
226
+ try:
227
+ with httpx.Client() as client:
228
+ response = client.get(
229
+ url=self._validation_url, headers={"Authorization": f"AnaplanAuthToken {token}"}
230
+ )
231
+ return self._parse_response(response)
232
+ except httpx.HTTPError as error:
233
+ logger.error(error)
234
+ raise AnaplanException("Error during token validation.") from error
235
+
236
+ def refresh_token(self, refresh_token: str) -> dict[str, str]:
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anaplan-sdk
3
- Version: 0.4.2
3
+ Version: 0.4.3a2
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,18 +1,19 @@
1
- anaplan_sdk/__init__.py,sha256=5fr-SZSsH6f3vkRUTDoK6xdAN31cCpe9Mwz2VNu47Uw,134
2
- anaplan_sdk/_auth.py,sha256=0htPrOYXDb2CCm4ZkwKQ4Zi26fsK6D0OIBiQdR6ESm8,11817
1
+ anaplan_sdk/__init__.py,sha256=lDFhs0IobOH7a34jqtAgcj9X1bR5hnFRkkkCl_vPHpo,357
2
+ anaplan_sdk/_auth.py,sha256=u7AHeTAWJnOQo7LtdgCmRY1CeITizX_Hqg1aKoZs9EE,12684
3
3
  anaplan_sdk/_base.py,sha256=9CdLshORWsLixOyoFa3A0Bka5lhLwlZrQI5sEdBcGFI,12298
4
+ anaplan_sdk/_oauth.py,sha256=FGOStYTtlTruQQDcDjN8JEEKEiEpdPwJM-chCrG1oao,12636
4
5
  anaplan_sdk/exceptions.py,sha256=ALkA9fBF0NQ7dufFxV6AivjmHyuJk9DOQ9jtJV2n7f0,1809
5
6
  anaplan_sdk/_async_clients/__init__.py,sha256=pZXgMMg4S9Aj_pxQCaSiPuNG-sePVGBtNJ0133VjqW4,364
6
7
  anaplan_sdk/_async_clients/_alm.py,sha256=O1_r-O1tNDq7vXRwE2UEFE5S2bPmPh4IAQPQ8bmZfQE,3297
7
8
  anaplan_sdk/_async_clients/_audit.py,sha256=a92RY0B3bWxp2CCAWjzqKfvBjG1LJGlai0Hn5qmwgF8,2312
8
- anaplan_sdk/_async_clients/_bulk.py,sha256=APhgKE4Deh90lm8rcCJMyQTJNMHAXFCKkqnGV_lAtgY,26908
9
+ anaplan_sdk/_async_clients/_bulk.py,sha256=j0yMoM8NWQH9BsSQ4LRYt8djfd1d11vkjNfU8pUeGLU,23737
9
10
  anaplan_sdk/_async_clients/_cloud_works.py,sha256=KPX9W55SF6h8fJd4Rx-HLq6eaRA-Vo3rFu343UiiaGQ,16642
10
11
  anaplan_sdk/_async_clients/_cw_flow.py,sha256=ZTNAbKDwb59Wg3u68hbtt1kpd-LNz9K0sftT-gvYzJQ,3651
11
12
  anaplan_sdk/_async_clients/_transactional.py,sha256=Mvr7OyBPjQRpBtzkJNfRzV4aNCzUiaYmm0zQubo62Wo,8035
12
13
  anaplan_sdk/_clients/__init__.py,sha256=FsbwvZC1FHrxuRXwbPxUzbhz_lO1DpXIxEOjx6-3QuA,219
13
14
  anaplan_sdk/_clients/_alm.py,sha256=UAdQxgHfax-VquC0YtbqrRBku2Rn35tVgwJdxYFScps,3202
14
15
  anaplan_sdk/_clients/_audit.py,sha256=xQQiwWIb4QQefolPvxNwBFE-pkRzzi8fYPyewjF63lc,2181
15
- anaplan_sdk/_clients/_bulk.py,sha256=4JkuutqCo7yt3Ik2f90ixkfPw1r-7TOq9xg1MUZ5es8,25568
16
+ anaplan_sdk/_clients/_bulk.py,sha256=nlsZHK8vjhvyC0auRuqyvJVvTISPqj9EIHBYLoqSpOc,23354
16
17
  anaplan_sdk/_clients/_cloud_works.py,sha256=KAMnLoeMJ2iwMXlDSbKynCE57BtkCfOgM5O8wT1kkSs,16291
17
18
  anaplan_sdk/_clients/_cw_flow.py,sha256=5IFWFT-qbyGvaSOOtaFOjHnOlyYbj4Rj3xiavfTlm8c,3527
18
19
  anaplan_sdk/_clients/_transactional.py,sha256=YUVbA54uhMloQcahwMtmZO3YooO6qQzwZN3ZRSu_z_c,7976
@@ -23,7 +24,7 @@ anaplan_sdk/models/_bulk.py,sha256=dHP3kMvsKONCZS6mHB271-wp2S4P3rM874Ita8TzABU,8
23
24
  anaplan_sdk/models/_transactional.py,sha256=_0UbVR9D5QABI29yloYrJTSgL-K0EU7PzPeJu5LdhnY,4854
24
25
  anaplan_sdk/models/cloud_works.py,sha256=nfn_LHPR-KmW7Tpvz-5qNCzmR8SYgvsVV-lx5iDlyqI,19425
25
26
  anaplan_sdk/models/flows.py,sha256=SuLgNj5-2SeE3U1i8iY8cq2IkjuUgd_3M1n2ENructk,3625
26
- anaplan_sdk-0.4.2.dist-info/METADATA,sha256=oyd3terF5C2LV55DARCix6LSpCJ6ch7jlub7nKE6wDo,3543
27
- anaplan_sdk-0.4.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
- anaplan_sdk-0.4.2.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
29
- anaplan_sdk-0.4.2.dist-info/RECORD,,
27
+ anaplan_sdk-0.4.3a2.dist-info/METADATA,sha256=OcYO34dO9jwqeirqm1BuB-UBB9LwjkdYP7IL4XdHzsg,3545
28
+ anaplan_sdk-0.4.3a2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
+ anaplan_sdk-0.4.3a2.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
30
+ anaplan_sdk-0.4.3a2.dist-info/RECORD,,