cognite-toolkit 0.7.70__py3-none-any.whl → 0.7.71__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.
- cognite_toolkit/_cdf_tk/client/_resource_base.py +0 -22
- cognite_toolkit/_cdf_tk/client/api/infield.py +56 -204
- cognite_toolkit/_cdf_tk/client/api/instances.py +239 -16
- cognite_toolkit/_cdf_tk/client/cdf_client/responses.py +3 -3
- cognite_toolkit/_cdf_tk/client/resource_classes/infield.py +133 -72
- cognite_toolkit/_cdf_tk/client/resource_classes/instance_api.py +119 -79
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/fieldops.py +45 -46
- cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
- cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
- cognite_toolkit/_resources/cdf.toml +1 -1
- cognite_toolkit/_version.py +1 -1
- {cognite_toolkit-0.7.70.dist-info → cognite_toolkit-0.7.71.dist-info}/METADATA +1 -1
- {cognite_toolkit-0.7.70.dist-info → cognite_toolkit-0.7.71.dist-info}/RECORD +15 -15
- {cognite_toolkit-0.7.70.dist-info → cognite_toolkit-0.7.71.dist-info}/WHEEL +0 -0
- {cognite_toolkit-0.7.70.dist-info → cognite_toolkit-0.7.71.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]
|
|
@@ -1,102 +1,41 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
|
-
from typing import Any
|
|
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.
|
|
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
|
-
|
|
11
|
-
|
|
10
|
+
ItemsSuccessResponse,
|
|
11
|
+
SuccessResponse,
|
|
12
12
|
)
|
|
13
13
|
from cognite_toolkit._cdf_tk.client.resource_classes.infield import (
|
|
14
14
|
DataExplorationConfig,
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
self._EXPLORATION_REF: {
|
|
110
49
|
"nodes": {
|
|
111
50
|
"from": "locationConfig",
|
|
112
51
|
"direction": "outwards",
|
|
113
52
|
"through": {
|
|
114
|
-
"source":
|
|
115
|
-
"identifier":
|
|
53
|
+
"source": InFieldLocationConfig.VIEW_ID.dump(),
|
|
54
|
+
"identifier": "dataExplorationConfig",
|
|
116
55
|
},
|
|
117
56
|
}
|
|
118
57
|
},
|
|
119
58
|
},
|
|
120
59
|
"select": {
|
|
121
|
-
|
|
122
|
-
"sources": [{"source":
|
|
60
|
+
self._LOCATION_REF: {
|
|
61
|
+
"sources": [{"source": InFieldLocationConfig.VIEW_ID.dump(), "properties": ["*"]}],
|
|
123
62
|
},
|
|
124
|
-
|
|
63
|
+
self._EXPLORATION_REF: {
|
|
125
64
|
"sources": [{"source": DataExplorationConfig.VIEW_ID.dump(), "properties": ["*"]}],
|
|
126
65
|
},
|
|
127
66
|
},
|
|
128
67
|
}
|
|
129
68
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
253
|
-
self.cdm_config = InFieldCDMConfigAPI(http_client
|
|
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
|
|
3
|
+
from itertools import zip_longest
|
|
4
|
+
from typing import Any, Generic, Literal
|
|
3
5
|
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
from cognite_toolkit._cdf_tk.client.
|
|
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
|
|
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:
|
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Generic, TypeVar
|
|
1
|
+
from typing import Any, Generic, TypeVar
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field, JsonValue
|
|
4
4
|
|
|
@@ -22,8 +22,8 @@ class PagedResponse(BaseModel, Generic[T]):
|
|
|
22
22
|
next_cursor: str | None = Field(None, alias="nextCursor")
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
class QueryResponse(BaseModel
|
|
26
|
-
items: dict[str, list[
|
|
25
|
+
class QueryResponse(BaseModel):
|
|
26
|
+
items: dict[str, list[dict[str, Any]]]
|
|
27
27
|
typing: dict[str, JsonValue] | None = None
|
|
28
28
|
next_cursor: dict[str, str] = Field(alias="nextCursor")
|
|
29
29
|
debug: dict[str, JsonValue] | None = None
|