anaplan-sdk 0.5.0a4__py3-none-any.whl → 0.5.0a6__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.
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
  from typing import Literal, overload
3
3
 
4
- from anaplan_sdk._services import _AsyncHttpService, sort_params
4
+ from anaplan_sdk._services import _AsyncHttpService
5
+ from anaplan_sdk._utils import sort_params
5
6
  from anaplan_sdk.exceptions import AnaplanActionError
6
7
  from anaplan_sdk.models import (
7
8
  ModelRevision,
@@ -1,6 +1,7 @@
1
1
  from typing import Any, Literal
2
2
 
3
- from anaplan_sdk._services import _AsyncHttpService, sort_params
3
+ from anaplan_sdk._services import _AsyncHttpService
4
+ from anaplan_sdk._utils import sort_params
4
5
  from anaplan_sdk.models import User
5
6
 
6
7
  Event = Literal["all", "byok", "user_activity"]
@@ -21,7 +22,9 @@ class _AsyncAuditClient:
21
22
  ) -> list[User]:
22
23
  """
23
24
  Lists all the Users in the authenticated users default tenant.
24
- :param search_pattern: Optionally filter for specific users. When provided,
25
+ :param search_pattern: **Caution: This is an undocumented Feature and may behave
26
+ unpredictably. It requires the Tenant Admin role. For non-admin users, it is
27
+ ignored.** Optionally filter for specific users. When provided,
25
28
  case-insensitive matches users with emails or names containing this string.
26
29
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
27
30
  When None (default), returns all users.
@@ -7,7 +7,8 @@ import httpx
7
7
  from typing_extensions import Self
8
8
 
9
9
  from anaplan_sdk._auth import _create_auth
10
- from anaplan_sdk._services import _AsyncHttpService, action_url, sort_params
10
+ from anaplan_sdk._services import _AsyncHttpService
11
+ from anaplan_sdk._utils import action_url, models_url, sort_params
11
12
  from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
12
13
  from anaplan_sdk.models import (
13
14
  Action,
@@ -142,34 +143,24 @@ class AsyncClient:
142
143
  f"Initialized AsyncClient with workspace_id={workspace_id}, model_id={model_id}"
143
144
  )
144
145
 
145
- @classmethod
146
- def from_existing(
147
- cls, existing: Self, *, workspace_id: str | None = None, model_id: str | None = None
148
- ) -> Self:
146
+ def with_model(self, model_id: str | None = None, workspace_id: str | None = None) -> Self:
149
147
  """
150
- Create a new instance of the Client from an existing instance. This is useful if you want
151
- to interact with multiple models or workspaces in the same script but share the same
152
- authentication and configuration. This creates a shallow copy of the existing client and
153
- optionally updates the relevant attributes to the new workspace and model. You can provide
154
- either a new workspace Id or a new model Id, or both. If you do not provide one of them,
155
- the existing value will be used. If you omit both, the new instance will be an identical
156
- copy of the existing instance.
157
-
158
- :param existing: The existing instance to copy.
148
+ Create a new instance of the Client with the given model and workspace Ids. **This creates
149
+ a copy of the current client. The current instance remains unchanged.**
159
150
  :param workspace_id: The workspace Id to use or None to use the existing workspace Id.
160
151
  :param model_id: The model Id to use or None to use the existing model Id.
161
152
  :return: A new instance of the Client.
162
153
  """
163
- client = copy(existing)
164
- new_ws_id = workspace_id or existing._workspace_id
165
- new_model_id = model_id or existing._model_id
154
+ client = copy(self)
155
+ new_ws_id = workspace_id or self._workspace_id
156
+ new_model_id = model_id or self._model_id
166
157
  logger.debug(
167
158
  f"Creating a new AsyncClient from existing instance "
168
159
  f"with workspace_id={new_ws_id}, model_id={new_model_id}."
169
160
  )
170
161
  client._url = f"https://api.anaplan.com/2/0/workspaces/{new_ws_id}/models/{new_model_id}"
171
- client._transactional_client = _AsyncTransactionalClient(existing._http, new_model_id)
172
- client._alm_client = _AsyncAlmClient(existing._http, new_model_id)
162
+ client._transactional_client = _AsyncTransactionalClient(self._http, new_model_id)
163
+ client._alm_client = _AsyncAlmClient(self._http, new_model_id)
173
164
  return client
174
165
 
175
166
  @property
@@ -235,7 +226,9 @@ class AsyncClient:
235
226
  ) -> list[Workspace]:
236
227
  """
237
228
  Lists all the Workspaces the authenticated user has access to.
238
- :param search_pattern: Optionally filter for specific workspaces. When provided,
229
+ :param search_pattern: **Caution: This is an undocumented Feature and may behave
230
+ unpredictably. It requires the Tenant Admin role. For non-admin users, it is
231
+ ignored.** Optionally filter for specific workspaces. When provided,
239
232
  case-insensitive matches workspaces with names containing this string.
240
233
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
241
234
  When None (default), returns all users.
@@ -253,13 +246,19 @@ class AsyncClient:
253
246
 
254
247
  async def get_models(
255
248
  self,
249
+ only_in_workspace: bool | str = False,
256
250
  search_pattern: str | None = None,
257
251
  sort_by: Literal["active_state", "name"] | None = None,
258
252
  descending: bool = False,
259
253
  ) -> list[Model]:
260
254
  """
261
255
  Lists all the Models the authenticated user has access to.
262
- :param search_pattern: Optionally filter for specific models. When provided,
256
+ :param only_in_workspace: If True, only lists models in the workspace provided when
257
+ instantiating the client. If a string is provided, only lists models in the workspace
258
+ with the given Id. If False (default), lists models in all workspaces the user
259
+ :param search_pattern: **Caution: This is an undocumented Feature and may behave
260
+ unpredictably. It requires the Tenant Admin role. For non-admin users, it is
261
+ ignored.** Optionally filter for specific models. When provided,
263
262
  case-insensitive matches model names containing this string.
264
263
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
265
264
  When None (default), returns all models.
@@ -271,7 +270,7 @@ class AsyncClient:
271
270
  if search_pattern:
272
271
  params["s"] = search_pattern
273
272
  res = await self._http.get_paginated(
274
- "https://api.anaplan.com/2/0/models", "models", params=params
273
+ models_url(only_in_workspace, self._workspace_id), "models", params=params
275
274
  )
276
275
  return [Model.model_validate(e) for e in res]
277
276
 
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
  from typing import Any, Literal
3
3
 
4
- from anaplan_sdk._services import (
5
- _AsyncHttpService,
4
+ from anaplan_sdk._services import _AsyncHttpService
5
+ from anaplan_sdk._utils import (
6
6
  connection_body_payload,
7
7
  construct_payload,
8
8
  integration_payload,
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
  from typing import Any
3
3
 
4
- from anaplan_sdk._services import _AsyncHttpService, construct_payload
4
+ from anaplan_sdk._services import _AsyncHttpService
5
+ from anaplan_sdk._utils import construct_payload
5
6
  from anaplan_sdk.models.flows import Flow, FlowInput, FlowSummary
6
7
 
7
8
  logger = logging.getLogger("anaplan_sdk")
@@ -3,8 +3,8 @@ from asyncio import gather
3
3
  from itertools import chain
4
4
  from typing import Any, Literal, overload
5
5
 
6
- from anaplan_sdk._services import (
7
- _AsyncHttpService,
6
+ from anaplan_sdk._services import _AsyncHttpService
7
+ from anaplan_sdk._utils import (
8
8
  parse_calendar_response,
9
9
  parse_insertion_response,
10
10
  sort_params,
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
  from typing import Literal, overload
3
3
 
4
- from anaplan_sdk._services import _HttpService, sort_params
4
+ from anaplan_sdk._services import _HttpService
5
+ from anaplan_sdk._utils import sort_params
5
6
  from anaplan_sdk.exceptions import AnaplanActionError
6
7
  from anaplan_sdk.models import (
7
8
  ModelRevision,
@@ -1,6 +1,7 @@
1
1
  from typing import Any, Literal
2
2
 
3
- from anaplan_sdk._services import _HttpService, sort_params
3
+ from anaplan_sdk._services import _HttpService
4
+ from anaplan_sdk._utils import sort_params
4
5
  from anaplan_sdk.models import User
5
6
 
6
7
  Event = Literal["all", "byok", "user_activity"]
@@ -21,7 +22,9 @@ class _AuditClient:
21
22
  ) -> list[User]:
22
23
  """
23
24
  Lists all the Users in the authenticated users default tenant.
24
- :param search_pattern: Optionally filter for specific users. When provided,
25
+ :param search_pattern: **Caution: This is an undocumented Feature and may behave
26
+ unpredictably. It requires the Tenant Admin role. For non-admin users, it is
27
+ ignored.** Optionally filter for specific users. When provided,
25
28
  case-insensitive matches users with emails or names containing this string.
26
29
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
27
30
  When None (default), returns all users.
@@ -8,7 +8,8 @@ import httpx
8
8
  from typing_extensions import Self
9
9
 
10
10
  from anaplan_sdk._auth import _create_auth
11
- from anaplan_sdk._services import _HttpService, action_url, sort_params
11
+ from anaplan_sdk._services import _HttpService
12
+ from anaplan_sdk._utils import action_url, models_url, sort_params
12
13
  from anaplan_sdk.exceptions import AnaplanActionError, InvalidIdentifierException
13
14
  from anaplan_sdk.models import (
14
15
  Action,
@@ -147,34 +148,24 @@ class Client:
147
148
  self.allow_file_creation = allow_file_creation
148
149
  logger.debug(f"Initialized Client with workspace_id={workspace_id}, model_id={model_id}")
149
150
 
150
- @classmethod
151
- def from_existing(
152
- cls, existing: Self, *, workspace_id: str | None = None, model_id: str | None = None
153
- ) -> Self:
151
+ def with_model(self, model_id: str | None = None, workspace_id: str | None = None) -> Self:
154
152
  """
155
- Create a new instance of the Client from an existing instance. This is useful if you want
156
- to interact with multiple models or workspaces in the same script but share the same
157
- authentication and configuration. This creates a shallow copy of the existing client and
158
- optionally updates the relevant attributes to the new workspace and model. You can provide
159
- either a new workspace Id or a new model Id, or both. If you do not provide one of them,
160
- the existing value will be used. If you omit both, the new instance will be an identical
161
- copy of the existing instance.
162
-
163
- :param existing: The existing instance to copy.
153
+ Create a new instance of the Client with the given model and workspace Ids. **This creates
154
+ a copy of the current client. The current instance remains unchanged.**
164
155
  :param workspace_id: The workspace Id to use or None to use the existing workspace Id.
165
156
  :param model_id: The model Id to use or None to use the existing model Id.
166
157
  :return: A new instance of the Client.
167
158
  """
168
- client = copy(existing)
169
- new_ws_id = workspace_id or existing._workspace_id
170
- new_model_id = model_id or existing._model_id
159
+ client = copy(self)
160
+ new_ws_id = workspace_id or self._workspace_id
161
+ new_model_id = model_id or self._model_id
171
162
  logger.debug(
172
163
  f"Creating a new AsyncClient from existing instance "
173
164
  f"with workspace_id={new_ws_id}, model_id={new_model_id}."
174
165
  )
175
166
  client._url = f"https://api.anaplan.com/2/0/workspaces/{new_ws_id}/models/{new_model_id}"
176
- client._transactional_client = _TransactionalClient(existing._http, new_model_id)
177
- client._alm_client = _AlmClient(existing._http, new_model_id)
167
+ client._transactional_client = _TransactionalClient(self._http, new_model_id)
168
+ client._alm_client = _AlmClient(self._http, new_model_id)
178
169
  return client
179
170
 
180
171
  @property
@@ -240,7 +231,9 @@ class Client:
240
231
  ) -> list[Workspace]:
241
232
  """
242
233
  Lists all the Workspaces the authenticated user has access to.
243
- :param search_pattern: Optionally filter for specific workspaces. When provided,
234
+ :param search_pattern: **Caution: This is an undocumented Feature and may behave
235
+ unpredictably. It requires the Tenant Admin role. For non-admin users, it is
236
+ ignored.** Optionally filter for specific workspaces. When provided,
244
237
  case-insensitive matches workspaces with names containing this string.
245
238
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
246
239
  When None (default), returns all users.
@@ -258,13 +251,19 @@ class Client:
258
251
 
259
252
  def get_models(
260
253
  self,
254
+ only_in_workspace: bool | str = False,
261
255
  search_pattern: str | None = None,
262
256
  sort_by: Literal["active_state", "name"] | None = None,
263
257
  descending: bool = False,
264
258
  ) -> list[Model]:
265
259
  """
266
260
  Lists all the Models the authenticated user has access to.
267
- :param search_pattern: Optionally filter for specific models. When provided,
261
+ :param only_in_workspace: If True, only lists models in the workspace provided when
262
+ instantiating the client. If a string is provided, only lists models in the workspace
263
+ with the given Id. If False (default), lists models in all workspaces the user
264
+ :param search_pattern: **Caution: This is an undocumented Feature and may behave
265
+ unpredictably. It requires the Tenant Admin role. For non-admin users, it is
266
+ ignored.** Optionally filter for specific models. When provided,
268
267
  case-insensitive matches model names containing this string.
269
268
  You can use the wildcards `%` for 0-n characters, and `_` for exactly 1 character.
270
269
  When None (default), returns all models.
@@ -276,7 +275,7 @@ class Client:
276
275
  if search_pattern:
277
276
  params["s"] = search_pattern
278
277
  res = self._http.get_paginated(
279
- "https://api.anaplan.com/2/0/models", "models", params=params
278
+ models_url(only_in_workspace, self._workspace_id), "models", params=params
280
279
  )
281
280
  return [Model.model_validate(e) for e in res]
282
281
 
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
  from typing import Any, Literal
3
3
 
4
- from anaplan_sdk._services import (
5
- _HttpService,
4
+ from anaplan_sdk._services import _HttpService
5
+ from anaplan_sdk._utils import (
6
6
  connection_body_payload,
7
7
  construct_payload,
8
8
  integration_payload,
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
  from typing import Any
3
3
 
4
- from anaplan_sdk._services import _HttpService, construct_payload
4
+ from anaplan_sdk._services import _HttpService
5
+ from anaplan_sdk._utils import construct_payload
5
6
  from anaplan_sdk.models.flows import Flow, FlowInput, FlowSummary
6
7
 
7
8
  logger = logging.getLogger("anaplan_sdk")
@@ -3,8 +3,8 @@ from concurrent.futures import ThreadPoolExecutor
3
3
  from itertools import chain
4
4
  from typing import Any, Literal, overload
5
5
 
6
- from anaplan_sdk._services import (
7
- _HttpService,
6
+ from anaplan_sdk._services import _HttpService
7
+ from anaplan_sdk._utils import (
8
8
  parse_calendar_response,
9
9
  parse_insertion_response,
10
10
  sort_params,
anaplan_sdk/_services.py CHANGED
@@ -6,32 +6,13 @@ from concurrent.futures import ThreadPoolExecutor
6
6
  from gzip import compress
7
7
  from itertools import chain
8
8
  from math import ceil
9
- from typing import Any, Awaitable, Callable, Coroutine, Iterator, Literal, Type, TypeVar
9
+ from typing import Any, Awaitable, Callable, Coroutine, Iterator, TypeVar
10
10
 
11
11
  import httpx
12
12
  from httpx import HTTPError, Response
13
- from pydantic.alias_generators import to_camel
14
13
 
15
14
  from .exceptions import AnaplanException, AnaplanTimeoutException, InvalidIdentifierException
16
- from .models import (
17
- AnaplanModel,
18
- InsertionResult,
19
- ModelCalendar,
20
- MonthsQuartersYearsCalendar,
21
- TaskSummary,
22
- WeeksGeneralCalendar,
23
- WeeksGroupingCalendar,
24
- WeeksPeriodsCalendar,
25
- )
26
- from .models.cloud_works import (
27
- AmazonS3ConnectionInput,
28
- AzureBlobConnectionInput,
29
- ConnectionBody,
30
- GoogleBigQueryConnectionInput,
31
- IntegrationInput,
32
- IntegrationProcessInput,
33
- ScheduleInput,
34
- )
15
+ from .models import TaskSummary
35
16
 
36
17
  SORT_WARNING = (
37
18
  "If you are sorting by a field that is potentially ambiguous (e.g., name), the order of "
@@ -47,7 +28,6 @@ logger = logging.getLogger("anaplan_sdk")
47
28
  _json_header = {"Content-Type": "application/json"}
48
29
  _gzip_header = {"Content-Type": "application/x-gzip"}
49
30
 
50
- T = TypeVar("T", bound=AnaplanModel)
51
31
  Task = TypeVar("Task", bound=TaskSummary)
52
32
 
53
33
 
@@ -134,15 +114,7 @@ class _HttpService:
134
114
  logger.debug(f"Fetching first page with limit={self._page_size} from {url}.")
135
115
  kwargs["params"] = (kwargs.get("params") or {}) | {"limit": self._page_size}
136
116
  res = self.get(url, **kwargs)
137
- total_items, first_page = res["meta"]["paging"]["totalSize"], res.get(result_key, [])
138
- actual_page_size = res["meta"]["paging"]["currentPageSize"]
139
- if actual_page_size < self._page_size and not actual_page_size == total_items:
140
- logger.warning(
141
- f"Page size {self._page_size} was silently truncated to {actual_page_size}."
142
- f"Using the server-side enforced page size {actual_page_size} for further requests."
143
- )
144
- logger.debug(f"Found {total_items} total items, retrieved {len(first_page)} in first page.")
145
- return first_page, total_items, actual_page_size
117
+ return _extract_first_page(res, result_key, self._page_size)
146
118
 
147
119
  def __run_with_retry(self, func: Callable[..., Response], *args, **kwargs) -> Response:
148
120
  for i in range(max(self._retry_count, 1)):
@@ -159,7 +131,7 @@ class _HttpService:
159
131
  return response
160
132
  except HTTPError as error:
161
133
  if i >= self._retry_count - 1:
162
- raise_error(error)
134
+ _raise_error(error)
163
135
  url = args[0] or kwargs.get("url")
164
136
  logger.info(f"Retrying for: {url}")
165
137
 
@@ -252,15 +224,7 @@ class _AsyncHttpService:
252
224
  logger.debug(f"Fetching first page with limit={self._page_size} from {url}.")
253
225
  kwargs["params"] = (kwargs.get("params") or {}) | {"limit": self._page_size}
254
226
  res = await self.get(url, **kwargs)
255
- total_items, first_page = res["meta"]["paging"]["totalSize"], res.get(result_key, [])
256
- actual_page_size = res["meta"]["paging"]["currentPageSize"]
257
- if actual_page_size < self._page_size and not actual_page_size == total_items:
258
- logger.warning(
259
- f"Page size {self._page_size} was silently truncated to {actual_page_size}."
260
- f"Using the server-side enforced page size {actual_page_size} for further requests."
261
- )
262
- logger.debug(f"Found {total_items} total items, retrieved {len(first_page)} in first page.")
263
- return first_page, total_items, actual_page_size
227
+ return _extract_first_page(res, result_key, self._page_size)
264
228
 
265
229
  async def _run_with_retry(
266
230
  self, func: Callable[..., Coroutine[Any, Any, Response]], *args, **kwargs
@@ -279,109 +243,28 @@ class _AsyncHttpService:
279
243
  return response
280
244
  except HTTPError as error:
281
245
  if i >= self._retry_count - 1:
282
- raise_error(error)
246
+ _raise_error(error)
283
247
  url = args[0] or kwargs.get("url")
284
248
  logger.info(f"Retrying for: {url}")
285
249
 
286
250
  raise AnaplanException("Exhausted all retries without a successful response or Error.")
287
251
 
288
252
 
289
- def sort_params(sort_by: str | None, descending: bool) -> dict[str, str | bool]:
290
- """
291
- Construct search parameters for sorting. This also converts snake_case to camelCase.
292
- :param sort_by: The field to sort by, optionally in snake_case.
293
- :param descending: Whether to sort in descending order.
294
- :return: A dictionary of search parameters in Anaplan's expected format.
295
- """
296
- if not sort_by:
297
- return {}
298
- return {"sort": f"{'-' if descending else '+'}{to_camel(sort_by)}"}
299
-
300
-
301
- def construct_payload(model: Type[T], body: T | dict[str, Any]) -> dict[str, Any]:
302
- """
303
- Construct a payload for the given model and body.
304
- :param model: The model class to use for validation.
305
- :param body: The body to validate and optionally convert to a dictionary.
306
- :return: A dictionary representation of the validated body.
307
- """
308
- if isinstance(body, dict):
309
- body = model.model_validate(body)
310
- return body.model_dump(exclude_none=True, by_alias=True)
311
-
312
-
313
- def connection_body_payload(body: ConnectionBody | dict[str, Any]) -> dict[str, Any]:
314
- """
315
- Construct a payload for the given integration body.
316
- :param body: The body to validate and optionally convert to a dictionary.
317
- :return: A dictionary representation of the validated body.
318
- """
319
- if isinstance(body, dict):
320
- if "sasToken" in body:
321
- body = AzureBlobConnectionInput.model_validate(body)
322
- elif "secretAccessKey" in body:
323
- body = AmazonS3ConnectionInput.model_validate(body)
324
- else:
325
- body = GoogleBigQueryConnectionInput.model_validate(body)
326
- return body.model_dump(exclude_none=True, by_alias=True)
327
-
328
-
329
- def integration_payload(
330
- body: IntegrationInput | IntegrationProcessInput | dict[str, Any],
331
- ) -> dict[str, Any]:
332
- """
333
- Construct a payload for the given integration body.
334
- :param body: The body to validate and optionally convert to a dictionary.
335
- :return: A dictionary representation of the validated body.
336
- """
337
- if isinstance(body, dict):
338
- body = (
339
- IntegrationInput.model_validate(body)
340
- if "jobs" in body
341
- else IntegrationProcessInput.model_validate(body)
253
+ def _extract_first_page(
254
+ res: dict[str, Any], result_key: str, page_size: int
255
+ ) -> tuple[list[dict[str, Any]], int, int]:
256
+ total_items, first_page = res["meta"]["paging"]["totalSize"], res.get(result_key, [])
257
+ actual_page_size = res["meta"]["paging"]["currentPageSize"]
258
+ if actual_page_size < page_size and not actual_page_size == total_items:
259
+ logger.warning(
260
+ f"Page size {page_size} was silently truncated to {actual_page_size}."
261
+ f"Using the server-side enforced page size {actual_page_size} for further requests."
342
262
  )
343
- return body.model_dump(exclude_none=True, by_alias=True)
344
-
345
-
346
- def schedule_payload(
347
- integration_id: str, schedule: ScheduleInput | dict[str, Any]
348
- ) -> dict[str, Any]:
349
- """
350
- Construct a payload for the given integration ID and schedule.
351
- :param integration_id: The ID of the integration.
352
- :param schedule: The schedule to validate and optionally convert to a dictionary.
353
- :return: A dictionary representation of the validated schedule.
354
- """
355
- if isinstance(schedule, dict):
356
- schedule = ScheduleInput.model_validate(schedule)
357
- return {
358
- "integrationId": integration_id,
359
- "schedule": schedule.model_dump(exclude_none=True, by_alias=True),
360
- }
361
-
362
-
363
- def action_url(action_id: int) -> Literal["imports", "exports", "actions", "processes"]:
364
- """
365
- Determine the type of action based on its identifier.
366
- :param action_id: The identifier of the action.
367
- :return: The type of action.
368
- """
369
- if 12000000000 <= action_id < 113000000000:
370
- return "imports"
371
- if 116000000000 <= action_id < 117000000000:
372
- return "exports"
373
- if 117000000000 <= action_id < 118000000000:
374
- return "actions"
375
- if 118000000000 <= action_id < 119000000000:
376
- return "processes"
377
- raise InvalidIdentifierException(f"Action '{action_id}' is not a valid identifier.")
378
-
379
-
380
- def raise_error(error: HTTPError) -> None:
381
- """
382
- Raise an appropriate exception based on the error.
383
- :param error: The error to raise an exception for.
384
- """
263
+ logger.debug(f"Found {total_items} total items, retrieved {len(first_page)} in first page.")
264
+ return first_page, total_items, actual_page_size
265
+
266
+
267
+ def _raise_error(error: HTTPError) -> None:
385
268
  if isinstance(error, httpx.TimeoutException):
386
269
  raise AnaplanTimeoutException from error
387
270
  if isinstance(error, httpx.HTTPStatusError):
@@ -392,59 +275,3 @@ def raise_error(error: HTTPError) -> None:
392
275
 
393
276
  logger.error(f"Error: {error}")
394
277
  raise AnaplanException from error
395
-
396
-
397
- def parse_calendar_response(data: dict) -> ModelCalendar:
398
- """
399
- Parse calendar response and return appropriate calendar model.
400
- :param data: The calendar data from the API response.
401
- :return: The calendar settings of the model based on calendar type.
402
- """
403
- calendar_data = data["modelCalendar"]
404
- cal_type = calendar_data["calendarType"]
405
- if cal_type == "Calendar Months/Quarters/Years":
406
- return MonthsQuartersYearsCalendar.model_validate(calendar_data)
407
- if cal_type == "Weeks: 4-4-5, 4-5-4 or 5-4-4":
408
- return WeeksGroupingCalendar.model_validate(calendar_data)
409
- if cal_type == "Weeks: General":
410
- return WeeksGeneralCalendar.model_validate(calendar_data)
411
- if cal_type == "Weeks: 13 4-week Periods":
412
- return WeeksPeriodsCalendar.model_validate(calendar_data)
413
- raise AnaplanException(
414
- "Unknown calendar type encountered. Please report this issue: "
415
- "https://github.com/VinzenzKlass/anaplan-sdk/issues/new"
416
- )
417
-
418
-
419
- def parse_insertion_response(data: list[dict]) -> InsertionResult:
420
- failures, added, ignored, total = [], 0, 0, 0
421
- for res in data:
422
- failures.append(res.get("failures", []))
423
- added += res.get("added", 0)
424
- total += res.get("total", 0)
425
- ignored += res.get("ignored", 0)
426
- return InsertionResult(
427
- added=added, ignored=ignored, total=total, failures=list(chain.from_iterable(failures))
428
- )
429
-
430
-
431
- def validate_dimension_id(dimension_id: int) -> int:
432
- if not (
433
- dimension_id == 101999999999
434
- or 101_000_000_000 <= dimension_id < 102_000_000_000
435
- or 109_000_000_000 <= dimension_id < 110_000_000_000
436
- or 114_000_000_000 <= dimension_id < 115_000_000_000
437
- ):
438
- raise InvalidIdentifierException(
439
- "Invalid dimension_id. Must be a List (101xxxxxxxxx), List Subset (109xxxxxxxxx), "
440
- "Line Item Subset (114xxxxxxxxx), or Users (101999999999)."
441
- )
442
- msg = (
443
- "Using `get_dimension_items` for {} is discouraged. "
444
- "Prefer `{}` for better performance and more details on the members."
445
- )
446
- if dimension_id == 101999999999:
447
- logger.warning(msg.format("Users", "get_users"))
448
- if 101000000000 <= dimension_id < 102000000000:
449
- logger.warning(msg.format("Lists", "get_list_items"))
450
- return dimension_id
anaplan_sdk/_utils.py ADDED
@@ -0,0 +1,188 @@
1
+ from itertools import chain
2
+ from typing import Any, Literal, Type, TypeVar
3
+
4
+ from pydantic.alias_generators import to_camel
5
+
6
+ from anaplan_sdk._services import logger
7
+ from anaplan_sdk.exceptions import AnaplanException, InvalidIdentifierException
8
+ from anaplan_sdk.models import (
9
+ AnaplanModel,
10
+ InsertionResult,
11
+ ModelCalendar,
12
+ MonthsQuartersYearsCalendar,
13
+ WeeksGeneralCalendar,
14
+ WeeksGroupingCalendar,
15
+ WeeksPeriodsCalendar,
16
+ )
17
+ from anaplan_sdk.models.cloud_works import (
18
+ AmazonS3ConnectionInput,
19
+ AzureBlobConnectionInput,
20
+ ConnectionBody,
21
+ GoogleBigQueryConnectionInput,
22
+ IntegrationInput,
23
+ IntegrationProcessInput,
24
+ ScheduleInput,
25
+ )
26
+
27
+ T = TypeVar("T", bound=AnaplanModel)
28
+
29
+
30
+ def models_url(only_in_workspace: bool | str, workspace_id: str | None) -> str:
31
+ if isinstance(only_in_workspace, bool) and only_in_workspace:
32
+ if not workspace_id:
33
+ raise ValueError(
34
+ "Cannot list models in the current workspace, since no workspace Id was "
35
+ "provided when instantiating the client. Either provide a workspace Id when "
36
+ "instantiating the client, or pass a specific workspace Id to this method."
37
+ )
38
+ return f"https://api.anaplan.com/2/0/workspaces/{workspace_id}/models"
39
+ if isinstance(only_in_workspace, str):
40
+ return f"https://api.anaplan.com/2/0/workspaces/{only_in_workspace}/models"
41
+ return "https://api.anaplan.com/2/0/models"
42
+
43
+
44
+ def sort_params(sort_by: str | None, descending: bool) -> dict[str, str | bool]:
45
+ """
46
+ Construct search parameters for sorting. This also converts snake_case to camelCase.
47
+ :param sort_by: The field to sort by, optionally in snake_case.
48
+ :param descending: Whether to sort in descending order.
49
+ :return: A dictionary of search parameters in Anaplan's expected format.
50
+ """
51
+ if not sort_by:
52
+ return {}
53
+ return {"sort": f"{'-' if descending else '+'}{to_camel(sort_by)}"}
54
+
55
+
56
+ def construct_payload(model: Type[T], body: T | dict[str, Any]) -> dict[str, Any]:
57
+ """
58
+ Construct a payload for the given model and body.
59
+ :param model: The model class to use for validation.
60
+ :param body: The body to validate and optionally convert to a dictionary.
61
+ :return: A dictionary representation of the validated body.
62
+ """
63
+ if isinstance(body, dict):
64
+ body = model.model_validate(body)
65
+ return body.model_dump(exclude_none=True, by_alias=True)
66
+
67
+
68
+ def connection_body_payload(body: ConnectionBody | dict[str, Any]) -> dict[str, Any]:
69
+ """
70
+ Construct a payload for the given integration body.
71
+ :param body: The body to validate and optionally convert to a dictionary.
72
+ :return: A dictionary representation of the validated body.
73
+ """
74
+ if isinstance(body, dict):
75
+ if "sasToken" in body:
76
+ body = AzureBlobConnectionInput.model_validate(body)
77
+ elif "secretAccessKey" in body:
78
+ body = AmazonS3ConnectionInput.model_validate(body)
79
+ else:
80
+ body = GoogleBigQueryConnectionInput.model_validate(body)
81
+ return body.model_dump(exclude_none=True, by_alias=True)
82
+
83
+
84
+ def integration_payload(
85
+ body: IntegrationInput | IntegrationProcessInput | dict[str, Any],
86
+ ) -> dict[str, Any]:
87
+ """
88
+ Construct a payload for the given integration body.
89
+ :param body: The body to validate and optionally convert to a dictionary.
90
+ :return: A dictionary representation of the validated body.
91
+ """
92
+ if isinstance(body, dict):
93
+ body = (
94
+ IntegrationInput.model_validate(body)
95
+ if "jobs" in body
96
+ else IntegrationProcessInput.model_validate(body)
97
+ )
98
+ return body.model_dump(exclude_none=True, by_alias=True)
99
+
100
+
101
+ def schedule_payload(
102
+ integration_id: str, schedule: ScheduleInput | dict[str, Any]
103
+ ) -> dict[str, Any]:
104
+ """
105
+ Construct a payload for the given integration ID and schedule.
106
+ :param integration_id: The ID of the integration.
107
+ :param schedule: The schedule to validate and optionally convert to a dictionary.
108
+ :return: A dictionary representation of the validated schedule.
109
+ """
110
+ if isinstance(schedule, dict):
111
+ schedule = ScheduleInput.model_validate(schedule)
112
+ return {
113
+ "integrationId": integration_id,
114
+ "schedule": schedule.model_dump(exclude_none=True, by_alias=True),
115
+ }
116
+
117
+
118
+ def action_url(action_id: int) -> Literal["imports", "exports", "actions", "processes"]:
119
+ """
120
+ Determine the type of action based on its identifier.
121
+ :param action_id: The identifier of the action.
122
+ :return: The type of action.
123
+ """
124
+ if 12000000000 <= action_id < 113000000000:
125
+ return "imports"
126
+ if 116000000000 <= action_id < 117000000000:
127
+ return "exports"
128
+ if 117000000000 <= action_id < 118000000000:
129
+ return "actions"
130
+ if 118000000000 <= action_id < 119000000000:
131
+ return "processes"
132
+ raise InvalidIdentifierException(f"Action '{action_id}' is not a valid identifier.")
133
+
134
+
135
+ def parse_calendar_response(data: dict) -> ModelCalendar:
136
+ """
137
+ Parse calendar response and return appropriate calendar model.
138
+ :param data: The calendar data from the API response.
139
+ :return: The calendar settings of the model based on calendar type.
140
+ """
141
+ calendar_data = data["modelCalendar"]
142
+ cal_type = calendar_data["calendarType"]
143
+ if cal_type == "Calendar Months/Quarters/Years":
144
+ return MonthsQuartersYearsCalendar.model_validate(calendar_data)
145
+ if cal_type == "Weeks: 4-4-5, 4-5-4 or 5-4-4":
146
+ return WeeksGroupingCalendar.model_validate(calendar_data)
147
+ if cal_type == "Weeks: General":
148
+ return WeeksGeneralCalendar.model_validate(calendar_data)
149
+ if cal_type == "Weeks: 13 4-week Periods":
150
+ return WeeksPeriodsCalendar.model_validate(calendar_data)
151
+ raise AnaplanException(
152
+ "Unknown calendar type encountered. Please report this issue: "
153
+ "https://github.com/VinzenzKlass/anaplan-sdk/issues/new"
154
+ )
155
+
156
+
157
+ def parse_insertion_response(data: list[dict]) -> InsertionResult:
158
+ failures, added, ignored, total = [], 0, 0, 0
159
+ for res in data:
160
+ failures.append(res.get("failures", []))
161
+ added += res.get("added", 0)
162
+ total += res.get("total", 0)
163
+ ignored += res.get("ignored", 0)
164
+ return InsertionResult(
165
+ added=added, ignored=ignored, total=total, failures=list(chain.from_iterable(failures))
166
+ )
167
+
168
+
169
+ def validate_dimension_id(dimension_id: int) -> int:
170
+ if not (
171
+ dimension_id == 101999999999
172
+ or 101_000_000_000 <= dimension_id < 102_000_000_000
173
+ or 109_000_000_000 <= dimension_id < 110_000_000_000
174
+ or 114_000_000_000 <= dimension_id < 115_000_000_000
175
+ ):
176
+ raise InvalidIdentifierException(
177
+ "Invalid dimension_id. Must be a List (101xxxxxxxxx), List Subset (109xxxxxxxxx), "
178
+ "Line Item Subset (114xxxxxxxxx), or Users (101999999999)."
179
+ )
180
+ msg = (
181
+ "Using `get_dimension_items` for {} is discouraged. "
182
+ "Prefer `{}` for better performance and more details on the members."
183
+ )
184
+ if dimension_id == 101999999999:
185
+ logger.warning(msg.format("Users", "get_users"))
186
+ if 101000000000 <= dimension_id < 102000000000:
187
+ logger.warning(msg.format("Lists", "get_list_items"))
188
+ return dimension_id
@@ -39,11 +39,13 @@ class Model(AnaplanModel):
39
39
  description="The unique identifier of the user who last modified this model."
40
40
  )
41
41
  memory_usage: int = Field(0, description="The memory usage of this model in bytes.")
42
- current_workspace_id: str = Field(
43
- description="The unique identifier of the workspace that this model is currently in."
42
+ workspace_id: str = Field(
43
+ validation_alias="currentWorkspaceId",
44
+ description="The unique identifier of the workspace that this model is currently in.",
44
45
  )
45
- current_workspace_name: str = Field(
46
- description="The name of the workspace that this model is currently in."
46
+ workspace_name: str = Field(
47
+ validation_alias="currentWorkspaceName",
48
+ description="The name of the workspace that this model is currently in.",
47
49
  )
48
50
  url: str = Field(validation_alias="modelUrl", description="The current URL of this model.")
49
51
  category_values: list = Field(description="The category values of this model.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anaplan-sdk
3
- Version: 0.5.0a4
3
+ Version: 0.5.0a6
4
4
  Summary: Streamlined Python Interface for the Anaplan API.
5
5
  Project-URL: Homepage, https://vinzenzklass.github.io/anaplan-sdk/
6
6
  Project-URL: Repository, https://github.com/VinzenzKlass/anaplan-sdk
@@ -0,0 +1,31 @@
1
+ anaplan_sdk/__init__.py,sha256=WScEKtXlnRLjCb-j3qW9W4kEACTyPsTLFs-L54et2TQ,351
2
+ anaplan_sdk/_auth.py,sha256=l5z2WCcfQ05OkuQ1dcmikp6dB87Rw1qy2zu8bbaAQTs,16620
3
+ anaplan_sdk/_oauth.py,sha256=AynlJDrGIinQT0jwxI2RSvtU4D7Wasyw3H1uicdlLVI,12672
4
+ anaplan_sdk/_services.py,sha256=n14DvTC2I5ub7yzjWBaAAX9YMF7LwI_BU7zuofAj0l4,13026
5
+ anaplan_sdk/_utils.py,sha256=jsrhdfLpriMoANukVvXpjpEJ5hWDNx7ZJKAguLvKgJA,7517
6
+ anaplan_sdk/exceptions.py,sha256=ALkA9fBF0NQ7dufFxV6AivjmHyuJk9DOQ9jtJV2n7f0,1809
7
+ anaplan_sdk/_async_clients/__init__.py,sha256=pZXgMMg4S9Aj_pxQCaSiPuNG-sePVGBtNJ0133VjqW4,364
8
+ anaplan_sdk/_async_clients/_alm.py,sha256=_PTD9Eght879HAadjcsfdvS0KCN93jgwpPOF8r3_E14,13178
9
+ anaplan_sdk/_async_clients/_audit.py,sha256=MkCTt7s6039GfDoyyINmDA1vAkZn2xPL4hFK8D4CYsE,2927
10
+ anaplan_sdk/_async_clients/_bulk.py,sha256=TsUUIGK_RPo1yKUyEbaGoFXFMBd0H4YHXI-ABYieZog,30100
11
+ anaplan_sdk/_async_clients/_cloud_works.py,sha256=aSgmJQvE7dSJawwK0A7GEBWs7wokWk7eCwRiQuiVg6I,17701
12
+ anaplan_sdk/_async_clients/_cw_flow.py,sha256=qJJFfnwLR7zIdZ_ay4fVI9zr3eP5B-qMcs4GlC9vqQY,3966
13
+ anaplan_sdk/_async_clients/_transactional.py,sha256=f-NlIjvRJ0NIKRcI6ZaO2YatLERwIaC0TY0fKvgUJ5Q,18050
14
+ anaplan_sdk/_clients/__init__.py,sha256=FsbwvZC1FHrxuRXwbPxUzbhz_lO1DpXIxEOjx6-3QuA,219
15
+ anaplan_sdk/_clients/_alm.py,sha256=oRHTjnCuwQYZDE2CBWA1u410jSImIroCsDOSeP9U9w8,12905
16
+ anaplan_sdk/_clients/_audit.py,sha256=Am6VviRw88_VC5i0N7TQTXEm6pJAHZb7xNaJTljD0fc,2850
17
+ anaplan_sdk/_clients/_bulk.py,sha256=9kN8LU3c0iDF-Ov47utBl-dj_hxX3twf7TOcolzogyI,30257
18
+ anaplan_sdk/_clients/_cloud_works.py,sha256=1bbMYM_g8MSorxTI9Au_dFzbJZgKGJVBE6DYlbWBR0U,17492
19
+ anaplan_sdk/_clients/_cw_flow.py,sha256=x64Ua2FwCpt8vab6gaLV8tDwW_ugJrDfU5dv-TnmM2M,3855
20
+ anaplan_sdk/_clients/_transactional.py,sha256=IgkvBaq1Ep5mB-uxu6QoE17cUCfsodvV8dppASQrIT4,17875
21
+ anaplan_sdk/models/__init__.py,sha256=zfwDQJQrXuLEXSpbJcm08a_YK1P7a7u-kMhwtJiJFmA,1783
22
+ anaplan_sdk/models/_alm.py,sha256=oeENd0YM7-LoIRBq2uATIQTxVgIP9rXx3UZE2UnQAp0,4670
23
+ anaplan_sdk/models/_base.py,sha256=6AZc9CfireUKgpZfMxYKu4MbwiyHQOsGLjKrxGXBLic,508
24
+ anaplan_sdk/models/_bulk.py,sha256=C0s6XdvHxuJHrPXU-pnZ1JXK1PJOl9FScHArpaox_mQ,8489
25
+ anaplan_sdk/models/_transactional.py,sha256=2bH10zvtMb5Lfh6DC7iQk72aEwq6tyLQ-XnH_0wYSqI,14172
26
+ anaplan_sdk/models/cloud_works.py,sha256=APUGDt_e-JshtXkba5cQh5rZkXOZBz0Aix0qVNdEWgw,19501
27
+ anaplan_sdk/models/flows.py,sha256=SuLgNj5-2SeE3U1i8iY8cq2IkjuUgd_3M1n2ENructk,3625
28
+ anaplan_sdk-0.5.0a6.dist-info/METADATA,sha256=VZfvzZQlLtBEOFFXMexsiEaetmGPe8fLilZBewT7iOg,3678
29
+ anaplan_sdk-0.5.0a6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ anaplan_sdk-0.5.0a6.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
31
+ anaplan_sdk-0.5.0a6.dist-info/RECORD,,
@@ -1,30 +0,0 @@
1
- anaplan_sdk/__init__.py,sha256=WScEKtXlnRLjCb-j3qW9W4kEACTyPsTLFs-L54et2TQ,351
2
- anaplan_sdk/_auth.py,sha256=l5z2WCcfQ05OkuQ1dcmikp6dB87Rw1qy2zu8bbaAQTs,16620
3
- anaplan_sdk/_oauth.py,sha256=AynlJDrGIinQT0jwxI2RSvtU4D7Wasyw3H1uicdlLVI,12672
4
- anaplan_sdk/_services.py,sha256=isBMmeoxgQi2Xoc5wdD7qtraIHBqYJNAp-CEKWyhqj4,20104
5
- anaplan_sdk/exceptions.py,sha256=ALkA9fBF0NQ7dufFxV6AivjmHyuJk9DOQ9jtJV2n7f0,1809
6
- anaplan_sdk/_async_clients/__init__.py,sha256=pZXgMMg4S9Aj_pxQCaSiPuNG-sePVGBtNJ0133VjqW4,364
7
- anaplan_sdk/_async_clients/_alm.py,sha256=zvKEvXlxNkcQim_XvyZLCbDafFldljg8APHqhAAIfvw,13147
8
- anaplan_sdk/_async_clients/_audit.py,sha256=j9CeWzIuGsZrVBbjS_T8w6le2cjieW7NW6fDiCY34TA,2718
9
- anaplan_sdk/_async_clients/_bulk.py,sha256=TTdTMXu1-3iNlD9oxGAbh58JXSRHiFAxnMt70zLz8kk,29929
10
- anaplan_sdk/_async_clients/_cloud_works.py,sha256=VB4l93426A0Xes5dZ6DsDu0go-BVNhs2RZn2zX5DSOc,17675
11
- anaplan_sdk/_async_clients/_cw_flow.py,sha256=_allKIOP-qb33wrOj6GV5VAOvrCXOVJ1QXvck-jsocQ,3935
12
- anaplan_sdk/_async_clients/_transactional.py,sha256=U6X5pW7By387JOgvHx-GmgVRi7MRJKALpx0lWI6xRMo,18024
13
- anaplan_sdk/_clients/__init__.py,sha256=FsbwvZC1FHrxuRXwbPxUzbhz_lO1DpXIxEOjx6-3QuA,219
14
- anaplan_sdk/_clients/_alm.py,sha256=3U7Cy5U5TsePF1YPogXvsOzNeQlQm_ezO5TlmD-Xbbs,12874
15
- anaplan_sdk/_clients/_audit.py,sha256=b7l9xNbUGLceeNlS2No53RKJmksUeIICDTCuYPXgyV0,2641
16
- anaplan_sdk/_clients/_bulk.py,sha256=lZhsbw-Zqtrwofgzds7Lct1HnUfN5M4QDxWP-PocotE,30085
17
- anaplan_sdk/_clients/_cloud_works.py,sha256=FsCp2wPxIoArAN1vcIfOI6ANNkK2ZebQ4MWJZB-nFJU,17466
18
- anaplan_sdk/_clients/_cw_flow.py,sha256=O6t4utbDZdSVXGC0PXUcPpQ4oXrPohU9_8SUBCpxTXw,3824
19
- anaplan_sdk/_clients/_transactional.py,sha256=SaHAnaGLZrhXmM8d6JnWWkwf-sVCEDW0nL2a4_wvjfk,17849
20
- anaplan_sdk/models/__init__.py,sha256=zfwDQJQrXuLEXSpbJcm08a_YK1P7a7u-kMhwtJiJFmA,1783
21
- anaplan_sdk/models/_alm.py,sha256=oeENd0YM7-LoIRBq2uATIQTxVgIP9rXx3UZE2UnQAp0,4670
22
- anaplan_sdk/models/_base.py,sha256=6AZc9CfireUKgpZfMxYKu4MbwiyHQOsGLjKrxGXBLic,508
23
- anaplan_sdk/models/_bulk.py,sha256=S72qujNr5STdiyKaCEvrQjKYHik_aemiJFNKE7docpI,8405
24
- anaplan_sdk/models/_transactional.py,sha256=2bH10zvtMb5Lfh6DC7iQk72aEwq6tyLQ-XnH_0wYSqI,14172
25
- anaplan_sdk/models/cloud_works.py,sha256=APUGDt_e-JshtXkba5cQh5rZkXOZBz0Aix0qVNdEWgw,19501
26
- anaplan_sdk/models/flows.py,sha256=SuLgNj5-2SeE3U1i8iY8cq2IkjuUgd_3M1n2ENructk,3625
27
- anaplan_sdk-0.5.0a4.dist-info/METADATA,sha256=GQk0AT6_rIKYU7o79QBL1ur0DKfYGZ4fqxKv_hqajsg,3678
28
- anaplan_sdk-0.5.0a4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- anaplan_sdk-0.5.0a4.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
30
- anaplan_sdk-0.5.0a4.dist-info/RECORD,,