anaplan-sdk 0.4.0a2__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/PKG-INFO +4 -4
  2. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_async_clients/_bulk.py +5 -0
  3. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_async_clients/_transactional.py +26 -4
  4. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_auth.py +13 -5
  5. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_base.py +1 -5
  6. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_clients/_bulk.py +6 -2
  7. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_clients/_transactional.py +26 -4
  8. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/models/_base.py +1 -6
  9. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/models/cloud_works.py +5 -9
  10. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/authentication.md +35 -0
  11. anaplan_sdk-0.4.1/docs/img/anaplan-sdk.webp +0 -0
  12. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/pyproject.toml +18 -10
  13. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/async/conftest.py +1 -0
  14. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/async/test_async_transactional_client.py +4 -0
  15. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/conftest.py +1 -6
  16. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/sync/conftest.py +1 -0
  17. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/sync/test_transactional_client.py +4 -0
  18. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/uv.lock +515 -701
  19. anaplan_sdk-0.4.0a2/docs/img/anaplan-sdk.webp +0 -0
  20. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/.github/dependabot.yml +0 -0
  21. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/.github/workflows/docs.yml +0 -0
  22. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/.github/workflows/lint.yml +0 -0
  23. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/.github/workflows/tests.yml +0 -0
  24. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/.gitignore +0 -0
  25. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/.pre-commit-config.yaml +0 -0
  26. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/LICENSE +0 -0
  27. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/README.md +0 -0
  28. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/__init__.py +0 -0
  29. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_async_clients/__init__.py +0 -0
  30. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_async_clients/_alm.py +0 -0
  31. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_async_clients/_audit.py +0 -0
  32. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_async_clients/_cloud_works.py +0 -0
  33. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_async_clients/_cw_flow.py +0 -0
  34. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_clients/__init__.py +0 -0
  35. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_clients/_alm.py +0 -0
  36. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_clients/_audit.py +0 -0
  37. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_clients/_cloud_works.py +0 -0
  38. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/_clients/_cw_flow.py +0 -0
  39. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/exceptions.py +0 -0
  40. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/models/__init__.py +0 -0
  41. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/models/_alm.py +0 -0
  42. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/models/_bulk.py +0 -0
  43. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/models/_transactional.py +0 -0
  44. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/anaplan_sdk/models/flows.py +0 -0
  45. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/anaplan_explained.md +0 -0
  46. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/async/async_alm_client.md +0 -0
  47. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/async/async_audit_client.md +0 -0
  48. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/async/async_client.md +0 -0
  49. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/async/async_cw_client.md +0 -0
  50. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/async/async_flows_client.md +0 -0
  51. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/async/async_transactional_client.md +0 -0
  52. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/exceptions.md +0 -0
  53. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/models.md +0 -0
  54. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/sync/sync_alm_client.md +0 -0
  55. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/sync/sync_audit_client.md +0 -0
  56. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/sync/sync_client.md +0 -0
  57. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/sync/sync_cw_client.md +0 -0
  58. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/sync/sync_flows_client.md +0 -0
  59. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/api/sync/sync_transactional_client.md +0 -0
  60. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/assets/overview.html +0 -0
  61. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/css/styles.css +0 -0
  62. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/alm.md +0 -0
  63. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/audit.md +0 -0
  64. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/bulk.md +0 -0
  65. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/bulk_vs_transactional.md +0 -0
  66. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/cloud_works.md +0 -0
  67. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/index.md +0 -0
  68. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/logging.md +0 -0
  69. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/multiple_models.md +0 -0
  70. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/guides/transactional.md +0 -0
  71. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/index.md +0 -0
  72. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/installation.md +0 -0
  73. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/js/assets/hljs.js +0 -0
  74. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/js/assets/hljs.min.js +0 -0
  75. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/js/assets/python.js +0 -0
  76. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/js/assets/python.min.js +0 -0
  77. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/js/highlight.js +0 -0
  78. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/js/highlight.min.js +0 -0
  79. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/docs/quickstart.md +0 -0
  80. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/mkdocs.yml +0 -0
  81. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/async/test_async_alm_client.py +0 -0
  82. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/async/test_async_audit_client.py +0 -0
  83. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/async/test_async_client.py +0 -0
  84. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/async/test_async_cloud_works_client.py +0 -0
  85. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/async/test_async_flows_client.py +0 -0
  86. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/sync/test_alm_client.py +0 -0
  87. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/sync/test_audit_client.py +0 -0
  88. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/sync/test_client.py +0 -0
  89. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/sync/test_cloud_works_client.py +0 -0
  90. {anaplan_sdk-0.4.0a2 → anaplan_sdk-0.4.1}/tests/sync/test_flows_client.py +0 -0
@@ -1,19 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anaplan-sdk
3
- Version: 0.4.0a2
4
- Summary: Provides pythonic access to the Anaplan API
3
+ Version: 0.4.1
4
+ Summary: Streamlined Python Interface for Anaplan
5
5
  Project-URL: Homepage, https://vinzenzklass.github.io/anaplan-sdk/
6
6
  Project-URL: Repository, https://github.com/VinzenzKlass/anaplan-sdk
7
7
  Project-URL: Documentation, https://vinzenzklass.github.io/anaplan-sdk/
8
8
  Author-email: Vinzenz Klass <vinzenz.klass@valantic.com>
9
9
  License-Expression: Apache-2.0
10
10
  License-File: LICENSE
11
- Keywords: anaplan,anaplan alm api,anaplan api,anaplan audit api,anaplan bulk api,anaplan integration
11
+ Keywords: anaplan,anaplan alm api,anaplan api,anaplan audit api,anaplan bulk api,anaplan cloudworks api,anaplan integration,anaplan integration api,anaplan sdk,anaplan transactional api
12
12
  Requires-Python: >=3.10.4
13
13
  Requires-Dist: httpx<1.0.0,>=0.27.0
14
14
  Requires-Dist: pydantic<3.0.0,>=2.7.2
15
15
  Provides-Extra: cert
16
- Requires-Dist: cryptography<45.0.0,>=42.0.7; extra == 'cert'
16
+ Requires-Dist: cryptography<46.0.0,>=42.0.7; extra == 'cert'
17
17
  Provides-Extra: oauth
18
18
  Requires-Dist: oauthlib<4.0.0,>=3.0.0; extra == 'oauth'
19
19
  Description-Content-Type: text/markdown
@@ -56,6 +56,7 @@ class AsyncClient(_AsyncBaseClient):
56
56
  redirect_uri: str | None = None,
57
57
  refresh_token: str | None = None,
58
58
  oauth2_scope: str = "openid profile email offline_access",
59
+ on_auth_code: Callable[[str], str] | None = None,
59
60
  on_token_refresh: Callable[[dict[str, str]], None] | None = None,
60
61
  timeout: float | httpx.Timeout = 30,
61
62
  retry_count: int = 2,
@@ -91,6 +92,9 @@ class AsyncClient(_AsyncBaseClient):
91
92
  :param refresh_token: If you have a valid refresh token, you can pass it to skip the
92
93
  interactive authentication code step.
93
94
  :param oauth2_scope: The scope of the Oauth2 token, if you want to narrow it.
95
+ :param on_auth_code: A callback that takes the redirect URI as a single argument and must
96
+ return the entire response URI. This will substitute the interactive
97
+ authentication code step in the terminal.
94
98
  :param on_token_refresh: A callback function that is called whenever the token is refreshed.
95
99
  With this you can for example securely store the token in your
96
100
  application or on your server for later reuse. The function
@@ -124,6 +128,7 @@ class AsyncClient(_AsyncBaseClient):
124
128
  redirect_uri=redirect_uri,
125
129
  refresh_token=refresh_token,
126
130
  oauth2_scope=oauth2_scope,
131
+ on_auth_code=on_auth_code,
127
132
  on_token_refresh=on_token_refresh,
128
133
  )
129
134
  ),
@@ -92,7 +92,15 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
92
92
  """
93
93
  Insert new items to the given list. The items must be a list of dictionaries with at least
94
94
  the keys `code` and `name`. You can optionally pass further keys for parents, extra
95
- properties etc.
95
+ properties etc. If you pass a long list, it will be split into chunks of 100,000 items, the
96
+ maximum allowed by the API.
97
+
98
+ **Warning**: If one or some of the requests timeout during large batch operations, the
99
+ operation may actually complete on the server. Retries for these chunks will then report
100
+ these items as "ignored" rather than "added", leading to misleading results. The results in
101
+ Anaplan will be correct, but this function may report otherwise. Be generous with your
102
+ timeouts and retries if you are using this function for large batch operations.
103
+
96
104
  :param list_id: The ID of the List.
97
105
  :param items: The items to insert into the List.
98
106
  :return: The result of the insertion, indicating how many items were added,
@@ -123,7 +131,16 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
123
131
 
124
132
  async def delete_list_items(self, list_id: int, items: list[dict[str, str | int]]) -> int:
125
133
  """
126
- Deletes items from a List.
134
+ Deletes items from a List. If you pass a long list, it will be split into chunks of 100,000
135
+ items, the maximum allowed by the API.
136
+
137
+ **Warning**: If one or some of the requests timeout during large batch operations, the
138
+ operation may actually complete on the server. Retries for these chunks will then report
139
+ none of these items as deleted, since on the retry none are removed, leading to misleading
140
+ results. The results in Anaplan will be correct, but this function may report otherwise.
141
+ Be generous with your timeouts and retries if you are using this function for large batch
142
+ operations.
143
+
127
144
  :param list_id: The ID of the List.
128
145
  :param items: The items to delete from the List. Must be a dict with either `code` or `id`
129
146
  as the keys to identify the records to delete.
@@ -158,8 +175,13 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
158
175
  """
159
176
  Write the passed items to the specified module. If successful, the number of cells changed
160
177
  is returned, if only partially successful or unsuccessful, the response with the according
161
- details is returned instead. For more details,
162
- see: https://anaplan.docs.apiary.io/#UpdateModuleCellData.
178
+ details is returned instead.
179
+
180
+ **You can update a maximum of 100,000 cells or 15 MB of data (whichever is lower) in a
181
+ single request.** You must chunk your data accordingly. This is not done by this SDK,
182
+ since it is discouraged. For larger imports, you should use the Bulk API instead.
183
+
184
+ For more details see: https://anaplan.docs.apiary.io/#UpdateModuleCellData.
163
185
  :param module_id: The ID of the Module.
164
186
  :param data: The data to write to the Module.
165
187
  :return: The number of cells changed or the response with the according error details.
@@ -144,6 +144,7 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
144
144
  redirect_uri: str,
145
145
  refresh_token: str | None = None,
146
146
  scope: str = "openid profile email offline_access",
147
+ on_auth_code: Callable[[str], str] | None = None,
147
148
  on_token_refresh: Callable[[dict[str, str]], None] | None = None,
148
149
  ):
149
150
  try:
@@ -163,6 +164,7 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
163
164
  self._refresh_token = refresh_token
164
165
  self._scope = scope
165
166
  self._id_token = None
167
+ self._on_auth_code = on_auth_code
166
168
  self._on_token_refresh = on_token_refresh
167
169
  if not refresh_token:
168
170
  self.__auth_code_flow()
@@ -195,13 +197,17 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
195
197
  try:
196
198
  logger.info("Creating Authentication Token with OAuth2 Authorization Code Flow.")
197
199
  url, _, _ = self._oauth.prepare_authorization_request(
198
- "https://us1a.app.anaplan.com/auth/authorize",
200
+ "https://us1a.app.anaplan.com/auth/prelogin",
199
201
  redirect_url=self._redirect_uri,
200
202
  scope=self._scope,
201
203
  )
202
- authorization_response = input(
203
- f"Please go to {url} and authorize the app.\n"
204
- "Then paste the entire redirect URL here: "
204
+ authorization_response = (
205
+ self._on_auth_code(url)
206
+ if self._on_auth_code
207
+ else input(
208
+ f"Please go to {url} and authorize the app.\n"
209
+ "Then paste the entire redirect URL here: "
210
+ )
205
211
  )
206
212
  url, headers, body = self._oauth.prepare_token_request(
207
213
  token_url=self._token_url,
@@ -211,7 +217,7 @@ class AnaplanOauth2AuthCodeAuth(_AnaplanAuth):
211
217
  )
212
218
  self._parse_auth_response(httpx.post(url=url, headers=headers, content=body))
213
219
  except (httpx.HTTPError, ValueError, TypeError, OAuth2Error) as error:
214
- raise AnaplanException("Error during OAuth2 authorization flow.") from error
220
+ raise InvalidCredentialsException("Error during OAuth2 authorization flow.") from error
215
221
 
216
222
 
217
223
  def create_auth(
@@ -225,6 +231,7 @@ def create_auth(
225
231
  redirect_uri: str | None = None,
226
232
  refresh_token: str | None = None,
227
233
  oauth2_scope: str = "openid profile email offline_access",
234
+ on_auth_code: Callable[[str], str] | None = None,
228
235
  on_token_refresh: Callable[[dict[str, str]], None] | None = None,
229
236
  ) -> _AnaplanAuth:
230
237
  if certificate and private_key:
@@ -238,6 +245,7 @@ def create_auth(
238
245
  redirect_uri=redirect_uri,
239
246
  refresh_token=refresh_token,
240
247
  scope=oauth2_scope,
248
+ on_auth_code=on_auth_code,
241
249
  on_token_refresh=on_token_refresh,
242
250
  )
243
251
  raise ValueError(
@@ -12,11 +12,7 @@ from typing import Any, Callable, Coroutine, Iterator, Literal, Type, TypeVar
12
12
  import httpx
13
13
  from httpx import HTTPError, Response
14
14
 
15
- from .exceptions import (
16
- AnaplanException,
17
- AnaplanTimeoutException,
18
- InvalidIdentifierException,
19
- )
15
+ from .exceptions import AnaplanException, AnaplanTimeoutException, InvalidIdentifierException
20
16
  from .models import AnaplanModel
21
17
  from .models.cloud_works import (
22
18
  AmazonS3ConnectionInput,
@@ -58,6 +58,7 @@ class Client(_BaseClient):
58
58
  redirect_uri: str | None = None,
59
59
  refresh_token: str | None = None,
60
60
  oauth2_scope: str = "openid profile email offline_access",
61
+ on_auth_code: Callable[[str], str] | None = None,
61
62
  on_token_refresh: Callable[[dict[str, str]], None] | None = None,
62
63
  timeout: float | httpx.Timeout = 30,
63
64
  retry_count: int = 2,
@@ -94,6 +95,9 @@ class Client(_BaseClient):
94
95
  :param refresh_token: If you have a valid refresh token, you can pass it to skip the
95
96
  interactive authentication code step.
96
97
  :param oauth2_scope: The scope of the Oauth2 token, if you want to narrow it.
98
+ :param on_auth_code: A callback that takes the redirect URI as a single argument and must
99
+ return the entire response URI. This will substitute the interactive
100
+ authentication code step in the terminal.
97
101
  :param on_token_refresh: A callback function that is called whenever the token is refreshed.
98
102
  With this you can for example securely store the token in your
99
103
  application or on your server for later reuse. The function
@@ -130,6 +134,7 @@ class Client(_BaseClient):
130
134
  redirect_uri=redirect_uri,
131
135
  refresh_token=refresh_token,
132
136
  oauth2_scope=oauth2_scope,
137
+ on_auth_code=on_auth_code,
133
138
  on_token_refresh=on_token_refresh,
134
139
  )
135
140
  ),
@@ -473,8 +478,7 @@ class Client(_BaseClient):
473
478
  :return: The identifier of the spawned Task.
474
479
  """
475
480
  response = self._post(
476
- f"{self._url}/{action_url(action_id)}/{action_id}/tasks",
477
- json={"localeName": "en_US"},
481
+ f"{self._url}/{action_url(action_id)}/{action_id}/tasks", json={"localeName": "en_US"}
478
482
  )
479
483
  task_id = response.get("task").get("taskId")
480
484
  logger.info(f"Invoked Action '{action_id}', spawned Task: '{task_id}'.")
@@ -86,7 +86,15 @@ class _TransactionalClient(_BaseClient):
86
86
  """
87
87
  Insert new items to the given list. The items must be a list of dictionaries with at least
88
88
  the keys `code` and `name`. You can optionally pass further keys for parents, extra
89
- properties etc.
89
+ properties etc. If you pass a long list, it will be split into chunks of 100,000 items, the
90
+ maximum allowed by the API.
91
+
92
+ **Warning**: If one or some of the requests timeout during large batch operations, the
93
+ operation may actually complete on the server. Retries for these chunks will then report
94
+ these items as "ignored" rather than "added", leading to misleading results. The results in
95
+ Anaplan will be correct, but this function may report otherwise. Be generous with your
96
+ timeouts and retries if you are using this function for large batch operations.
97
+
90
98
  :param list_id: The ID of the List.
91
99
  :param items: The items to insert into the List.
92
100
  :return: The result of the insertion, indicating how many items were added,
@@ -120,7 +128,16 @@ class _TransactionalClient(_BaseClient):
120
128
 
121
129
  def delete_list_items(self, list_id: int, items: list[dict[str, str | int]]) -> int:
122
130
  """
123
- Deletes items from a List.
131
+ Deletes items from a List. If you pass a long list, it will be split into chunks of 100,000
132
+ items, the maximum allowed by the API.
133
+
134
+ **Warning**: If one or some of the requests timeout during large batch operations, the
135
+ operation may actually complete on the server. Retries for these chunks will then report
136
+ none of these items as deleted, since on the retry none are removed, leading to misleading
137
+ results. The results in Anaplan will be correct, but this function may report otherwise.
138
+ Be generous with your timeouts and retries if you are using this function for large batch
139
+ operations.
140
+
124
141
  :param list_id: The ID of the List.
125
142
  :param items: The items to delete from the List. Must be a dict with either `code` or `id`
126
143
  as the keys to identify the records to delete.
@@ -155,8 +172,13 @@ class _TransactionalClient(_BaseClient):
155
172
  """
156
173
  Write the passed items to the specified module. If successful, the number of cells changed
157
174
  is returned, if only partially successful or unsuccessful, the response with the according
158
- details is returned instead. For more details,
159
- see: https://anaplan.docs.apiary.io/#UpdateModuleCellData.
175
+ details is returned instead.
176
+
177
+ **You can update a maximum of 100,000 cells or 15 MB of data (whichever is lower) in a
178
+ single request.** You must chunk your data accordingly. This is not done by this SDK,
179
+ since it is discouraged. For larger imports, you should use the Bulk API instead.
180
+
181
+ For more details see: https://anaplan.docs.apiary.io/#UpdateModuleCellData.
160
182
  :param module_id: The ID of the Module.
161
183
  :param data: The data to write to the Module.
162
184
  :return: The number of cells changed or the response with the according error details.
@@ -6,12 +6,7 @@ class AnaplanModel(BaseModel):
6
6
  model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
7
7
 
8
8
  @field_serializer(
9
- "action_id",
10
- "file_id",
11
- "process_id",
12
- "id",
13
- when_used="unless-none",
14
- check_fields=False,
9
+ "action_id", "file_id", "process_id", "id", when_used="unless-none", check_fields=False
15
10
  ) # While these are of type int, they are serialized as strings in the API payloads
16
11
  def str_serializer(self, v: int) -> str:
17
12
  return str(v)
@@ -262,8 +262,7 @@ class SingleIntegration(Integration):
262
262
 
263
263
  class AnaplanSource(AnaplanModel):
264
264
  type: Literal["Anaplan"] = Field(
265
- default="Anaplan",
266
- description="Literal signifying this is an Anaplan source.",
265
+ default="Anaplan", description="Literal signifying this is an Anaplan source."
267
266
  )
268
267
  action_id: int = Field(
269
268
  description=(
@@ -294,20 +293,18 @@ class TableSource(AnaplanModel):
294
293
 
295
294
  class TableTarget(TableSource):
296
295
  overwrite: bool = Field(
297
- default=False,
298
- description="Whether to overwrite the table if it exists.",
296
+ default=False, description="Whether to overwrite the table if it exists."
299
297
  )
300
298
 
301
299
 
302
300
  class AnaplanTarget(AnaplanModel):
303
301
  type: Literal["Anaplan"] = Field(
304
- default="Anaplan",
305
- description="Literal signifying this is an Anaplan target.",
302
+ default="Anaplan", description="Literal signifying this is an Anaplan target."
306
303
  )
307
304
  action_id: int = Field(
308
305
  description=(
309
306
  "The ID of the action to be used as a target. This can be a process, or import."
310
- ),
307
+ )
311
308
  )
312
309
  file_id: int = Field(description="The ID of the file to be used as a target.")
313
310
 
@@ -342,8 +339,7 @@ class IntegrationInput(AnaplanModel):
342
339
  ),
343
340
  )
344
341
  nux_visible: bool = Field(
345
- default=False,
346
- description="Whether this integration is visible in the UI.",
342
+ default=False, description="Whether this integration is visible in the UI."
347
343
  )
348
344
  jobs: list[IntegrationJobInput] = Field(
349
345
  description="The jobs in this integration.", min_length=1
@@ -142,6 +142,41 @@ will need to copy the entire redirect URI from your browser and paste it into th
142
142
  Unfortunately, registering localhost redirect URIs is not supported by Anaplan. This means we cannot intercept the
143
143
  redirect URI and extract the `authorization_code` automatically. This is a limitation of Anaplan's OAuth2 implementation. See [this Community Note](https://community.anaplan.com/discussion/156599/oauth-rediredt-url-port-for-desktop-apps).
144
144
 
145
+ ### Authorization Code
146
+
147
+ When using OAuth authentication, the default behavior prompts you to manually open a URL, authorize the application, and paste the redirect URL back into your terminal. However, you can customize this flow by providing the `on_auth_code` callback.
148
+
149
+ The `on_auth_code` callback lets you hook into the Auth Flow to handle the authorization URL programmatically and return the authorization response. `on_auth_code` must be a callable that takes the authorization URL as a single argument of type `str` and returns the redirect URL as a `str`.
150
+
151
+ === "Synchronous"
152
+ ```python
153
+ def on_auth_code(redirect_uri: str) -> str:
154
+ return input(f"Go fetch! {redirect_uri}\nPaste here: ")
155
+
156
+ anaplan = anaplan_sdk.Client(
157
+ workspace_id="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
158
+ model_id="11111111111111111111111111111111",
159
+ redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
160
+ client_id="my_anaplan_oauth_client_id",
161
+ client_secret="my_anaplan_oauth_client_secret",
162
+ on_auth_code=on_auth_code,
163
+ )
164
+ ```
165
+ === "Asynchronous"
166
+ ```python
167
+ def on_auth_code(redirect_uri: str) -> str:
168
+ return input(f"Go fetch! {redirect_uri}\nPaste here: ")
169
+
170
+ anaplan = anaplan_sdk.AsyncClient(
171
+ workspace_id="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
172
+ model_id="11111111111111111111111111111111",
173
+ redirect_uri="https://vinzenzklass.github.io/anaplan-sdk",
174
+ client_id="my_anaplan_oauth_client_id",
175
+ client_secret="my_anaplan_oauth_client_secret",
176
+ on_auth_code=on_auth_code,
177
+ )
178
+ ```
179
+
145
180
  ### Refresh Tokens
146
181
 
147
182
  You can extend the above example to also pass a `refresh_token` to authenticate without any user interaction, and
@@ -1,11 +1,22 @@
1
1
  [project]
2
2
  name = "anaplan-sdk"
3
- version = "0.4.0a2"
4
- description = "Provides pythonic access to the Anaplan API"
3
+ version = "0.4.1"
4
+ description = "Streamlined Python Interface for Anaplan"
5
5
  license = "Apache-2.0"
6
6
  authors = [{ name = "Vinzenz Klass", email = "vinzenz.klass@valantic.com" }]
7
7
  readme = "README.md"
8
- keywords = ["anaplan", "anaplan api", "anaplan bulk api", "anaplan integration", "anaplan alm api", "anaplan audit api"]
8
+ keywords = [
9
+ "anaplan",
10
+ "anaplan api",
11
+ "anaplan sdk",
12
+ "anaplan bulk api",
13
+ "anaplan integration",
14
+ "anaplan integration api",
15
+ "anaplan alm api",
16
+ "anaplan audit api",
17
+ "anaplan cloudworks api",
18
+ "anaplan transactional api",
19
+ ]
9
20
  requires-python = ">=3.10.4"
10
21
  dependencies = [
11
22
  "pydantic>=2.7.2,<3.0.0",
@@ -13,7 +24,7 @@ dependencies = [
13
24
  ]
14
25
 
15
26
  [project.optional-dependencies]
16
- cert = ["cryptography>=42.0.7,<45.0.0"]
27
+ cert = ["cryptography>=42.0.7,<46.0.0"]
17
28
  oauth = ["oauthlib>=3.0.0,<4.0.0"]
18
29
 
19
30
  [dependency-groups]
@@ -29,8 +40,7 @@ dev = [
29
40
  "polars>=1.29.0",
30
41
  "griffe-fieldz>=0.2.1",
31
42
  "oauthlib>=3.2.2",
32
- "cryptography>=42.0.7",
33
- "pykeepass>=4.1.1.post1",
43
+ "cryptography>=45.0.0",
34
44
  ]
35
45
 
36
46
  [project.urls]
@@ -77,7 +87,7 @@ fix = true
77
87
  target-version = "py312"
78
88
 
79
89
  [tool.ruff.format]
80
- skip-magic-trailing-comma = false
90
+ skip-magic-trailing-comma = true
81
91
 
82
92
  [tool.ruff.lint]
83
93
  select = ["E", "F", "B", "I"]
@@ -89,6 +99,4 @@ asyncio_default_test_loop_scope = "session"
89
99
  minversion = "8.0"
90
100
  addopts = "-ra -q"
91
101
  pythonpath = "anaplan_sdk/"
92
- testpaths = [
93
- "tests",
94
- ]
102
+ testpaths = ["tests"]
@@ -13,6 +13,7 @@ def client() -> AsyncClient:
13
13
  certificate=getenv("ANAPLAN_SDK_TEST_CERT"),
14
14
  private_key=getenv("ANAPLAN_SDK_TEST_PK"),
15
15
  retry_count=3,
16
+ timeout=120,
16
17
  )
17
18
 
18
19
 
@@ -33,6 +33,8 @@ async def test_get_model_status(client: AsyncClient):
33
33
  async def test_long_list_insertion(client: AsyncClient, test_list, list_items_long):
34
34
  result = await client.transactional.insert_list_items(test_list, list_items_long)
35
35
  assert isinstance(result, InsertionResult)
36
+ assert result.failures == []
37
+ assert result.added == 200_000
36
38
  assert result.total == 200_000
37
39
 
38
40
 
@@ -44,6 +46,8 @@ async def test_long_list_deletion(client: AsyncClient, test_list, list_items_lon
44
46
  async def test_short_list_insertion(client: AsyncClient, test_list, list_items_short):
45
47
  result = await client.transactional.insert_list_items(test_list, list_items_short)
46
48
  assert isinstance(result, InsertionResult)
49
+ assert result.failures == []
50
+ assert result.added == 1_000
47
51
  assert result.total == 1_000
48
52
 
49
53
 
@@ -247,12 +247,7 @@ def notification_dict(name):
247
247
  "integrationIds": [],
248
248
  "channels": ["in_app"],
249
249
  "notifications": {
250
- "config": [
251
- {
252
- "type": "full_failure",
253
- "users": ["8a868cd97f8f98a3017fe45cbdc65e25"],
254
- }
255
- ],
250
+ "config": [{"type": "full_failure", "users": ["8a868cd97f8f98a3017fe45cbdc65e25"]}]
256
251
  },
257
252
  }
258
253
 
@@ -13,6 +13,7 @@ def client() -> Client:
13
13
  certificate=getenv("ANAPLAN_SDK_TEST_CERT"),
14
14
  private_key=getenv("ANAPLAN_SDK_TEST_PK"),
15
15
  retry_count=3,
16
+ timeout=120,
16
17
  )
17
18
 
18
19
 
@@ -33,6 +33,8 @@ def test_get_model_status(client: Client):
33
33
  def test_long_list_insertion(client: Client, test_list, list_items_long):
34
34
  result = client.transactional.insert_list_items(test_list, list_items_long)
35
35
  assert isinstance(result, InsertionResult)
36
+ assert result.failures == []
37
+ assert result.added == 200_000
36
38
  assert result.total == 200_000
37
39
 
38
40
 
@@ -44,6 +46,8 @@ def test_long_list_deletion(client: Client, test_list, list_items_long):
44
46
  def test_short_list_insertion(client: Client, test_list, list_items_short):
45
47
  result = client.transactional.insert_list_items(test_list, list_items_short)
46
48
  assert isinstance(result, InsertionResult)
49
+ assert result.failures == []
50
+ assert result.added == 1_000
47
51
  assert result.total == 1_000
48
52
 
49
53