cognite-toolkit 0.7.30__py3-none-any.whl → 0.7.39__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 (121) hide show
  1. cognite_toolkit/_cdf.py +5 -6
  2. cognite_toolkit/_cdf_tk/apps/__init__.py +2 -0
  3. cognite_toolkit/_cdf_tk/apps/_core_app.py +7 -1
  4. cognite_toolkit/_cdf_tk/apps/_download_app.py +2 -2
  5. cognite_toolkit/_cdf_tk/apps/_dump_app.py +1 -1
  6. cognite_toolkit/_cdf_tk/apps/_import_app.py +41 -0
  7. cognite_toolkit/_cdf_tk/apps/_migrate_app.py +177 -2
  8. cognite_toolkit/_cdf_tk/builders/_raw.py +1 -1
  9. cognite_toolkit/_cdf_tk/client/_toolkit_client.py +9 -9
  10. cognite_toolkit/_cdf_tk/client/api/infield.py +38 -33
  11. cognite_toolkit/_cdf_tk/client/api/{canvas.py → legacy/canvas.py} +15 -7
  12. cognite_toolkit/_cdf_tk/client/api/{charts.py → legacy/charts.py} +1 -1
  13. cognite_toolkit/_cdf_tk/client/api/{extended_data_modeling.py → legacy/extended_data_modeling.py} +1 -1
  14. cognite_toolkit/_cdf_tk/client/api/{extended_files.py → legacy/extended_files.py} +2 -2
  15. cognite_toolkit/_cdf_tk/client/api/{extended_functions.py → legacy/extended_functions.py} +9 -9
  16. cognite_toolkit/_cdf_tk/client/api/{extended_raw.py → legacy/extended_raw.py} +1 -1
  17. cognite_toolkit/_cdf_tk/client/api/{extended_timeseries.py → legacy/extended_timeseries.py} +5 -2
  18. cognite_toolkit/_cdf_tk/client/api/{location_filters.py → legacy/location_filters.py} +1 -1
  19. cognite_toolkit/_cdf_tk/client/api/legacy/robotics/__init__.py +8 -0
  20. cognite_toolkit/_cdf_tk/client/api/{robotics → legacy/robotics}/capabilities.py +1 -1
  21. cognite_toolkit/_cdf_tk/client/api/{robotics → legacy/robotics}/data_postprocessing.py +1 -1
  22. cognite_toolkit/_cdf_tk/client/api/{robotics → legacy/robotics}/frames.py +1 -1
  23. cognite_toolkit/_cdf_tk/client/api/{robotics → legacy/robotics}/locations.py +1 -1
  24. cognite_toolkit/_cdf_tk/client/api/{robotics → legacy/robotics}/maps.py +1 -1
  25. cognite_toolkit/_cdf_tk/client/api/{robotics → legacy/robotics}/robots.py +2 -2
  26. cognite_toolkit/_cdf_tk/client/api/{search_config.py → legacy/search_config.py} +5 -1
  27. cognite_toolkit/_cdf_tk/client/api/migration.py +2 -3
  28. cognite_toolkit/_cdf_tk/client/api/project.py +9 -8
  29. cognite_toolkit/_cdf_tk/client/api/search.py +2 -2
  30. cognite_toolkit/_cdf_tk/client/api/streams.py +21 -17
  31. cognite_toolkit/_cdf_tk/client/api/three_d.py +343 -9
  32. cognite_toolkit/_cdf_tk/client/data_classes/api_classes.py +13 -0
  33. cognite_toolkit/_cdf_tk/client/data_classes/base.py +12 -32
  34. cognite_toolkit/_cdf_tk/client/data_classes/instance_api.py +18 -13
  35. cognite_toolkit/_cdf_tk/client/data_classes/legacy/__init__.py +0 -0
  36. cognite_toolkit/_cdf_tk/client/data_classes/{canvas.py → legacy/canvas.py} +1 -1
  37. cognite_toolkit/_cdf_tk/client/data_classes/three_d.py +65 -0
  38. cognite_toolkit/_cdf_tk/client/testing.py +24 -16
  39. cognite_toolkit/_cdf_tk/commands/__init__.py +1 -0
  40. cognite_toolkit/_cdf_tk/commands/_migrate/conversion.py +8 -2
  41. cognite_toolkit/_cdf_tk/commands/_migrate/creators.py +1 -1
  42. cognite_toolkit/_cdf_tk/commands/_migrate/data_classes.py +35 -4
  43. cognite_toolkit/_cdf_tk/commands/_migrate/data_mapper.py +149 -14
  44. cognite_toolkit/_cdf_tk/commands/_migrate/data_model.py +1 -0
  45. cognite_toolkit/_cdf_tk/commands/_migrate/default_mappings.py +1 -1
  46. cognite_toolkit/_cdf_tk/commands/_migrate/issues.py +19 -1
  47. cognite_toolkit/_cdf_tk/commands/_migrate/migration_io.py +220 -3
  48. cognite_toolkit/_cdf_tk/commands/_profile.py +1 -1
  49. cognite_toolkit/_cdf_tk/commands/_purge.py +9 -11
  50. cognite_toolkit/_cdf_tk/commands/build_cmd.py +1 -1
  51. cognite_toolkit/_cdf_tk/commands/build_v2/__init__.py +0 -0
  52. cognite_toolkit/_cdf_tk/commands/build_v2/build_cmd.py +241 -0
  53. cognite_toolkit/_cdf_tk/commands/build_v2/build_input.py +85 -0
  54. cognite_toolkit/_cdf_tk/commands/build_v2/build_issues.py +27 -0
  55. cognite_toolkit/_cdf_tk/commands/dump_resource.py +4 -4
  56. cognite_toolkit/_cdf_tk/commands/run.py +1 -1
  57. cognite_toolkit/_cdf_tk/cruds/_data_cruds.py +2 -2
  58. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/auth.py +1 -1
  59. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/classic.py +1 -1
  60. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/configuration.py +1 -1
  61. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/datamodel.py +1 -1
  62. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/extraction_pipeline.py +1 -1
  63. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/fieldops.py +22 -20
  64. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/file.py +1 -1
  65. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/function.py +14 -2
  66. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/industrial_tool.py +1 -1
  67. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/location.py +1 -1
  68. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/migration.py +1 -1
  69. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/raw.py +1 -1
  70. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/robotics.py +1 -1
  71. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/transformation.py +49 -14
  72. cognite_toolkit/_cdf_tk/data_classes/_module_toml.py +1 -0
  73. cognite_toolkit/_cdf_tk/resource_classes/search_config.py +1 -1
  74. cognite_toolkit/_cdf_tk/resource_classes/workflow_version.py +164 -5
  75. cognite_toolkit/_cdf_tk/storageio/_applications.py +2 -2
  76. cognite_toolkit/_cdf_tk/storageio/_file_content.py +1 -2
  77. cognite_toolkit/_cdf_tk/storageio/_instances.py +1 -1
  78. cognite_toolkit/_cdf_tk/storageio/selectors/__init__.py +10 -1
  79. cognite_toolkit/_cdf_tk/storageio/selectors/_three_d.py +34 -0
  80. cognite_toolkit/_cdf_tk/utils/cdf.py +1 -1
  81. cognite_toolkit/_cdf_tk/utils/http_client/__init__.py +28 -0
  82. cognite_toolkit/_cdf_tk/utils/http_client/_client.py +3 -2
  83. cognite_toolkit/_cdf_tk/utils/http_client/_data_classes.py +6 -0
  84. cognite_toolkit/_cdf_tk/utils/http_client/_data_classes2.py +67 -7
  85. cognite_toolkit/_cdf_tk/utils/http_client/_tracker.py +5 -2
  86. cognite_toolkit/_cdf_tk/utils/interactive_select.py +51 -4
  87. cognite_toolkit/_cdf_tk/validation.py +4 -0
  88. cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
  89. cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
  90. cognite_toolkit/_resources/cdf.toml +1 -1
  91. cognite_toolkit/_version.py +1 -1
  92. {cognite_toolkit-0.7.30.dist-info → cognite_toolkit-0.7.39.dist-info}/METADATA +1 -1
  93. {cognite_toolkit-0.7.30.dist-info → cognite_toolkit-0.7.39.dist-info}/RECORD +119 -113
  94. {cognite_toolkit-0.7.30.dist-info → cognite_toolkit-0.7.39.dist-info}/WHEEL +1 -1
  95. cognite_toolkit/_cdf_tk/client/api/robotics/__init__.py +0 -3
  96. cognite_toolkit/_cdf_tk/prototypes/import_app.py +0 -41
  97. /cognite_toolkit/_cdf_tk/{prototypes/commands → client/api/legacy}/__init__.py +0 -0
  98. /cognite_toolkit/_cdf_tk/client/api/{dml.py → legacy/dml.py} +0 -0
  99. /cognite_toolkit/_cdf_tk/client/api/{fixed_transformations.py → legacy/fixed_transformations.py} +0 -0
  100. /cognite_toolkit/_cdf_tk/client/api/{robotics → legacy/robotics}/api.py +0 -0
  101. /cognite_toolkit/_cdf_tk/client/api/{robotics → legacy/robotics}/utlis.py +0 -0
  102. /cognite_toolkit/_cdf_tk/client/data_classes/{apm_config_v1.py → legacy/apm_config_v1.py} +0 -0
  103. /cognite_toolkit/_cdf_tk/client/data_classes/{charts.py → legacy/charts.py} +0 -0
  104. /cognite_toolkit/_cdf_tk/client/data_classes/{extendable_cognite_file.py → legacy/extendable_cognite_file.py} +0 -0
  105. /cognite_toolkit/_cdf_tk/client/data_classes/{extended_filemetadata.py → legacy/extended_filemetadata.py} +0 -0
  106. /cognite_toolkit/_cdf_tk/client/data_classes/{extended_filemetdata.py → legacy/extended_filemetdata.py} +0 -0
  107. /cognite_toolkit/_cdf_tk/client/data_classes/{extended_timeseries.py → legacy/extended_timeseries.py} +0 -0
  108. /cognite_toolkit/_cdf_tk/client/data_classes/{functions.py → legacy/functions.py} +0 -0
  109. /cognite_toolkit/_cdf_tk/client/data_classes/{graphql_data_models.py → legacy/graphql_data_models.py} +0 -0
  110. /cognite_toolkit/_cdf_tk/client/data_classes/{instances.py → legacy/instances.py} +0 -0
  111. /cognite_toolkit/_cdf_tk/client/data_classes/{location_filters.py → legacy/location_filters.py} +0 -0
  112. /cognite_toolkit/_cdf_tk/client/data_classes/{migration.py → legacy/migration.py} +0 -0
  113. /cognite_toolkit/_cdf_tk/client/data_classes/{pending_instances_ids.py → legacy/pending_instances_ids.py} +0 -0
  114. /cognite_toolkit/_cdf_tk/client/data_classes/{project.py → legacy/project.py} +0 -0
  115. /cognite_toolkit/_cdf_tk/client/data_classes/{raw.py → legacy/raw.py} +0 -0
  116. /cognite_toolkit/_cdf_tk/client/data_classes/{robotics.py → legacy/robotics.py} +0 -0
  117. /cognite_toolkit/_cdf_tk/client/data_classes/{search_config.py → legacy/search_config.py} +0 -0
  118. /cognite_toolkit/_cdf_tk/client/data_classes/{sequences.py → legacy/sequences.py} +0 -0
  119. /cognite_toolkit/_cdf_tk/client/data_classes/{streamlit_.py → legacy/streamlit_.py} +0 -0
  120. /cognite_toolkit/_cdf_tk/{prototypes/commands/import_.py → commands/_import_cmd.py} +0 -0
  121. {cognite_toolkit-0.7.30.dist-info → cognite_toolkit-0.7.39.dist-info}/entry_points.txt +0 -0
@@ -1,19 +1,80 @@
1
+ from collections import defaultdict
2
+ from collections.abc import Iterable, Sequence
3
+ from typing import Any, TypeVar
4
+
5
+ from pydantic import TypeAdapter
1
6
  from rich.console import Console
2
7
 
3
- from cognite_toolkit._cdf_tk.client.data_classes.api_classes import PagedResponse
4
- from cognite_toolkit._cdf_tk.client.data_classes.three_d import ThreeDModelResponse
5
- from cognite_toolkit._cdf_tk.utils.http_client import HTTPClient, ParamRequest
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
17
+ from cognite_toolkit._cdf_tk.utils.http_client import (
18
+ HTTPClient,
19
+ ItemsRequest2,
20
+ RequestMessage2,
21
+ )
6
22
  from cognite_toolkit._cdf_tk.utils.useful_types import PrimitiveType
7
23
 
8
24
 
9
25
  class ThreeDModelAPI:
10
26
  ENDPOINT = "/3d/models"
27
+ MAX_CLASSIC_MODELS_PER_CREATE_REQUEST = 1000
28
+ MAX_MODELS_PER_DELETE_REQUEST = 1000
29
+ _LIST_REQUEST_MAX_LIMIT = 1000
11
30
 
12
31
  def __init__(self, http_client: HTTPClient, console: Console) -> None:
13
32
  self._http_client = http_client
14
33
  self._console = console
15
34
  self._config = http_client.config
16
35
 
36
+ def create(self, models: Sequence[ThreeDModelClassicRequest]) -> list[ThreeDModelResponse]:
37
+ """Create 3D models in classic format.
38
+
39
+ Args:
40
+ models (Sequence[ThreeDModelClassicRequest]): The 3D model(s) to create.
41
+
42
+ Returns:
43
+ list[ThreeDModelResponse]: The created 3D model(s).
44
+ """
45
+ if not models:
46
+ return []
47
+ if len(models) > self.MAX_CLASSIC_MODELS_PER_CREATE_REQUEST:
48
+ raise ValueError("Cannot create more than 1000 3D models in a single request.")
49
+ responses = self._http_client.request_items_retries(
50
+ ItemsRequest2(
51
+ endpoint_url=self._config.create_api_url(self.ENDPOINT),
52
+ method="POST",
53
+ items=models,
54
+ )
55
+ )
56
+ responses.raise_for_status()
57
+ return TypeAdapter(list[ThreeDModelResponse]).validate_python(responses.get_items())
58
+
59
+ def delete(self, ids: Sequence[int]) -> None:
60
+ """Delete 3D models by their IDs.
61
+
62
+ Args:
63
+ ids (Sequence[int]): The IDs of the 3D models to delete.
64
+ """
65
+ if not ids:
66
+ return None
67
+ if len(ids) > self.MAX_MODELS_PER_DELETE_REQUEST:
68
+ raise ValueError("Cannot delete more than 1000 3D models in a single request.")
69
+ responses = self._http_client.request_items_retries(
70
+ ItemsRequest2(
71
+ endpoint_url=self._config.create_api_url(self.ENDPOINT + "/delete"),
72
+ method="POST",
73
+ items=InternalIdRequest.from_ids(list(ids)),
74
+ )
75
+ )
76
+ responses.raise_for_status()
77
+
17
78
  def iterate(
18
79
  self,
19
80
  published: bool | None = None,
@@ -21,8 +82,8 @@ class ThreeDModelAPI:
21
82
  limit: int = 100,
22
83
  cursor: str | None = None,
23
84
  ) -> PagedResponse[ThreeDModelResponse]:
24
- if not (0 < limit <= 1000):
25
- raise ValueError("Limit must be between 1 and 1000.")
85
+ if not (0 < limit <= self._LIST_REQUEST_MAX_LIMIT):
86
+ raise ValueError(f"Limit must be between 1 and {self._LIST_REQUEST_MAX_LIMIT}, got {limit}.")
26
87
  parameters: dict[str, PrimitiveType] = {
27
88
  # There is a bug in the API. The parameter includeRevisionInfo is expected to be lower case and not
28
89
  # camel case as documented. You get error message: Unrecognized query parameter includeRevisionInfo,
@@ -34,17 +95,290 @@ class ThreeDModelAPI:
34
95
  parameters["published"] = published
35
96
  if cursor is not None:
36
97
  parameters["cursor"] = cursor
37
- responses = self._http_client.request_with_retries(
38
- ParamRequest(
98
+ responses = self._http_client.request_single_retries(
99
+ RequestMessage2(
39
100
  endpoint_url=self._config.create_api_url(self.ENDPOINT),
40
101
  method="GET",
41
102
  parameters=parameters,
42
103
  )
43
104
  )
44
- responses.raise_for_status()
45
- return PagedResponse[ThreeDModelResponse].model_validate(responses.get_first_body())
105
+ success_response = responses.get_success_or_raise()
106
+ return PagedResponse[ThreeDModelResponse].model_validate(success_response.body_json)
107
+
108
+ def list(
109
+ self,
110
+ published: bool | None = None,
111
+ include_revision_info: bool = False,
112
+ limit: int | None = 100,
113
+ cursor: str | None = None,
114
+ ) -> list[ThreeDModelResponse]:
115
+ results: list[ThreeDModelResponse] = []
116
+ while True:
117
+ request_limit = (
118
+ self._LIST_REQUEST_MAX_LIMIT
119
+ if limit is None
120
+ else min(limit - len(results), self._LIST_REQUEST_MAX_LIMIT)
121
+ )
122
+ if request_limit <= 0:
123
+ break
124
+ page = self.iterate(
125
+ published=published,
126
+ include_revision_info=include_revision_info,
127
+ limit=request_limit,
128
+ cursor=cursor,
129
+ )
130
+ results.extend(page.items)
131
+ if page.next_cursor is None:
132
+ break
133
+ cursor = page.next_cursor
134
+ return results
135
+
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
46
379
 
47
380
 
48
381
  class ThreeDAPI:
49
382
  def __init__(self, http_client: HTTPClient, console: Console) -> None:
50
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,9 +3,11 @@ 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 BaseModel, ConfigDict
6
+ from pydantic import ConfigDict
7
7
  from pydantic.alias_generators import to_camel
8
8
 
9
+ from cognite_toolkit._cdf_tk.utils.http_client._data_classes2 import BaseModelObject, RequestResource
10
+
9
11
  if TYPE_CHECKING:
10
12
  from cognite.client import CogniteClient
11
13
 
@@ -15,28 +17,6 @@ else:
15
17
  from typing_extensions import Self
16
18
 
17
19
 
18
- class BaseModelObject(BaseModel):
19
- """Base class for all object. This includes resources and nested objects."""
20
-
21
- # We allow extra fields to support forward compatibility.
22
- model_config = ConfigDict(alias_generator=to_camel, extra="allow")
23
-
24
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
25
- """Dump the resource to a dictionary.
26
-
27
- This is the default serialization method for request resources.
28
- """
29
- return self.model_dump(mode="json", by_alias=camel_case)
30
-
31
- @classmethod
32
- def _load(cls, resource: dict[str, Any]) -> "Self":
33
- """Load method to match CogniteResource signature."""
34
- return cls.model_validate(resource)
35
-
36
-
37
- class RequestResource(BaseModelObject): ...
38
-
39
-
40
20
  T_RequestResource = TypeVar("T_RequestResource", bound=RequestResource)
41
21
 
42
22
 
@@ -46,22 +26,22 @@ class ResponseResource(BaseModelObject, Generic[T_RequestResource], ABC):
46
26
  """Convert the response resource to a request resource."""
47
27
  ...
48
28
 
29
+ def as_write(self) -> T_RequestResource:
30
+ """Alias for as_request_resource to match protocol signature."""
31
+ return self.as_request_resource()
49
32
 
50
- class Identifier(BaseModel):
33
+
34
+ class Identifier(RequestResource, ABC):
51
35
  """Base class for all identifier classes."""
52
36
 
53
37
  model_config = ConfigDict(alias_generator=to_camel, extra="ignore", populate_by_name=True, frozen=True)
54
38
 
55
- def dump(self, include_type: bool = True) -> dict[str, Any]:
56
- """Dump the identifier to a dictionary.
57
-
58
- Args:
59
- include_type (bool): Whether to include the type of the identifier in the output.
39
+ def dump(self, camel_case: bool = True, include_type: bool = True) -> dict[str, Any]:
40
+ """Dump the resource to a dictionary.
60
41
 
61
- Returns:
62
- dict[str, Any]: The dumped identifier.
42
+ This is the default serialization method for request resources.
63
43
  """
64
- return self.model_dump(mode="json", by_alias=True, exclude_defaults=not include_type)
44
+ return self.model_dump(mode="json", by_alias=camel_case, exclude_unset=not include_type)
65
45
 
66
46
  def as_id(self) -> Self:
67
47
  return self
@@ -7,7 +7,7 @@ from .base import BaseModelObject, Identifier, RequestResource
7
7
  InstanceType: TypeAlias = Literal["node", "edge"]
8
8
 
9
9
 
10
- class InstanceIdentifier(Identifier):
10
+ class TypedInstanceIdentifier(Identifier):
11
11
  """Identifier for an Instance instance."""
12
12
 
13
13
  instance_type: InstanceType
@@ -15,14 +15,19 @@ class InstanceIdentifier(Identifier):
15
15
  external_id: str
16
16
 
17
17
 
18
- class NodeIdentifier(InstanceIdentifier):
18
+ class TypedNodeIdentifier(TypedInstanceIdentifier):
19
19
  instance_type: Literal["node"] = "node"
20
20
 
21
21
 
22
- class EdgeIdentifier(InstanceIdentifier):
22
+ class TypedEdgeIdentifier(TypedInstanceIdentifier):
23
23
  instance_type: Literal["edge"] = "edge"
24
24
 
25
25
 
26
+ class InstanceIdentifier(Identifier):
27
+ space: str
28
+ external_id: str
29
+
30
+
26
31
  class InstanceResult(BaseModelObject):
27
32
  instance_type: InstanceType
28
33
  version: int
@@ -32,8 +37,8 @@ class InstanceResult(BaseModelObject):
32
37
  created_time: int
33
38
  last_updated_time: int
34
39
 
35
- def as_id(self) -> InstanceIdentifier:
36
- return InstanceIdentifier(
40
+ def as_id(self) -> TypedInstanceIdentifier:
41
+ return TypedInstanceIdentifier(
37
42
  instance_type=self.instance_type,
38
43
  space=self.space,
39
44
  external_id=self.external_id,
@@ -62,8 +67,8 @@ class InstanceRequestResource(RequestResource):
62
67
  space: str
63
68
  external_id: str
64
69
 
65
- def as_id(self) -> InstanceIdentifier:
66
- return InstanceIdentifier(
70
+ def as_id(self) -> TypedInstanceIdentifier:
71
+ return TypedInstanceIdentifier(
67
72
  instance_type=self.instance_type,
68
73
  space=self.space,
69
74
  external_id=self.external_id,
@@ -107,7 +112,7 @@ class InstanceSource(BaseModelObject):
107
112
  return value
108
113
 
109
114
 
110
- class InstanceRequestItem(BaseModelObject):
115
+ class InstanceRequestItem(RequestResource):
111
116
  model_config = ConfigDict(populate_by_name=True)
112
117
  instance_type: InstanceType
113
118
  space: str
@@ -115,8 +120,8 @@ class InstanceRequestItem(BaseModelObject):
115
120
  existing_version: int | None = None
116
121
  sources: list[InstanceSource] | None = None
117
122
 
118
- def as_id(self) -> InstanceIdentifier:
119
- return InstanceIdentifier(
123
+ def as_id(self) -> TypedInstanceIdentifier:
124
+ return TypedInstanceIdentifier(
120
125
  instance_type=self.instance_type,
121
126
  space=self.space,
122
127
  external_id=self.external_id,
@@ -128,7 +133,7 @@ class InstanceResponseItem(BaseModelObject):
128
133
  space: str
129
134
  external_id: str
130
135
  version: int
131
- type: InstanceIdentifier | None = None
136
+ type: TypedInstanceIdentifier | None = None
132
137
  created_time: int
133
138
  last_updated_time: int
134
139
  deleted_time: int | None = None
@@ -149,8 +154,8 @@ class InstanceResponseItem(BaseModelObject):
149
154
  output.update(space_properties.get(view_version, {}))
150
155
  return output
151
156
 
152
- def as_id(self) -> InstanceIdentifier:
153
- return InstanceIdentifier(
157
+ def as_id(self) -> TypedInstanceIdentifier:
158
+ return TypedInstanceIdentifier(
154
159
  instance_type=self.instance_type,
155
160
  space=self.space,
156
161
  external_id=self.external_id,
@@ -30,7 +30,7 @@ from cognite.client.data_classes.data_modeling.instances import (
30
30
  TypedNodeApply,
31
31
  )
32
32
 
33
- from cognite_toolkit._cdf_tk.client.data_classes.migration import AssetCentricId
33
+ from cognite_toolkit._cdf_tk.client.data_classes.legacy.migration import AssetCentricId
34
34
  from cognite_toolkit._cdf_tk.utils.useful_types import JsonVal
35
35
 
36
36
  if sys.version_info >= (3, 11):
@@ -1,5 +1,8 @@
1
+ from collections.abc import Hashable
1
2
  from typing import Literal
2
3
 
4
+ from pydantic import Field
5
+
3
6
  from .base import BaseModelObject, RequestResource, ResponseResource
4
7
 
5
8
 
@@ -19,11 +22,17 @@ class RevisionStatus(BaseModelObject):
19
22
  class ThreeDModelRequest(RequestResource):
20
23
  name: str
21
24
 
25
+ def as_id(self) -> str:
26
+ return self.name
27
+
22
28
 
23
29
  class ThreeDModelClassicRequest(ThreeDModelRequest):
24
30
  data_set_id: int | None = None
25
31
  metadata: dict[str, str] | None = None
26
32
 
33
+ def as_id(self) -> str:
34
+ return self.name
35
+
27
36
 
28
37
  class ThreeDModelDMSRequest(ThreeDModelRequest):
29
38
  space: str
@@ -45,3 +54,59 @@ class ThreeDModelResponse(ResponseResource[ThreeDModelRequest]):
45
54
  return ThreeDModelClassicRequest._load(self.dump())
46
55
  else:
47
56
  return ThreeDModelDMSRequest._load(self.dump())
57
+
58
+
59
+ class AssetMappingDMRequest(RequestResource):
60
+ node_id: int
61
+ asset_instance_id: NodeReference
62
+ # These fields are part of the path request and not the body schema.
63
+ model_id: int = Field(exclude=True)
64
+ revision_id: int = Field(exclude=True)
65
+
66
+ def as_id(self) -> Hashable:
67
+ return (
68
+ self.model_id,
69
+ self.revision_id,
70
+ self.node_id,
71
+ self.asset_instance_id.space,
72
+ self.asset_instance_id.external_id,
73
+ )
74
+
75
+
76
+ class AssetMappingClassicRequest(RequestResource):
77
+ node_id: int
78
+ asset_id: int | None = None
79
+ asset_instance_id: NodeReference | None = None
80
+ # These fields are part of the path request and not the body schema.
81
+ model_id: int = Field(exclude=True)
82
+ revision_id: int = Field(exclude=True)
83
+
84
+ def as_id(self) -> Hashable:
85
+ if self.asset_id:
86
+ return self.model_id, self.revision_id, self.node_id, self.asset_id
87
+ elif self.asset_instance_id:
88
+ return (
89
+ self.model_id,
90
+ self.revision_id,
91
+ self.node_id,
92
+ self.asset_instance_id.space,
93
+ self.asset_instance_id.external_id,
94
+ )
95
+ else:
96
+ raise AttributeError("asset_id or asset_instance_id is required")
97
+
98
+
99
+ class AssetMappingResponse(ResponseResource[AssetMappingClassicRequest]):
100
+ node_id: int
101
+ asset_id: int | None = None
102
+ asset_instance_id: NodeReference | None = None
103
+ tree_index: int | None = None
104
+ subtree_size: int | None = None
105
+ # These fields are part of the path request and response, but they are included here for convenience.
106
+ model_id: int = Field(exclude=True)
107
+ revision_id: int = Field(exclude=True)
108
+
109
+ def as_request_resource(self) -> AssetMappingClassicRequest:
110
+ return AssetMappingClassicRequest.model_validate(
111
+ {**self.dump(), "modelId": self.model_id, "revisionId": self.revision_id}
112
+ )