cognite-toolkit 0.7.26__py3-none-any.whl → 0.7.28__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.
@@ -8,17 +8,14 @@ from cognite.client.data_classes import Annotation
8
8
  from cognite.client.data_classes.data_modeling import ContainerId
9
9
 
10
10
  from cognite_toolkit._cdf_tk.client import ToolkitClient
11
- from cognite_toolkit._cdf_tk.commands import (
12
- MigrationCanvasCommand,
13
- MigrationPrepareCommand,
14
- )
11
+ from cognite_toolkit._cdf_tk.commands import MigrationPrepareCommand
15
12
  from cognite_toolkit._cdf_tk.commands._migrate import MigrationCommand
16
13
  from cognite_toolkit._cdf_tk.commands._migrate.creators import (
17
14
  InfieldV2ConfigCreator,
18
15
  InstanceSpaceCreator,
19
16
  SourceSystemCreator,
20
17
  )
21
- from cognite_toolkit._cdf_tk.commands._migrate.data_mapper import AssetCentricMapper, ChartMapper
18
+ from cognite_toolkit._cdf_tk.commands._migrate.data_mapper import AssetCentricMapper, CanvasMapper, ChartMapper
22
19
  from cognite_toolkit._cdf_tk.commands._migrate.migration_io import (
23
20
  AnnotationMigrationIO,
24
21
  AssetCentricMigrationIO,
@@ -28,14 +25,15 @@ from cognite_toolkit._cdf_tk.commands._migrate.selectors import (
28
25
  MigrateDataSetSelector,
29
26
  MigrationCSVFileSelector,
30
27
  )
31
- from cognite_toolkit._cdf_tk.storageio import ChartIO
32
- from cognite_toolkit._cdf_tk.storageio.selectors import ChartExternalIdSelector
28
+ from cognite_toolkit._cdf_tk.storageio import CanvasIO, ChartIO
29
+ from cognite_toolkit._cdf_tk.storageio.selectors import CanvasExternalIdSelector, ChartExternalIdSelector
33
30
  from cognite_toolkit._cdf_tk.utils.auth import EnvironmentVariables
34
31
  from cognite_toolkit._cdf_tk.utils.cli_args import parse_view_str
35
32
  from cognite_toolkit._cdf_tk.utils.interactive_select import (
36
33
  AssetInteractiveSelect,
37
34
  DataModelingSelect,
38
35
  FileMetadataInteractiveSelect,
36
+ InteractiveCanvasSelect,
39
37
  InteractiveChartSelect,
40
38
  ResourceViewMappingInteractiveSelect,
41
39
  )
@@ -855,6 +853,23 @@ class MigrateApp(typer.Typer):
855
853
  "performed to select the Canvas to migrate."
856
854
  ),
857
855
  ] = None,
856
+ skip_on_missing_ref: Annotated[
857
+ bool,
858
+ typer.Option(
859
+ "--skip-on-missing-ref",
860
+ "-s",
861
+ help="If set, the migration will skip Canvases that reference resources that have not been migrated to data modeling. "
862
+ "If not set, the migration will continue but the result will exclude the missing references.",
863
+ ),
864
+ ] = False,
865
+ log_dir: Annotated[
866
+ Path,
867
+ typer.Option(
868
+ "--log-dir",
869
+ "-l",
870
+ help="Path to the directory where migration logs will be stored.",
871
+ ),
872
+ ] = Path(f"migration_logs_{TODAY}"),
858
873
  dry_run: Annotated[
859
874
  bool,
860
875
  typer.Option(
@@ -880,12 +895,24 @@ class MigrateApp(typer.Typer):
880
895
  is populated with the mapping from Asset-Centric resources to the new data modeling resources.
881
896
  """
882
897
  client = EnvironmentVariables.create_from_environment().get_client()
898
+ if external_id is None:
899
+ interactive = InteractiveCanvasSelect(client)
900
+ external_id = interactive.select_external_ids()
901
+ log_dir = questionary.path("Specify log directory for migration logs:", default=str(log_dir)).ask()
902
+ dry_run = questionary.confirm("Do you want to perform a dry run?", default=dry_run).ask()
903
+ verbose = questionary.confirm("Do you want verbose output?", default=verbose).ask()
904
+ if any(res is None for res in [log_dir, dry_run, verbose]):
905
+ raise typer.Abort()
906
+ log_dir = Path(log_dir)
883
907
 
884
- cmd = MigrationCanvasCommand()
908
+ cmd = MigrationCommand()
909
+ selector = CanvasExternalIdSelector(external_ids=tuple(external_id))
885
910
  cmd.run(
886
- lambda: cmd.migrate_canvas(
887
- client,
888
- external_ids=external_id,
911
+ lambda: cmd.migrate(
912
+ selected=selector,
913
+ data=CanvasIO(client, exclude_existing_version=True),
914
+ mapper=CanvasMapper(client, dry_run=dry_run, skip_on_missing_ref=skip_on_missing_ref),
915
+ log_dir=log_dir,
889
916
  dry_run=dry_run,
890
917
  verbose=verbose,
891
918
  )
@@ -36,7 +36,7 @@ class ChartCore(WriteableCogniteResource["ChartWrite"], ABC):
36
36
  def dump(self, camel_case: bool = True) -> dict[str, Any]:
37
37
  """Convert the chart to a dictionary representation."""
38
38
  output = super().dump(camel_case=camel_case)
39
- output["data"] = self.data.dump(camel_case=camel_case)
39
+ output["data"] = self.data.model_dump(mode="json", by_alias=camel_case, exclude_unset=True)
40
40
  return output
41
41
 
42
42
 
@@ -58,7 +58,7 @@ class ChartWrite(ChartCore):
58
58
  return cls(
59
59
  external_id=resource["externalId"],
60
60
  visibility=resource["visibility"],
61
- data=ChartData._load(resource["data"], cognite_client=cognite_client),
61
+ data=ChartData._load(resource["data"]),
62
62
  )
63
63
 
64
64
 
@@ -98,7 +98,7 @@ class Chart(ChartCore):
98
98
  created_time=resource["createdTime"],
99
99
  last_updated_time=resource["lastUpdatedTime"],
100
100
  visibility=resource["visibility"],
101
- data=ChartData._load(resource["data"], cognite_client=cognite_client),
101
+ data=ChartData._load(resource["data"]),
102
102
  owner_id=resource["ownerId"],
103
103
  )
104
104
 
@@ -1,70 +1,30 @@
1
- import sys
2
- from dataclasses import dataclass, field, fields
3
- from functools import lru_cache
4
1
  from typing import Any
5
2
 
6
- from cognite.client import CogniteClient
7
- from cognite.client.data_classes._base import CogniteObject
8
3
  from cognite.client.data_classes.data_modeling import NodeId, ViewId
9
- from cognite.client.utils._auxiliary import to_camel_case
4
+ from pydantic import JsonValue, field_serializer, field_validator
10
5
 
11
- if sys.version_info >= (3, 11):
12
- from typing import Self
13
- else:
14
- from typing_extensions import Self
6
+ from .base import BaseModelObject
15
7
 
16
8
 
17
- @dataclass
18
- class ChartObject(CogniteObject):
19
- # ChartObjects are used in the frontend and the backend does not do any validation of these fields.
20
- # Therefore, to ensure that we do not lose any data, we store unknown fields in a separate dictionary.
21
- # This allows unknown fields to be preserved when loading and dumping ChartObjects
22
- # (serialization and deserialization).
23
- _unknown_fields: dict[str, object] | None = field(default=None, init=False, repr=False)
24
-
25
- @classmethod
26
- def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
27
- """Load a ChartObject from a dictionary."""
28
- instance = super()._load(resource, cognite_client=cognite_client)
29
- instance._unknown_fields = {k: v for k, v in resource.items() if k not in cls._known_camel_case_props()}
30
- return instance
31
-
32
- @classmethod
33
- @lru_cache(maxsize=1)
34
- def _known_camel_case_props(cls) -> set[str]:
35
- return {to_camel_case(f.name) for f in fields(cls)}
36
-
37
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
38
- """Dump the ChartObject to a dictionary."""
39
- data = super().dump(camel_case=camel_case)
40
- if self._unknown_fields:
41
- data.update(self._unknown_fields)
42
- return data
43
-
44
-
45
- @dataclass
46
- class UserInfo(ChartObject):
9
+ class UserInfo(BaseModelObject):
47
10
  id: str | None = None
48
11
  email: str | None = None
49
12
  display_name: str | None = None
50
13
 
51
14
 
52
- @dataclass
53
- class ChartSettings(ChartObject):
15
+ class ChartSettings(BaseModelObject):
54
16
  show_y_axis: bool = True
55
17
  show_min_max: bool = True
56
18
  show_gridlines: bool = True
57
19
  merge_units: bool = False
58
20
 
59
21
 
60
- @dataclass
61
- class ThresholdFilter(ChartObject):
22
+ class ThresholdFilter(BaseModelObject):
62
23
  min_unit: str | None = None
63
24
  max_unit: str | None = None
64
25
 
65
26
 
66
- @dataclass
67
- class ChartCall(ChartObject):
27
+ class ChartCall(BaseModelObject):
68
28
  id: str | None = None
69
29
  hash: int | None = None
70
30
  call_id: str | None = None
@@ -72,182 +32,143 @@ class ChartCall(ChartObject):
72
32
  status: str | None = None
73
33
 
74
34
 
75
- @dataclass
76
- class SubSetting(ChartObject):
35
+ class SubSetting(BaseModelObject):
77
36
  auto_align: bool | None = None
78
37
 
79
38
 
80
- @dataclass
81
- class FlowElement(ChartObject):
39
+ class ChartPosition(BaseModelObject):
40
+ x: float | None = None
41
+ y: float | None = None
42
+
43
+
44
+ class FlowElement(BaseModelObject):
82
45
  id: str | None = None
83
46
  type: str | None = None
84
- position: tuple[float | None, float | None] | None = None
85
- data: dict[str, object] | None = None
47
+ position: ChartPosition | None = None
48
+ data: JsonValue | None = None
49
+ source: str | None = None
50
+ target: str | None = None
51
+ source_handle: str | None = None
52
+ target_handle: str | None = None
86
53
 
87
54
 
88
- @dataclass
89
- class Flow(ChartObject):
55
+ class Flow(BaseModelObject):
90
56
  zoom: float | None = None
91
57
  elements: list[FlowElement] | None = None
92
58
  position: tuple[float | None, float | None] | None = None
93
59
 
94
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
95
- data = super().dump(camel_case=camel_case)
96
- if self.elements:
97
- data["elements"] = [el.dump(camel_case=camel_case) for el in self.elements]
98
- return data
99
60
 
100
- @classmethod
101
- def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
102
- """Load a Flow object from a dictionary."""
103
- instance = super()._load(resource, cognite_client=cognite_client)
104
- if "elements" in resource:
105
- instance.elements = [FlowElement._load(el, cognite_client=cognite_client) for el in resource["elements"]]
106
- return instance
61
+ class ChartElement(BaseModelObject):
62
+ id: str | None = None
63
+ type: str | None = None
107
64
 
108
65
 
109
- @dataclass
110
- class ChartSource(ChartObject):
111
- type: str | None = None
112
- id: str | None = None
66
+ class ChartSource(ChartElement): ...
113
67
 
114
68
 
115
- @dataclass
116
- class BaseChartElement(ChartObject):
117
- type: str | None = None
118
- id: str | None = None
119
- name: str | None = None
69
+ class ChartCoreTimeseries(ChartElement):
70
+ node_reference: NodeId | None = None
71
+ view_reference: ViewId | None = None
72
+ display_mode: str | None = None
120
73
  color: str | None = None
74
+ created_at: int | None = None
121
75
  enabled: bool | None = None
122
- line_weight: float | None = None
123
- line_style: str | None = None
124
76
  interpolation: str | None = None
125
- unit: str | None = None
77
+ line_style: str | None = None
78
+ line_weight: float | None = None
79
+ name: str | None = None
126
80
  preferred_unit: str | None = None
127
- created_at: int | None = None
128
- range: tuple[float | None, float | None] | None = None
129
- description: str | None = None
81
+ range: list[float | None] | None = None
130
82
 
83
+ @field_serializer("node_reference", when_used="always")
84
+ def serialize_node_reference(self, node_reference: NodeId | None) -> dict[str, Any] | None:
85
+ if node_reference:
86
+ return node_reference.dump(include_instance_type=False)
87
+ return None
131
88
 
132
- @dataclass
133
- class ChartCoreTimeseries(BaseChartElement):
134
- node_reference: NodeId | None = None
135
- view_reference: ViewId | None = None
136
- display_mode: str | None = None
89
+ @field_serializer("view_reference", when_used="always")
90
+ def serialize_view_reference(self, view_reference: ViewId | None) -> dict[str, Any] | None:
91
+ if view_reference:
92
+ return view_reference.dump(include_type=False)
93
+ return None
137
94
 
138
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
139
- data = super().dump(camel_case=camel_case)
140
- if self.node_reference:
141
- key = "nodeReference" if camel_case else "node_reference"
142
- data[key] = self.node_reference.dump(include_instance_type=False)
143
- if self.view_reference:
144
- key = "viewReference" if camel_case else "view_reference"
145
- data[key] = self.view_reference.dump(include_type=False)
146
- return data
95
+ @field_validator("node_reference", mode="before")
96
+ @classmethod
97
+ def validate_node_reference(cls, value: Any) -> NodeId | None:
98
+ if value is None or isinstance(value, NodeId):
99
+ return value
100
+ return NodeId.load(value)
147
101
 
102
+ @field_validator("view_reference", mode="before")
148
103
  @classmethod
149
- def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
150
- """Load a ChartCoreTimeseries object from a dictionary."""
151
- instance = super()._load(resource, cognite_client=cognite_client)
152
- if "nodeReference" in resource:
153
- instance.node_reference = NodeId.load(resource["nodeReference"])
154
- if "viewReference" in resource:
155
- instance.view_reference = ViewId.load(resource["viewReference"])
156
- return instance
157
-
158
-
159
- @dataclass
160
- class ChartTimeseries(BaseChartElement):
104
+ def validate_view_reference(cls, value: Any) -> ViewId | None:
105
+ if value is None or isinstance(value, ViewId):
106
+ return value
107
+ return ViewId.load(value)
108
+
109
+
110
+ class ChartTimeseries(ChartElement):
111
+ color: str | None = None
112
+ created_at: int | None = None
113
+ enabled: bool | None = None
114
+ interpolation: str | None = None
115
+ line_style: str | None = None
116
+ line_weight: float | None = None
117
+ name: str | None = None
118
+ preferred_unit: str | None = None
119
+ range: list[float | None] | None = None
120
+ unit: str | None = None
161
121
  ts_id: int | None = None
162
122
  ts_external_id: str | None = None
163
123
  display_mode: str | None = None
164
124
  original_unit: str | None = None
125
+ description: str | None = None
165
126
 
166
127
 
167
- @dataclass
168
- class ChartWorkflow(BaseChartElement):
128
+ class ChartWorkflow(ChartElement):
169
129
  version: str | None = None
130
+ name: str | None = None
131
+ color: str | None = None
132
+ enabled: bool | None = None
133
+ line_weight: float | None = None
134
+ line_style: str | None = None
135
+ interpolation: str | None = None
136
+ unit: str | None = None
137
+ preferred_unit: str | None = None
138
+ range: list[float | None] | None = None
139
+ created_at: int | None = None
170
140
  settings: SubSetting | None = None
171
141
  flow: Flow | None = None
172
142
  calls: list[ChartCall] | None = None
173
143
 
174
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
175
- data = super().dump(camel_case=camel_case)
176
- if self.settings:
177
- data["settings"] = self.settings.dump(camel_case=camel_case)
178
- if self.flow:
179
- data["flow"] = self.flow.dump(camel_case=camel_case)
180
- if self.calls:
181
- data["calls"] = [c.dump(camel_case=camel_case) for c in self.calls]
182
- return data
183
144
 
184
- @classmethod
185
- def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
186
- """Load a ChartWorkflow object from a dictionary."""
187
- instance = super()._load(resource, cognite_client=cognite_client)
188
- if "settings" in resource:
189
- instance.settings = SubSetting._load(resource["settings"], cognite_client=cognite_client)
190
- if "flow" in resource:
191
- instance.flow = Flow._load(resource["flow"], cognite_client=cognite_client)
192
- if "calls" in resource:
193
- instance.calls = [ChartCall._load(call, cognite_client=cognite_client) for call in resource["calls"]]
194
- return instance
195
-
196
-
197
- @dataclass
198
- class ChartThreshold(BaseChartElement):
145
+ class ChartThreshold(ChartElement):
199
146
  visible: bool | None = None
147
+ name: str | None = None
200
148
  source_id: str | None = None
201
149
  upper_limit: float | None = None
202
150
  filter: ThresholdFilter | None = None
203
151
  calls: list[ChartCall] | None = None
204
152
 
205
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
206
- data = super().dump(camel_case=camel_case)
207
- if self.filter:
208
- data["filter"] = self.filter.dump(camel_case=camel_case)
209
- if self.calls:
210
- data["calls"] = [c.dump(camel_case=camel_case) for c in self.calls]
211
- return data
212
153
 
213
- @classmethod
214
- def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
215
- """Load a ChartThreshold object from a dictionary."""
216
- instance = super()._load(resource, cognite_client=cognite_client)
217
- if "filter" in resource:
218
- instance.filter = ThresholdFilter._load(resource["filter"], cognite_client=cognite_client)
219
- if "calls" in resource:
220
- instance.calls = [ChartCall._load(call, cognite_client=cognite_client) for call in resource["calls"]]
221
- return instance
222
-
223
-
224
- @dataclass
225
- class ChartScheduledCalculation(BaseChartElement):
154
+ class ChartScheduledCalculation(ChartElement):
155
+ color: str | None = None
156
+ created_at: int | None = None
157
+ description: str | None = None
158
+ enabled: bool | None = None
159
+ interpolation: str | None = None
160
+ line_style: str | None = None
161
+ line_weight: float | None = None
162
+ name: str | None = None
163
+ preferred_unit: str | None = None
164
+ range: list[float | None] | None = None
165
+ unit: str | None = None
226
166
  version: str | None = None
227
167
  settings: SubSetting | None = None
228
168
  flow: Flow | None = None
229
169
 
230
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
231
- data = super().dump(camel_case=camel_case)
232
- if self.settings:
233
- data["settings"] = self.settings.dump(camel_case=camel_case)
234
- if self.flow:
235
- data["flow"] = self.flow.dump(camel_case=camel_case)
236
- return data
237
170
 
238
- @classmethod
239
- def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
240
- """Load a ChartScheduledCalculation object from a dictionary."""
241
- instance = super()._load(resource, cognite_client=cognite_client)
242
- if "settings" in resource:
243
- instance.settings = SubSetting._load(resource["settings"], cognite_client=cognite_client)
244
- if "flow" in resource:
245
- instance.flow = Flow._load(resource["flow"], cognite_client=cognite_client)
246
- return instance
247
-
248
-
249
- @dataclass
250
- class ChartData(ChartObject):
171
+ class ChartData(BaseModelObject):
251
172
  version: int | None = None
252
173
  name: str | None = None
253
174
  date_from: str | None = None
@@ -261,62 +182,3 @@ class ChartData(ChartObject):
261
182
  threshold_collection: list[ChartThreshold] | None = None
262
183
  scheduled_calculation_collection: list[ChartScheduledCalculation] | None = None
263
184
  settings: ChartSettings | None = None
264
-
265
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
266
- """Dump the ChartData object to a dictionary."""
267
- data = super().dump(camel_case=camel_case)
268
- list_attrs = [
269
- "time_series_collection",
270
- "core_timeseries_collection",
271
- "workflow_collection",
272
- "source_collection",
273
- "threshold_collection",
274
- "scheduled_calculation_collection",
275
- ]
276
- for attr_name in list_attrs:
277
- if collection := getattr(self, attr_name):
278
- key = to_camel_case(attr_name) if camel_case else attr_name
279
- data[key] = [item.dump(camel_case=camel_case) for item in collection]
280
-
281
- single_attrs_map = {
282
- "user_info": "userInfo",
283
- "settings": "settings",
284
- }
285
- for attr_name, camel_key in single_attrs_map.items():
286
- if item := getattr(self, attr_name):
287
- key = camel_key if camel_case else attr_name
288
- data[key] = item.dump(camel_case=camel_case)
289
- return data
290
-
291
- @classmethod
292
- def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
293
- """Load a ChartData object from a dictionary."""
294
- instance = super()._load(resource, cognite_client=cognite_client)
295
- collections_map = [
296
- ("timeSeriesCollection", "time_series_collection", ChartTimeseries),
297
- ("coreTimeseriesCollection", "core_timeseries_collection", ChartCoreTimeseries),
298
- ("workflowCollection", "workflow_collection", ChartWorkflow),
299
- ("sourceCollection", "source_collection", ChartSource),
300
- ("thresholdCollection", "threshold_collection", ChartThreshold),
301
- ("scheduledCalculationCollection", "scheduled_calculation_collection", ChartScheduledCalculation),
302
- ]
303
- for resource_key, attr_name, subclass in collections_map:
304
- if resource_key in resource:
305
- setattr(
306
- instance,
307
- attr_name,
308
- [subclass._load(item, cognite_client=cognite_client) for item in resource[resource_key]], # type: ignore[attr-defined]
309
- )
310
- attribute_map = [
311
- ("userInfo", "user_info", UserInfo),
312
- ("settings", "settings", ChartSettings),
313
- ]
314
- for resource_key, attr_name, subclass in attribute_map:
315
- if resource_key in resource:
316
- setattr(
317
- instance,
318
- attr_name,
319
- subclass._load(resource[resource_key], cognite_client=cognite_client), # type: ignore[attr-defined]
320
- )
321
-
322
- return instance
@@ -1,6 +1,6 @@
1
1
  from ._download import DownloadCommand
2
2
  from ._migrate import (
3
- MigrationCanvasCommand,
3
+ MigrationCommand,
4
4
  MigrationPrepareCommand,
5
5
  )
6
6
  from ._profile import ProfileAssetCentricCommand, ProfileAssetCommand, ProfileRawCommand, ProfileTransformationCommand
@@ -30,7 +30,7 @@ __all__ = [
30
30
  "DownloadCommand",
31
31
  "DumpResourceCommand",
32
32
  "InitCommand",
33
- "MigrationCanvasCommand",
33
+ "MigrationCommand",
34
34
  "MigrationPrepareCommand",
35
35
  "ModulesCommand",
36
36
  "ProfileAssetCentricCommand",
@@ -1,9 +1,7 @@
1
- from .canvas import MigrationCanvasCommand
2
1
  from .command import MigrationCommand
3
2
  from .prepare import MigrationPrepareCommand
4
3
 
5
4
  __all__ = [
6
- "MigrationCanvasCommand",
7
5
  "MigrationCommand",
8
6
  "MigrationPrepareCommand",
9
7
  ]
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
- from collections.abc import Sequence
2
+ from collections import defaultdict
3
+ from collections.abc import Callable, Sequence
3
4
  from typing import Generic, cast
4
5
  from uuid import uuid4
5
6
 
@@ -11,8 +12,15 @@ from cognite.client.data_classes.data_modeling import (
11
12
  View,
12
13
  ViewId,
13
14
  )
15
+ from cognite.client.exceptions import CogniteException
14
16
 
15
17
  from cognite_toolkit._cdf_tk.client import ToolkitClient
18
+ from cognite_toolkit._cdf_tk.client.data_classes.canvas import (
19
+ ContainerReferenceApply,
20
+ FdmInstanceContainerReferenceApply,
21
+ IndustrialCanvas,
22
+ IndustrialCanvasApply,
23
+ )
16
24
  from cognite_toolkit._cdf_tk.client.data_classes.charts import Chart, ChartWrite
17
25
  from cognite_toolkit._cdf_tk.client.data_classes.charts_data import (
18
26
  ChartCoreTimeseries,
@@ -23,13 +31,18 @@ from cognite_toolkit._cdf_tk.client.data_classes.migration import ResourceViewMa
23
31
  from cognite_toolkit._cdf_tk.commands._migrate.conversion import DirectRelationCache, asset_centric_to_dm
24
32
  from cognite_toolkit._cdf_tk.commands._migrate.data_classes import AssetCentricMapping
25
33
  from cognite_toolkit._cdf_tk.commands._migrate.default_mappings import create_default_mappings
26
- from cognite_toolkit._cdf_tk.commands._migrate.issues import ChartMigrationIssue, ConversionIssue, MigrationIssue
34
+ from cognite_toolkit._cdf_tk.commands._migrate.issues import (
35
+ CanvasMigrationIssue,
36
+ ChartMigrationIssue,
37
+ ConversionIssue,
38
+ MigrationIssue,
39
+ )
27
40
  from cognite_toolkit._cdf_tk.commands._migrate.selectors import AssetCentricMigrationSelector
28
41
  from cognite_toolkit._cdf_tk.constants import MISSING_INSTANCE_SPACE
29
- from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError
42
+ from cognite_toolkit._cdf_tk.exceptions import ToolkitMigrationError, ToolkitValueError
30
43
  from cognite_toolkit._cdf_tk.protocols import T_ResourceRequest, T_ResourceResponse
31
44
  from cognite_toolkit._cdf_tk.storageio._base import T_Selector
32
- from cognite_toolkit._cdf_tk.storageio.selectors import ChartSelector
45
+ from cognite_toolkit._cdf_tk.storageio.selectors import CanvasSelector, ChartSelector
33
46
  from cognite_toolkit._cdf_tk.utils import humanize_collection
34
47
  from cognite_toolkit._cdf_tk.utils.useful_types import (
35
48
  T_AssetCentricResourceExtended,
@@ -197,16 +210,14 @@ class ChartMapper(DataMapper[ChartSelector, Chart, ChartWrite]):
197
210
  def _create_new_timeseries_core(
198
211
  self, ts_item: ChartTimeseries, node_id: NodeId, consumer_view_id: ViewId | None
199
212
  ) -> ChartCoreTimeseries:
200
- dumped = ts_item.dump(camel_case=True)
201
- for asset_centric_key in ["tsId", "tsExternalId", "originalUnit"]:
202
- dumped.pop(asset_centric_key, None)
203
-
213
+ dumped = ts_item.model_dump(mode="json", by_alias=True, exclude_unset=True)
204
214
  dumped["nodeReference"] = node_id
205
215
  dumped["viewReference"] = consumer_view_id
206
216
  new_uuid = str(uuid4())
207
217
  dumped["id"] = new_uuid
208
218
  dumped["type"] = "coreTimeseries"
209
- core_timeseries = ChartCoreTimeseries._load(dumped)
219
+ # We ignore extra here to only include the fields that are shared between ChartTimeseries and ChartCoreTimeseries
220
+ core_timeseries = ChartCoreTimeseries.model_validate(dumped, extra="ignore")
210
221
  return core_timeseries
211
222
 
212
223
  def _get_node_id_consumer_view_id(self, ts_item: ChartTimeseries) -> tuple[NodeId | None, ViewId | None]:
@@ -245,3 +256,130 @@ class ChartMapper(DataMapper[ChartSelector, Chart, ChartWrite]):
245
256
  new_source_item = ChartSource(id=cast(str, core_ts_item.id), type=cast(str, core_ts_item.type))
246
257
  updated_source_collection.append(new_source_item)
247
258
  return updated_source_collection
259
+
260
+
261
+ class CanvasMapper(DataMapper[CanvasSelector, IndustrialCanvas, IndustrialCanvasApply]):
262
+ # Note sequences are not supported in Canvas, so we do not include them here.
263
+ asset_centric_resource_types = tuple(("asset", "event", "timeseries", "file"))
264
+ DEFAULT_ASSET_VIEW = ViewId("cdf_cdm", "CogniteAsset", "v1")
265
+ DEFAULT_EVENT_VIEW = ViewId("cdf_cdm", "CogniteActivity", "v1")
266
+ DEFAULT_FILE_VIEW = ViewId("cdf_cdm", "CogniteFile", "v1")
267
+ DEFAULT_TIMESERIES_VIEW = ViewId("cdf_cdm", "CogniteTimeSeries", "v1")
268
+
269
+ def __init__(self, client: ToolkitClient, dry_run: bool, skip_on_missing_ref: bool = False) -> None:
270
+ self.client = client
271
+ self.dry_run = dry_run
272
+ self.skip_on_missing_ref = skip_on_missing_ref
273
+
274
+ def map(self, source: Sequence[IndustrialCanvas]) -> Sequence[tuple[IndustrialCanvasApply | None, MigrationIssue]]:
275
+ self._populate_cache(source)
276
+ output: list[tuple[IndustrialCanvasApply | None, MigrationIssue]] = []
277
+ for item in source:
278
+ mapped_item, issue = self._map_single_item(item)
279
+ output.append((mapped_item, issue))
280
+ return output
281
+
282
+ @property
283
+ def lookup_methods(self) -> dict[str, Callable]:
284
+ return {
285
+ "asset": self.client.migration.lookup.assets,
286
+ "event": self.client.migration.lookup.events,
287
+ "timeseries": self.client.migration.lookup.time_series,
288
+ "file": self.client.migration.lookup.files,
289
+ }
290
+
291
+ def _populate_cache(self, source: Sequence[IndustrialCanvas]) -> None:
292
+ """Populate the internal cache with references from the source canvases.
293
+
294
+ Note that the consumption views are also cached as part of the timeseries lookup.
295
+ """
296
+ ids_by_type: dict[str, set[int]] = defaultdict(set)
297
+ for canvas in source:
298
+ for ref in canvas.container_references or []:
299
+ if ref.container_reference_type in self.asset_centric_resource_types:
300
+ ids_by_type[ref.container_reference_type].add(ref.resource_id)
301
+
302
+ for resource_type, lookup_method in self.lookup_methods.items():
303
+ ids = ids_by_type.get(resource_type)
304
+ if ids:
305
+ lookup_method(list(ids))
306
+
307
+ def _get_node_id(self, resource_id: int, resource_type: str) -> NodeId | None:
308
+ """Look up the node ID for a given resource ID and type."""
309
+ try:
310
+ return self.lookup_methods[resource_type](resource_id)
311
+ except KeyError:
312
+ raise ToolkitValueError(f"Unsupported resource type '{resource_type}' for container reference migration.")
313
+
314
+ def _get_consumer_view_id(self, resource_id: int, resource_type: str) -> ViewId:
315
+ """Look up the consumer view ID for a given resource ID and type."""
316
+ lookup_map = {
317
+ "asset": (self.client.migration.lookup.assets.consumer_view, self.DEFAULT_ASSET_VIEW),
318
+ "event": (self.client.migration.lookup.events.consumer_view, self.DEFAULT_EVENT_VIEW),
319
+ "timeseries": (self.client.migration.lookup.time_series.consumer_view, self.DEFAULT_TIMESERIES_VIEW),
320
+ "file": (self.client.migration.lookup.files.consumer_view, self.DEFAULT_FILE_VIEW),
321
+ }
322
+ if lookup_tuple := lookup_map.get(resource_type):
323
+ method, fallback = lookup_tuple
324
+ return method(resource_id) or fallback
325
+
326
+ raise ToolkitValueError(f"Unsupported resource type '{resource_type}' for container reference migration.")
327
+
328
+ def _map_single_item(self, canvas: IndustrialCanvas) -> tuple[IndustrialCanvasApply | None, CanvasMigrationIssue]:
329
+ update = canvas.as_write()
330
+ issue = CanvasMigrationIssue(canvas_external_id=canvas.canvas.external_id, canvas_name=canvas.canvas.name)
331
+
332
+ remaining_container_references: list[ContainerReferenceApply] = []
333
+ new_fdm_references: list[FdmInstanceContainerReferenceApply] = []
334
+ for ref in update.container_references or []:
335
+ if ref.container_reference_type not in self.asset_centric_resource_types:
336
+ remaining_container_references.append(ref)
337
+ continue
338
+ node_id = self._get_node_id(ref.resource_id, ref.container_reference_type)
339
+ if node_id is None:
340
+ issue.missing_reference_ids.append(ref.as_asset_centric_id())
341
+ else:
342
+ consumer_view = self._get_consumer_view_id(ref.resource_id, ref.container_reference_type)
343
+ fdm_ref = self.migrate_container_reference(ref, canvas.canvas.external_id, node_id, consumer_view)
344
+ new_fdm_references.append(fdm_ref)
345
+ if issue.missing_reference_ids and self.skip_on_missing_ref:
346
+ return None, issue
347
+
348
+ update.container_references = remaining_container_references
349
+ update.fdm_instance_container_references.extend(new_fdm_references)
350
+ if not self.dry_run:
351
+ backup = canvas.as_write().create_backup()
352
+ try:
353
+ self.client.canvas.industrial.create(backup)
354
+ except CogniteException as e:
355
+ raise ToolkitMigrationError(
356
+ f"Failed to create backup for canvas '{canvas.canvas.name}': {e!s}. "
357
+ ) from e
358
+
359
+ return update, issue
360
+
361
+ @classmethod
362
+ def migrate_container_reference(
363
+ cls, reference: ContainerReferenceApply, canvas_external_id: str, instance_id: NodeId, consumer_view: ViewId
364
+ ) -> FdmInstanceContainerReferenceApply:
365
+ """Migrate a single container reference by replacing the asset-centric ID with the data model instance ID."""
366
+ new_id = str(uuid4())
367
+ new_external_id = f"{canvas_external_id}_{new_id}"
368
+ return FdmInstanceContainerReferenceApply(
369
+ external_id=new_external_id,
370
+ id_=new_id,
371
+ container_reference_type="fdmInstance",
372
+ instance_space=instance_id.space,
373
+ instance_external_id=instance_id.external_id,
374
+ view_space=consumer_view.space,
375
+ view_external_id=consumer_view.external_id,
376
+ view_version=consumer_view.version,
377
+ label=reference.label,
378
+ properties_=reference.properties_,
379
+ x=reference.x,
380
+ y=reference.y,
381
+ width=reference.width,
382
+ height=reference.height,
383
+ max_width=reference.max_width,
384
+ max_height=reference.max_height,
385
+ )
@@ -51,6 +51,18 @@ class ChartMigrationIssue(MigrationIssue):
51
51
  )
52
52
 
53
53
 
54
+ class CanvasMigrationIssue(MigrationIssue):
55
+ type: ClassVar[str] = "canvasMigration"
56
+ canvas_external_id: str
57
+ canvas_name: str
58
+ missing_reference_ids: list[AssetCentricId] = Field(default_factory=list)
59
+
60
+ @property
61
+ def has_issues(self) -> bool:
62
+ """Check if there are any issues recorded in this CanvasMigrationIssue."""
63
+ return bool(self.missing_reference_ids)
64
+
65
+
54
66
  class ReadIssue(MigrationIssue):
55
67
  """Represents a read issue encountered during migration."""
56
68
 
@@ -1,6 +1,7 @@
1
1
  from collections.abc import Iterable, Sequence
2
2
  from typing import Any
3
3
 
4
+ from cognite_toolkit._cdf_tk.client import ToolkitClient
4
5
  from cognite_toolkit._cdf_tk.client.data_classes.canvas import (
5
6
  IndustrialCanvas,
6
7
  IndustrialCanvasApply,
@@ -127,6 +128,15 @@ class ChartIO(UploadableStorageIO[ChartSelector, Chart, ChartWrite]):
127
128
 
128
129
 
129
130
  class CanvasIO(UploadableStorageIO[CanvasSelector, IndustrialCanvas, IndustrialCanvasApply]):
131
+ """Download and upload Industrial Canvases to/from CDF.
132
+
133
+ Args:
134
+ client (ToolkitClient): The Cognite Toolkit client to use for API interactions.
135
+ exclude_existing_version (bool): Whether to exclude the 'existingVersion' field when uploading canvases.
136
+ Defaults to True. If you set this to False, the upload may fail if the existing version in CDF is
137
+ lower or equal to the one in the uploaded data.
138
+ """
139
+
130
140
  KIND = "IndustrialCanvas"
131
141
  SUPPORTED_DOWNLOAD_FORMATS = frozenset({".ndjson"})
132
142
  SUPPORTED_COMPRESSIONS = frozenset({".gz"})
@@ -134,6 +144,10 @@ class CanvasIO(UploadableStorageIO[CanvasSelector, IndustrialCanvas, IndustrialC
134
144
  CHUNK_SIZE = 10
135
145
  BASE_SELECTOR = CanvasSelector
136
146
 
147
+ def __init__(self, client: ToolkitClient, exclude_existing_version: bool = True) -> None:
148
+ super().__init__(client)
149
+ self.exclude_existing_version = exclude_existing_version
150
+
137
151
  def as_id(self, item: IndustrialCanvas) -> str:
138
152
  return item.as_id()
139
153
 
@@ -171,12 +185,19 @@ class CanvasIO(UploadableStorageIO[CanvasSelector, IndustrialCanvas, IndustrialC
171
185
  results: list[HTTPMessage] = []
172
186
  for item in data_chunk:
173
187
  instances = item.item.as_instances()
188
+ items: list[dict[str, JsonVal]] = []
189
+ for instance in instances:
190
+ dumped = instance.dump()
191
+ if self.exclude_existing_version:
192
+ dumped.pop("existingVersion", None)
193
+ items.append(dumped)
194
+
174
195
  responses = http_client.request_with_retries(
175
196
  message=SimpleBodyRequest(
176
197
  endpoint_url=config.create_api_url("/models/instances"),
177
198
  method="POST",
178
199
  # MyPy does not understand that .dump is valid json
179
- body_content={"items": [instance.dump() for instance in instances]},
200
+ body_content={"items": items}, # type: ignore[dict-item]
180
201
  )
181
202
  )
182
203
  results.extend(responses.as_item_responses(item.source_id))
@@ -12,7 +12,7 @@ jobs:
12
12
  environment: dev
13
13
  name: Deploy
14
14
  container:
15
- image: cognite/toolkit:0.7.26
15
+ image: cognite/toolkit:0.7.28
16
16
  env:
17
17
  CDF_CLUSTER: ${{ vars.CDF_CLUSTER }}
18
18
  CDF_PROJECT: ${{ vars.CDF_PROJECT }}
@@ -10,7 +10,7 @@ jobs:
10
10
  environment: dev
11
11
  name: Deploy Dry Run
12
12
  container:
13
- image: cognite/toolkit:0.7.26
13
+ image: cognite/toolkit:0.7.28
14
14
  env:
15
15
  CDF_CLUSTER: ${{ vars.CDF_CLUSTER }}
16
16
  CDF_PROJECT: ${{ vars.CDF_PROJECT }}
@@ -4,7 +4,7 @@ default_env = "<DEFAULT_ENV_PLACEHOLDER>"
4
4
  [modules]
5
5
  # This is the version of the modules. It should not be changed manually.
6
6
  # It will be updated by the 'cdf modules upgrade' command.
7
- version = "0.7.26"
7
+ version = "0.7.28"
8
8
 
9
9
 
10
10
  [plugins]
@@ -1 +1 @@
1
- __version__ = "0.7.26"
1
+ __version__ = "0.7.28"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite_toolkit
3
- Version: 0.7.26
3
+ Version: 0.7.28
4
4
  Summary: Official Cognite Data Fusion tool for project templates and configuration deployment
5
5
  Author: Cognite AS
6
6
  Author-email: Cognite AS <support@cognite.com>
@@ -9,7 +9,7 @@ cognite_toolkit/_cdf_tk/apps/_dev_app.py,sha256=FaY67PFdKwdiMKgJbTcjHT1X2Xfbog2P
9
9
  cognite_toolkit/_cdf_tk/apps/_download_app.py,sha256=2nPn9P_9br9poynSpKKSZF7WYTYT--BfxlxXkSEeH-8,41156
10
10
  cognite_toolkit/_cdf_tk/apps/_dump_app.py,sha256=EPq6fWSaScj9ncKfRY253rRJ37er47PIM60IFgkQK_k,37127
11
11
  cognite_toolkit/_cdf_tk/apps/_landing_app.py,sha256=YR9z83OY7PhhgBVC5gmRLgo9iTXoGoZfRhOU3gd_r2o,888
12
- cognite_toolkit/_cdf_tk/apps/_migrate_app.py,sha256=g4S_53kbIgk57ziPLdRMuR6xUe434gkMqa69VmVm5Vg,39619
12
+ cognite_toolkit/_cdf_tk/apps/_migrate_app.py,sha256=_woM0D2j6VzuYC0LJKteALbQ4U8vGj0B1LSBj_WszKQ,41198
13
13
  cognite_toolkit/_cdf_tk/apps/_modules_app.py,sha256=t0SPvulgbgkF_OO2E68mQ_ZUcJ6HoaurYe0IkmXie0o,7449
14
14
  cognite_toolkit/_cdf_tk/apps/_profile_app.py,sha256=vSRJW54bEvIul8_4rOqyOYA7ztXx7TFOvZRZWZTxMbg,7007
15
15
  cognite_toolkit/_cdf_tk/apps/_purge.py,sha256=KYI1wFy7yHFEM1qJnTYc4_8E2FVGu4QhPsWsxop1sZA,14242
@@ -67,8 +67,8 @@ cognite_toolkit/_cdf_tk/client/data_classes/apm_config_v1.py,sha256=0bPq7R0qvdf8
67
67
  cognite_toolkit/_cdf_tk/client/data_classes/base.py,sha256=QG4S0HlByMB6zwxUXWaVHwP-DrA2Y97XGN_o6QsL6FY,2776
68
68
  cognite_toolkit/_cdf_tk/client/data_classes/canvas.py,sha256=DrE-7HOLnk1ELhydySsEhw-VOjriUqB_zzon5qb7CDk,50721
69
69
  cognite_toolkit/_cdf_tk/client/data_classes/capabilities.py,sha256=muqpAC2JLCFcEpRPzuh_3sS3o_q42WFyfsGzl-LfB_U,8773
70
- cognite_toolkit/_cdf_tk/client/data_classes/charts.py,sha256=l4LM1eqn8_2lRzw-DdwwL0zxgSczC0lp1LjgW-4wqPI,3770
71
- cognite_toolkit/_cdf_tk/client/data_classes/charts_data.py,sha256=Zcx3st3bMJE2aa5U6TepDqRzqf-LuTTWW5grSaVGr9E,12244
70
+ cognite_toolkit/_cdf_tk/client/data_classes/charts.py,sha256=4ZSZDJhDP8uNubXfzphuLJzKJhL1F01grB4UesxtSbQ,3745
71
+ cognite_toolkit/_cdf_tk/client/data_classes/charts_data.py,sha256=-dFfY53cos5DwASLU18aBfYF1VC6bfaUshC2HiGJ2uI,5571
72
72
  cognite_toolkit/_cdf_tk/client/data_classes/extendable_cognite_file.py,sha256=0iyLiXEzB4WBU-DL6DZS6nD5E526cDsftMGEYXwI8r8,9764
73
73
  cognite_toolkit/_cdf_tk/client/data_classes/extended_filemetadata.py,sha256=8zfXl_bhkums3quJzdOwAjxVNY6B0hpAs6jbkekn79o,5488
74
74
  cognite_toolkit/_cdf_tk/client/data_classes/extended_filemetdata.py,sha256=gKA5UcDKweH7SlzXfyZCspMyHUo0t8R5DbzeCPpzInM,6002
@@ -93,21 +93,20 @@ cognite_toolkit/_cdf_tk/client/testing.py,sha256=mXqEXPMZcbETrXBn6D-SiAcjD7xAkuu
93
93
  cognite_toolkit/_cdf_tk/client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
94
  cognite_toolkit/_cdf_tk/client/utils/_concurrency.py,sha256=3GtQbKDaosyKHEt-KzxKK9Yie4TvZPdoou2vUk6dUa8,2298
95
95
  cognite_toolkit/_cdf_tk/client/utils/_http_client.py,sha256=oXNKrIaizG4WiSAhL_kSCHAuL4aaaEhCU4pOJGxh6Xs,483
96
- cognite_toolkit/_cdf_tk/commands/__init__.py,sha256=HQIHw18EU09fdo7bxbDXi8-0Lc5t2KE6BpuhNZehpqo,1402
96
+ cognite_toolkit/_cdf_tk/commands/__init__.py,sha256=wXRbOwMyhGjMj83_bXTOUXtCh70YS4-eiYzzCMN_YOs,1390
97
97
  cognite_toolkit/_cdf_tk/commands/_base.py,sha256=1gl8Y-yqfedRMfdbwM3iPTIUIZriX1UvC1deLsJSJwM,2667
98
98
  cognite_toolkit/_cdf_tk/commands/_changes.py,sha256=sU0KaTtPVSJgAZcaZ1Tkcajj36pmhd13kh7V8QbIED8,22987
99
99
  cognite_toolkit/_cdf_tk/commands/_cli_commands.py,sha256=TK6U_rm6VZT_V941kTyHMoulWgJzbDC8YIIQDPJ5x3w,1011
100
100
  cognite_toolkit/_cdf_tk/commands/_download.py,sha256=dVddH9t7oGx1kdQ3CCYYQb96Uxxy-xC8Opph98lo46U,6869
101
- cognite_toolkit/_cdf_tk/commands/_migrate/__init__.py,sha256=i5ldcTah59K0E4fH5gHTV0GRvtDCEvVses9WQzn9Lno,226
102
- cognite_toolkit/_cdf_tk/commands/_migrate/canvas.py,sha256=R-z0yfOFcJZj-zRLhN-7z_-SLxqzSmONMgrbzNF9dGs,8843
101
+ cognite_toolkit/_cdf_tk/commands/_migrate/__init__.py,sha256=8ki04tJGH1dHdF2NtVF4HyhaC0XDDS7onrH_nvd9KtE,153
103
102
  cognite_toolkit/_cdf_tk/commands/_migrate/command.py,sha256=l2P0Em05aEJvNZH4WkEIm-QfO3TAjG1rc_YxELuQIQM,14214
104
103
  cognite_toolkit/_cdf_tk/commands/_migrate/conversion.py,sha256=Ew9JRYrd-Ol9G9csTzpnhXAgCFnX67MwDYOTsdJLP3E,16803
105
104
  cognite_toolkit/_cdf_tk/commands/_migrate/creators.py,sha256=FTu7w3G8KyPY8pagG3KdPpOmpLcjehaAg2auEy6iM7A,9605
106
105
  cognite_toolkit/_cdf_tk/commands/_migrate/data_classes.py,sha256=_vMS_qAPj4yup1VnmmojPVigAZtyPQH7PM0Raby5tao,10619
107
- cognite_toolkit/_cdf_tk/commands/_migrate/data_mapper.py,sha256=nNaM8D1-kKfgkah28L7S8agA-DRcpeeEd2kazyhodIc,11713
106
+ cognite_toolkit/_cdf_tk/commands/_migrate/data_mapper.py,sha256=b_6_yYibtzWiBFrYq5pB8NZUi1TRuf-DIy_GRroj4wg,18551
108
107
  cognite_toolkit/_cdf_tk/commands/_migrate/data_model.py,sha256=i1eUsNX6Dueol9STIEwyksBnBsWUk13O8qHIjW964pM,7860
109
108
  cognite_toolkit/_cdf_tk/commands/_migrate/default_mappings.py,sha256=ERn3qFrJFXdtXaMjHq3Gk7MxH03MGFk3FrtWCOBJQts,5544
110
- cognite_toolkit/_cdf_tk/commands/_migrate/issues.py,sha256=L2-kODPavEwcuhte7EXANK2-rH7reiq-uNqr-3ub-no,6575
109
+ cognite_toolkit/_cdf_tk/commands/_migrate/issues.py,sha256=n8en744-r7GL9eUyxEojFes1yk69V04SnlpVXHrdPOQ,6972
111
110
  cognite_toolkit/_cdf_tk/commands/_migrate/migration_io.py,sha256=wrdBH5P6NgiZQSYLR0iJ3ZvqfQ5fY-_Ne2yKv9E1g4o,16277
112
111
  cognite_toolkit/_cdf_tk/commands/_migrate/prepare.py,sha256=RfqaNoso5CyBwc-p6ckwcYqBfZXKhdJgdGIyd0TATaI,2635
113
112
  cognite_toolkit/_cdf_tk/commands/_migrate/selectors.py,sha256=N1H_-rBpPUD6pbrlcofn1uEK1bA694EUXEe1zIXeqyo,2489
@@ -240,7 +239,7 @@ cognite_toolkit/_cdf_tk/resource_classes/workflow_trigger.py,sha256=aSN0WFPupQ38
240
239
  cognite_toolkit/_cdf_tk/resource_classes/workflow_version.py,sha256=ui724EaM9Nlm3wTnm7Givgv6GLQ-xbsfZgidyRKv09U,2991
241
240
  cognite_toolkit/_cdf_tk/storageio/__init__.py,sha256=h5Wr4i7zNIgsslrsRJxmp7ls4bNRKl0uZzQ7GLRMP7g,1920
242
241
  cognite_toolkit/_cdf_tk/storageio/_annotations.py,sha256=JI_g18_Y9S7pbc9gm6dZMyo3Z-bCndJXF9C2lOva0bQ,4848
243
- cognite_toolkit/_cdf_tk/storageio/_applications.py,sha256=ozWeuTqay1_GSFuQZUxXJspsBhLEBIS7EksAi93B8_4,18722
242
+ cognite_toolkit/_cdf_tk/storageio/_applications.py,sha256=M7FEK4xC0BjP2i6FyYs1589zEA3afJiOKCzY56RV6NU,19685
244
243
  cognite_toolkit/_cdf_tk/storageio/_asset_centric.py,sha256=TirKLSNPoLqKjczsw0djWAsR0VvopwmU23aUxrBOJN8,32464
245
244
  cognite_toolkit/_cdf_tk/storageio/_base.py,sha256=ElvqhIEBnhcz0yY1Ds164wVN0_7CFNK-uT0-z7LcR9U,13067
246
245
  cognite_toolkit/_cdf_tk/storageio/_data_classes.py,sha256=s3TH04BJ1q7rXndRhEbVMEnoOXjxrGg4n-w9Z5uUL-o,3480
@@ -303,14 +302,14 @@ cognite_toolkit/_repo_files/.gitignore,sha256=ip9kf9tcC5OguF4YF4JFEApnKYw0nG0vPi
303
302
  cognite_toolkit/_repo_files/AzureDevOps/.devops/README.md,sha256=OLA0D7yCX2tACpzvkA0IfkgQ4_swSd-OlJ1tYcTBpsA,240
304
303
  cognite_toolkit/_repo_files/AzureDevOps/.devops/deploy-pipeline.yml,sha256=brULcs8joAeBC_w_aoWjDDUHs3JheLMIR9ajPUK96nc,693
305
304
  cognite_toolkit/_repo_files/AzureDevOps/.devops/dry-run-pipeline.yml,sha256=OBFDhFWK1mlT4Dc6mDUE2Es834l8sAlYG50-5RxRtHk,723
306
- cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml,sha256=9mYiPtg7jVmpXB5yiGWLB6JSc5HCpqGrwAUYs7NXNNA,667
307
- cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml,sha256=FCy8QFumohU4QJ9whQXPMSMNMLraBZriASZkhnmvL9U,2430
308
- cognite_toolkit/_resources/cdf.toml,sha256=RJefbjEm4AZjfOov6Dy4L2Fuxww97ZREuKfE_xMNcgk,475
309
- cognite_toolkit/_version.py,sha256=bdHnwFVnv19lO1d9o0Rjk5Js7MqJglg3CX1USYBiklY,23
305
+ cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml,sha256=WjbXrAgIoTssRXTsTBHolXqsIg4BKfCLRtfmT37lTM4,667
306
+ cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml,sha256=qfSb9a7NFcwbEDJo8JoRrnvPvjyrjlWO7SVjxqVMjjM,2430
307
+ cognite_toolkit/_resources/cdf.toml,sha256=kw_vOm_sjuQP0yBUwjl4b5PP2mLyTEyG3HzTXFWtGFk,475
308
+ cognite_toolkit/_version.py,sha256=zxRa3gUEpxj8fTU5qOT60NDmCfmdbg7_Z6ZkpKAhrNg,23
310
309
  cognite_toolkit/config.dev.yaml,sha256=M33FiIKdS3XKif-9vXniQ444GTZ-bLXV8aFH86u9iUQ,332
311
310
  cognite_toolkit/demo/__init__.py,sha256=-m1JoUiwRhNCL18eJ6t7fZOL7RPfowhCuqhYFtLgrss,72
312
311
  cognite_toolkit/demo/_base.py,sha256=6xKBUQpXZXGQ3fJ5f7nj7oT0s2n7OTAGIa17ZlKHZ5U,8052
313
- cognite_toolkit-0.7.26.dist-info/WHEEL,sha256=93kfTGt3a0Dykt_T-gsjtyS5_p8F_d6CE1NwmBOirzo,79
314
- cognite_toolkit-0.7.26.dist-info/entry_points.txt,sha256=EtZ17K2mUjh-AY0QNU1CPIB_aDSSOdmtNI_4Fj967mA,84
315
- cognite_toolkit-0.7.26.dist-info/METADATA,sha256=x7-BAwenGCMfvJ77dEB3e7EEb3vv1YemhWyRCJBTZzM,4507
316
- cognite_toolkit-0.7.26.dist-info/RECORD,,
312
+ cognite_toolkit-0.7.28.dist-info/WHEEL,sha256=93kfTGt3a0Dykt_T-gsjtyS5_p8F_d6CE1NwmBOirzo,79
313
+ cognite_toolkit-0.7.28.dist-info/entry_points.txt,sha256=EtZ17K2mUjh-AY0QNU1CPIB_aDSSOdmtNI_4Fj967mA,84
314
+ cognite_toolkit-0.7.28.dist-info/METADATA,sha256=8484D-s0BzjQMFkFVIPhKFv2p4jELcUAVka0_qC7yT4,4507
315
+ cognite_toolkit-0.7.28.dist-info/RECORD,,
@@ -1,201 +0,0 @@
1
- from uuid import uuid4
2
-
3
- from cognite.client.data_classes.capabilities import (
4
- Capability,
5
- DataModelInstancesAcl,
6
- DataModelsAcl,
7
- SpaceIDScope,
8
- )
9
- from cognite.client.exceptions import CogniteException
10
-
11
- from cognite_toolkit._cdf_tk.client import ToolkitClient
12
- from cognite_toolkit._cdf_tk.client.data_classes.canvas import (
13
- CANVAS_INSTANCE_SPACE,
14
- Canvas,
15
- ContainerReferenceApply,
16
- FdmInstanceContainerReferenceApply,
17
- )
18
- from cognite_toolkit._cdf_tk.client.data_classes.migration import InstanceSource
19
- from cognite_toolkit._cdf_tk.commands._base import ToolkitCommand
20
- from cognite_toolkit._cdf_tk.commands._migrate.data_model import (
21
- INSTANCE_SOURCE_VIEW_ID,
22
- MODEL_ID,
23
- RESOURCE_VIEW_MAPPING_VIEW_ID,
24
- )
25
- from cognite_toolkit._cdf_tk.exceptions import AuthenticationError, ToolkitMigrationError
26
- from cognite_toolkit._cdf_tk.tk_warnings import HighSeverityWarning, LowSeverityWarning, MediumSeverityWarning
27
- from cognite_toolkit._cdf_tk.utils import humanize_collection
28
- from cognite_toolkit._cdf_tk.utils.interactive_select import InteractiveCanvasSelect
29
-
30
-
31
- class MigrationCanvasCommand(ToolkitCommand):
32
- canvas_schema_space = Canvas.get_source().space
33
- # Note sequences are not supported in Canvas, so we do not include them here.
34
- asset_centric_resource_types = frozenset({"asset", "event", "file", "timeseries"})
35
-
36
- def migrate_canvas(
37
- self,
38
- client: ToolkitClient,
39
- external_ids: list[str] | None = None,
40
- dry_run: bool = False,
41
- verbose: bool = False,
42
- ) -> None:
43
- self.validate_access(
44
- client,
45
- instance_spaces=[CANVAS_INSTANCE_SPACE],
46
- schema_spaces=[self.canvas_schema_space, INSTANCE_SOURCE_VIEW_ID.space],
47
- )
48
- self.validate_migration_model_available(client)
49
- external_ids = external_ids or InteractiveCanvasSelect(client).select_external_ids()
50
- if external_ids is None or not external_ids:
51
- self.console("No canvases selected for migration.")
52
- return
53
- action = "Would migrate" if dry_run else "Migrating"
54
- self.console(f"{action} {len(external_ids)} canvases.")
55
- for external_id in external_ids:
56
- self._migrate_single_canvas(client, external_id, dry_run=dry_run, verbose=verbose)
57
-
58
- def _migrate_single_canvas(
59
- self,
60
- client: ToolkitClient,
61
- external_id: str,
62
- dry_run: bool = False,
63
- verbose: bool = False,
64
- ) -> None:
65
- canvas = client.canvas.industrial.retrieve(external_id=external_id)
66
- if canvas is None:
67
- self.warn(MediumSeverityWarning(f"Canvas with external ID '{external_id}' not found. Skipping.. "))
68
- return
69
- update = canvas.as_write()
70
- to_migrate = [
71
- ref
72
- for ref in update.container_references
73
- if ref.container_reference_type in self.asset_centric_resource_types
74
- ]
75
- if not to_migrate:
76
- self.warn(
77
- LowSeverityWarning(
78
- f"Canvas with name '{canvas.canvas.name}' does not have any asset-centric references. Skipping.. "
79
- )
80
- )
81
- if verbose:
82
- self.console(f"Found canvas: {canvas.canvas.name}")
83
- reference_ids = [ref.as_asset_centric_id() for ref in to_migrate]
84
- instance_sources = client.migration.instance_source.retrieve(reference_ids)
85
- source_by_reference_id = {source.as_asset_centric_id(): source for source in instance_sources}
86
- missing = set(reference_ids) - set(source_by_reference_id.keys())
87
- if missing:
88
- self.warn(
89
- HighSeverityWarning(
90
- f"Canvas '{canvas.canvas.name}' has references to resources that are not been migrated: {humanize_collection(missing)}. Skipping.. "
91
- )
92
- )
93
- return
94
- if dry_run:
95
- self.console(
96
- f"Canvas '{canvas.canvas.name}' is ready for migration all {len(instance_sources)} references asset-centric resources found."
97
- )
98
- return
99
- if verbose:
100
- self.console(
101
- f"Migrating canvas '{canvas.canvas.name}' with {len(instance_sources)} references to asset-centric resources."
102
- )
103
- backup = canvas.as_write().create_backup()
104
-
105
- update.container_references = [
106
- ref
107
- for ref in update.container_references
108
- if ref.container_reference_type not in self.asset_centric_resource_types
109
- ]
110
- for ref in to_migrate:
111
- source = source_by_reference_id[ref.as_asset_centric_id()]
112
- fdm_ref = self.migrate_container_reference(ref, source, canvas.canvas.external_id)
113
- update.fdm_instance_container_references.append(fdm_ref)
114
-
115
- try:
116
- client.canvas.industrial.create(backup)
117
- except CogniteException as e:
118
- raise ToolkitMigrationError(f"Failed to create backup for canvas '{canvas.canvas.name}': {e!s}. ") from e
119
- try:
120
- client.canvas.industrial.update(update)
121
- except CogniteException as e:
122
- raise ToolkitMigrationError(
123
- f"Failed to migrate canvas '{canvas.canvas.name}': {e!s}. A backup was created with external ID '{backup.canvas.external_id}'."
124
- ) from e
125
- else:
126
- self.console(
127
- f'Canvas "{canvas.canvas.name}" migrated successfully with {len(to_migrate)} references to data model instances.'
128
- )
129
-
130
- @classmethod
131
- def migrate_container_reference(
132
- cls, reference: ContainerReferenceApply, source: InstanceSource, canvas_external_id: str
133
- ) -> FdmInstanceContainerReferenceApply:
134
- """Migrate a single container reference by replacing the asset-centric ID with the data model instance ID."""
135
- consumer_view = source.consumer_view()
136
- new_id = str(uuid4())
137
- new_external_id = f"{canvas_external_id}_{new_id}"
138
- return FdmInstanceContainerReferenceApply(
139
- external_id=new_external_id,
140
- id_=new_id,
141
- container_reference_type="fdmInstance",
142
- instance_space=source.space,
143
- instance_external_id=source.external_id,
144
- view_space=consumer_view.space,
145
- view_external_id=consumer_view.external_id,
146
- view_version=consumer_view.version,
147
- label=reference.label,
148
- properties_=reference.properties_,
149
- x=reference.x,
150
- y=reference.y,
151
- width=reference.width,
152
- height=reference.height,
153
- max_width=reference.max_width,
154
- max_height=reference.max_height,
155
- )
156
-
157
- @staticmethod
158
- def validate_access(
159
- client: ToolkitClient,
160
- instance_spaces: list[str] | None = None,
161
- schema_spaces: list[str] | None = None,
162
- ) -> None:
163
- required_capabilities: list[Capability] = []
164
- if instance_spaces is not None:
165
- required_capabilities.append(
166
- DataModelInstancesAcl(
167
- actions=[
168
- DataModelInstancesAcl.Action.Read,
169
- DataModelInstancesAcl.Action.Write,
170
- DataModelInstancesAcl.Action.Write_Properties,
171
- ],
172
- scope=SpaceIDScope(instance_spaces),
173
- )
174
- )
175
- if schema_spaces is not None:
176
- required_capabilities.append(
177
- DataModelsAcl(actions=[DataModelsAcl.Action.Read], scope=SpaceIDScope(schema_spaces)),
178
- )
179
- if missing := client.iam.verify_capabilities(required_capabilities):
180
- raise AuthenticationError(f"Missing required capabilities: {humanize_collection(missing)}.", missing)
181
-
182
- @staticmethod
183
- def validate_migration_model_available(client: ToolkitClient) -> None:
184
- models = client.data_modeling.data_models.retrieve([MODEL_ID], inline_views=False)
185
- if not models:
186
- raise ToolkitMigrationError(
187
- f"The migration data model {MODEL_ID!r} does not exist. "
188
- "Please run the `cdf migrate prepare` command to deploy the migration data model."
189
- )
190
- elif len(models) > 1:
191
- raise ToolkitMigrationError(
192
- f"Multiple migration models {MODEL_ID!r}. "
193
- "Please delete the duplicate models before proceeding with the migration."
194
- )
195
- model = models[0]
196
- missing_views = {INSTANCE_SOURCE_VIEW_ID, RESOURCE_VIEW_MAPPING_VIEW_ID} - set(model.views or [])
197
- if missing_views:
198
- raise ToolkitMigrationError(
199
- f"Invalid migration model. Missing views {humanize_collection(missing_views)}. "
200
- f"Please run the `cdf migrate prepare` command to deploy the migration data model."
201
- )