cognite-toolkit 0.6.97__py3-none-any.whl → 0.7.30__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 (136) hide show
  1. cognite_toolkit/_cdf.py +16 -17
  2. cognite_toolkit/_cdf_tk/apps/__init__.py +2 -0
  3. cognite_toolkit/_cdf_tk/apps/_core_app.py +13 -5
  4. cognite_toolkit/_cdf_tk/apps/_data_app.py +1 -1
  5. cognite_toolkit/_cdf_tk/apps/_dev_app.py +86 -0
  6. cognite_toolkit/_cdf_tk/apps/_download_app.py +692 -24
  7. cognite_toolkit/_cdf_tk/apps/_dump_app.py +43 -101
  8. cognite_toolkit/_cdf_tk/apps/_landing_app.py +18 -4
  9. cognite_toolkit/_cdf_tk/apps/_migrate_app.py +249 -9
  10. cognite_toolkit/_cdf_tk/apps/_modules_app.py +0 -3
  11. cognite_toolkit/_cdf_tk/apps/_purge.py +15 -43
  12. cognite_toolkit/_cdf_tk/apps/_run.py +11 -0
  13. cognite_toolkit/_cdf_tk/apps/_upload_app.py +45 -6
  14. cognite_toolkit/_cdf_tk/builders/__init__.py +2 -2
  15. cognite_toolkit/_cdf_tk/builders/_base.py +28 -42
  16. cognite_toolkit/_cdf_tk/cdf_toml.py +20 -1
  17. cognite_toolkit/_cdf_tk/client/_toolkit_client.py +23 -3
  18. cognite_toolkit/_cdf_tk/client/api/extended_functions.py +6 -9
  19. cognite_toolkit/_cdf_tk/client/api/infield.py +93 -1
  20. cognite_toolkit/_cdf_tk/client/api/migration.py +175 -1
  21. cognite_toolkit/_cdf_tk/client/api/streams.py +84 -0
  22. cognite_toolkit/_cdf_tk/client/api/three_d.py +50 -0
  23. cognite_toolkit/_cdf_tk/client/data_classes/base.py +25 -1
  24. cognite_toolkit/_cdf_tk/client/data_classes/canvas.py +46 -3
  25. cognite_toolkit/_cdf_tk/client/data_classes/charts.py +3 -3
  26. cognite_toolkit/_cdf_tk/client/data_classes/charts_data.py +95 -213
  27. cognite_toolkit/_cdf_tk/client/data_classes/infield.py +32 -18
  28. cognite_toolkit/_cdf_tk/client/data_classes/migration.py +10 -2
  29. cognite_toolkit/_cdf_tk/client/data_classes/streams.py +90 -0
  30. cognite_toolkit/_cdf_tk/client/data_classes/three_d.py +47 -0
  31. cognite_toolkit/_cdf_tk/client/testing.py +18 -2
  32. cognite_toolkit/_cdf_tk/commands/__init__.py +6 -6
  33. cognite_toolkit/_cdf_tk/commands/_changes.py +3 -42
  34. cognite_toolkit/_cdf_tk/commands/_download.py +21 -11
  35. cognite_toolkit/_cdf_tk/commands/_migrate/__init__.py +0 -2
  36. cognite_toolkit/_cdf_tk/commands/_migrate/command.py +22 -20
  37. cognite_toolkit/_cdf_tk/commands/_migrate/conversion.py +133 -91
  38. cognite_toolkit/_cdf_tk/commands/_migrate/data_classes.py +73 -22
  39. cognite_toolkit/_cdf_tk/commands/_migrate/data_mapper.py +311 -43
  40. cognite_toolkit/_cdf_tk/commands/_migrate/default_mappings.py +5 -5
  41. cognite_toolkit/_cdf_tk/commands/_migrate/issues.py +33 -0
  42. cognite_toolkit/_cdf_tk/commands/_migrate/migration_io.py +157 -8
  43. cognite_toolkit/_cdf_tk/commands/_migrate/selectors.py +9 -4
  44. cognite_toolkit/_cdf_tk/commands/_purge.py +27 -28
  45. cognite_toolkit/_cdf_tk/commands/_questionary_style.py +16 -0
  46. cognite_toolkit/_cdf_tk/commands/_upload.py +109 -86
  47. cognite_toolkit/_cdf_tk/commands/about.py +221 -0
  48. cognite_toolkit/_cdf_tk/commands/auth.py +19 -12
  49. cognite_toolkit/_cdf_tk/commands/build_cmd.py +15 -61
  50. cognite_toolkit/_cdf_tk/commands/clean.py +63 -16
  51. cognite_toolkit/_cdf_tk/commands/deploy.py +20 -17
  52. cognite_toolkit/_cdf_tk/commands/dump_resource.py +6 -4
  53. cognite_toolkit/_cdf_tk/commands/init.py +225 -3
  54. cognite_toolkit/_cdf_tk/commands/modules.py +20 -44
  55. cognite_toolkit/_cdf_tk/commands/pull.py +6 -19
  56. cognite_toolkit/_cdf_tk/commands/resources.py +179 -0
  57. cognite_toolkit/_cdf_tk/constants.py +20 -1
  58. cognite_toolkit/_cdf_tk/cruds/__init__.py +19 -5
  59. cognite_toolkit/_cdf_tk/cruds/_base_cruds.py +14 -70
  60. cognite_toolkit/_cdf_tk/cruds/_data_cruds.py +8 -17
  61. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/__init__.py +4 -1
  62. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/agent.py +11 -9
  63. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/auth.py +4 -14
  64. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/classic.py +44 -43
  65. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/configuration.py +4 -11
  66. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/data_organization.py +4 -13
  67. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/datamodel.py +205 -66
  68. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/extraction_pipeline.py +5 -17
  69. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/fieldops.py +116 -27
  70. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/file.py +6 -27
  71. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/function.py +9 -28
  72. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/hosted_extractors.py +12 -30
  73. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/industrial_tool.py +3 -7
  74. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/location.py +3 -15
  75. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/migration.py +4 -12
  76. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/raw.py +4 -10
  77. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/relationship.py +3 -8
  78. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/robotics.py +15 -44
  79. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/streams.py +94 -0
  80. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/three_d_model.py +3 -7
  81. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/timeseries.py +5 -15
  82. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/transformation.py +39 -31
  83. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/workflow.py +20 -40
  84. cognite_toolkit/_cdf_tk/cruds/_worker.py +24 -36
  85. cognite_toolkit/_cdf_tk/feature_flags.py +16 -36
  86. cognite_toolkit/_cdf_tk/plugins.py +2 -1
  87. cognite_toolkit/_cdf_tk/resource_classes/__init__.py +4 -0
  88. cognite_toolkit/_cdf_tk/resource_classes/capabilities.py +12 -0
  89. cognite_toolkit/_cdf_tk/resource_classes/functions.py +3 -1
  90. cognite_toolkit/_cdf_tk/resource_classes/infield_cdm_location_config.py +109 -0
  91. cognite_toolkit/_cdf_tk/resource_classes/migration.py +8 -17
  92. cognite_toolkit/_cdf_tk/resource_classes/streams.py +29 -0
  93. cognite_toolkit/_cdf_tk/storageio/__init__.py +9 -21
  94. cognite_toolkit/_cdf_tk/storageio/_annotations.py +19 -16
  95. cognite_toolkit/_cdf_tk/storageio/_applications.py +338 -26
  96. cognite_toolkit/_cdf_tk/storageio/_asset_centric.py +67 -104
  97. cognite_toolkit/_cdf_tk/storageio/_base.py +61 -29
  98. cognite_toolkit/_cdf_tk/storageio/_datapoints.py +276 -20
  99. cognite_toolkit/_cdf_tk/storageio/_file_content.py +436 -0
  100. cognite_toolkit/_cdf_tk/storageio/_instances.py +34 -2
  101. cognite_toolkit/_cdf_tk/storageio/_raw.py +26 -0
  102. cognite_toolkit/_cdf_tk/storageio/selectors/__init__.py +62 -4
  103. cognite_toolkit/_cdf_tk/storageio/selectors/_base.py +14 -2
  104. cognite_toolkit/_cdf_tk/storageio/selectors/_canvas.py +14 -0
  105. cognite_toolkit/_cdf_tk/storageio/selectors/_charts.py +14 -0
  106. cognite_toolkit/_cdf_tk/storageio/selectors/_datapoints.py +23 -3
  107. cognite_toolkit/_cdf_tk/storageio/selectors/_file_content.py +164 -0
  108. cognite_toolkit/_cdf_tk/tk_warnings/other.py +4 -0
  109. cognite_toolkit/_cdf_tk/tracker.py +2 -2
  110. cognite_toolkit/_cdf_tk/utils/dtype_conversion.py +9 -3
  111. cognite_toolkit/_cdf_tk/utils/fileio/__init__.py +2 -0
  112. cognite_toolkit/_cdf_tk/utils/fileio/_base.py +5 -1
  113. cognite_toolkit/_cdf_tk/utils/fileio/_readers.py +112 -20
  114. cognite_toolkit/_cdf_tk/utils/fileio/_writers.py +15 -15
  115. cognite_toolkit/_cdf_tk/utils/http_client/_client.py +284 -18
  116. cognite_toolkit/_cdf_tk/utils/http_client/_data_classes.py +50 -4
  117. cognite_toolkit/_cdf_tk/utils/http_client/_data_classes2.py +187 -0
  118. cognite_toolkit/_cdf_tk/utils/interactive_select.py +9 -14
  119. cognite_toolkit/_cdf_tk/utils/sql_parser.py +2 -3
  120. cognite_toolkit/_cdf_tk/utils/useful_types.py +6 -2
  121. cognite_toolkit/_cdf_tk/validation.py +79 -1
  122. cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
  123. cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
  124. cognite_toolkit/_resources/cdf.toml +5 -4
  125. cognite_toolkit/_version.py +1 -1
  126. cognite_toolkit/config.dev.yaml +13 -0
  127. {cognite_toolkit-0.6.97.dist-info → cognite_toolkit-0.7.30.dist-info}/METADATA +24 -24
  128. {cognite_toolkit-0.6.97.dist-info → cognite_toolkit-0.7.30.dist-info}/RECORD +153 -143
  129. cognite_toolkit-0.7.30.dist-info/WHEEL +4 -0
  130. {cognite_toolkit-0.6.97.dist-info → cognite_toolkit-0.7.30.dist-info}/entry_points.txt +1 -0
  131. cognite_toolkit/_cdf_tk/commands/_migrate/canvas.py +0 -201
  132. cognite_toolkit/_cdf_tk/commands/dump_data.py +0 -489
  133. cognite_toolkit/_cdf_tk/commands/featureflag.py +0 -27
  134. cognite_toolkit/_cdf_tk/utils/table_writers.py +0 -434
  135. cognite_toolkit-0.6.97.dist-info/WHEEL +0 -4
  136. cognite_toolkit-0.6.97.dist-info/licenses/LICENSE +0 -18
@@ -1,25 +1,55 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Generic
2
+ from collections import defaultdict
3
+ from collections.abc import Callable, Sequence
4
+ from typing import Generic, cast
5
+ from uuid import uuid4
3
6
 
4
- from cognite.client.data_classes._base import (
5
- T_CogniteResource,
7
+ from cognite.client.data_classes.data_modeling import (
8
+ EdgeApply,
9
+ InstanceApply,
10
+ NodeApply,
11
+ NodeId,
12
+ View,
13
+ ViewId,
6
14
  )
7
- from cognite.client.data_classes.data_modeling import DirectRelationReference, InstanceApply, View, ViewId
15
+ from cognite.client.exceptions import CogniteException
8
16
 
9
17
  from cognite_toolkit._cdf_tk.client import ToolkitClient
10
- from cognite_toolkit._cdf_tk.client.data_classes.migration import ResourceViewMapping
11
- from cognite_toolkit._cdf_tk.commands._migrate.conversion import asset_centric_to_dm
18
+ from cognite_toolkit._cdf_tk.client.data_classes.canvas import (
19
+ ContainerReferenceApply,
20
+ FdmInstanceContainerReferenceApply,
21
+ IndustrialCanvas,
22
+ IndustrialCanvasApply,
23
+ )
24
+ from cognite_toolkit._cdf_tk.client.data_classes.charts import Chart, ChartWrite
25
+ from cognite_toolkit._cdf_tk.client.data_classes.charts_data import (
26
+ ChartCoreTimeseries,
27
+ ChartSource,
28
+ ChartTimeseries,
29
+ )
30
+ from cognite_toolkit._cdf_tk.client.data_classes.migration import ResourceViewMappingApply
31
+ from cognite_toolkit._cdf_tk.commands._migrate.conversion import DirectRelationCache, asset_centric_to_dm
12
32
  from cognite_toolkit._cdf_tk.commands._migrate.data_classes import AssetCentricMapping
13
- from cognite_toolkit._cdf_tk.commands._migrate.issues import ConversionIssue, MigrationIssue
33
+ from cognite_toolkit._cdf_tk.commands._migrate.default_mappings import create_default_mappings
34
+ from cognite_toolkit._cdf_tk.commands._migrate.issues import (
35
+ CanvasMigrationIssue,
36
+ ChartMigrationIssue,
37
+ ConversionIssue,
38
+ MigrationIssue,
39
+ )
14
40
  from cognite_toolkit._cdf_tk.commands._migrate.selectors import AssetCentricMigrationSelector
15
41
  from cognite_toolkit._cdf_tk.constants import MISSING_INSTANCE_SPACE
16
- from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError
17
- from cognite_toolkit._cdf_tk.storageio._base import T_Selector, T_WriteCogniteResource
42
+ from cognite_toolkit._cdf_tk.exceptions import ToolkitMigrationError, ToolkitValueError
43
+ from cognite_toolkit._cdf_tk.protocols import T_ResourceRequest, T_ResourceResponse
44
+ from cognite_toolkit._cdf_tk.storageio._base import T_Selector
45
+ from cognite_toolkit._cdf_tk.storageio.selectors import CanvasSelector, ChartSelector
18
46
  from cognite_toolkit._cdf_tk.utils import humanize_collection
19
- from cognite_toolkit._cdf_tk.utils.useful_types import T_AssetCentricResource
47
+ from cognite_toolkit._cdf_tk.utils.useful_types import (
48
+ T_AssetCentricResourceExtended,
49
+ )
20
50
 
21
51
 
22
- class DataMapper(Generic[T_Selector, T_CogniteResource, T_WriteCogniteResource], ABC):
52
+ class DataMapper(Generic[T_Selector, T_ResourceResponse, T_ResourceRequest], ABC):
23
53
  def prepare(self, source_selector: T_Selector) -> None:
24
54
  """Prepare the data mapper with the given source selector.
25
55
 
@@ -31,7 +61,7 @@ class DataMapper(Generic[T_Selector, T_CogniteResource, T_WriteCogniteResource],
31
61
  pass
32
62
 
33
63
  @abstractmethod
34
- def map(self, source: T_CogniteResource) -> tuple[T_WriteCogniteResource | None, MigrationIssue]:
64
+ def map(self, source: Sequence[T_ResourceResponse]) -> Sequence[tuple[T_ResourceRequest | None, MigrationIssue]]:
35
65
  """Map a chunk of source data to the target format.
36
66
 
37
67
  Args:
@@ -45,28 +75,27 @@ class DataMapper(Generic[T_Selector, T_CogniteResource, T_WriteCogniteResource],
45
75
 
46
76
 
47
77
  class AssetCentricMapper(
48
- DataMapper[AssetCentricMigrationSelector, AssetCentricMapping[T_AssetCentricResource], InstanceApply]
78
+ DataMapper[AssetCentricMigrationSelector, AssetCentricMapping[T_AssetCentricResourceExtended], InstanceApply]
49
79
  ):
50
80
  def __init__(self, client: ToolkitClient) -> None:
51
81
  self.client = client
52
82
  self._ingestion_view_by_id: dict[ViewId, View] = {}
53
- self._view_mapping_by_id: dict[str, ResourceViewMapping] = {}
54
- # This is used to keep track of already mapped assets, such that we can creat direct relations
55
- # to them from files, events, and time series.
56
- self._asset_mapping_by_id: dict[int, DirectRelationReference] = {}
57
- self._source_system_mapping_by_id: dict[str, DirectRelationReference] = {}
83
+ self._view_mapping_by_id: dict[str, ResourceViewMappingApply] = {}
84
+ self._direct_relation_cache = DirectRelationCache(client)
58
85
 
59
86
  def prepare(self, source_selector: AssetCentricMigrationSelector) -> None:
60
87
  ingestion_view_ids = source_selector.get_ingestion_mappings()
61
88
  ingestion_views = self.client.migration.resource_view_mapping.retrieve(ingestion_view_ids)
62
- self._view_mapping_by_id = {view.external_id: view for view in ingestion_views}
89
+ defaults = {mapping.external_id: mapping for mapping in create_default_mappings()}
90
+ # Custom mappings from CDF override the default mappings
91
+ self._view_mapping_by_id = defaults | {view.external_id: view.as_write() for view in ingestion_views}
63
92
  missing_mappings = set(ingestion_view_ids) - set(self._view_mapping_by_id.keys())
64
93
  if missing_mappings:
65
94
  raise ToolkitValueError(
66
95
  f"The following ingestion views were not found: {humanize_collection(missing_mappings)}"
67
96
  )
68
97
 
69
- view_ids = list({view.view_id for view in ingestion_views})
98
+ view_ids = list({mapping.view_id for mapping in self._view_mapping_by_id.values()})
70
99
  views = self.client.data_modeling.views.retrieve(view_ids)
71
100
  self._ingestion_view_by_id = {view.as_id(): view for view in views}
72
101
  missing_views = set(view_ids) - set(self._ingestion_view_by_id.keys())
@@ -74,22 +103,23 @@ class AssetCentricMapper(
74
103
  raise ToolkitValueError(
75
104
  f"The following ingestion views were not found in Data Modeling: {humanize_collection(missing_views)}"
76
105
  )
77
- # We just look-up all source system for now. This can be optimized to only
78
- # look-up the ones that are actually used in the ingestion views. However, SourceSystem is typically in
79
- # the order ~10 instances, so this is not a big deal for now. See task CDF-25974.
80
- source_systems = self.client.migration.created_source_system.list(limit=-1)
81
- self._source_system_mapping_by_id = {
82
- source_system.source: source_system.as_direct_relation_reference() for source_system in source_systems
83
- }
84
-
85
- # We look-up all existing asset mappings to be able to create direct relations to already mapped assets.
86
- # This is needed in case the migration is run multiple times, or if assets are mapped
87
- asset_mappings = self.client.migration.instance_source.list(resource_type="asset", limit=-1)
88
- self._asset_mapping_by_id = {mapping.id_: mapping.as_direct_relation_reference() for mapping in asset_mappings}
89
106
 
90
- def map(self, source: AssetCentricMapping[T_AssetCentricResource]) -> tuple[InstanceApply | None, ConversionIssue]:
107
+ def map(
108
+ self, source: Sequence[AssetCentricMapping[T_AssetCentricResourceExtended]]
109
+ ) -> Sequence[tuple[InstanceApply | None, ConversionIssue]]:
91
110
  """Map a chunk of asset-centric data to InstanceApplyList format."""
92
- mapping = source.mapping
111
+ # We update the direct relation cache in bulk for all resources in the chunk.
112
+ self._direct_relation_cache.update(item.resource for item in source)
113
+ output: list[tuple[InstanceApply | None, ConversionIssue]] = []
114
+ for item in source:
115
+ instance, conversion_issue = self._map_single_item(item)
116
+ output.append((instance, conversion_issue))
117
+ return output
118
+
119
+ def _map_single_item(
120
+ self, item: AssetCentricMapping[T_AssetCentricResourceExtended]
121
+ ) -> tuple[NodeApply | EdgeApply | None, ConversionIssue]:
122
+ mapping = item.mapping
93
123
  ingestion_view = mapping.get_ingestion_view()
94
124
  try:
95
125
  view_source = self._view_mapping_by_id[ingestion_view]
@@ -99,19 +129,257 @@ class AssetCentricMapper(
99
129
  f"Failed to lookup mapping or view for ingestion view '{ingestion_view}'. Did you forget to call .prepare()?"
100
130
  ) from e
101
131
  instance, conversion_issue = asset_centric_to_dm(
102
- source.resource,
132
+ item.resource,
103
133
  instance_id=mapping.instance_id,
104
134
  view_source=view_source,
105
135
  view_properties=view_properties,
106
- asset_instance_id_by_id=self._asset_mapping_by_id,
107
- source_instance_id_by_external_id=self._source_system_mapping_by_id,
108
- file_instance_id_by_id={}, # Todo implement file direct relations
136
+ direct_relation_cache=self._direct_relation_cache,
109
137
  )
110
138
  if mapping.instance_id.space == MISSING_INSTANCE_SPACE:
111
139
  conversion_issue.missing_instance_space = f"Missing instance space for dataset ID {mapping.data_set_id!r}"
112
-
113
- if mapping.resource_type == "asset":
114
- self._asset_mapping_by_id[mapping.id] = DirectRelationReference(
115
- space=mapping.instance_id.space, external_id=mapping.instance_id.external_id
116
- )
117
140
  return instance, conversion_issue
141
+
142
+
143
+ class ChartMapper(DataMapper[ChartSelector, Chart, ChartWrite]):
144
+ def __init__(self, client: ToolkitClient) -> None:
145
+ self.client = client
146
+
147
+ def map(self, source: Sequence[Chart]) -> Sequence[tuple[ChartWrite | None, MigrationIssue]]:
148
+ self._populate_cache(source)
149
+ output: list[tuple[ChartWrite | None, MigrationIssue]] = []
150
+ for item in source:
151
+ mapped_item, issue = self._map_single_item(item)
152
+ output.append((mapped_item, issue))
153
+ return output
154
+
155
+ def _populate_cache(self, source: Sequence[Chart]) -> None:
156
+ """Populate the internal cache with timeseries from the source charts.
157
+
158
+ Note that the consumption views are also cached as part of the timeseries lookup.
159
+ """
160
+ timeseries_ids: set[int] = set()
161
+ timeseries_external_ids: set[str] = set()
162
+ for chart in source:
163
+ for item in chart.data.time_series_collection or []:
164
+ if item.ts_id:
165
+ timeseries_ids.add(item.ts_id)
166
+ if item.ts_external_id:
167
+ timeseries_external_ids.add(item.ts_external_id)
168
+ if timeseries_ids:
169
+ self.client.migration.lookup.time_series(list(timeseries_ids))
170
+ if timeseries_external_ids:
171
+ self.client.migration.lookup.time_series(external_id=list(timeseries_external_ids))
172
+
173
+ def _map_single_item(self, item: Chart) -> tuple[ChartWrite | None, ChartMigrationIssue]:
174
+ issue = ChartMigrationIssue(chart_external_id=item.external_id)
175
+ time_series_collection = item.data.time_series_collection or []
176
+ timeseries_core_collection = self._create_timeseries_core_collection(time_series_collection, issue)
177
+ if issue.has_issues:
178
+ return None, issue
179
+
180
+ updated_source_collection = self._update_source_collection(
181
+ item.data.source_collection or [], time_series_collection, timeseries_core_collection
182
+ )
183
+
184
+ mapped_chart = item.as_write()
185
+ mapped_chart.data.core_timeseries_collection = timeseries_core_collection
186
+ mapped_chart.data.time_series_collection = None
187
+ mapped_chart.data.source_collection = updated_source_collection
188
+ return mapped_chart, issue
189
+
190
+ def _create_timeseries_core_collection(
191
+ self, time_series_collection: list[ChartTimeseries], issue: ChartMigrationIssue
192
+ ) -> list[ChartCoreTimeseries]:
193
+ timeseries_core_collection: list[ChartCoreTimeseries] = []
194
+ for ts_item in time_series_collection or []:
195
+ node_id, consumer_view_id = self._get_node_id_consumer_view_id(ts_item)
196
+
197
+ if node_id is None:
198
+ if ts_item.ts_id is not None:
199
+ issue.missing_timeseries_ids.append(ts_item.ts_id)
200
+ elif ts_item.ts_external_id is not None:
201
+ issue.missing_timeseries_external_ids.append(ts_item.ts_external_id)
202
+ else:
203
+ issue.missing_timeseries_identifier.append(ts_item.id or "unknown")
204
+ continue
205
+
206
+ core_timeseries = self._create_new_timeseries_core(ts_item, node_id, consumer_view_id)
207
+ timeseries_core_collection.append(core_timeseries)
208
+ return timeseries_core_collection
209
+
210
+ def _create_new_timeseries_core(
211
+ self, ts_item: ChartTimeseries, node_id: NodeId, consumer_view_id: ViewId | None
212
+ ) -> ChartCoreTimeseries:
213
+ dumped = ts_item.model_dump(mode="json", by_alias=True, exclude_unset=True)
214
+ dumped["nodeReference"] = node_id
215
+ dumped["viewReference"] = consumer_view_id
216
+ new_uuid = str(uuid4())
217
+ dumped["id"] = new_uuid
218
+ dumped["type"] = "coreTimeseries"
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")
221
+ return core_timeseries
222
+
223
+ def _get_node_id_consumer_view_id(self, ts_item: ChartTimeseries) -> tuple[NodeId | None, ViewId | None]:
224
+ """Look up the node ID and consumer view ID for a given timeseries item.
225
+
226
+ Prioritizes lookup by internal ID, then by external ID.
227
+
228
+ Args:
229
+ ts_item: The ChartTimeseries item to look up.
230
+
231
+ Returns:
232
+ A tuple containing the consumer view ID and node ID, or None if not found.
233
+ """
234
+ node_id: NodeId | None = None
235
+ consumer_view_id: ViewId | None = None
236
+ for id_name, id_value in [("id", ts_item.ts_id), ("external_id", ts_item.ts_external_id)]:
237
+ if id_value is None:
238
+ continue
239
+ arg = {id_name: id_value}
240
+ node_id = self.client.migration.lookup.time_series(**arg) # type: ignore[arg-type]
241
+ consumer_view_id = self.client.migration.lookup.time_series.consumer_view(**arg) # type: ignore[arg-type]
242
+ if node_id is not None:
243
+ break
244
+ return node_id, consumer_view_id
245
+
246
+ def _update_source_collection(
247
+ self,
248
+ source_collection: list[ChartSource],
249
+ time_series_collection: list[ChartTimeseries],
250
+ timeseries_core_collection: list[ChartCoreTimeseries],
251
+ ) -> list[ChartSource]:
252
+ remove_ids = {ts_item.id for ts_item in time_series_collection if ts_item.id is not None}
253
+ updated_source_collection = [ts_item for ts_item in source_collection if ts_item.id not in remove_ids]
254
+ for core_ts_item in timeseries_core_collection:
255
+ # We cast there two as we set them in the _create_timeseries_core_collection method
256
+ new_source_item = ChartSource(id=cast(str, core_ts_item.id), type=cast(str, core_ts_item.type))
257
+ updated_source_collection.append(new_source_item)
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
+ )
@@ -93,7 +93,7 @@ def create_default_mappings() -> list[ResourceViewMappingApply]:
93
93
  view_id=ViewId("cdf_cdm", "CogniteDiagramAnnotation", "v1"),
94
94
  property_mapping={
95
95
  # We are ignoring the symbol region in the default mapping.
96
- "annotatedResource.id": "edge.startNode",
96
+ "annotatedResourceId": "edge.startNode",
97
97
  "annotationType": "edge.type.externalId",
98
98
  "creatingUser": "sourceCreatedUser",
99
99
  "creatingApp": "sourceId",
@@ -114,16 +114,16 @@ def create_default_mappings() -> list[ResourceViewMappingApply]:
114
114
  ResourceViewMappingApply(
115
115
  external_id=FILE_ANNOTATIONS_ID,
116
116
  resource_type="fileAnnotation",
117
- view_id=ViewId("cdf_cdm", "CogniteFileAnnotation", "v1"),
117
+ view_id=ViewId("cdf_cdm", "CogniteDiagramAnnotation", "v1"),
118
118
  property_mapping={
119
- "annotatedResource.id": "edge.startNode",
119
+ "annotatedResourceId": "edge.startNode",
120
120
  "annotationType": "edge.type.externalId",
121
121
  "creatingUser": "sourceCreatedUser",
122
122
  "creatingApp": "sourceId",
123
123
  "creatingAppVersion": "sourceContext",
124
124
  "status": "status",
125
- "data.fileRef.id": "edge.startNode",
126
- "data.fileRef.externalId": "edge.startNode",
125
+ "data.fileRef.id": "edge.endNode",
126
+ "data.fileRef.externalId": "edge.endNode",
127
127
  "data.description": "description",
128
128
  "data.pageNumber": "startNodePageNumber",
129
129
  "data.textRegion.confidence": "confidence",
@@ -30,6 +30,39 @@ class MigrationIssue(MigrationObject):
30
30
  return True
31
31
 
32
32
 
33
+ class ChartMigrationIssue(MigrationIssue):
34
+ """Represents a chart migration issue encountered during migration.
35
+
36
+ Attributes:
37
+ chart_external_id (str): The external ID of the chart that could not be migrated.
38
+ """
39
+
40
+ type: ClassVar[str] = "chartMigration"
41
+ chart_external_id: str
42
+ missing_timeseries_ids: list[int] = Field(default_factory=list)
43
+ missing_timeseries_external_ids: list[str] = Field(default_factory=list)
44
+ missing_timeseries_identifier: list[str] = Field(default_factory=list)
45
+
46
+ @property
47
+ def has_issues(self) -> bool:
48
+ """Check if there are any issues recorded in this ChartMigrationIssue."""
49
+ return bool(
50
+ self.missing_timeseries_ids or self.missing_timeseries_external_ids or self.missing_timeseries_identifier
51
+ )
52
+
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
+
33
66
  class ReadIssue(MigrationIssue):
34
67
  """Represents a read issue encountered during migration."""
35
68