cognite-toolkit 0.6.88__py3-none-any.whl → 0.6.90__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.
Potentially problematic release.
This version of cognite-toolkit might be problematic. Click here for more details.
- cognite_toolkit/_cdf_tk/commands/_migrate/canvas.py +60 -5
- cognite_toolkit/_cdf_tk/commands/_migrate/command.py +4 -2
- cognite_toolkit/_cdf_tk/commands/_migrate/conversion.py +161 -44
- cognite_toolkit/_cdf_tk/commands/_migrate/data_classes.py +10 -10
- cognite_toolkit/_cdf_tk/commands/_migrate/data_mapper.py +7 -3
- cognite_toolkit/_cdf_tk/commands/_migrate/migration_io.py +8 -10
- cognite_toolkit/_cdf_tk/commands/build_cmd.py +1 -1
- cognite_toolkit/_cdf_tk/commands/pull.py +6 -5
- cognite_toolkit/_cdf_tk/data_classes/_build_variables.py +120 -14
- cognite_toolkit/_cdf_tk/data_classes/_built_resources.py +1 -1
- cognite_toolkit/_cdf_tk/resource_classes/agent.py +1 -0
- cognite_toolkit/_cdf_tk/resource_classes/infield_cdmv1.py +92 -0
- cognite_toolkit/_cdf_tk/storageio/__init__.py +2 -0
- cognite_toolkit/_cdf_tk/storageio/_annotations.py +102 -0
- cognite_toolkit/_cdf_tk/tracker.py +6 -6
- cognite_toolkit/_cdf_tk/utils/fileio/_readers.py +90 -44
- cognite_toolkit/_cdf_tk/utils/http_client/_client.py +6 -4
- cognite_toolkit/_cdf_tk/utils/http_client/_data_classes.py +2 -0
- cognite_toolkit/_cdf_tk/utils/useful_types.py +7 -4
- cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
- cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
- cognite_toolkit/_resources/cdf.toml +1 -1
- cognite_toolkit/_version.py +1 -1
- {cognite_toolkit-0.6.88.dist-info → cognite_toolkit-0.6.90.dist-info}/METADATA +1 -1
- {cognite_toolkit-0.6.88.dist-info → cognite_toolkit-0.6.90.dist-info}/RECORD +28 -27
- cognite_toolkit/_cdf_tk/commands/_migrate/base.py +0 -106
- {cognite_toolkit-0.6.88.dist-info → cognite_toolkit-0.6.90.dist-info}/WHEEL +0 -0
- {cognite_toolkit-0.6.88.dist-info → cognite_toolkit-0.6.90.dist-info}/entry_points.txt +0 -0
- {cognite_toolkit-0.6.88.dist-info → cognite_toolkit-0.6.90.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
from uuid import uuid4
|
|
2
2
|
|
|
3
|
+
from cognite.client.data_classes.capabilities import (
|
|
4
|
+
Capability,
|
|
5
|
+
DataModelInstancesAcl,
|
|
6
|
+
DataModelsAcl,
|
|
7
|
+
SpaceIDScope,
|
|
8
|
+
)
|
|
3
9
|
from cognite.client.exceptions import CogniteException
|
|
4
10
|
|
|
5
11
|
from cognite_toolkit._cdf_tk.client import ToolkitClient
|
|
@@ -10,16 +16,19 @@ from cognite_toolkit._cdf_tk.client.data_classes.canvas import (
|
|
|
10
16
|
FdmInstanceContainerReferenceApply,
|
|
11
17
|
)
|
|
12
18
|
from cognite_toolkit._cdf_tk.client.data_classes.migration import InstanceSource
|
|
13
|
-
from cognite_toolkit._cdf_tk.
|
|
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
|
|
14
26
|
from cognite_toolkit._cdf_tk.tk_warnings import HighSeverityWarning, LowSeverityWarning, MediumSeverityWarning
|
|
15
27
|
from cognite_toolkit._cdf_tk.utils import humanize_collection
|
|
16
28
|
from cognite_toolkit._cdf_tk.utils.interactive_select import InteractiveCanvasSelect
|
|
17
29
|
|
|
18
|
-
from .base import BaseMigrateCommand
|
|
19
|
-
from .data_model import INSTANCE_SOURCE_VIEW_ID
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
class MigrationCanvasCommand(BaseMigrateCommand):
|
|
31
|
+
class MigrationCanvasCommand(ToolkitCommand):
|
|
23
32
|
canvas_schema_space = Canvas.get_source().space
|
|
24
33
|
# Note sequences are not supported in Canvas, so we do not include them here.
|
|
25
34
|
asset_centric_resource_types = frozenset({"asset", "event", "file", "timeseries"})
|
|
@@ -144,3 +153,49 @@ class MigrationCanvasCommand(BaseMigrateCommand):
|
|
|
144
153
|
max_width=reference.max_width,
|
|
145
154
|
max_height=reference.max_height,
|
|
146
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
|
+
)
|
|
@@ -162,12 +162,14 @@ class MigrationCommand(ToolkitCommand):
|
|
|
162
162
|
for item in source:
|
|
163
163
|
target, issue = mapper.map(item)
|
|
164
164
|
id_ = data.as_id(item)
|
|
165
|
-
|
|
165
|
+
result: Status = "failed" if target is None else "success"
|
|
166
|
+
tracker.set_progress(id_, step=self.Steps.CONVERT, status=result)
|
|
166
167
|
|
|
167
168
|
if issue.has_issues:
|
|
168
169
|
# MyPy fails to understand that dict[str, JsonVal] is a Chunk
|
|
169
170
|
issues.append(issue.dump()) # type: ignore[arg-type]
|
|
170
|
-
|
|
171
|
+
if target is not None:
|
|
172
|
+
targets.append(UploadItem(source_id=id_, item=target))
|
|
171
173
|
if issues:
|
|
172
174
|
log_file.write_chunks(issues)
|
|
173
175
|
return targets
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
from collections.abc import Mapping, Set
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Any, ClassVar
|
|
3
|
+
from typing import Any, ClassVar, overload
|
|
4
4
|
|
|
5
|
-
from cognite.client.data_classes import Asset, Event, FileMetadata,
|
|
6
|
-
from cognite.client.data_classes.
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
from cognite.client.data_classes import Annotation, Asset, Event, FileMetadata, TimeSeries
|
|
6
|
+
from cognite.client.data_classes.data_modeling import (
|
|
7
|
+
DirectRelation,
|
|
8
|
+
DirectRelationReference,
|
|
9
|
+
EdgeId,
|
|
10
|
+
MappedProperty,
|
|
11
|
+
NodeApply,
|
|
12
|
+
NodeId,
|
|
13
|
+
)
|
|
14
|
+
from cognite.client.data_classes.data_modeling.instances import EdgeApply, NodeOrEdgeData, PropertyValueWrite
|
|
9
15
|
from cognite.client.data_classes.data_modeling.views import ViewProperty
|
|
10
16
|
|
|
11
|
-
from cognite_toolkit._cdf_tk.client.data_classes.extended_filemetadata import ExtendedFileMetadata
|
|
12
|
-
from cognite_toolkit._cdf_tk.client.data_classes.extended_timeseries import ExtendedTimeSeries
|
|
13
17
|
from cognite_toolkit._cdf_tk.client.data_classes.migration import AssetCentricId, ResourceViewMapping
|
|
14
18
|
from cognite_toolkit._cdf_tk.utils.collection import flatten_dict_json_path
|
|
15
19
|
from cognite_toolkit._cdf_tk.utils.dtype_conversion import (
|
|
16
20
|
asset_centric_convert_to_primary_property,
|
|
21
|
+
convert_to_primary_property,
|
|
22
|
+
)
|
|
23
|
+
from cognite_toolkit._cdf_tk.utils.useful_types import (
|
|
24
|
+
AssetCentricResourceExtended,
|
|
25
|
+
AssetCentricType,
|
|
17
26
|
)
|
|
18
|
-
from cognite_toolkit._cdf_tk.utils.useful_types import AssetCentricType
|
|
19
27
|
|
|
20
28
|
from .data_model import INSTANCE_SOURCE_VIEW_ID
|
|
21
29
|
from .issues import ConversionIssue, FailedConversion, InvalidPropertyDataType
|
|
@@ -49,37 +57,71 @@ class DirectRelationCache:
|
|
|
49
57
|
("event", "assetIds"),
|
|
50
58
|
("sequence", "assetId"),
|
|
51
59
|
("asset", "parentId"),
|
|
60
|
+
("fileAnnotation", "data.assetRef.id"),
|
|
52
61
|
}
|
|
53
62
|
SOURCE_REFERENCE_PROPERTIES: ClassVar[Set[tuple[AssetCentricType, str]]] = {
|
|
54
63
|
("asset", "source"),
|
|
55
64
|
("event", "source"),
|
|
56
65
|
("file", "source"),
|
|
57
66
|
}
|
|
67
|
+
FILE_REFERENCE_PROPERTIES: ClassVar[Set[tuple[AssetCentricType, str]]] = {
|
|
68
|
+
("fileAnnotation", "data.fileRef.id"),
|
|
69
|
+
("fileAnnotation", "annotatedResourceId"),
|
|
70
|
+
}
|
|
58
71
|
|
|
59
72
|
asset: Mapping[int, DirectRelationReference]
|
|
60
73
|
source: Mapping[str, DirectRelationReference]
|
|
74
|
+
file: Mapping[int, DirectRelationReference]
|
|
61
75
|
|
|
62
76
|
def get(self, resource_type: AssetCentricType, property_id: str) -> Mapping[str | int, DirectRelationReference]:
|
|
63
|
-
|
|
77
|
+
key = resource_type, property_id
|
|
78
|
+
if key in self.ASSET_REFERENCE_PROPERTIES:
|
|
64
79
|
return self.asset # type: ignore[return-value]
|
|
65
|
-
if
|
|
80
|
+
if key in self.SOURCE_REFERENCE_PROPERTIES:
|
|
66
81
|
return self.source # type: ignore[return-value]
|
|
82
|
+
if key in self.FILE_REFERENCE_PROPERTIES:
|
|
83
|
+
return self.file # type: ignore[return-value]
|
|
67
84
|
return {}
|
|
68
85
|
|
|
69
86
|
|
|
87
|
+
@overload
|
|
70
88
|
def asset_centric_to_dm(
|
|
71
|
-
resource:
|
|
89
|
+
resource: AssetCentricResourceExtended,
|
|
72
90
|
instance_id: NodeId,
|
|
73
91
|
view_source: ResourceViewMapping,
|
|
74
92
|
view_properties: dict[str, ViewProperty],
|
|
75
93
|
asset_instance_id_by_id: Mapping[int, DirectRelationReference],
|
|
76
94
|
source_instance_id_by_external_id: Mapping[str, DirectRelationReference],
|
|
77
|
-
|
|
95
|
+
file_instance_id_by_id: Mapping[int, DirectRelationReference],
|
|
96
|
+
) -> tuple[NodeApply | None, ConversionIssue]: ...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@overload
|
|
100
|
+
def asset_centric_to_dm(
|
|
101
|
+
resource: AssetCentricResourceExtended,
|
|
102
|
+
instance_id: EdgeId,
|
|
103
|
+
view_source: ResourceViewMapping,
|
|
104
|
+
view_properties: dict[str, ViewProperty],
|
|
105
|
+
asset_instance_id_by_id: Mapping[int, DirectRelationReference],
|
|
106
|
+
source_instance_id_by_external_id: Mapping[str, DirectRelationReference],
|
|
107
|
+
file_instance_id_by_id: Mapping[int, DirectRelationReference],
|
|
108
|
+
) -> tuple[EdgeApply | None, ConversionIssue]: ...
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def asset_centric_to_dm(
|
|
112
|
+
resource: AssetCentricResourceExtended,
|
|
113
|
+
instance_id: NodeId | EdgeId,
|
|
114
|
+
view_source: ResourceViewMapping,
|
|
115
|
+
view_properties: dict[str, ViewProperty],
|
|
116
|
+
asset_instance_id_by_id: Mapping[int, DirectRelationReference],
|
|
117
|
+
source_instance_id_by_external_id: Mapping[str, DirectRelationReference],
|
|
118
|
+
file_instance_id_by_id: Mapping[int, DirectRelationReference],
|
|
119
|
+
) -> tuple[NodeApply | EdgeApply | None, ConversionIssue]:
|
|
78
120
|
"""Convert an asset-centric resource to a data model instance.
|
|
79
121
|
|
|
80
122
|
Args:
|
|
81
123
|
resource (CogniteResource): The asset-centric resource to convert.
|
|
82
|
-
instance_id (NodeId): The ID of the instance to create or update.
|
|
124
|
+
instance_id (NodeId | EdgeApply): The ID of the instance to create or update.
|
|
83
125
|
view_source (ResourceViewMapping): The view source defining how to map the resource to the data model.
|
|
84
126
|
view_properties (dict[str, ViewProperty]): The defined properties referenced in the view source mapping.
|
|
85
127
|
asset_instance_id_by_id (dict[int, DirectRelationReference]): A mapping from asset IDs to their corresponding
|
|
@@ -88,12 +130,17 @@ def asset_centric_to_dm(
|
|
|
88
130
|
source_instance_id_by_external_id (dict[str, DirectRelationReference]): A mapping from source strings to their
|
|
89
131
|
corresponding DirectRelationReference in the data model. This is used to create direct relations for resources
|
|
90
132
|
that reference sources.
|
|
133
|
+
file_instance_id_by_id (dict[int, DirectRelationReference]): A mapping from file IDs to their corresponding
|
|
134
|
+
DirectRelationReference in the data model. This is used to create direct relations for resources that
|
|
135
|
+
reference files.
|
|
91
136
|
|
|
92
137
|
Returns:
|
|
93
|
-
tuple[NodeApply, ConversionIssue]: A tuple containing the converted NodeApply and any ConversionIssue encountered.
|
|
138
|
+
tuple[NodeApply | EdgeApply, ConversionIssue]: A tuple containing the converted NodeApply and any ConversionIssue encountered.
|
|
94
139
|
"""
|
|
95
|
-
cache = DirectRelationCache(
|
|
96
|
-
|
|
140
|
+
cache = DirectRelationCache(
|
|
141
|
+
asset=asset_instance_id_by_id, source=source_instance_id_by_external_id, file=file_instance_id_by_id
|
|
142
|
+
)
|
|
143
|
+
resource_type = _lookup_resource_type(resource)
|
|
97
144
|
dumped = resource.dump()
|
|
98
145
|
try:
|
|
99
146
|
id_ = dumped.pop("id")
|
|
@@ -117,37 +164,54 @@ def asset_centric_to_dm(
|
|
|
117
164
|
sources: list[NodeOrEdgeData] = []
|
|
118
165
|
if properties:
|
|
119
166
|
sources.append(NodeOrEdgeData(source=view_source.view_id, properties=properties))
|
|
120
|
-
instance_source_properties = {
|
|
121
|
-
"resourceType": resource_type,
|
|
122
|
-
"id": id_,
|
|
123
|
-
"dataSetId": data_set_id,
|
|
124
|
-
"classicExternalId": external_id,
|
|
125
|
-
}
|
|
126
|
-
sources.append(NodeOrEdgeData(source=INSTANCE_SOURCE_VIEW_ID, properties=instance_source_properties))
|
|
127
167
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
168
|
+
if resource_type != "fileAnnotation":
|
|
169
|
+
instance_source_properties = {
|
|
170
|
+
"resourceType": resource_type,
|
|
171
|
+
"id": id_,
|
|
172
|
+
"dataSetId": data_set_id,
|
|
173
|
+
"classicExternalId": external_id,
|
|
174
|
+
}
|
|
175
|
+
sources.append(NodeOrEdgeData(source=INSTANCE_SOURCE_VIEW_ID, properties=instance_source_properties))
|
|
133
176
|
|
|
134
|
-
|
|
177
|
+
instance: NodeApply | EdgeApply
|
|
178
|
+
if isinstance(instance_id, EdgeId):
|
|
179
|
+
edge_properties = create_edge_properties(
|
|
180
|
+
dumped, view_source.property_mapping, resource_type, issue, cache, instance_id.space
|
|
181
|
+
)
|
|
182
|
+
if any(key not in edge_properties for key in ("start_node", "end_node", "type")):
|
|
183
|
+
# Failed conversion of edge properties
|
|
184
|
+
return None, issue
|
|
185
|
+
instance = EdgeApply(
|
|
186
|
+
space=instance_id.space,
|
|
187
|
+
external_id=instance_id.external_id,
|
|
188
|
+
sources=sources,
|
|
189
|
+
**edge_properties, # type: ignore[arg-type]
|
|
190
|
+
)
|
|
191
|
+
elif isinstance(instance_id, NodeId):
|
|
192
|
+
instance = NodeApply(space=instance_id.space, external_id=instance_id.external_id, sources=sources)
|
|
193
|
+
else:
|
|
194
|
+
raise RuntimeError(f"Unexpected instance_id type {type(instance_id)}")
|
|
135
195
|
|
|
196
|
+
return instance, issue
|
|
136
197
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
198
|
+
|
|
199
|
+
def _lookup_resource_type(resource_type: AssetCentricResourceExtended) -> AssetCentricType:
|
|
200
|
+
if isinstance(resource_type, Asset):
|
|
201
|
+
return "asset"
|
|
202
|
+
elif isinstance(resource_type, FileMetadata):
|
|
203
|
+
return "file"
|
|
204
|
+
elif isinstance(resource_type, Event):
|
|
205
|
+
return "event"
|
|
206
|
+
elif isinstance(resource_type, TimeSeries):
|
|
207
|
+
return "timeseries"
|
|
208
|
+
elif isinstance(resource_type, Annotation):
|
|
209
|
+
if resource_type.annotated_resource_type == "file" and resource_type.annotation_type in (
|
|
210
|
+
"diagrams.AssetLink",
|
|
211
|
+
"diagrams.FileLink",
|
|
212
|
+
):
|
|
213
|
+
return "fileAnnotation"
|
|
214
|
+
raise ValueError(f"Unsupported resource type: {resource_type}")
|
|
151
215
|
|
|
152
216
|
|
|
153
217
|
def create_properties(
|
|
@@ -210,5 +274,58 @@ def create_properties(
|
|
|
210
274
|
(set(flatten_dump.keys()) - set(property_mapping.keys())) | ignored_asset_centric_properties
|
|
211
275
|
)
|
|
212
276
|
issue.missing_asset_centric_properties = sorted(set(property_mapping.keys()) - set(flatten_dump.keys()))
|
|
213
|
-
|
|
277
|
+
# Node and edge properties are handled separately
|
|
278
|
+
issue.missing_instance_properties = sorted(
|
|
279
|
+
{
|
|
280
|
+
prop_id
|
|
281
|
+
for prop_id in property_mapping.values()
|
|
282
|
+
if not (prop_id.startswith("edge.") or prop_id.startswith("node."))
|
|
283
|
+
}
|
|
284
|
+
- set(view_properties.keys())
|
|
285
|
+
)
|
|
214
286
|
return properties
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def create_edge_properties(
|
|
290
|
+
dumped: dict[str, Any],
|
|
291
|
+
property_mapping: dict[str, str],
|
|
292
|
+
resource_type: AssetCentricType,
|
|
293
|
+
issue: ConversionIssue,
|
|
294
|
+
direct_relation_cache: DirectRelationCache,
|
|
295
|
+
default_instance_space: str,
|
|
296
|
+
) -> dict[str, DirectRelationReference]:
|
|
297
|
+
flatten_dump = flatten_dict_json_path(dumped)
|
|
298
|
+
edge_properties: dict[str, DirectRelationReference] = {}
|
|
299
|
+
for prop_json_path, prop_id in property_mapping.items():
|
|
300
|
+
if not prop_id.startswith("edge."):
|
|
301
|
+
continue
|
|
302
|
+
if prop_json_path not in flatten_dump:
|
|
303
|
+
continue
|
|
304
|
+
edge_prop_id = prop_id.removeprefix("edge.")
|
|
305
|
+
if edge_prop_id in ("startNode", "endNode", "type"):
|
|
306
|
+
# DirectRelation lookup.
|
|
307
|
+
try:
|
|
308
|
+
value = convert_to_primary_property(
|
|
309
|
+
flatten_dump[prop_json_path],
|
|
310
|
+
DirectRelation(),
|
|
311
|
+
False,
|
|
312
|
+
direct_relation_lookup=direct_relation_cache.get(resource_type, prop_json_path),
|
|
313
|
+
)
|
|
314
|
+
except (ValueError, TypeError, NotImplementedError) as e:
|
|
315
|
+
issue.failed_conversions.append(
|
|
316
|
+
FailedConversion(property_id=prop_json_path, value=flatten_dump[prop_json_path], error=str(e))
|
|
317
|
+
)
|
|
318
|
+
continue
|
|
319
|
+
elif edge_prop_id.endswith(".externalId"):
|
|
320
|
+
# Just an external ID string.
|
|
321
|
+
edge_prop_id = edge_prop_id.removesuffix(".externalId")
|
|
322
|
+
value = DirectRelationReference(default_instance_space, str(flatten_dump[prop_json_path]))
|
|
323
|
+
else:
|
|
324
|
+
issue.invalid_instance_property_types.append(
|
|
325
|
+
InvalidPropertyDataType(property_id=prop_id, expected_type="EdgeProperty")
|
|
326
|
+
)
|
|
327
|
+
continue
|
|
328
|
+
# We know that value is DirectRelationReference here
|
|
329
|
+
edge_properties[edge_prop_id.replace("Node", "_node")] = value # type: ignore[assignment]
|
|
330
|
+
|
|
331
|
+
return edge_properties
|
|
@@ -3,7 +3,6 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Any, Generic, Literal
|
|
4
4
|
|
|
5
5
|
from cognite.client.data_classes._base import (
|
|
6
|
-
T_WritableCogniteResource,
|
|
7
6
|
WriteableCogniteResource,
|
|
8
7
|
WriteableCogniteResourceList,
|
|
9
8
|
)
|
|
@@ -15,11 +14,14 @@ from cognite_toolkit._cdf_tk.client.data_classes.instances import InstanceApplyL
|
|
|
15
14
|
from cognite_toolkit._cdf_tk.client.data_classes.migration import AssetCentricId
|
|
16
15
|
from cognite_toolkit._cdf_tk.client.data_classes.pending_instances_ids import PendingInstanceId
|
|
17
16
|
from cognite_toolkit._cdf_tk.commands._migrate.default_mappings import create_default_mappings
|
|
18
|
-
from cognite_toolkit._cdf_tk.exceptions import
|
|
19
|
-
ToolkitValueError,
|
|
20
|
-
)
|
|
17
|
+
from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError
|
|
21
18
|
from cognite_toolkit._cdf_tk.storageio._data_classes import ModelList
|
|
22
|
-
from cognite_toolkit._cdf_tk.utils.useful_types import
|
|
19
|
+
from cognite_toolkit._cdf_tk.utils.useful_types import (
|
|
20
|
+
AssetCentricKind,
|
|
21
|
+
AssetCentricType,
|
|
22
|
+
JsonVal,
|
|
23
|
+
T_AssetCentricResource,
|
|
24
|
+
)
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
class MigrationMapping(BaseModel, alias_generator=to_camel_case, extra="ignore", populate_by_name=True):
|
|
@@ -186,9 +188,9 @@ class TimeSeriesMigrationMappingList(MigrationMappingList):
|
|
|
186
188
|
|
|
187
189
|
|
|
188
190
|
@dataclass
|
|
189
|
-
class AssetCentricMapping(Generic[
|
|
191
|
+
class AssetCentricMapping(Generic[T_AssetCentricResource], WriteableCogniteResource[InstanceApply]):
|
|
190
192
|
mapping: MigrationMapping
|
|
191
|
-
resource:
|
|
193
|
+
resource: T_AssetCentricResource
|
|
192
194
|
|
|
193
195
|
def as_write(self) -> InstanceApply:
|
|
194
196
|
raise NotImplementedError()
|
|
@@ -203,9 +205,7 @@ class AssetCentricMapping(Generic[T_WritableCogniteResource], WriteableCogniteRe
|
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
|
|
206
|
-
class AssetCentricMappingList(
|
|
207
|
-
WriteableCogniteResourceList[InstanceApply, AssetCentricMapping[T_WritableCogniteResource]]
|
|
208
|
-
):
|
|
208
|
+
class AssetCentricMappingList(WriteableCogniteResourceList[InstanceApply, AssetCentricMapping[T_AssetCentricResource]]):
|
|
209
209
|
_RESOURCE: type = AssetCentricMapping
|
|
210
210
|
|
|
211
211
|
def as_write(self) -> InstanceApplyList:
|
|
@@ -16,6 +16,7 @@ from cognite_toolkit._cdf_tk.constants import MISSING_INSTANCE_SPACE
|
|
|
16
16
|
from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError
|
|
17
17
|
from cognite_toolkit._cdf_tk.storageio._base import T_Selector, T_WriteCogniteResource
|
|
18
18
|
from cognite_toolkit._cdf_tk.utils import humanize_collection
|
|
19
|
+
from cognite_toolkit._cdf_tk.utils.useful_types import T_AssetCentricResource
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class DataMapper(Generic[T_Selector, T_CogniteResource, T_WriteCogniteResource], ABC):
|
|
@@ -30,7 +31,7 @@ class DataMapper(Generic[T_Selector, T_CogniteResource, T_WriteCogniteResource],
|
|
|
30
31
|
pass
|
|
31
32
|
|
|
32
33
|
@abstractmethod
|
|
33
|
-
def map(self, source: T_CogniteResource) -> tuple[T_WriteCogniteResource, MigrationIssue]:
|
|
34
|
+
def map(self, source: T_CogniteResource) -> tuple[T_WriteCogniteResource | None, MigrationIssue]:
|
|
34
35
|
"""Map a chunk of source data to the target format.
|
|
35
36
|
|
|
36
37
|
Args:
|
|
@@ -43,7 +44,9 @@ class DataMapper(Generic[T_Selector, T_CogniteResource, T_WriteCogniteResource],
|
|
|
43
44
|
raise NotImplementedError("Subclasses must implement this method.")
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
class AssetCentricMapper(
|
|
47
|
+
class AssetCentricMapper(
|
|
48
|
+
DataMapper[AssetCentricMigrationSelector, AssetCentricMapping[T_AssetCentricResource], InstanceApply]
|
|
49
|
+
):
|
|
47
50
|
def __init__(self, client: ToolkitClient) -> None:
|
|
48
51
|
self.client = client
|
|
49
52
|
self._ingestion_view_by_id: dict[ViewId, View] = {}
|
|
@@ -84,7 +87,7 @@ class AssetCentricMapper(DataMapper[AssetCentricMigrationSelector, AssetCentricM
|
|
|
84
87
|
asset_mappings = self.client.migration.instance_source.list(resource_type="asset", limit=-1)
|
|
85
88
|
self._asset_mapping_by_id = {mapping.id_: mapping.as_direct_relation_reference() for mapping in asset_mappings}
|
|
86
89
|
|
|
87
|
-
def map(self, source: AssetCentricMapping) -> tuple[InstanceApply, ConversionIssue]:
|
|
90
|
+
def map(self, source: AssetCentricMapping[T_AssetCentricResource]) -> tuple[InstanceApply | None, ConversionIssue]:
|
|
88
91
|
"""Map a chunk of asset-centric data to InstanceApplyList format."""
|
|
89
92
|
mapping = source.mapping
|
|
90
93
|
ingestion_view = mapping.get_ingestion_view()
|
|
@@ -102,6 +105,7 @@ class AssetCentricMapper(DataMapper[AssetCentricMigrationSelector, AssetCentricM
|
|
|
102
105
|
view_properties=view_properties,
|
|
103
106
|
asset_instance_id_by_id=self._asset_mapping_by_id,
|
|
104
107
|
source_instance_id_by_external_id=self._source_system_mapping_by_id,
|
|
108
|
+
file_instance_id_by_id={}, # Todo implement file direct relations
|
|
105
109
|
)
|
|
106
110
|
if mapping.instance_id.space == MISSING_INSTANCE_SPACE:
|
|
107
111
|
conversion_issue.missing_instance_space = f"Missing instance space for dataset ID {mapping.data_set_id!r}"
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
from collections.abc import Iterator, Mapping, Sequence
|
|
2
2
|
from typing import ClassVar, cast
|
|
3
3
|
|
|
4
|
-
from cognite.client.data_classes._base import (
|
|
5
|
-
T_WritableCogniteResource,
|
|
6
|
-
)
|
|
7
4
|
from cognite.client.data_classes.data_modeling import InstanceApply, NodeId
|
|
8
5
|
|
|
9
6
|
from cognite_toolkit._cdf_tk.client import ToolkitClient
|
|
@@ -22,6 +19,7 @@ from cognite_toolkit._cdf_tk.utils.useful_types import (
|
|
|
22
19
|
AssetCentricKind,
|
|
23
20
|
AssetCentricType,
|
|
24
21
|
JsonVal,
|
|
22
|
+
T_AssetCentricResource,
|
|
25
23
|
)
|
|
26
24
|
|
|
27
25
|
from .data_classes import AssetCentricMapping, AssetCentricMappingList, MigrationMapping, MigrationMappingList
|
|
@@ -30,7 +28,7 @@ from .selectors import AssetCentricMigrationSelector, MigrateDataSetSelector, Mi
|
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
class AssetCentricMigrationIO(
|
|
33
|
-
UploadableStorageIO[AssetCentricMigrationSelector, AssetCentricMapping[
|
|
31
|
+
UploadableStorageIO[AssetCentricMigrationSelector, AssetCentricMapping[T_AssetCentricResource], InstanceApply]
|
|
34
32
|
):
|
|
35
33
|
KIND = "AssetCentricMigration"
|
|
36
34
|
SUPPORTED_DOWNLOAD_FORMATS = frozenset({".parquet", ".csv", ".ndjson"})
|
|
@@ -63,11 +61,11 @@ class AssetCentricMigrationIO(
|
|
|
63
61
|
|
|
64
62
|
def _stream_from_csv(
|
|
65
63
|
self, selector: MigrationCSVFileSelector, limit: int | None = None
|
|
66
|
-
) -> Iterator[Sequence[AssetCentricMapping[
|
|
64
|
+
) -> Iterator[Sequence[AssetCentricMapping[T_AssetCentricResource]]]:
|
|
67
65
|
items = selector.items
|
|
68
66
|
if limit is not None:
|
|
69
67
|
items = MigrationMappingList(items[:limit])
|
|
70
|
-
chunk: list[AssetCentricMapping[
|
|
68
|
+
chunk: list[AssetCentricMapping[T_AssetCentricResource]] = []
|
|
71
69
|
for current_batch in chunker_sequence(items, self.CHUNK_SIZE):
|
|
72
70
|
resources = self.hierarchy.get_resource_io(selector.kind).retrieve(current_batch.get_ids())
|
|
73
71
|
for mapping, resource in zip(current_batch, resources, strict=True):
|
|
@@ -86,12 +84,12 @@ class AssetCentricMigrationIO(
|
|
|
86
84
|
|
|
87
85
|
def _stream_given_dataset(
|
|
88
86
|
self, selector: MigrateDataSetSelector, limit: int | None = None
|
|
89
|
-
) -> Iterator[Sequence[AssetCentricMapping[
|
|
87
|
+
) -> Iterator[Sequence[AssetCentricMapping[T_AssetCentricResource]]]:
|
|
90
88
|
asset_centric_selector = selector.as_asset_centric_selector()
|
|
91
89
|
for data_chunk in self.hierarchy.stream_data(asset_centric_selector, limit):
|
|
92
|
-
mapping_list = AssetCentricMappingList[
|
|
90
|
+
mapping_list = AssetCentricMappingList[T_AssetCentricResource]([])
|
|
93
91
|
for resource in data_chunk.items:
|
|
94
|
-
# We
|
|
92
|
+
# We got the resource from a dataset selector, so we know it is there
|
|
95
93
|
data_set_id = cast(int, resource.data_set_id)
|
|
96
94
|
space_source = self.client.migration.space_source.retrieve(data_set_id=data_set_id)
|
|
97
95
|
instance_space = space_source.instance_space if space_source else None
|
|
@@ -129,7 +127,7 @@ class AssetCentricMigrationIO(
|
|
|
129
127
|
|
|
130
128
|
def data_to_json_chunk(
|
|
131
129
|
self,
|
|
132
|
-
data_chunk: Sequence[AssetCentricMapping[
|
|
130
|
+
data_chunk: Sequence[AssetCentricMapping[T_AssetCentricResource]],
|
|
133
131
|
selector: AssetCentricMigrationSelector | None = None,
|
|
134
132
|
) -> list[dict[str, JsonVal]]:
|
|
135
133
|
return [item.dump() for item in data_chunk]
|
|
@@ -492,7 +492,7 @@ class BuildCommand(ToolkitCommand):
|
|
|
492
492
|
# which is what we do in the deploy step to verify that the source file has not changed.
|
|
493
493
|
source = SourceLocationEager(source_path, calculate_hash(source_path, shorten=True))
|
|
494
494
|
|
|
495
|
-
content = variables.replace(content, source_path
|
|
495
|
+
content = variables.replace(content, source_path)
|
|
496
496
|
|
|
497
497
|
replace_warnings = self._check_variables_replaced(content, module_dir, source_path)
|
|
498
498
|
|
|
@@ -561,7 +561,7 @@ class PullCommand(ToolkitCommand):
|
|
|
561
561
|
|
|
562
562
|
if has_changes and not dry_run:
|
|
563
563
|
new_content, extra_files = self._to_write_content(
|
|
564
|
-
safe_read(source_file), to_write, resources, environment_variables, loader
|
|
564
|
+
safe_read(source_file), to_write, resources, environment_variables, loader, source_file
|
|
565
565
|
)
|
|
566
566
|
with source_file.open("w", encoding=ENCODING, newline=NEWLINE) as f:
|
|
567
567
|
f.write(new_content)
|
|
@@ -649,6 +649,7 @@ class PullCommand(ToolkitCommand):
|
|
|
649
649
|
loader: ResourceCRUD[
|
|
650
650
|
T_ID, T_WriteClass, T_WritableCogniteResource, T_CogniteResourceList, T_WritableCogniteResourceList
|
|
651
651
|
],
|
|
652
|
+
source_file: Path,
|
|
652
653
|
) -> tuple[str, dict[Path, str]]:
|
|
653
654
|
# 1. Replace all variables with placeholders
|
|
654
655
|
# 2. Load source and keep the comments
|
|
@@ -682,7 +683,7 @@ class PullCommand(ToolkitCommand):
|
|
|
682
683
|
variables_with_environment_list.append(variable)
|
|
683
684
|
variables = BuildVariables(variables_with_environment_list)
|
|
684
685
|
|
|
685
|
-
content, value_by_placeholder = variables.replace(source, use_placeholder=True)
|
|
686
|
+
content, value_by_placeholder = variables.replace(source, source_file, use_placeholder=True)
|
|
686
687
|
comments = YAMLComments.load(source)
|
|
687
688
|
# If there is a variable in the identifier, we need to replace it with the value
|
|
688
689
|
# such that we can look it up in the to_write dict.
|
|
@@ -690,10 +691,10 @@ class PullCommand(ToolkitCommand):
|
|
|
690
691
|
# The safe read in ExtractionPipelineConfigLoader stringifies the config dict,
|
|
691
692
|
# but we need to load it as a dict so we can write it back to the file maintaining
|
|
692
693
|
# the order or the keys.
|
|
693
|
-
loaded = read_yaml_content(variables.replace(source))
|
|
694
|
+
loaded = read_yaml_content(variables.replace(source, source_file))
|
|
694
695
|
loaded_with_placeholder = read_yaml_content(content)
|
|
695
696
|
else:
|
|
696
|
-
loaded = read_yaml_content(loader.safe_read(variables.replace(source)))
|
|
697
|
+
loaded = read_yaml_content(loader.safe_read(variables.replace(source, source_file)))
|
|
697
698
|
loaded_with_placeholder = read_yaml_content(loader.safe_read(content))
|
|
698
699
|
|
|
699
700
|
built_by_identifier = {r.identifier: r for r in resources}
|
|
@@ -756,7 +757,7 @@ class PullCommand(ToolkitCommand):
|
|
|
756
757
|
builder = create_builder(built.resource_dir, None)
|
|
757
758
|
for extra in built.extra_sources:
|
|
758
759
|
extra_content, extra_placeholders = built.build_variables.replace(
|
|
759
|
-
safe_read(extra.path), extra.path
|
|
760
|
+
safe_read(extra.path), extra.path, use_placeholder=True
|
|
760
761
|
)
|
|
761
762
|
key, _ = builder.load_extra_field(extra_content)
|
|
762
763
|
if key in item_write:
|