cognite-toolkit 0.7.70__py3-none-any.whl → 0.7.72__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.
Files changed (35) hide show
  1. cognite_toolkit/_cdf_tk/client/_resource_base.py +0 -22
  2. cognite_toolkit/_cdf_tk/client/_toolkit_client.py +0 -2
  3. cognite_toolkit/_cdf_tk/client/api/infield.py +56 -204
  4. cognite_toolkit/_cdf_tk/client/api/instances.py +239 -16
  5. cognite_toolkit/_cdf_tk/client/api/robotics_capabilities.py +9 -3
  6. cognite_toolkit/_cdf_tk/client/api/robotics_data_postprocessing.py +12 -3
  7. cognite_toolkit/_cdf_tk/client/api/robotics_frames.py +10 -3
  8. cognite_toolkit/_cdf_tk/client/api/robotics_locations.py +10 -3
  9. cognite_toolkit/_cdf_tk/client/api/robotics_maps.py +10 -3
  10. cognite_toolkit/_cdf_tk/client/api/robotics_robots.py +10 -3
  11. cognite_toolkit/_cdf_tk/client/cdf_client/responses.py +3 -3
  12. cognite_toolkit/_cdf_tk/client/http_client/_client.py +1 -1
  13. cognite_toolkit/_cdf_tk/client/resource_classes/infield.py +133 -72
  14. cognite_toolkit/_cdf_tk/client/resource_classes/instance_api.py +119 -79
  15. cognite_toolkit/_cdf_tk/client/testing.py +14 -16
  16. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/fieldops.py +45 -46
  17. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/robotics.py +154 -160
  18. cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
  19. cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
  20. cognite_toolkit/_resources/cdf.toml +1 -1
  21. cognite_toolkit/_version.py +1 -1
  22. {cognite_toolkit-0.7.70.dist-info → cognite_toolkit-0.7.72.dist-info}/METADATA +1 -1
  23. {cognite_toolkit-0.7.70.dist-info → cognite_toolkit-0.7.72.dist-info}/RECORD +25 -35
  24. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/__init__.py +0 -8
  25. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/api.py +0 -20
  26. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/capabilities.py +0 -142
  27. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/data_postprocessing.py +0 -144
  28. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/frames.py +0 -136
  29. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/locations.py +0 -136
  30. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/maps.py +0 -136
  31. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/robots.py +0 -132
  32. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/utlis.py +0 -13
  33. cognite_toolkit/_cdf_tk/client/resource_classes/legacy/robotics.py +0 -970
  34. {cognite_toolkit-0.7.70.dist-info → cognite_toolkit-0.7.72.dist-info}/WHEEL +0 -0
  35. {cognite_toolkit-0.7.70.dist-info → cognite_toolkit-0.7.72.dist-info}/entry_points.txt +0 -0
@@ -3,10 +3,8 @@
3
3
  import sys
4
4
  import types
5
5
  from abc import ABC, abstractmethod
6
- from collections import UserList
7
6
  from typing import Any, ClassVar, Generic, Literal, TypeVar, Union, get_args, get_origin
8
7
 
9
- from cognite.client import CogniteClient
10
8
  from pydantic import BaseModel, ConfigDict
11
9
  from pydantic.alias_generators import to_camel
12
10
 
@@ -165,23 +163,3 @@ class ResponseResource(BaseModelObject, Generic[T_RequestResource], ABC):
165
163
 
166
164
 
167
165
  T_ResponseResource = TypeVar("T_ResponseResource", bound=ResponseResource)
168
-
169
- # Todo: Delete this class and use list[T_Resource] directly
170
- T_Resource = TypeVar("T_Resource", bound=RequestResource | ResponseResource)
171
-
172
-
173
- class BaseResourceList(UserList[T_Resource]):
174
- """Base class for resource lists."""
175
-
176
- _RESOURCE: type[T_Resource]
177
-
178
- def __init__(self, initlist: list[T_Resource] | None = None, **_: Any) -> None:
179
- super().__init__(initlist or [])
180
-
181
- def dump(self, camel_case: bool = True) -> list[dict[str, Any]]:
182
- return [item.dump(camel_case) for item in self.data]
183
-
184
- @classmethod
185
- def load(cls, data: list[dict[str, Any]], cognite_client: "CogniteClient | None" = None) -> Self:
186
- items = [cls._RESOURCE.model_validate(item) for item in data]
187
- return cls(items) # type: ignore[arg-type]
@@ -11,7 +11,6 @@ from cognite_toolkit._cdf_tk.client.api.legacy.extended_files import ExtendedFil
11
11
  from cognite_toolkit._cdf_tk.client.api.legacy.extended_functions import ExtendedFunctionsAPI
12
12
  from cognite_toolkit._cdf_tk.client.api.legacy.extended_raw import ExtendedRawAPI
13
13
  from cognite_toolkit._cdf_tk.client.api.legacy.extended_timeseries import ExtendedTimeSeriesAPI
14
- from cognite_toolkit._cdf_tk.client.api.legacy.robotics import RoboticsAPI as RoboticsLegacyAPI
15
14
  from cognite_toolkit._cdf_tk.client.http_client import HTTPClient
16
15
 
17
16
  from .api.assets import AssetsAPI
@@ -80,7 +79,6 @@ class ToolkitClient(CogniteClient):
80
79
  self.console = console or Console()
81
80
  self.tool = ToolAPI(http_client, self.console)
82
81
  self.search = SearchAPI(self._config, self._API_VERSION, self)
83
- self.robotics = RoboticsLegacyAPI(self._config, self._API_VERSION, self)
84
82
  self.dml = DMLAPI(self._config, self._API_VERSION, self)
85
83
  self.verify = VerifyAPI(self._config, self._API_VERSION, self)
86
84
  self.lookup = LookUpGroup(self._config, self._API_VERSION, self, self.console)
@@ -1,102 +1,41 @@
1
1
  from collections.abc import Sequence
2
- from typing import Any, cast
2
+ from typing import Any
3
3
 
4
- from pydantic import TypeAdapter
5
4
  from rich.console import Console
6
5
 
7
- from cognite_toolkit._cdf_tk.client.cdf_client.responses import QueryResponse
6
+ from cognite_toolkit._cdf_tk.client.api.instances import MultiWrappedInstancesAPI, WrappedInstancesAPI
7
+ from cognite_toolkit._cdf_tk.client.cdf_client import PagedResponse, QueryResponse, ResponseItems
8
8
  from cognite_toolkit._cdf_tk.client.http_client import (
9
9
  HTTPClient,
10
- ItemsRequest,
11
- RequestMessage,
10
+ ItemsSuccessResponse,
11
+ SuccessResponse,
12
12
  )
13
13
  from cognite_toolkit._cdf_tk.client.resource_classes.infield import (
14
14
  DataExplorationConfig,
15
- InFieldCDMLocationConfig,
16
- InfieldLocationConfig,
15
+ InFieldCDMLocationConfigRequest,
16
+ InFieldCDMLocationConfigResponse,
17
+ InFieldLocationConfig,
18
+ InFieldLocationConfigRequest,
19
+ InFieldLocationConfigResponse,
17
20
  )
18
21
  from cognite_toolkit._cdf_tk.client.resource_classes.instance_api import (
19
- InstanceResponseItem,
20
- InstanceResult,
22
+ TypedInstanceIdentifier,
21
23
  TypedNodeIdentifier,
22
24
  )
23
- from cognite_toolkit._cdf_tk.tk_warnings import HighSeverityWarning
24
25
 
25
26
 
26
- class InfieldConfigAPI:
27
- ENDPOINT = "/models/instances"
28
- LOCATION_REF = "locationConfig"
29
- EXPLORATION_REF = "explorerConfig"
30
- # We know that this key exists and it has alias set.
31
- DATA_EXPLORATION_PROP_ID = cast(str, InfieldLocationConfig.model_fields["data_exploration_config"].alias)
27
+ class InfieldConfigAPI(MultiWrappedInstancesAPI[InFieldLocationConfigRequest, InFieldLocationConfigResponse]):
28
+ _LOCATION_REF = "locationConfig"
29
+ _EXPLORATION_REF = "dataExplorationConfig"
32
30
 
33
- def __init__(self, http_client: HTTPClient, console: Console) -> None:
34
- self._http_client = http_client
35
- self._console = console
36
- self._config = http_client.config
37
-
38
- def apply(self, items: Sequence[InfieldLocationConfig]) -> list[InstanceResult]:
39
- if len(items) > 500:
40
- raise ValueError("Cannot apply more than 500 InfieldLocationConfig items at once.")
41
-
42
- request_items = (
43
- [item.as_request_item()]
44
- if item.data_exploration_config is None
45
- else [item.as_request_item(), item.data_exploration_config.as_request_item()]
46
- for item in items
47
- )
48
- responses = self._http_client.request_items_retries(
49
- ItemsRequest(
50
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
51
- method="POST",
52
- items=[item for sublist in request_items for item in sublist],
53
- )
54
- )
55
- responses.raise_for_status()
56
- return TypeAdapter(list[InstanceResult]).validate_python(responses.get_items())
57
-
58
- def retrieve(self, items: Sequence[TypedNodeIdentifier]) -> list[InfieldLocationConfig]:
59
- if len(items) > 100:
60
- raise ValueError("Cannot retrieve more than 100 InfieldLocationConfig items at once.")
61
- if not items:
62
- return []
63
- response = self._http_client.request_single_retries(
64
- RequestMessage(
65
- # We use the query endpoint to be able to retrieve linked DataExplorationConfig items
66
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/query"),
67
- method="POST",
68
- body_content=self._retrieve_query(items),
69
- )
70
- )
71
- success = response.get_success_or_raise()
72
- parsed_response = QueryResponse[InstanceResponseItem].model_validate(success.body_json)
73
- return self._parse_retrieve_response(parsed_response)
74
-
75
- def delete(self, items: Sequence[InfieldLocationConfig]) -> list[TypedNodeIdentifier]:
76
- if len(items) > 500:
77
- raise ValueError("Cannot delete more than 500 InfieldLocationConfig items at once.")
78
-
79
- identifiers = (
80
- [item.as_id()]
81
- if item.data_exploration_config is None
82
- else [item.as_id(), item.data_exploration_config.as_id()]
83
- for item in items
84
- )
85
- responses = self._http_client.request_items_retries(
86
- ItemsRequest(
87
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
88
- method="POST",
89
- items=[identifier for sublist in identifiers for identifier in sublist],
90
- )
91
- )
92
- responses.raise_for_status()
93
- return TypeAdapter(list[TypedNodeIdentifier]).validate_python(responses.get_items())
31
+ def __init__(self, http_client: HTTPClient) -> None:
32
+ # 500 is chosen as 1000 is the maximum for nodes, and each location config consists of 1 or 2 nodes
33
+ super().__init__(http_client, query_chunk=500)
94
34
 
95
- @classmethod
96
- def _retrieve_query(cls, items: Sequence[TypedNodeIdentifier]) -> dict[str, Any]:
35
+ def _retrieve_query(self, items: Sequence[TypedInstanceIdentifier]) -> dict[str, Any]:
97
36
  return {
98
37
  "with": {
99
- cls.LOCATION_REF: {
38
+ self._LOCATION_REF: {
100
39
  "limit": len(items),
101
40
  "nodes": {
102
41
  "filter": {
@@ -106,148 +45,61 @@ class InfieldConfigAPI:
106
45
  },
107
46
  },
108
47
  },
109
- cls.EXPLORATION_REF: {
48
+ self._EXPLORATION_REF: {
110
49
  "nodes": {
111
50
  "from": "locationConfig",
112
51
  "direction": "outwards",
113
52
  "through": {
114
- "source": InfieldLocationConfig.VIEW_ID.dump(),
115
- "identifier": cls.DATA_EXPLORATION_PROP_ID,
53
+ "source": InFieldLocationConfig.VIEW_ID.dump(),
54
+ "identifier": "dataExplorationConfig",
116
55
  },
117
56
  }
118
57
  },
119
58
  },
120
59
  "select": {
121
- cls.LOCATION_REF: {
122
- "sources": [{"source": InfieldLocationConfig.VIEW_ID.dump(), "properties": ["*"]}],
60
+ self._LOCATION_REF: {
61
+ "sources": [{"source": InFieldLocationConfig.VIEW_ID.dump(), "properties": ["*"]}],
123
62
  },
124
- cls.EXPLORATION_REF: {
63
+ self._EXPLORATION_REF: {
125
64
  "sources": [{"source": DataExplorationConfig.VIEW_ID.dump(), "properties": ["*"]}],
126
65
  },
127
66
  },
128
67
  }
129
68
 
130
- def _parse_retrieve_response(
131
- self, parsed_response: QueryResponse[InstanceResponseItem]
132
- ) -> list[InfieldLocationConfig]:
133
- data_exploration_results = (
134
- DataExplorationConfig.model_validate(
135
- item.get_properties_for_source(DataExplorationConfig.VIEW_ID, include_identifier=True)
136
- )
137
- for item in parsed_response.items[self.EXPLORATION_REF]
138
- )
139
- data_exploration_config_map = {(dec.space, dec.external_id): dec for dec in data_exploration_results}
140
- result: list[InfieldLocationConfig] = []
141
- for item in parsed_response.items[self.LOCATION_REF]:
142
- properties = item.get_properties_for_source(InfieldLocationConfig.VIEW_ID, include_identifier=True)
143
- data_exploration = properties.pop(self.DATA_EXPLORATION_PROP_ID, None)
144
- if isinstance(data_exploration, dict):
145
- space = data_exploration["space"]
146
- external_id = data_exploration["externalId"]
147
- if (space, external_id) not in data_exploration_config_map:
148
- HighSeverityWarning(
149
- f"{self.DATA_EXPLORATION_PROP_ID} with space '{space}' and externalId '{external_id}' referenced in InfieldLocationConfig '{properties['externalId']}' was not found in the retrieved results."
150
- ).print_warning(console=self._console)
151
- else:
152
- # Pydantic allow already validated models to be assigned to fields
153
- properties[self.DATA_EXPLORATION_PROP_ID] = data_exploration_config_map[(space, external_id)] # type: ignore[assignment,index]
154
- else:
155
- HighSeverityWarning(
156
- f"InfieldLocationConfig '{properties['externalId']}' is missing a valid {self.DATA_EXPLORATION_PROP_ID} reference."
157
- ).print_warning(console=self._console)
158
- result.append(InfieldLocationConfig.model_validate(properties))
159
- return result
160
-
161
-
162
- class InFieldCDMConfigAPI:
163
- ENDPOINT = "/models/instances"
164
- LOCATION_REF = "cdmLocationConfig"
165
-
166
- def __init__(self, http_client: HTTPClient, console: Console) -> None:
167
- self._http_client = http_client
168
- self._console = console
169
- self._config = http_client.config
170
-
171
- def apply(self, items: Sequence[InFieldCDMLocationConfig]) -> list[InstanceResult]:
172
- if len(items) > 500:
173
- raise ValueError("Cannot apply more than 500 InFieldCDMLocationConfig items at once.")
174
-
175
- request_items = [item.as_request_item() for item in items]
176
- results = self._http_client.request_items_retries(
177
- ItemsRequest(
178
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
179
- method="POST",
180
- items=request_items,
181
- )
182
- )
183
- results.raise_for_status()
184
- return TypeAdapter(list[InstanceResult]).validate_python(results.get_items())
185
-
186
- def retrieve(self, items: Sequence[TypedNodeIdentifier]) -> list[InFieldCDMLocationConfig]:
187
- if len(items) > 100:
188
- raise ValueError("Cannot retrieve more than 100 InFieldCDMLocationConfig items at once.")
189
- if not items:
190
- return []
191
- result = self._http_client.request_single_retries(
192
- RequestMessage(
193
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/query"),
194
- method="POST",
195
- body_content=self._retrieve_query(items),
196
- )
69
+ def _validate_query_response(self, query_response: QueryResponse) -> list[InFieldLocationConfigResponse]:
70
+ exploration_config_results = (
71
+ DataExplorationConfig.model_validate(item) for item in query_response.items.get(self._EXPLORATION_REF, [])
197
72
  )
198
- success = result.get_success_or_raise()
199
- parsed_response = QueryResponse[InstanceResponseItem].model_validate(success.body_json)
200
- return self._parse_retrieve_response(parsed_response)
201
-
202
- def delete(self, items: Sequence[InFieldCDMLocationConfig]) -> list[TypedNodeIdentifier]:
203
- if len(items) > 500:
204
- raise ValueError("Cannot delete more than 500 InFieldCDMLocationConfig items at once.")
205
-
206
- identifiers = [item.as_id() for item in items]
207
- responses = self._http_client.request_items_retries(
208
- ItemsRequest(
209
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
210
- method="POST",
211
- items=identifiers,
212
- )
213
- )
214
- responses.raise_for_status()
215
- return TypeAdapter(list[TypedNodeIdentifier]).validate_python(responses.get_items())
216
-
217
- @classmethod
218
- def _retrieve_query(cls, items: Sequence[TypedNodeIdentifier]) -> dict[str, Any]:
219
- return {
220
- "with": {
221
- cls.LOCATION_REF: {
222
- "limit": len(items),
223
- "nodes": {
224
- "filter": {
225
- "instanceReferences": [
226
- {"space": item.space, "externalId": item.external_id} for item in items
227
- ]
228
- },
229
- },
230
- },
231
- },
232
- "select": {
233
- cls.LOCATION_REF: {
234
- "sources": [{"source": InFieldCDMLocationConfig.VIEW_ID.dump(), "properties": ["*"]}],
235
- },
236
- },
237
- }
238
-
239
- def _parse_retrieve_response(
240
- self, parsed_response: QueryResponse[InstanceResponseItem]
241
- ) -> list[InFieldCDMLocationConfig]:
242
- result: list[InFieldCDMLocationConfig] = []
243
- for item in parsed_response.items[self.LOCATION_REF]:
244
- properties = item.get_properties_for_source(InFieldCDMLocationConfig.VIEW_ID, include_identifier=True)
245
- result.append(InFieldCDMLocationConfig.model_validate(properties))
246
- return result
73
+ exploration_config_map = {(item.space, item.external_id): item for item in exploration_config_results}
74
+ results: list[InFieldLocationConfigResponse] = []
75
+ for item in query_response.items.get(self._LOCATION_REF, []):
76
+ location_config = InFieldLocationConfigResponse.model_validate(item)
77
+ exploration_config = location_config.data_exploration_config
78
+ if exploration_config is not None:
79
+ location_config.data_exploration_config = exploration_config_map.get(
80
+ (exploration_config.space, exploration_config.external_id), exploration_config
81
+ )
82
+ results.append(location_config)
83
+ return results
84
+
85
+
86
+ class InFieldCDMConfigAPI(
87
+ WrappedInstancesAPI[TypedNodeIdentifier, InFieldCDMLocationConfigRequest, InFieldCDMLocationConfigResponse]
88
+ ):
89
+ def __init__(self, http_client: HTTPClient) -> None:
90
+ super().__init__(http_client, InFieldCDMLocationConfigRequest.VIEW_ID)
91
+
92
+ def _validate_response(self, response: SuccessResponse) -> ResponseItems[TypedNodeIdentifier]:
93
+ return ResponseItems[TypedNodeIdentifier].model_validate_json(response.body)
94
+
95
+ def _validate_page_response(
96
+ self, response: SuccessResponse | ItemsSuccessResponse
97
+ ) -> PagedResponse[InFieldCDMLocationConfigResponse]:
98
+ return PagedResponse[InFieldCDMLocationConfigResponse].model_validate_json(response.body)
247
99
 
248
100
 
249
101
  class InfieldAPI:
250
102
  def __init__(self, http_client: HTTPClient, console: Console) -> None:
251
103
  self._http_client = http_client
252
- self.config = InfieldConfigAPI(http_client, console)
253
- self.cdm_config = InFieldCDMConfigAPI(http_client, console)
104
+ self.config = InfieldConfigAPI(http_client)
105
+ self.cdm_config = InFieldCDMConfigAPI(http_client)
@@ -1,30 +1,43 @@
1
+ from abc import ABC, abstractmethod
1
2
  from collections.abc import Iterable, Sequence
2
- from typing import Any, Literal
3
+ from itertools import zip_longest
4
+ from typing import Any, Generic, Literal
3
5
 
4
- from cognite_toolkit._cdf_tk.client.cdf_client import CDFResourceAPI, PagedResponse, ResponseItems
5
- from cognite_toolkit._cdf_tk.client.cdf_client.api import Endpoint
6
- from cognite_toolkit._cdf_tk.client.http_client import HTTPClient, ItemsSuccessResponse, SuccessResponse
6
+ from pydantic import JsonValue
7
+
8
+ from cognite_toolkit._cdf_tk.client.cdf_client import CDFResourceAPI, PagedResponse, QueryResponse, ResponseItems
9
+ from cognite_toolkit._cdf_tk.client.cdf_client.api import APIMethod, Endpoint
10
+ from cognite_toolkit._cdf_tk.client.http_client import HTTPClient, ItemsSuccessResponse, RequestMessage, SuccessResponse
7
11
  from cognite_toolkit._cdf_tk.client.request_classes.filters import InstanceFilter
8
12
  from cognite_toolkit._cdf_tk.client.resource_classes.data_modeling import (
9
13
  InstanceRequest,
10
14
  InstanceResponse,
11
- ViewReference,
12
15
  )
13
16
  from cognite_toolkit._cdf_tk.client.resource_classes.data_modeling._instance import InstanceSlimDefinition
14
- from cognite_toolkit._cdf_tk.client.resource_classes.instance_api import TypedInstanceIdentifier
17
+ from cognite_toolkit._cdf_tk.client.resource_classes.instance_api import (
18
+ T_InstancesListRequest,
19
+ T_InstancesListResponse,
20
+ T_TypedInstanceIdentifier,
21
+ T_WrappedInstanceRequest,
22
+ T_WrappedInstanceResponse,
23
+ TypedInstanceIdentifier,
24
+ TypedNodeIdentifier,
25
+ TypedViewReference,
26
+ )
27
+ from cognite_toolkit._cdf_tk.utils.collection import chunker_sequence
28
+
29
+ METHOD_MAP: dict[APIMethod, Endpoint] = {
30
+ "upsert": Endpoint(method="POST", path="/models/instances", item_limit=1000),
31
+ "retrieve": Endpoint(method="POST", path="/models/instances/byids", item_limit=1000),
32
+ "delete": Endpoint(method="POST", path="/models/instances/delete", item_limit=1000),
33
+ "list": Endpoint(method="POST", path="/models/instances/list", item_limit=1000),
34
+ }
35
+ QUERY_ENDPOINT = Endpoint(method="POST", path="/models/instances/query", item_limit=1000)
15
36
 
16
37
 
17
38
  class InstancesAPI(CDFResourceAPI[TypedInstanceIdentifier, InstanceRequest, InstanceResponse]):
18
39
  def __init__(self, http_client: HTTPClient) -> None:
19
- super().__init__(
20
- http_client=http_client,
21
- method_endpoint_map={
22
- "upsert": Endpoint(method="POST", path="/models/instances", item_limit=1000),
23
- "retrieve": Endpoint(method="POST", path="/models/instances/byids", item_limit=1000),
24
- "delete": Endpoint(method="POST", path="/models/instances/delete", item_limit=1000),
25
- "list": Endpoint(method="POST", path="/models/instances/list", item_limit=1000),
26
- },
27
- )
40
+ super().__init__(http_client=http_client, method_endpoint_map=METHOD_MAP)
28
41
 
29
42
  def _validate_page_response(
30
43
  self, response: SuccessResponse | ItemsSuccessResponse
@@ -48,7 +61,7 @@ class InstancesAPI(CDFResourceAPI[TypedInstanceIdentifier, InstanceRequest, Inst
48
61
  return response_items
49
62
 
50
63
  def retrieve(
51
- self, items: Sequence[TypedInstanceIdentifier], source: ViewReference | None = None
64
+ self, items: Sequence[TypedInstanceIdentifier], source: TypedViewReference | None = None
52
65
  ) -> list[InstanceResponse]:
53
66
  """Retrieve instances from CDF.
54
67
 
@@ -141,3 +154,213 @@ class InstancesAPI(CDFResourceAPI[TypedInstanceIdentifier, InstanceRequest, Inst
141
154
  List of InstanceResponse objects.
142
155
  """
143
156
  return self._list(limit=limit, body=self._create_body(filter))
157
+
158
+
159
+ class WrappedInstancesAPI(
160
+ CDFResourceAPI[T_TypedInstanceIdentifier, T_WrappedInstanceRequest, T_WrappedInstanceResponse], ABC
161
+ ):
162
+ """API for wrapped instances in CDF. It is intended to be subclassed for specific wrapped instance types."""
163
+
164
+ def __init__(self, http_client: HTTPClient, view_id: TypedViewReference) -> None:
165
+ super().__init__(http_client=http_client, method_endpoint_map=METHOD_MAP)
166
+ self._view_id = view_id
167
+
168
+ @abstractmethod
169
+ def _validate_response(self, response: SuccessResponse) -> ResponseItems[T_TypedInstanceIdentifier]:
170
+ raise NotImplementedError()
171
+
172
+ def create(self, items: Sequence[T_WrappedInstanceRequest]) -> list[InstanceSlimDefinition]:
173
+ """Create instances in CDF.
174
+
175
+ Args:
176
+ items: List of InstanceRequest objects to create.
177
+ Returns:
178
+ List of created InstanceSlimDefinition objects.
179
+ """
180
+ response_items: list[InstanceSlimDefinition] = []
181
+ for response in self._chunk_requests(items, "upsert", self._serialize_items):
182
+ response_items.extend(PagedResponse[InstanceSlimDefinition].model_validate_json(response.body).items)
183
+ return response_items
184
+
185
+ def retrieve(self, items: Sequence[T_TypedInstanceIdentifier]) -> list[T_WrappedInstanceResponse]:
186
+ """Retrieve instances from CDF.
187
+
188
+ Args:
189
+ items: List of ExternalId objects to retrieve.
190
+ source: Optional ViewReference to specify the source view for the instances.
191
+ Returns:
192
+ List of retrieved InstanceResponse objects.
193
+ """
194
+ return self._request_item_response(
195
+ items, method="retrieve", extra_body={"sources": [{"source": self._view_id.dump()}]}
196
+ )
197
+
198
+ def delete(self, items: Sequence[T_TypedInstanceIdentifier]) -> list[T_TypedInstanceIdentifier]:
199
+ """Delete instances from CDF.
200
+
201
+ Args:
202
+ items: List of TypedInstanceIdentifier objects to delete.
203
+ """
204
+ response_items: list[T_TypedInstanceIdentifier] = []
205
+ for response in self._chunk_requests(items, "delete", self._serialize_items):
206
+ response_items.extend(self._validate_response(response).items)
207
+ return response_items
208
+
209
+
210
+ class MultiWrappedInstancesAPI(Generic[T_InstancesListRequest, T_InstancesListResponse], ABC):
211
+ """API for objects that wraps multiple instances in CDF.
212
+
213
+ For example, a Canvas is a node with edges to elements in the Canvas represented as nodes. This wrapper class
214
+ allows creating, retrieving, updating, and deleting such objects in CDF.
215
+
216
+ """
217
+
218
+ def __init__(self, http_client: HTTPClient, query_chunk: int) -> None:
219
+ self._http_client = http_client
220
+ self._method_endpoint_map = METHOD_MAP
221
+ self._query_chunk = query_chunk
222
+
223
+ @abstractmethod
224
+ def _retrieve_query(self, item: Sequence[TypedInstanceIdentifier]) -> dict[str, Any]:
225
+ raise NotImplementedError()
226
+
227
+ @abstractmethod
228
+ def _validate_query_response(self, query_response: QueryResponse) -> list[T_InstancesListResponse]:
229
+ raise NotImplementedError()
230
+
231
+ def create(self, items: Sequence[T_InstancesListRequest]) -> list[InstanceSlimDefinition]:
232
+ """Create instances in CDF.
233
+
234
+ Args:
235
+ items: List of InstanceRequest objects to create.
236
+ Returns:
237
+ List of created InstanceSlimDefinition objects.
238
+ """
239
+ endpoint = self._method_endpoint_map["upsert"]
240
+ response_items: list[InstanceSlimDefinition] = []
241
+ for item in items:
242
+ instance_dicts = item.dump_instances()
243
+ item_response: list[InstanceSlimDefinition] = []
244
+ for chunk in chunker_sequence(instance_dicts, endpoint.item_limit):
245
+ request = RequestMessage(
246
+ endpoint_url=self._http_client.config.create_api_url(endpoint.path),
247
+ method=endpoint.method,
248
+ body_content={"items": chunk}, # type: ignore[dict-item]
249
+ )
250
+ response = self._http_client.request_single_retries(request)
251
+ success = response.get_success_or_raise()
252
+ paged_response = PagedResponse[InstanceSlimDefinition].model_validate_json(success.body)
253
+ item_response.extend(paged_response.items)
254
+ response_items.append(self._merge_instance_slim_definitions(item_response))
255
+ return response_items
256
+
257
+ def _merge_instance_slim_definitions(self, items: list[InstanceSlimDefinition]) -> InstanceSlimDefinition:
258
+ """Merge multiple InstanceSlimDefinition objects into one.
259
+
260
+ Args:
261
+ items: List of InstanceSlimDefinition objects to merge.
262
+ Returns:
263
+ Merged InstanceSlimDefinition object.
264
+ """
265
+ if not items:
266
+ raise ValueError("No items to merge.")
267
+ # Assuming all items have the same space and external_id
268
+ base_item = items[0]
269
+ merged_item = InstanceSlimDefinition(
270
+ instance_type=base_item.instance_type,
271
+ version=base_item.version,
272
+ was_modified=any(item.was_modified for item in items),
273
+ space=base_item.space,
274
+ external_id=base_item.external_id,
275
+ created_time=min(item.created_time for item in items),
276
+ last_updated_time=max(item.last_updated_time for item in items),
277
+ )
278
+ return merged_item
279
+
280
+ def update(self, items: Sequence[T_InstancesListRequest]) -> list[InstanceSlimDefinition]:
281
+ """Update multi-wrapped instances in CDF.
282
+
283
+ This method automatically removes underlying instances that are part of the old object but not the new one.
284
+
285
+ Args:
286
+ items: A sequence of multi-wrapped instance requests to update.
287
+
288
+ Returns:
289
+ List of updated InstanceSlimDefinition objects, one for each item in the input sequence.
290
+ """
291
+ endpoint = self._method_endpoint_map["upsert"]
292
+ updated: list[InstanceSlimDefinition] = []
293
+ for item in items:
294
+ identifier = item.as_id()
295
+ retrieved = self.retrieve([identifier])
296
+ if not retrieved:
297
+ raise ValueError(f"Item with identifier {identifier} not found for update.")
298
+ to_delete = [id.dump() for id in (set(retrieved[0].as_ids()) - set(item.as_ids()))]
299
+ to_update = item.dump_instances()
300
+ item_response: list[InstanceSlimDefinition] = []
301
+ for upsert_chunk, delete_chunk in zip_longest(
302
+ chunker_sequence(to_update, endpoint.item_limit),
303
+ chunker_sequence(to_delete, endpoint.item_limit),
304
+ fillvalue=None,
305
+ ):
306
+ body_content: dict[str, JsonValue] = {}
307
+ if upsert_chunk:
308
+ # MyPy fails do understand that list[dict[str, JsonValue]] is a subtype of JsonValue
309
+ body_content["items"] = upsert_chunk # type: ignore[assignment]
310
+ if delete_chunk:
311
+ body_content["delete"] = delete_chunk # type: ignore[assignment]
312
+
313
+ response = self._http_client.request_single_retries(
314
+ message=RequestMessage(
315
+ endpoint_url=self._http_client.config.create_api_url(endpoint.path),
316
+ method=endpoint.method,
317
+ body_content=body_content,
318
+ )
319
+ )
320
+ success = response.get_success_or_raise()
321
+ paged_response = PagedResponse[InstanceSlimDefinition].model_validate_json(success.body)
322
+ item_response.extend(paged_response.items)
323
+ updated.append(self._merge_instance_slim_definitions(item_response))
324
+ return updated
325
+
326
+ def retrieve(self, items: Sequence[TypedNodeIdentifier]) -> list[T_InstancesListResponse]:
327
+ """Retrieve instances from CDF.
328
+
329
+ Args:
330
+ items: List of ExternalId objects to retrieve.
331
+ Returns:
332
+ List of retrieved InstanceResponse objects.
333
+ """
334
+ retrieved: list[T_InstancesListResponse] = []
335
+ for chunk in chunker_sequence(items, self._query_chunk):
336
+ query_body = self._retrieve_query(chunk)
337
+ request = RequestMessage(
338
+ endpoint_url=self._http_client.config.create_api_url(QUERY_ENDPOINT.path),
339
+ method=QUERY_ENDPOINT.method,
340
+ body_content=query_body,
341
+ )
342
+ response = self._http_client.request_single_retries(request)
343
+ success = response.get_success_or_raise()
344
+ paged_response = QueryResponse.model_validate_json(success.body)
345
+ retrieved.extend(self._validate_query_response(paged_response))
346
+ return retrieved
347
+
348
+ def delete(self, items: Sequence[TypedNodeIdentifier]) -> list[TypedNodeIdentifier]:
349
+ """Delete instances from CDF.
350
+
351
+ Args:
352
+ items: List of TypedInstanceIdentifier objects to delete.
353
+ """
354
+ endpoint = self._method_endpoint_map["delete"]
355
+ response_items: list[TypedNodeIdentifier] = []
356
+ for chunk in chunker_sequence(items, endpoint.item_limit):
357
+ request = RequestMessage(
358
+ endpoint_url=self._http_client.config.create_api_url(endpoint.path),
359
+ method=endpoint.method,
360
+ body_content={"items": [item.dump() for item in chunk]},
361
+ )
362
+ response = self._http_client.request_single_retries(request)
363
+ success = response.get_success_or_raise()
364
+ validated_response = ResponseItems[TypedNodeIdentifier].model_validate_json(success.body)
365
+ response_items.extend(validated_response.items)
366
+ return response_items