anaplan-sdk 0.4.5__py3-none-any.whl → 0.5.0__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,66 +1,136 @@
1
+ import logging
1
2
  from asyncio import gather
2
3
  from itertools import chain
3
- from typing import Any
4
+ from typing import Any, Literal, overload
4
5
 
5
- import httpx
6
-
7
- from anaplan_sdk._base import _AsyncBaseClient
6
+ from anaplan_sdk._services import _AsyncHttpService
7
+ from anaplan_sdk._utils import (
8
+ parse_calendar_response,
9
+ parse_insertion_response,
10
+ sort_params,
11
+ validate_dimension_id,
12
+ )
13
+ from anaplan_sdk.exceptions import InvalidIdentifierException
8
14
  from anaplan_sdk.models import (
15
+ CurrentPeriod,
16
+ Dimension,
17
+ DimensionWithCode,
18
+ FiscalYear,
9
19
  InsertionResult,
10
20
  LineItem,
11
21
  List,
22
+ ListDeletionResult,
12
23
  ListItem,
13
24
  ListMetadata,
25
+ Model,
26
+ ModelCalendar,
14
27
  ModelStatus,
15
28
  Module,
29
+ View,
30
+ ViewInfo,
16
31
  )
17
32
 
33
+ SortBy = Literal["id", "name"] | None
34
+
35
+ logger = logging.getLogger("anaplan_sdk")
18
36
 
19
- class _AsyncTransactionalClient(_AsyncBaseClient):
20
- def __init__(self, client: httpx.AsyncClient, model_id: str, retry_count: int) -> None:
37
+
38
+ class _AsyncTransactionalClient:
39
+ def __init__(self, http: _AsyncHttpService, model_id: str) -> None:
40
+ self._http = http
21
41
  self._url = f"https://api.anaplan.com/2/0/models/{model_id}"
22
- super().__init__(retry_count, client)
42
+ self._model_id = model_id
23
43
 
24
- async def list_modules(self) -> list[Module]:
44
+ async def get_model_details(self) -> Model:
25
45
  """
26
- Lists all the Modules in the Model.
27
- :return: The List of Modules.
46
+ Retrieves the Model details for the current Model.
47
+ :return: The Model details.
28
48
  """
29
- return [
30
- Module.model_validate(e)
31
- for e in await self._get_paginated(f"{self._url}/modules", "modules")
32
- ]
49
+ res = await self._http.get(self._url, params={"modelDetails": "true"})
50
+ return Model.model_validate(res["model"])
33
51
 
34
52
  async def get_model_status(self) -> ModelStatus:
35
53
  """
36
54
  Gets the current status of the Model.
37
55
  :return: The current status of the Model.
38
56
  """
39
- return ModelStatus.model_validate(
40
- (await self._get(f"{self._url}/status")).get("requestStatus")
57
+ res = await self._http.get(f"{self._url}/status")
58
+ return ModelStatus.model_validate(res["requestStatus"])
59
+
60
+ async def wake_model(self) -> None:
61
+ """Wake up the current model."""
62
+ await self._http.post_empty(
63
+ f"{self._url}/open", headers={"Content-Type": "application/text"}
64
+ )
65
+ logger.info(f"Woke up model '{self._model_id}'.")
66
+
67
+ async def close_model(self) -> None:
68
+ """Close the current model."""
69
+ await self._http.post_empty(
70
+ f"{self._url}/close", headers={"Content-Type": "application/text"}
71
+ )
72
+ logger.info(f"Closed model '{self._model_id}'.")
73
+
74
+ async def get_modules(self, sort_by: SortBy = None, descending: bool = False) -> list[Module]:
75
+ """
76
+ Lists all the Modules in the Model.
77
+ :param sort_by: The field to sort the results by.
78
+ :param descending: If True, the results will be sorted in descending order.
79
+ :return: The List of Modules.
80
+ """
81
+ res = await self._http.get_paginated(
82
+ f"{self._url}/modules", "modules", params=sort_params(sort_by, descending)
41
83
  )
84
+ return [Module.model_validate(e) for e in res]
42
85
 
43
- async def list_line_items(self, only_module_id: int | None = None) -> list[LineItem]:
86
+ async def get_views(
87
+ self, sort_by: Literal["id", "module_id", "name"] | None = None, descending: bool = False
88
+ ) -> list[View]:
89
+ """
90
+ Lists all the Views in the Model. This will include all Modules and potentially other saved
91
+ views.
92
+ :param sort_by: The field to sort the results by.
93
+ :param descending: If True, the results will be sorted in descending order.
94
+ :return: The List of Views.
95
+ """
96
+ params = {"includesubsidiaryviews": True} | sort_params(sort_by, descending)
97
+ return [
98
+ View.model_validate(e)
99
+ for e in await self._http.get_paginated(f"{self._url}/views", "views", params=params)
100
+ ]
101
+
102
+ async def get_view_info(self, view_id: int) -> ViewInfo:
103
+ """
104
+ Gets the detailed information about a View.
105
+ :param view_id: The ID of the View.
106
+ :return: The information about the View.
107
+ """
108
+ return ViewInfo.model_validate((await self._http.get(f"{self._url}/views/{view_id}")))
109
+
110
+ async def get_line_items(self, only_module_id: int | None = None) -> list[LineItem]:
44
111
  """
45
112
  Lists all the Line Items in the Model.
46
113
  :param only_module_id: If provided, only Line Items from this Module will be returned.
47
- :return: All Line Items on this Model.
114
+ :return: All Line Items on this Model or only from the specified Module.
48
115
  """
49
- url = (
116
+ res = await self._http.get(
50
117
  f"{self._url}/modules/{only_module_id}/lineItems?includeAll=true"
51
118
  if only_module_id
52
119
  else f"{self._url}/lineItems?includeAll=true"
53
120
  )
54
- return [LineItem.model_validate(e) for e in (await self._get(url)).get("items", [])]
121
+ return [LineItem.model_validate(e) for e in res.get("items", [])]
55
122
 
56
- async def list_lists(self) -> list[List]:
123
+ async def get_lists(self, sort_by: SortBy = None, descending: bool = False) -> list[List]:
57
124
  """
58
125
  Lists all the Lists in the Model.
126
+ :param sort_by: The field to sort the results by.
127
+ :param descending: If True, the results will be sorted in descending order.
59
128
  :return: All Lists on this model.
60
129
  """
61
- return [
62
- List.model_validate(e) for e in await self._get_paginated(f"{self._url}/lists", "lists")
63
- ]
130
+ res = await self._http.get_paginated(
131
+ f"{self._url}/lists", "lists", params=sort_params(sort_by, descending)
132
+ )
133
+ return [List.model_validate(e) for e in res]
64
134
 
65
135
  async def get_list_metadata(self, list_id: int) -> ListMetadata:
66
136
  """
@@ -70,21 +140,34 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
70
140
  """
71
141
 
72
142
  return ListMetadata.model_validate(
73
- (await self._get(f"{self._url}/lists/{list_id}")).get("metadata")
143
+ (await self._http.get(f"{self._url}/lists/{list_id}")).get("metadata")
74
144
  )
75
145
 
76
- async def get_list_items(self, list_id: int) -> list[ListItem]:
146
+ @overload
147
+ async def get_list_items(
148
+ self, list_id: int, return_raw: Literal[False] = False
149
+ ) -> list[ListItem]: ...
150
+
151
+ @overload
152
+ async def get_list_items(
153
+ self, list_id: int, return_raw: Literal[True] = True
154
+ ) -> list[dict[str, Any]]: ...
155
+
156
+ async def get_list_items(
157
+ self, list_id: int, return_raw: bool = False
158
+ ) -> list[ListItem] | list[dict[str, Any]]:
77
159
  """
78
160
  Gets all the items in a List.
79
161
  :param list_id: The ID of the List.
162
+ :param return_raw: If True, returns the items as a list of dictionaries instead of ListItem
163
+ objects. If you use the result of this call in a DataFrame or you simply pass on the
164
+ data, you will want to set this to avoid unnecessary (de-)serialization.
80
165
  :return: All items in the List.
81
166
  """
82
- return [
83
- ListItem.model_validate(e)
84
- for e in (await self._get(f"{self._url}/lists/{list_id}/items?includeAll=true")).get(
85
- "listItems"
86
- )
87
- ]
167
+ res = await self._http.get(f"{self._url}/lists/{list_id}/items?includeAll=true")
168
+ if return_raw:
169
+ return res.get("listItems", [])
170
+ return [ListItem.model_validate(e) for e in res.get("listItems", [])]
88
171
 
89
172
  async def insert_list_items(
90
173
  self, list_id: int, items: list[dict[str, str | int | dict]]
@@ -106,30 +189,31 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
106
189
  :return: The result of the insertion, indicating how many items were added,
107
190
  ignored or failed.
108
191
  """
192
+ if not items:
193
+ return InsertionResult(added=0, ignored=0, failures=[], total=0)
109
194
  if len(items) <= 100_000:
110
- return InsertionResult.model_validate(
111
- await self._post(
195
+ result = InsertionResult.model_validate(
196
+ await self._http.post(
112
197
  f"{self._url}/lists/{list_id}/items?action=add", json={"items": items}
113
198
  )
114
199
  )
200
+ logger.info(f"Inserted {result.added} items into list '{list_id}'.")
201
+ return result
115
202
  responses = await gather(
116
203
  *(
117
- self._post(f"{self._url}/lists/{list_id}/items?action=add", json={"items": chunk})
204
+ self._http.post(
205
+ f"{self._url}/lists/{list_id}/items?action=add", json={"items": chunk}
206
+ )
118
207
  for chunk in (items[i : i + 100_000] for i in range(0, len(items), 100_000))
119
208
  )
120
209
  )
121
- failures, added, ignored, total = [], 0, 0, 0
122
- for res in responses:
123
- failures.append(res.get("failures", []))
124
- added += res.get("added", 0)
125
- total += res.get("total", 0)
126
- ignored += res.get("ignored", 0)
127
-
128
- return InsertionResult(
129
- added=added, ignored=ignored, total=total, failures=list(chain.from_iterable(failures))
130
- )
210
+ result = parse_insertion_response(responses)
211
+ logger.info(f"Inserted {result.added} items into list '{list_id}'.")
212
+ return result
131
213
 
132
- async def delete_list_items(self, list_id: int, items: list[dict[str, str | int]]) -> int:
214
+ async def delete_list_items(
215
+ self, list_id: int, items: list[dict[str, str | int]]
216
+ ) -> ListDeletionResult:
133
217
  """
134
218
  Deletes items from a List. If you pass a long list, it will be split into chunks of 100,000
135
219
  items, the maximum allowed by the API.
@@ -143,31 +227,41 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
143
227
 
144
228
  :param list_id: The ID of the List.
145
229
  :param items: The items to delete from the List. Must be a dict with either `code` or `id`
146
- as the keys to identify the records to delete.
230
+ as the keys to identify the records to delete. Specifying both will error.
231
+ :return: The result of the deletion, indicating how many items were deleted or failed.
147
232
  """
233
+ if not items:
234
+ return ListDeletionResult(deleted=0, failures=[])
148
235
  if len(items) <= 100_000:
149
- return (
150
- await self._post(
151
- f"{self._url}/lists/{list_id}/items?action=delete", json={"items": items}
152
- )
153
- ).get("deleted", 0)
236
+ res = await self._http.post(
237
+ f"{self._url}/lists/{list_id}/items?action=delete", json={"items": items}
238
+ )
239
+ info = ListDeletionResult.model_validate(res)
240
+ logger.info(f"Deleted {info.deleted} items from list '{list_id}'.")
241
+ return info
154
242
 
155
243
  responses = await gather(
156
244
  *(
157
- self._post(
245
+ self._http.post(
158
246
  f"{self._url}/lists/{list_id}/items?action=delete", json={"items": chunk}
159
247
  )
160
248
  for chunk in (items[i : i + 100_000] for i in range(0, len(items), 100_000))
161
249
  )
162
250
  )
163
- return sum(res.get("deleted", 0) for res in responses)
251
+ info = ListDeletionResult(
252
+ deleted=sum(res.get("deleted", 0) for res in responses),
253
+ failures=list(chain.from_iterable(res.get("failures", []) for res in responses)),
254
+ )
255
+ logger.info(f"Deleted {info} items from list '{list_id}'.")
256
+ return info
164
257
 
165
258
  async def reset_list_index(self, list_id: int) -> None:
166
259
  """
167
260
  Resets the index of a List. The List must be empty to do so.
168
261
  :param list_id: The ID of the List.
169
262
  """
170
- await self._post_empty(f"{self._url}/lists/{list_id}/resetIndex")
263
+ await self._http.post_empty(f"{self._url}/lists/{list_id}/resetIndex")
264
+ logger.info(f"Reset index for list '{list_id}'.")
171
265
 
172
266
  async def update_module_data(
173
267
  self, module_id: int, data: list[dict[str, Any]]
@@ -186,5 +280,120 @@ class _AsyncTransactionalClient(_AsyncBaseClient):
186
280
  :param data: The data to write to the Module.
187
281
  :return: The number of cells changed or the response with the according error details.
188
282
  """
189
- res = await self._post(f"{self._url}/modules/{module_id}/data", json=data)
283
+ res = await self._http.post(f"{self._url}/modules/{module_id}/data", json=data)
284
+ if "failures" not in res:
285
+ logger.info(f"Updated {res['numberOfCellsChanged']} cells in module '{module_id}'.")
190
286
  return res if "failures" in res else res["numberOfCellsChanged"]
287
+
288
+ async def get_current_period(self) -> CurrentPeriod:
289
+ """
290
+ Gets the current period of the model.
291
+ :return: The current period of the model.
292
+ """
293
+ res = await self._http.get(f"{self._url}/currentPeriod")
294
+ return CurrentPeriod.model_validate(res["currentPeriod"])
295
+
296
+ async def set_current_period(self, date: str) -> CurrentPeriod:
297
+ """
298
+ Sets the current period of the model to the given date.
299
+ :param date: The date to set the current period to, in the format 'YYYY-MM-DD'.
300
+ :return: The updated current period of the model.
301
+ """
302
+ res = await self._http.put(f"{self._url}/currentPeriod", {"date": date})
303
+ logger.info(f"Set current period to '{date}'.")
304
+ return CurrentPeriod.model_validate(res["currentPeriod"])
305
+
306
+ async def set_current_fiscal_year(self, year: str) -> FiscalYear:
307
+ """
308
+ Sets the current fiscal year of the model.
309
+ :param year: The fiscal year to set, in the format specified in the model, e.g. FY24.
310
+ :return: The updated fiscal year of the model.
311
+ """
312
+ res = await self._http.put(f"{self._url}/modelCalendar/fiscalYear", {"year": year})
313
+ logger.info(f"Set current fiscal year to '{year}'.")
314
+ return FiscalYear.model_validate(res["modelCalendar"]["fiscalYear"])
315
+
316
+ async def get_model_calendar(self) -> ModelCalendar:
317
+ """
318
+ Get the calendar settings of the model.
319
+ :return: The calendar settings of the model.
320
+ """
321
+ return parse_calendar_response(await self._http.get(f"{self._url}/modelCalendar"))
322
+
323
+ async def get_dimension_items(self, dimension_id: int) -> list[DimensionWithCode]:
324
+ """
325
+ Get all items in a dimension. This will fail if the dimensions holds more than 1_000_000
326
+ items. Valid Dimensions are:
327
+
328
+ - Lists (101xxxxxxxxx)
329
+ - List Subsets (109xxxxxxxxx)
330
+ - Line Item Subsets (114xxxxxxxxx)
331
+ - Users (101999999999)
332
+ For lists and users, you should prefer using the `get_list_items` and `get_users` methods,
333
+ respectively, instead.
334
+ :param dimension_id: The ID of the dimension to list items for.
335
+ :return: A list of Dimension items.
336
+ """
337
+ res = await self._http.get(
338
+ f"{self._url}/dimensions/{validate_dimension_id(dimension_id)}/items"
339
+ )
340
+ return [DimensionWithCode.model_validate(e) for e in res.get("items", [])]
341
+
342
+ async def lookup_dimension_items(
343
+ self, dimension_id: int, codes: list[str] = None, names: list[str] = None
344
+ ) -> list[DimensionWithCode]:
345
+ """
346
+ Looks up items in a dimension by their codes or names. If both are provided, both will be
347
+ searched for. You must provide at least one of `codes` or `names`. Valid Dimensions to
348
+ lookup are:
349
+
350
+ - Lists (101xxxxxxxxx)
351
+ - Time (20000000003)
352
+ - Version (20000000020)
353
+ - Users (101999999999)
354
+ :param dimension_id: The ID of the dimension to lookup items for.
355
+ :param codes: A list of codes to lookup in the dimension.
356
+ :param names: A list of names to lookup in the dimension.
357
+ :return: A list of Dimension items that match the provided codes or names.
358
+ """
359
+ if not codes and not names:
360
+ raise ValueError("At least one of 'codes' or 'names' must be provided.")
361
+ if not (
362
+ dimension_id == 101999999999
363
+ or 101_000_000_000 <= dimension_id < 102_000_000_000
364
+ or dimension_id == 20000000003
365
+ or dimension_id == 20000000020
366
+ ):
367
+ raise InvalidIdentifierException(
368
+ "Invalid dimension_id. Must be a List (101xxxxxxxxx), Time (20000000003), "
369
+ "Version (20000000020), or Users (101999999999)."
370
+ )
371
+ res = await self._http.post(
372
+ f"{self._url}/dimensions/{dimension_id}/items", json={"codes": codes, "names": names}
373
+ )
374
+ return [DimensionWithCode.model_validate(e) for e in res.get("items", [])]
375
+
376
+ async def get_view_dimension_items(self, view_id: int, dimension_id: int) -> list[Dimension]:
377
+ """
378
+ Get the members of a dimension that are part of the given View. This call returns data as
379
+ filtered by the page builder when they configure the view. This call respects hidden items,
380
+ filtering selections, and Selective Access. If the view contains hidden or filtered items,
381
+ these do not display in the response. This will fail if the dimensions holds more than
382
+ 1_000_000 items. The response returns Items within a flat list (no hierarchy) and order
383
+ is not guaranteed.
384
+ :param view_id: The ID of the View.
385
+ :param dimension_id: The ID of the Dimension to get items for.
386
+ :return: A list of Dimensions used in the View.
387
+ """
388
+ res = await self._http.get(f"{self._url}/views/{view_id}/dimensions/{dimension_id}/items")
389
+ return [Dimension.model_validate(e) for e in res.get("items", [])]
390
+
391
+ async def get_line_item_dimensions(self, line_item_id: int) -> list[Dimension]:
392
+ """
393
+ Get the dimensions of a Line Item. This will return all dimensions that are used in the
394
+ Line Item.
395
+ :param line_item_id: The ID of the Line Item.
396
+ :return: A list of Dimensions used in the Line Item.
397
+ """
398
+ res = await self._http.get(f"{self._url}/lineItems/{line_item_id}/dimensions")
399
+ return [Dimension.model_validate(e) for e in res.get("dimensions", [])]
@@ -1,6 +1,17 @@
1
1
  from ._alm import _AlmClient
2
2
  from ._audit import _AuditClient
3
3
  from ._bulk import Client
4
+ from ._cloud_works import _CloudWorksClient
5
+ from ._cw_flow import _FlowClient
6
+ from ._scim import _ScimClient
4
7
  from ._transactional import _TransactionalClient
5
8
 
6
- __all__ = ["Client", "_AlmClient", "_AuditClient", "_TransactionalClient"]
9
+ __all__ = [
10
+ "Client",
11
+ "_AlmClient",
12
+ "_AuditClient",
13
+ "_CloudWorksClient",
14
+ "_FlowClient",
15
+ "_ScimClient",
16
+ "_TransactionalClient",
17
+ ]