cognite-toolkit 0.7.35__py3-none-any.whl → 0.7.37__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/apps/_migrate_app.py +101 -0
- cognite_toolkit/_cdf_tk/client/api/infield.py +16 -17
- cognite_toolkit/_cdf_tk/client/api/streams.py +3 -4
- cognite_toolkit/_cdf_tk/client/api/three_d.py +266 -13
- cognite_toolkit/_cdf_tk/client/data_classes/api_classes.py +13 -0
- cognite_toolkit/_cdf_tk/client/data_classes/base.py +6 -10
- cognite_toolkit/_cdf_tk/client/data_classes/instance_api.py +7 -7
- cognite_toolkit/_cdf_tk/client/data_classes/three_d.py +59 -0
- cognite_toolkit/_cdf_tk/commands/_migrate/conversion.py +7 -1
- cognite_toolkit/_cdf_tk/commands/_migrate/data_mapper.py +50 -1
- cognite_toolkit/_cdf_tk/commands/_migrate/data_model.py +1 -0
- cognite_toolkit/_cdf_tk/commands/_migrate/migration_io.py +119 -8
- cognite_toolkit/_cdf_tk/commands/_purge.py +9 -11
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/fieldops.py +17 -15
- cognite_toolkit/_cdf_tk/utils/http_client/_data_classes2.py +1 -3
- 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.35.dist-info → cognite_toolkit-0.7.37.dist-info}/METADATA +1 -1
- {cognite_toolkit-0.7.35.dist-info → cognite_toolkit-0.7.37.dist-info}/RECORD +23 -23
- {cognite_toolkit-0.7.35.dist-info → cognite_toolkit-0.7.37.dist-info}/WHEEL +1 -1
- {cognite_toolkit-0.7.35.dist-info → cognite_toolkit-0.7.37.dist-info}/entry_points.txt +0 -0
|
@@ -19,11 +19,13 @@ from cognite_toolkit._cdf_tk.commands._migrate.data_mapper import (
|
|
|
19
19
|
AssetCentricMapper,
|
|
20
20
|
CanvasMapper,
|
|
21
21
|
ChartMapper,
|
|
22
|
+
ThreeDAssetMapper,
|
|
22
23
|
ThreeDMapper,
|
|
23
24
|
)
|
|
24
25
|
from cognite_toolkit._cdf_tk.commands._migrate.migration_io import (
|
|
25
26
|
AnnotationMigrationIO,
|
|
26
27
|
AssetCentricMigrationIO,
|
|
28
|
+
ThreeDAssetMappingMigrationIO,
|
|
27
29
|
ThreeDMigrationIO,
|
|
28
30
|
)
|
|
29
31
|
from cognite_toolkit._cdf_tk.commands._migrate.selectors import (
|
|
@@ -68,6 +70,7 @@ class MigrateApp(typer.Typer):
|
|
|
68
70
|
self.command("canvas")(self.canvas)
|
|
69
71
|
self.command("charts")(self.charts)
|
|
70
72
|
self.command("3d")(self.three_d)
|
|
73
|
+
self.command("3d-mappings")(self.three_d_asset_mapping)
|
|
71
74
|
# Uncomment when infield v2 config migration is ready
|
|
72
75
|
# self.command("infield-configs")(self.infield_configs)
|
|
73
76
|
|
|
@@ -1054,6 +1057,104 @@ class MigrateApp(typer.Typer):
|
|
|
1054
1057
|
)
|
|
1055
1058
|
)
|
|
1056
1059
|
|
|
1060
|
+
@staticmethod
|
|
1061
|
+
def three_d_asset_mapping(
|
|
1062
|
+
ctx: typer.Context,
|
|
1063
|
+
model_id: Annotated[
|
|
1064
|
+
list[int] | None,
|
|
1065
|
+
typer.Argument(
|
|
1066
|
+
help="The IDs of the 3D model to migrate asset mappings for. If not provided, an interactive selection will be "
|
|
1067
|
+
"performed to select the."
|
|
1068
|
+
),
|
|
1069
|
+
] = None,
|
|
1070
|
+
object_3D_space: Annotated[
|
|
1071
|
+
str | None,
|
|
1072
|
+
typer.Option(
|
|
1073
|
+
"--object-3d-space",
|
|
1074
|
+
"-o",
|
|
1075
|
+
help="The instance space to ceate the 3D object nodes in.",
|
|
1076
|
+
),
|
|
1077
|
+
] = None,
|
|
1078
|
+
cad_node_space: Annotated[
|
|
1079
|
+
str | None,
|
|
1080
|
+
typer.Option(
|
|
1081
|
+
"--cad-node-space",
|
|
1082
|
+
"-c",
|
|
1083
|
+
help="The instance space to create the CAD node nodes in.",
|
|
1084
|
+
),
|
|
1085
|
+
] = None,
|
|
1086
|
+
log_dir: Annotated[
|
|
1087
|
+
Path,
|
|
1088
|
+
typer.Option(
|
|
1089
|
+
"--log-dir",
|
|
1090
|
+
"-l",
|
|
1091
|
+
help="Path to the directory where migration logs will be stored.",
|
|
1092
|
+
),
|
|
1093
|
+
] = Path(f"migration_logs_{TODAY}"),
|
|
1094
|
+
dry_run: Annotated[
|
|
1095
|
+
bool,
|
|
1096
|
+
typer.Option(
|
|
1097
|
+
"--dry-run",
|
|
1098
|
+
"-d",
|
|
1099
|
+
help="If set, the migration will not be executed, but only a report of what would be done is printed.",
|
|
1100
|
+
),
|
|
1101
|
+
] = False,
|
|
1102
|
+
verbose: Annotated[
|
|
1103
|
+
bool,
|
|
1104
|
+
typer.Option(
|
|
1105
|
+
"--verbose",
|
|
1106
|
+
"-v",
|
|
1107
|
+
help="Turn on to get more verbose output when running the command",
|
|
1108
|
+
),
|
|
1109
|
+
] = False,
|
|
1110
|
+
) -> None:
|
|
1111
|
+
"""Migrate 3D Model Asset Mappings from Asset-Centric to data modeling in CDF.
|
|
1112
|
+
|
|
1113
|
+
This command expects that the selected 3D model has already been migrated to data modeling.
|
|
1114
|
+
"""
|
|
1115
|
+
client = EnvironmentVariables.create_from_environment().get_client()
|
|
1116
|
+
selected_ids: list[int]
|
|
1117
|
+
if model_id is not None:
|
|
1118
|
+
selected_ids = model_id
|
|
1119
|
+
else:
|
|
1120
|
+
# Interactive selection
|
|
1121
|
+
selected_models = ThreeDInteractiveSelect(client, "migrate").select_three_d_models("dm")
|
|
1122
|
+
selected_ids = [model.id for model in selected_models]
|
|
1123
|
+
space_selector = DataModelingSelect(client, "migrate")
|
|
1124
|
+
object_3D_space = space_selector.select_instance_space(
|
|
1125
|
+
multiselect=False,
|
|
1126
|
+
message="In which instance space do you want to create the 3D Object nodes?",
|
|
1127
|
+
include_empty=False,
|
|
1128
|
+
)
|
|
1129
|
+
cad_node_space = space_selector.select_instance_space(
|
|
1130
|
+
multiselect=False,
|
|
1131
|
+
message="In which instance space do you want to create the CAD Node nodes?",
|
|
1132
|
+
include_empty=False,
|
|
1133
|
+
)
|
|
1134
|
+
dry_run = questionary.confirm("Do you want to perform a dry run?", default=dry_run).ask()
|
|
1135
|
+
verbose = questionary.confirm("Do you want verbose output?", default=verbose).ask()
|
|
1136
|
+
if any(res is None for res in [dry_run, verbose]):
|
|
1137
|
+
raise typer.Abort()
|
|
1138
|
+
|
|
1139
|
+
if object_3D_space is None or cad_node_space is None:
|
|
1140
|
+
raise typer.BadParameter(
|
|
1141
|
+
"--object-3d-space and --cad-node-space are required when specifying IDs directly."
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
cmd = MigrationCommand()
|
|
1145
|
+
cmd.run(
|
|
1146
|
+
lambda: cmd.migrate(
|
|
1147
|
+
selected=ThreeDModelIdSelector(ids=tuple(selected_ids)),
|
|
1148
|
+
data=ThreeDAssetMappingMigrationIO(
|
|
1149
|
+
client, object_3D_space=object_3D_space, cad_node_space=cad_node_space
|
|
1150
|
+
),
|
|
1151
|
+
mapper=ThreeDAssetMapper(client),
|
|
1152
|
+
log_dir=log_dir,
|
|
1153
|
+
dry_run=dry_run,
|
|
1154
|
+
verbose=verbose,
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1057
1158
|
@staticmethod
|
|
1058
1159
|
def infield_configs(
|
|
1059
1160
|
ctx: typer.Context,
|
|
@@ -4,7 +4,7 @@ from typing import Any, cast
|
|
|
4
4
|
from pydantic import TypeAdapter
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
|
|
7
|
-
from cognite_toolkit._cdf_tk.client.data_classes.api_classes import
|
|
7
|
+
from cognite_toolkit._cdf_tk.client.data_classes.api_classes import QueryResponse
|
|
8
8
|
from cognite_toolkit._cdf_tk.client.data_classes.infield import (
|
|
9
9
|
DataExplorationConfig,
|
|
10
10
|
InFieldCDMLocationConfig,
|
|
@@ -13,12 +13,11 @@ from cognite_toolkit._cdf_tk.client.data_classes.infield import (
|
|
|
13
13
|
from cognite_toolkit._cdf_tk.client.data_classes.instance_api import (
|
|
14
14
|
InstanceResponseItem,
|
|
15
15
|
InstanceResult,
|
|
16
|
-
|
|
16
|
+
TypedNodeIdentifier,
|
|
17
17
|
)
|
|
18
18
|
from cognite_toolkit._cdf_tk.tk_warnings import HighSeverityWarning
|
|
19
19
|
from cognite_toolkit._cdf_tk.utils.http_client import (
|
|
20
20
|
HTTPClient,
|
|
21
|
-
ItemsRequest,
|
|
22
21
|
ItemsRequest2,
|
|
23
22
|
RequestMessage2,
|
|
24
23
|
)
|
|
@@ -56,7 +55,7 @@ class InfieldConfigAPI:
|
|
|
56
55
|
responses.raise_for_status()
|
|
57
56
|
return TypeAdapter(list[InstanceResult]).validate_python(responses.get_items())
|
|
58
57
|
|
|
59
|
-
def retrieve(self, items: Sequence[
|
|
58
|
+
def retrieve(self, items: Sequence[TypedNodeIdentifier]) -> list[InfieldLocationConfig]:
|
|
60
59
|
if len(items) > 100:
|
|
61
60
|
raise ValueError("Cannot retrieve more than 100 InfieldLocationConfig items at once.")
|
|
62
61
|
if not items:
|
|
@@ -73,7 +72,7 @@ class InfieldConfigAPI:
|
|
|
73
72
|
parsed_response = QueryResponse[InstanceResponseItem].model_validate(success.body_json)
|
|
74
73
|
return self._parse_retrieve_response(parsed_response)
|
|
75
74
|
|
|
76
|
-
def delete(self, items: Sequence[InfieldLocationConfig]) -> list[
|
|
75
|
+
def delete(self, items: Sequence[InfieldLocationConfig]) -> list[TypedNodeIdentifier]:
|
|
77
76
|
if len(items) > 500:
|
|
78
77
|
raise ValueError("Cannot delete more than 500 InfieldLocationConfig items at once.")
|
|
79
78
|
|
|
@@ -83,18 +82,18 @@ class InfieldConfigAPI:
|
|
|
83
82
|
else [item.as_id(), item.data_exploration_config.as_id()]
|
|
84
83
|
for item in items
|
|
85
84
|
)
|
|
86
|
-
responses = self._http_client.
|
|
87
|
-
|
|
85
|
+
responses = self._http_client.request_items_retries(
|
|
86
|
+
ItemsRequest2(
|
|
88
87
|
endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
|
|
89
88
|
method="POST",
|
|
90
89
|
items=[identifier for sublist in identifiers for identifier in sublist],
|
|
91
90
|
)
|
|
92
91
|
)
|
|
93
92
|
responses.raise_for_status()
|
|
94
|
-
return
|
|
93
|
+
return TypeAdapter(list[TypedNodeIdentifier]).validate_python(responses.get_items())
|
|
95
94
|
|
|
96
95
|
@classmethod
|
|
97
|
-
def _retrieve_query(cls, items: Sequence[
|
|
96
|
+
def _retrieve_query(cls, items: Sequence[TypedNodeIdentifier]) -> dict[str, Any]:
|
|
98
97
|
return {
|
|
99
98
|
"with": {
|
|
100
99
|
cls.LOCATION_REF: {
|
|
@@ -184,7 +183,7 @@ class InFieldCDMConfigAPI:
|
|
|
184
183
|
results.raise_for_status()
|
|
185
184
|
return TypeAdapter(list[InstanceResult]).validate_python(results.get_items())
|
|
186
185
|
|
|
187
|
-
def retrieve(self, items: Sequence[
|
|
186
|
+
def retrieve(self, items: Sequence[TypedNodeIdentifier]) -> list[InFieldCDMLocationConfig]:
|
|
188
187
|
if len(items) > 100:
|
|
189
188
|
raise ValueError("Cannot retrieve more than 100 InFieldCDMLocationConfig items at once.")
|
|
190
189
|
if not items:
|
|
@@ -200,23 +199,23 @@ class InFieldCDMConfigAPI:
|
|
|
200
199
|
parsed_response = QueryResponse[InstanceResponseItem].model_validate(success.body_json)
|
|
201
200
|
return self._parse_retrieve_response(parsed_response)
|
|
202
201
|
|
|
203
|
-
def delete(self, items: Sequence[InFieldCDMLocationConfig]) -> list[
|
|
202
|
+
def delete(self, items: Sequence[InFieldCDMLocationConfig]) -> list[TypedNodeIdentifier]:
|
|
204
203
|
if len(items) > 500:
|
|
205
204
|
raise ValueError("Cannot delete more than 500 InFieldCDMLocationConfig items at once.")
|
|
206
205
|
|
|
207
|
-
identifiers
|
|
208
|
-
responses = self._http_client.
|
|
209
|
-
|
|
206
|
+
identifiers = [item.as_id() for item in items]
|
|
207
|
+
responses = self._http_client.request_items_retries(
|
|
208
|
+
ItemsRequest2(
|
|
210
209
|
endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
|
|
211
210
|
method="POST",
|
|
212
|
-
items=identifiers,
|
|
211
|
+
items=identifiers,
|
|
213
212
|
)
|
|
214
213
|
)
|
|
215
214
|
responses.raise_for_status()
|
|
216
|
-
return
|
|
215
|
+
return TypeAdapter(list[TypedNodeIdentifier]).validate_python(responses.get_items())
|
|
217
216
|
|
|
218
217
|
@classmethod
|
|
219
|
-
def _retrieve_query(cls, items: Sequence[
|
|
218
|
+
def _retrieve_query(cls, items: Sequence[TypedNodeIdentifier]) -> dict[str, Any]:
|
|
220
219
|
return {
|
|
221
220
|
"with": {
|
|
222
221
|
cls.LOCATION_REF: {
|
|
@@ -8,7 +8,6 @@ from cognite_toolkit._cdf_tk.client.data_classes.streams import StreamRequest, S
|
|
|
8
8
|
from cognite_toolkit._cdf_tk.utils.http_client import (
|
|
9
9
|
HTTPClient,
|
|
10
10
|
ItemsRequest2,
|
|
11
|
-
ParamRequest,
|
|
12
11
|
RequestMessage2,
|
|
13
12
|
)
|
|
14
13
|
|
|
@@ -46,13 +45,13 @@ class StreamsAPI:
|
|
|
46
45
|
Args:
|
|
47
46
|
external_id: External ID of the stream to delete.
|
|
48
47
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
response = self._http_client.request_single_retries(
|
|
49
|
+
RequestMessage2(
|
|
51
50
|
endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/{external_id}"),
|
|
52
51
|
method="DELETE",
|
|
53
52
|
)
|
|
54
53
|
)
|
|
55
|
-
|
|
54
|
+
_ = response.get_success_or_raise()
|
|
56
55
|
|
|
57
56
|
def list(self) -> list[StreamResponse]:
|
|
58
57
|
"""List streams.
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
from collections
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from collections.abc import Iterable, Sequence
|
|
3
|
+
from typing import Any, TypeVar
|
|
2
4
|
|
|
5
|
+
from pydantic import TypeAdapter
|
|
3
6
|
from rich.console import Console
|
|
4
7
|
|
|
5
|
-
from cognite_toolkit._cdf_tk.client.data_classes.api_classes import PagedResponse
|
|
6
|
-
from cognite_toolkit._cdf_tk.client.data_classes.three_d import
|
|
8
|
+
from cognite_toolkit._cdf_tk.client.data_classes.api_classes import InternalIdRequest, PagedResponse
|
|
9
|
+
from cognite_toolkit._cdf_tk.client.data_classes.three_d import (
|
|
10
|
+
AssetMappingClassicRequest,
|
|
11
|
+
AssetMappingDMRequest,
|
|
12
|
+
AssetMappingResponse,
|
|
13
|
+
ThreeDModelClassicRequest,
|
|
14
|
+
ThreeDModelResponse,
|
|
15
|
+
)
|
|
16
|
+
from cognite_toolkit._cdf_tk.utils.collection import chunker_sequence
|
|
7
17
|
from cognite_toolkit._cdf_tk.utils.http_client import (
|
|
8
18
|
HTTPClient,
|
|
9
|
-
|
|
19
|
+
ItemsRequest2,
|
|
10
20
|
RequestMessage2,
|
|
11
|
-
SimpleBodyRequest,
|
|
12
21
|
)
|
|
13
22
|
from cognite_toolkit._cdf_tk.utils.useful_types import PrimitiveType
|
|
14
23
|
|
|
@@ -37,16 +46,15 @@ class ThreeDModelAPI:
|
|
|
37
46
|
return []
|
|
38
47
|
if len(models) > self.MAX_CLASSIC_MODELS_PER_CREATE_REQUEST:
|
|
39
48
|
raise ValueError("Cannot create more than 1000 3D models in a single request.")
|
|
40
|
-
responses = self._http_client.
|
|
41
|
-
|
|
49
|
+
responses = self._http_client.request_items_retries(
|
|
50
|
+
ItemsRequest2(
|
|
42
51
|
endpoint_url=self._config.create_api_url(self.ENDPOINT),
|
|
43
52
|
method="POST",
|
|
44
|
-
items=
|
|
53
|
+
items=models,
|
|
45
54
|
)
|
|
46
55
|
)
|
|
47
56
|
responses.raise_for_status()
|
|
48
|
-
|
|
49
|
-
return PagedResponse[ThreeDModelResponse].model_validate(body).items
|
|
57
|
+
return TypeAdapter(list[ThreeDModelResponse]).validate_python(responses.get_items())
|
|
50
58
|
|
|
51
59
|
def delete(self, ids: Sequence[int]) -> None:
|
|
52
60
|
"""Delete 3D models by their IDs.
|
|
@@ -58,11 +66,11 @@ class ThreeDModelAPI:
|
|
|
58
66
|
return None
|
|
59
67
|
if len(ids) > self.MAX_MODELS_PER_DELETE_REQUEST:
|
|
60
68
|
raise ValueError("Cannot delete more than 1000 3D models in a single request.")
|
|
61
|
-
responses = self._http_client.
|
|
62
|
-
|
|
69
|
+
responses = self._http_client.request_items_retries(
|
|
70
|
+
ItemsRequest2(
|
|
63
71
|
endpoint_url=self._config.create_api_url(self.ENDPOINT + "/delete"),
|
|
64
72
|
method="POST",
|
|
65
|
-
|
|
73
|
+
items=InternalIdRequest.from_ids(list(ids)),
|
|
66
74
|
)
|
|
67
75
|
)
|
|
68
76
|
responses.raise_for_status()
|
|
@@ -126,6 +134,251 @@ class ThreeDModelAPI:
|
|
|
126
134
|
return results
|
|
127
135
|
|
|
128
136
|
|
|
137
|
+
T_RequestMapping = TypeVar("T_RequestMapping", bound=AssetMappingClassicRequest | AssetMappingDMRequest)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ThreeDAssetMappingAPI:
|
|
141
|
+
ENDPOINT = "/3d/models/{modelId}/revisions/{revisionId}/mappings"
|
|
142
|
+
CREATE_CLASSIC_MAX_MAPPINGS_PER_REQUEST = 1000
|
|
143
|
+
CREATE_DM_MAX_MAPPINGS_PER_REQUEST = 100
|
|
144
|
+
DELETE_CLASSIC_MAX_MAPPINGS_PER_REQUEST = 1000
|
|
145
|
+
DELETE_DM_MAX_MAPPINGS_PER_REQUEST = 100
|
|
146
|
+
LIST_REQUEST_MAX_LIMIT = 1000
|
|
147
|
+
|
|
148
|
+
def __init__(self, http_client: HTTPClient, console: Console) -> None:
|
|
149
|
+
self._http_client = http_client
|
|
150
|
+
self._console = console
|
|
151
|
+
self._config = http_client.config
|
|
152
|
+
|
|
153
|
+
def create(self, mappings: Sequence[AssetMappingClassicRequest]) -> list[AssetMappingResponse]:
|
|
154
|
+
"""Create 3D asset mappings.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
mappings (Sequence[AssetMappingClassicRequest]):
|
|
158
|
+
The 3D asset mapping(s) to create.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
list[AssetMappingResponse]: The created 3D asset mapping(s).
|
|
162
|
+
"""
|
|
163
|
+
results: list[AssetMappingResponse] = []
|
|
164
|
+
for endpoint, model_id, revision_id, revision_mappings in self._chunk_mappings_by_endpoint(
|
|
165
|
+
mappings, self.CREATE_CLASSIC_MAX_MAPPINGS_PER_REQUEST
|
|
166
|
+
):
|
|
167
|
+
responses = self._http_client.request_items_retries(
|
|
168
|
+
ItemsRequest2(
|
|
169
|
+
endpoint_url=self._config.create_api_url(endpoint),
|
|
170
|
+
method="POST",
|
|
171
|
+
items=revision_mappings,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
responses.raise_for_status()
|
|
175
|
+
items = responses.get_items()
|
|
176
|
+
for item in items:
|
|
177
|
+
# We append modelId and revisionId to each item since the API does not return them
|
|
178
|
+
# this is needed to fully populate the AssetMappingResponse data class
|
|
179
|
+
item["modelId"] = model_id
|
|
180
|
+
item["revisionId"] = revision_id
|
|
181
|
+
results.extend(TypeAdapter(list[AssetMappingResponse]).validate_python(items))
|
|
182
|
+
return results
|
|
183
|
+
|
|
184
|
+
def create_dm(
|
|
185
|
+
self, mappings: Sequence[AssetMappingDMRequest], object_3d_space: str, cad_node_space: str
|
|
186
|
+
) -> list[AssetMappingResponse]:
|
|
187
|
+
"""Create 3D asset mappings in Data Modeling format.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
mappings (Sequence[AssetMappingDMRequest]):
|
|
191
|
+
The 3D asset mapping(s) to create
|
|
192
|
+
object_3d_space (str):
|
|
193
|
+
The instance space where the Cognite3DObject are located.
|
|
194
|
+
cad_node_space (str):
|
|
195
|
+
The instance space where the CogniteCADNode are located.
|
|
196
|
+
Returns:
|
|
197
|
+
list[AssetMappingResponse]: The created 3D asset mapping(s).
|
|
198
|
+
"""
|
|
199
|
+
results: list[AssetMappingResponse] = []
|
|
200
|
+
for endpoint, model_id, revision_id, revision_mappings in self._chunk_mappings_by_endpoint(
|
|
201
|
+
mappings, self.CREATE_DM_MAX_MAPPINGS_PER_REQUEST
|
|
202
|
+
):
|
|
203
|
+
responses = self._http_client.request_items_retries(
|
|
204
|
+
ItemsRequest2(
|
|
205
|
+
endpoint_url=self._config.create_api_url(endpoint),
|
|
206
|
+
method="POST",
|
|
207
|
+
items=revision_mappings,
|
|
208
|
+
extra_body_fields={
|
|
209
|
+
"dmsContextualizationConfig": {
|
|
210
|
+
"object3DSpace": object_3d_space,
|
|
211
|
+
"cadNodeSpace": cad_node_space,
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
responses.raise_for_status()
|
|
217
|
+
items = responses.get_items()
|
|
218
|
+
for item in items:
|
|
219
|
+
# We append modelId and revisionId to each item since the API does not return them
|
|
220
|
+
# this is needed to fully populate the AssetMappingResponse data class
|
|
221
|
+
item["modelId"] = model_id
|
|
222
|
+
item["revisionId"] = revision_id
|
|
223
|
+
results.extend(TypeAdapter(list[AssetMappingResponse]).validate_python(items))
|
|
224
|
+
return results
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def _chunk_mappings_by_endpoint(
|
|
228
|
+
cls, mappings: Sequence[T_RequestMapping], chunk_size: int
|
|
229
|
+
) -> Iterable[tuple[str, int, int, list[T_RequestMapping]]]:
|
|
230
|
+
chunked_mappings: dict[tuple[int, int], list[T_RequestMapping]] = defaultdict(list)
|
|
231
|
+
for mapping in mappings:
|
|
232
|
+
key = mapping.model_id, mapping.revision_id
|
|
233
|
+
chunked_mappings[key].append(mapping)
|
|
234
|
+
for (model_id, revision_id), revision_mappings in chunked_mappings.items():
|
|
235
|
+
endpoint = cls.ENDPOINT.format(modelId=model_id, revisionId=revision_id)
|
|
236
|
+
for chunk in chunker_sequence(revision_mappings, chunk_size):
|
|
237
|
+
yield endpoint, model_id, revision_id, chunk
|
|
238
|
+
|
|
239
|
+
def delete(self, mappings: Sequence[AssetMappingClassicRequest]) -> None:
|
|
240
|
+
"""Delete 3D asset mappings.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
mappings (Sequence[AssetMappingClassicRequest]):
|
|
244
|
+
The 3D asset mapping(s) to delete.
|
|
245
|
+
"""
|
|
246
|
+
for endpoint, *_, revision_mappings in self._chunk_mappings_by_endpoint(
|
|
247
|
+
mappings, self.DELETE_CLASSIC_MAX_MAPPINGS_PER_REQUEST
|
|
248
|
+
):
|
|
249
|
+
responses = self._http_client.request_items_retries(
|
|
250
|
+
ItemsRequest2(
|
|
251
|
+
endpoint_url=self._config.create_api_url(f"{endpoint}/delete"),
|
|
252
|
+
method="DELETE",
|
|
253
|
+
items=revision_mappings,
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
responses.raise_for_status()
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
def delete_dm(self, mappings: Sequence[AssetMappingDMRequest], object_3d_space: str, cad_node_space: str) -> None:
|
|
260
|
+
"""Delete 3D asset mappings in Data Modeling format.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
mappings (Sequence[AssetMappingDMRequest]):
|
|
264
|
+
The 3D asset mapping(s) to delete.
|
|
265
|
+
object_3d_space (str):
|
|
266
|
+
The instance space where the Cognite3DObject are located.
|
|
267
|
+
cad_node_space (str):
|
|
268
|
+
The instance space where the CogniteCADNode are located.
|
|
269
|
+
"""
|
|
270
|
+
for endpoint, *_, revision_mappings in self._chunk_mappings_by_endpoint(
|
|
271
|
+
mappings, self.DELETE_DM_MAX_MAPPINGS_PER_REQUEST
|
|
272
|
+
):
|
|
273
|
+
responses = self._http_client.request_items_retries(
|
|
274
|
+
ItemsRequest2(
|
|
275
|
+
endpoint_url=self._config.create_api_url(f"{endpoint}/delete"),
|
|
276
|
+
method="DELETE",
|
|
277
|
+
items=revision_mappings,
|
|
278
|
+
extra_body_fields={
|
|
279
|
+
"dmsContextualizationConfig": {
|
|
280
|
+
"object3DSpace": object_3d_space,
|
|
281
|
+
"cadNodeSpace": cad_node_space,
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
responses.raise_for_status()
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
def iterate(
|
|
290
|
+
self,
|
|
291
|
+
model_id: int,
|
|
292
|
+
revision_id: int,
|
|
293
|
+
asset_ids: list[int] | None = None,
|
|
294
|
+
asset_instance_ids: list[str] | None = None,
|
|
295
|
+
node_ids: list[int] | None = None,
|
|
296
|
+
tree_indexes: list[int] | None = None,
|
|
297
|
+
get_dms_instances: bool = False,
|
|
298
|
+
limit: int = 100,
|
|
299
|
+
cursor: str | None = None,
|
|
300
|
+
) -> PagedResponse[AssetMappingResponse]:
|
|
301
|
+
if not (0 < limit <= self.LIST_REQUEST_MAX_LIMIT):
|
|
302
|
+
raise ValueError(f"Limit must be between 1 and {self.LIST_REQUEST_MAX_LIMIT}, got {limit}.")
|
|
303
|
+
if sum(param is not None for param in [asset_ids, asset_instance_ids, node_ids, tree_indexes]) > 1:
|
|
304
|
+
raise ValueError("Only one of asset_ids, asset_instance_ids, node_ids, or tree_indexes can be provided.")
|
|
305
|
+
body: dict[str, Any] = {
|
|
306
|
+
"getDmsInstances": get_dms_instances,
|
|
307
|
+
"limit": limit,
|
|
308
|
+
}
|
|
309
|
+
if asset_ids is not None:
|
|
310
|
+
if not (0 < len(asset_ids) <= 100):
|
|
311
|
+
raise ValueError("asset_ids must contain between 1 and 100 IDs.")
|
|
312
|
+
body["filter"] = {"assetIds": asset_ids}
|
|
313
|
+
elif asset_instance_ids is not None:
|
|
314
|
+
if not (0 < len(asset_instance_ids) <= 100):
|
|
315
|
+
raise ValueError("asset_instance_ids must contain between 1 and 100 IDs.")
|
|
316
|
+
body["filter"] = {"assetInstanceIds": asset_instance_ids}
|
|
317
|
+
elif node_ids is not None:
|
|
318
|
+
if not (0 < len(node_ids) <= 100):
|
|
319
|
+
raise ValueError("node_ids must contain between 1 and 100 IDs.")
|
|
320
|
+
body["filter"] = {"nodeIds": node_ids}
|
|
321
|
+
elif tree_indexes is not None:
|
|
322
|
+
if not (0 < len(tree_indexes) <= 100):
|
|
323
|
+
raise ValueError("tree_indexes must contain between 1 and 100 indexes.")
|
|
324
|
+
body["filter"] = {"treeIndexes": tree_indexes}
|
|
325
|
+
if cursor is not None:
|
|
326
|
+
body["cursor"] = cursor
|
|
327
|
+
|
|
328
|
+
endpoint = self.ENDPOINT.format(modelId=model_id, revisionId=revision_id)
|
|
329
|
+
responses = self._http_client.request_single_retries(
|
|
330
|
+
RequestMessage2(
|
|
331
|
+
endpoint_url=self._config.create_api_url(f"{endpoint}/list"),
|
|
332
|
+
method="POST",
|
|
333
|
+
body_content=body,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
success_response = responses.get_success_or_raise()
|
|
337
|
+
body_json = success_response.body_json
|
|
338
|
+
# Add modelId and revisionId to items since the API does not return them
|
|
339
|
+
for item in body_json.get("items", []):
|
|
340
|
+
item["modelId"] = model_id
|
|
341
|
+
item["revisionId"] = revision_id
|
|
342
|
+
return PagedResponse[AssetMappingResponse].model_validate(body_json)
|
|
343
|
+
|
|
344
|
+
def list(
|
|
345
|
+
self,
|
|
346
|
+
model_id: int,
|
|
347
|
+
revision_id: int,
|
|
348
|
+
asset_ids: list[int] | None = None,
|
|
349
|
+
asset_instance_ids: list[str] | None = None,
|
|
350
|
+
node_ids: list[int] | None = None,
|
|
351
|
+
tree_indexes: list[int] | None = None,
|
|
352
|
+
get_dms_instances: bool = False,
|
|
353
|
+
limit: int | None = 100,
|
|
354
|
+
) -> list[AssetMappingResponse]:
|
|
355
|
+
results: list[AssetMappingResponse] = []
|
|
356
|
+
cursor: str | None = None
|
|
357
|
+
while True:
|
|
358
|
+
request_limit = (
|
|
359
|
+
self.LIST_REQUEST_MAX_LIMIT if limit is None else min(limit - len(results), self.LIST_REQUEST_MAX_LIMIT)
|
|
360
|
+
)
|
|
361
|
+
if request_limit <= 0:
|
|
362
|
+
break
|
|
363
|
+
page = self.iterate(
|
|
364
|
+
model_id=model_id,
|
|
365
|
+
revision_id=revision_id,
|
|
366
|
+
asset_ids=asset_ids,
|
|
367
|
+
asset_instance_ids=asset_instance_ids,
|
|
368
|
+
node_ids=node_ids,
|
|
369
|
+
tree_indexes=tree_indexes,
|
|
370
|
+
get_dms_instances=get_dms_instances,
|
|
371
|
+
limit=request_limit,
|
|
372
|
+
cursor=cursor,
|
|
373
|
+
)
|
|
374
|
+
results.extend(page.items)
|
|
375
|
+
if page.next_cursor is None:
|
|
376
|
+
break
|
|
377
|
+
cursor = page.next_cursor
|
|
378
|
+
return results
|
|
379
|
+
|
|
380
|
+
|
|
129
381
|
class ThreeDAPI:
|
|
130
382
|
def __init__(self, http_client: HTTPClient, console: Console) -> None:
|
|
131
383
|
self.models = ThreeDModelAPI(http_client, console)
|
|
384
|
+
self.asset_mappings = ThreeDAssetMappingAPI(http_client, console)
|
|
@@ -2,6 +2,8 @@ from typing import Generic, TypeVar
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field, JsonValue
|
|
4
4
|
|
|
5
|
+
from cognite_toolkit._cdf_tk.utils.http_client._data_classes2 import RequestResource
|
|
6
|
+
|
|
5
7
|
T = TypeVar("T", bound=BaseModel)
|
|
6
8
|
|
|
7
9
|
|
|
@@ -15,3 +17,14 @@ class QueryResponse(BaseModel, Generic[T]):
|
|
|
15
17
|
typing: dict[str, JsonValue] | None = None
|
|
16
18
|
next_cursor: dict[str, str] = Field(alias="nextCursor")
|
|
17
19
|
debug: dict[str, JsonValue] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InternalIdRequest(RequestResource):
|
|
23
|
+
id: int
|
|
24
|
+
|
|
25
|
+
def as_id(self) -> int:
|
|
26
|
+
return self.id
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_ids(cls, ids: list[int]) -> list["InternalIdRequest"]:
|
|
30
|
+
return [cls(id=id_) for id_ in ids]
|
|
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
|
|
|
3
3
|
from collections import UserList
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
5
5
|
|
|
6
|
-
from pydantic import
|
|
6
|
+
from pydantic import ConfigDict
|
|
7
7
|
from pydantic.alias_generators import to_camel
|
|
8
8
|
|
|
9
9
|
from cognite_toolkit._cdf_tk.utils.http_client._data_classes2 import BaseModelObject, RequestResource
|
|
@@ -31,21 +31,17 @@ class ResponseResource(BaseModelObject, Generic[T_RequestResource], ABC):
|
|
|
31
31
|
return self.as_request_resource()
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class Identifier(
|
|
34
|
+
class Identifier(RequestResource, ABC):
|
|
35
35
|
"""Base class for all identifier classes."""
|
|
36
36
|
|
|
37
37
|
model_config = ConfigDict(alias_generator=to_camel, extra="ignore", populate_by_name=True, frozen=True)
|
|
38
38
|
|
|
39
|
-
def dump(self, include_type: bool = True) -> dict[str, Any]:
|
|
40
|
-
"""Dump the
|
|
39
|
+
def dump(self, camel_case: bool = True, include_type: bool = True) -> dict[str, Any]:
|
|
40
|
+
"""Dump the resource to a dictionary.
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
include_type (bool): Whether to include the type of the identifier in the output.
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
dict[str, Any]: The dumped identifier.
|
|
42
|
+
This is the default serialization method for request resources.
|
|
47
43
|
"""
|
|
48
|
-
return self.model_dump(mode="json", by_alias=
|
|
44
|
+
return self.model_dump(mode="json", by_alias=camel_case, exclude_unset=not include_type)
|
|
49
45
|
|
|
50
46
|
def as_id(self) -> Self:
|
|
51
47
|
return self
|
|
@@ -15,19 +15,19 @@ class TypedInstanceIdentifier(Identifier):
|
|
|
15
15
|
external_id: str
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class
|
|
19
|
-
space: str
|
|
20
|
-
external_id: str
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class NodeIdentifier(TypedInstanceIdentifier):
|
|
18
|
+
class TypedNodeIdentifier(TypedInstanceIdentifier):
|
|
24
19
|
instance_type: Literal["node"] = "node"
|
|
25
20
|
|
|
26
21
|
|
|
27
|
-
class
|
|
22
|
+
class TypedEdgeIdentifier(TypedInstanceIdentifier):
|
|
28
23
|
instance_type: Literal["edge"] = "edge"
|
|
29
24
|
|
|
30
25
|
|
|
26
|
+
class InstanceIdentifier(Identifier):
|
|
27
|
+
space: str
|
|
28
|
+
external_id: str
|
|
29
|
+
|
|
30
|
+
|
|
31
31
|
class InstanceResult(BaseModelObject):
|
|
32
32
|
instance_type: InstanceType
|
|
33
33
|
version: int
|