cognite-neat 0.126.0__py3-none-any.whl → 0.126.1__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-neat might be problematic. Click here for more details.
- cognite/neat/_client/__init__.py +4 -0
- cognite/neat/_client/api.py +8 -0
- cognite/neat/_client/client.py +19 -0
- cognite/neat/_client/config.py +40 -0
- cognite/neat/_client/containers_api.py +73 -0
- cognite/neat/_client/data_classes.py +10 -0
- cognite/neat/_client/data_model_api.py +63 -0
- cognite/neat/_client/spaces_api.py +67 -0
- cognite/neat/_client/views_api.py +82 -0
- cognite/neat/_data_model/_analysis.py +127 -0
- cognite/neat/_data_model/_constants.py +59 -0
- cognite/neat/_data_model/_shared.py +46 -0
- cognite/neat/_data_model/deployer/__init__.py +0 -0
- cognite/neat/_data_model/deployer/_differ.py +113 -0
- cognite/neat/_data_model/deployer/_differ_container.py +354 -0
- cognite/neat/_data_model/deployer/_differ_data_model.py +29 -0
- cognite/neat/_data_model/deployer/_differ_space.py +9 -0
- cognite/neat/_data_model/deployer/_differ_view.py +194 -0
- cognite/neat/_data_model/deployer/data_classes.py +176 -0
- cognite/neat/_data_model/exporters/__init__.py +4 -0
- cognite/neat/_data_model/exporters/_base.py +6 -1
- cognite/neat/_data_model/exporters/_table_exporter/__init__.py +0 -0
- cognite/neat/_data_model/exporters/_table_exporter/exporter.py +106 -0
- cognite/neat/_data_model/exporters/_table_exporter/workbook.py +414 -0
- cognite/neat/_data_model/exporters/_table_exporter/writer.py +391 -0
- cognite/neat/_data_model/importers/__init__.py +2 -1
- cognite/neat/_data_model/importers/_api_importer.py +88 -0
- cognite/neat/_data_model/importers/_table_importer/data_classes.py +48 -8
- cognite/neat/_data_model/importers/_table_importer/importer.py +74 -5
- cognite/neat/_data_model/importers/_table_importer/reader.py +63 -7
- cognite/neat/_data_model/models/dms/__init__.py +17 -1
- cognite/neat/_data_model/models/dms/_base.py +12 -8
- cognite/neat/_data_model/models/dms/_constants.py +1 -1
- cognite/neat/_data_model/models/dms/_constraints.py +2 -1
- cognite/neat/_data_model/models/dms/_container.py +5 -5
- cognite/neat/_data_model/models/dms/_data_model.py +3 -3
- cognite/neat/_data_model/models/dms/_data_types.py +8 -1
- cognite/neat/_data_model/models/dms/_http.py +18 -0
- cognite/neat/_data_model/models/dms/_indexes.py +2 -1
- cognite/neat/_data_model/models/dms/_references.py +17 -4
- cognite/neat/_data_model/models/dms/_space.py +11 -7
- cognite/neat/_data_model/models/dms/_view_property.py +7 -4
- cognite/neat/_data_model/models/dms/_views.py +16 -6
- cognite/neat/_data_model/validation/__init__.py +0 -0
- cognite/neat/_data_model/validation/_base.py +16 -0
- cognite/neat/_data_model/validation/dms/__init__.py +9 -0
- cognite/neat/_data_model/validation/dms/_orchestrator.py +68 -0
- cognite/neat/_data_model/validation/dms/_validators.py +139 -0
- cognite/neat/_exceptions.py +15 -3
- cognite/neat/_issues.py +39 -6
- cognite/neat/_session/__init__.py +3 -0
- cognite/neat/_session/_physical.py +88 -0
- cognite/neat/_session/_session.py +34 -25
- cognite/neat/_session/_wrappers.py +61 -0
- cognite/neat/_state_machine/__init__.py +10 -0
- cognite/neat/{_session/_state_machine → _state_machine}/_base.py +11 -1
- cognite/neat/_state_machine/_states.py +53 -0
- cognite/neat/_store/__init__.py +3 -0
- cognite/neat/_store/_provenance.py +55 -0
- cognite/neat/_store/_store.py +124 -0
- cognite/neat/_utils/_reader.py +194 -0
- cognite/neat/_utils/http_client/__init__.py +14 -20
- cognite/neat/_utils/http_client/_client.py +22 -61
- cognite/neat/_utils/http_client/_data_classes.py +167 -268
- cognite/neat/_utils/text.py +6 -0
- cognite/neat/_utils/useful_types.py +23 -2
- cognite/neat/_version.py +1 -1
- cognite/neat/v0/core/_data_model/importers/_rdf/_shared.py +2 -2
- {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/METADATA +1 -1
- {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/RECORD +72 -38
- cognite/neat/_data_model/exporters/_table_exporter.py +0 -35
- cognite/neat/_session/_state_machine/__init__.py +0 -23
- cognite/neat/_session/_state_machine/_states.py +0 -150
- {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/WHEEL +0 -0
- {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from cognite.neat._data_model.importers._table_importer.data_classes import (
|
|
7
|
+
DMSContainer,
|
|
8
|
+
DMSEnum,
|
|
9
|
+
DMSNode,
|
|
10
|
+
DMSProperty,
|
|
11
|
+
DMSView,
|
|
12
|
+
MetadataValue,
|
|
13
|
+
TableDMS,
|
|
14
|
+
)
|
|
15
|
+
from cognite.neat._data_model.models.dms import (
|
|
16
|
+
ContainerPropertyDefinition,
|
|
17
|
+
ContainerReference,
|
|
18
|
+
ContainerRequest,
|
|
19
|
+
DataModelRequest,
|
|
20
|
+
DataType,
|
|
21
|
+
DirectNodeRelation,
|
|
22
|
+
EnumProperty,
|
|
23
|
+
ListablePropertyTypeDefinition,
|
|
24
|
+
NodeReference,
|
|
25
|
+
RequestSchema,
|
|
26
|
+
RequiresConstraintDefinition,
|
|
27
|
+
UniquenessConstraintDefinition,
|
|
28
|
+
ViewCorePropertyRequest,
|
|
29
|
+
ViewReference,
|
|
30
|
+
ViewRequest,
|
|
31
|
+
ViewRequestProperty,
|
|
32
|
+
)
|
|
33
|
+
from cognite.neat._data_model.models.dms._view_property import (
|
|
34
|
+
EdgeProperty,
|
|
35
|
+
MultiEdgeProperty,
|
|
36
|
+
MultiReverseDirectRelationPropertyRequest,
|
|
37
|
+
ReverseDirectRelationProperty,
|
|
38
|
+
SingleEdgeProperty,
|
|
39
|
+
SingleReverseDirectRelationPropertyRequest,
|
|
40
|
+
)
|
|
41
|
+
from cognite.neat._data_model.models.entities import ParsedEntity
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ViewProperties:
|
|
46
|
+
properties: list[DMSProperty] = field(default_factory=list)
|
|
47
|
+
nodes: list[DMSNode] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ContainerProperties:
|
|
52
|
+
properties_by_id: dict[tuple[ContainerReference, str], dict] = field(default_factory=dict)
|
|
53
|
+
enum_collections: list[DMSEnum] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DMSTableWriter:
|
|
57
|
+
def __init__(self, default_space: str, default_version: str) -> None:
|
|
58
|
+
self.default_space = default_space
|
|
59
|
+
self.default_version = default_version
|
|
60
|
+
|
|
61
|
+
## Main Entry Point ###
|
|
62
|
+
def write_tables(self, schema: RequestSchema) -> TableDMS:
|
|
63
|
+
metadata = self.write_metadata(schema.data_model)
|
|
64
|
+
container_properties = self.write_container_properties(schema.containers)
|
|
65
|
+
view_properties = self.write_view_properties(schema.views, container_properties)
|
|
66
|
+
views = self.write_views(schema.views, set(schema.data_model.views or []))
|
|
67
|
+
containers = self.write_containers(schema.containers)
|
|
68
|
+
|
|
69
|
+
return TableDMS(
|
|
70
|
+
metadata=metadata,
|
|
71
|
+
properties=view_properties.properties,
|
|
72
|
+
views=views,
|
|
73
|
+
containers=containers,
|
|
74
|
+
enum=container_properties.enum_collections,
|
|
75
|
+
nodes=view_properties.nodes,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
### Metadata Sheet ###
|
|
79
|
+
@staticmethod
|
|
80
|
+
def write_metadata(data_model: DataModelRequest) -> list[MetadataValue]:
|
|
81
|
+
return [
|
|
82
|
+
MetadataValue(key=key, value=value)
|
|
83
|
+
for key, value in data_model.model_dump(
|
|
84
|
+
mode="json", by_alias=True, exclude_none=True, exclude={"views"}
|
|
85
|
+
).items()
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
### Container Properties Sheet ###
|
|
89
|
+
|
|
90
|
+
def write_containers(self, containers: list[ContainerRequest]) -> list[DMSContainer]:
|
|
91
|
+
return [
|
|
92
|
+
DMSContainer(
|
|
93
|
+
container=self._create_container_entity(container),
|
|
94
|
+
name=container.name,
|
|
95
|
+
description=container.description,
|
|
96
|
+
constraint=self._create_container_constraints(container),
|
|
97
|
+
used_for=container.used_for,
|
|
98
|
+
)
|
|
99
|
+
for container in containers
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
def write_container_properties(self, containers: list[ContainerRequest]) -> ContainerProperties:
|
|
103
|
+
indices_by_container_property = self._write_container_indices(containers)
|
|
104
|
+
constraints_by_container_property = self._write_container_property_constraints(containers)
|
|
105
|
+
|
|
106
|
+
output = ContainerProperties()
|
|
107
|
+
for container in containers:
|
|
108
|
+
for prop_id, prop in container.properties.items():
|
|
109
|
+
container_property = self._write_container_property(
|
|
110
|
+
container.as_reference(),
|
|
111
|
+
prop_id,
|
|
112
|
+
prop,
|
|
113
|
+
indices_by_container_property,
|
|
114
|
+
constraints_by_container_property,
|
|
115
|
+
)
|
|
116
|
+
output.properties_by_id[(container.as_reference(), prop_id)] = container_property
|
|
117
|
+
if isinstance(prop.type, EnumProperty):
|
|
118
|
+
output.enum_collections.extend(
|
|
119
|
+
self._write_enum_collection(container.as_reference(), prop_id, prop.type)
|
|
120
|
+
)
|
|
121
|
+
return output
|
|
122
|
+
|
|
123
|
+
def _write_container_property(
|
|
124
|
+
self,
|
|
125
|
+
container_ref: ContainerReference,
|
|
126
|
+
prop_id: str,
|
|
127
|
+
prop: ContainerPropertyDefinition,
|
|
128
|
+
indices_by_container_property: dict[tuple[ContainerReference, str], list[ParsedEntity]],
|
|
129
|
+
constraints_by_container_property: dict[tuple[ContainerReference, str], list[ParsedEntity]],
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
return dict(
|
|
132
|
+
connection=self._write_container_property_connection(prop.type),
|
|
133
|
+
value_type=self._write_container_property_value_type(prop, prop_id, container_ref),
|
|
134
|
+
min_count=0 if prop.nullable else 1,
|
|
135
|
+
max_count=self._write_container_property_max_count(prop.type),
|
|
136
|
+
immutable=prop.immutable,
|
|
137
|
+
default=json.dumps(prop.default_value) if isinstance(prop.default_value, dict) else prop.default_value,
|
|
138
|
+
auto_increment=prop.auto_increment,
|
|
139
|
+
container=self._create_container_entity(container_ref),
|
|
140
|
+
container_property=prop_id,
|
|
141
|
+
container_property_name=prop.name,
|
|
142
|
+
container_property_description=prop.description,
|
|
143
|
+
index=indices_by_container_property.get((container_ref, prop_id)),
|
|
144
|
+
constraint=constraints_by_container_property.get((container_ref, prop_id)),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _write_container_property_connection(self, dtype: DataType) -> ParsedEntity | None:
|
|
148
|
+
if not isinstance(dtype, DirectNodeRelation):
|
|
149
|
+
return None
|
|
150
|
+
properties: dict[str, str] = {}
|
|
151
|
+
if dtype.container is not None:
|
|
152
|
+
properties["container"] = str(self._create_container_entity(dtype.container))
|
|
153
|
+
return ParsedEntity("", "direct", properties=properties)
|
|
154
|
+
|
|
155
|
+
def _write_container_property_value_type(
|
|
156
|
+
self, prop: ContainerPropertyDefinition, prop_id: str, container_ref: ContainerReference
|
|
157
|
+
) -> ParsedEntity:
|
|
158
|
+
if isinstance(prop.type, DirectNodeRelation):
|
|
159
|
+
# Will be overwritten if the view property has source set.
|
|
160
|
+
return ParsedEntity("", "#N/A", properties={})
|
|
161
|
+
elif isinstance(prop.type, EnumProperty):
|
|
162
|
+
enum_properties = {"collection": self._enum_collection_name(container_ref, prop_id)}
|
|
163
|
+
if prop.type.unknown_value is not None:
|
|
164
|
+
enum_properties["unknownValue"] = prop.type.unknown_value
|
|
165
|
+
return ParsedEntity("", "enum", properties=enum_properties)
|
|
166
|
+
elif isinstance(prop.type, ListablePropertyTypeDefinition):
|
|
167
|
+
# List and maxListSize are included in the maxCount of the property, so we exclude them here.
|
|
168
|
+
entity_properties = prop.type.model_dump(
|
|
169
|
+
mode="json", by_alias=True, exclude={"list", "maxListSize", "type"}, exclude_none=True
|
|
170
|
+
)
|
|
171
|
+
return ParsedEntity("", prop.type.type, properties=entity_properties)
|
|
172
|
+
else:
|
|
173
|
+
# Should not happen as all types are either ListablePropertyTypeDefinition or EnumProperty.
|
|
174
|
+
return ParsedEntity("", prop.type.type, properties={})
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _write_container_property_max_count(dtype: DataType) -> int | None:
|
|
178
|
+
if isinstance(dtype, ListablePropertyTypeDefinition) and dtype.list:
|
|
179
|
+
return dtype.max_list_size
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def _write_container_indices(
|
|
184
|
+
containers: list[ContainerRequest],
|
|
185
|
+
) -> dict[tuple[ContainerReference, str], list[ParsedEntity]]:
|
|
186
|
+
"""Writes container indices and groups them by (container_reference, property_id)."""
|
|
187
|
+
indices_by_id: dict[tuple[ContainerReference, str], list[ParsedEntity]] = defaultdict(list)
|
|
188
|
+
for container in containers:
|
|
189
|
+
if not container.indexes:
|
|
190
|
+
continue
|
|
191
|
+
for index_id, index in container.indexes.items():
|
|
192
|
+
for order, prop_id in enumerate(index.properties, 1):
|
|
193
|
+
entity_properties = index.model_dump(
|
|
194
|
+
mode="json", by_alias=True, exclude={"index_type", "properties"}, exclude_none=True
|
|
195
|
+
)
|
|
196
|
+
if len(index.properties) > 1:
|
|
197
|
+
entity_properties["order"] = str(order)
|
|
198
|
+
entity = ParsedEntity(index.index_type, index_id, properties=entity_properties)
|
|
199
|
+
indices_by_id[(container.as_reference(), prop_id)].append(entity)
|
|
200
|
+
return indices_by_id
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _write_container_property_constraints(
|
|
204
|
+
containers: list[ContainerRequest],
|
|
205
|
+
) -> dict[tuple[ContainerReference, str], list[ParsedEntity]]:
|
|
206
|
+
"""Writes container constraints and groups them by (container_reference, property_id).
|
|
207
|
+
|
|
208
|
+
Note this only includes uniqueness constraints, the require constraints is handled
|
|
209
|
+
in the writing of the container itself.
|
|
210
|
+
"""
|
|
211
|
+
constraints_by_id: dict[tuple[ContainerReference, str], list[ParsedEntity]] = defaultdict(list)
|
|
212
|
+
for container in containers:
|
|
213
|
+
if not container.constraints:
|
|
214
|
+
continue
|
|
215
|
+
for constraint_id, constraint in container.constraints.items():
|
|
216
|
+
if not isinstance(constraint, UniquenessConstraintDefinition):
|
|
217
|
+
continue
|
|
218
|
+
for order, prop_id in enumerate(constraint.properties, 1):
|
|
219
|
+
entity_properties = constraint.model_dump(
|
|
220
|
+
mode="json", by_alias=True, exclude={"constraint_type", "properties"}, exclude_none=True
|
|
221
|
+
)
|
|
222
|
+
if len(constraint.properties) > 1:
|
|
223
|
+
entity_properties["order"] = str(order)
|
|
224
|
+
entity = ParsedEntity(constraint.constraint_type, constraint_id, properties=entity_properties)
|
|
225
|
+
constraints_by_id[(container.as_reference(), prop_id)].append(entity)
|
|
226
|
+
return constraints_by_id
|
|
227
|
+
|
|
228
|
+
def _create_container_constraints(self, container: ContainerRequest) -> list[ParsedEntity] | None:
|
|
229
|
+
if not container.constraints:
|
|
230
|
+
return None
|
|
231
|
+
output: list[ParsedEntity] = []
|
|
232
|
+
for constraint_id, constraint in container.constraints.items():
|
|
233
|
+
if not isinstance(constraint, RequiresConstraintDefinition):
|
|
234
|
+
continue
|
|
235
|
+
entity_properties = {"require": str(self._create_container_entity(constraint.require))}
|
|
236
|
+
output.append(
|
|
237
|
+
ParsedEntity(prefix=constraint.constraint_type, suffix=constraint_id, properties=entity_properties)
|
|
238
|
+
)
|
|
239
|
+
return output or None
|
|
240
|
+
|
|
241
|
+
### Enum Sheet ###
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _enum_collection_name(container_ref: ContainerReference, prop_id: str) -> str:
|
|
244
|
+
return f"{container_ref.external_id}.{prop_id}"
|
|
245
|
+
|
|
246
|
+
def _write_enum_collection(
|
|
247
|
+
self, container_ref: ContainerReference, prop_id: str, enum: EnumProperty
|
|
248
|
+
) -> list[DMSEnum]:
|
|
249
|
+
output: list[DMSEnum] = []
|
|
250
|
+
name = self._enum_collection_name(container_ref, prop_id)
|
|
251
|
+
for value_id, value in enum.values.items():
|
|
252
|
+
output.append(
|
|
253
|
+
DMSEnum(
|
|
254
|
+
collection=name,
|
|
255
|
+
value=value_id,
|
|
256
|
+
name=value.name,
|
|
257
|
+
description=value.description,
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
return output
|
|
261
|
+
|
|
262
|
+
### View Sheet ###
|
|
263
|
+
def write_views(self, views: list[ViewRequest], model_views: set[ViewReference]) -> list[DMSView]:
|
|
264
|
+
return [
|
|
265
|
+
DMSView(
|
|
266
|
+
view=self._create_view_entity(view),
|
|
267
|
+
name=view.name,
|
|
268
|
+
description=view.description,
|
|
269
|
+
implements=[self._create_view_entity(parent) for parent in view.implements]
|
|
270
|
+
if view.implements
|
|
271
|
+
else None,
|
|
272
|
+
filter=json.dumps(view.filter) if view.filter else None,
|
|
273
|
+
in_model=None if view.as_reference() in model_views else False,
|
|
274
|
+
)
|
|
275
|
+
for view in views
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
def write_view_properties(self, views: list[ViewRequest], container: ContainerProperties) -> ViewProperties:
|
|
279
|
+
output = ViewProperties()
|
|
280
|
+
for view in views:
|
|
281
|
+
if not view.properties:
|
|
282
|
+
continue
|
|
283
|
+
for prop_id, prop in view.properties.items():
|
|
284
|
+
output.properties.append(self._write_view_property(view, prop_id, prop, container))
|
|
285
|
+
if isinstance(prop, EdgeProperty):
|
|
286
|
+
output.nodes.append(self._write_node(prop))
|
|
287
|
+
return output
|
|
288
|
+
|
|
289
|
+
def _write_view_property(
|
|
290
|
+
self, view: ViewRequest, prop_id: str, prop: ViewRequestProperty, container: ContainerProperties
|
|
291
|
+
) -> DMSProperty:
|
|
292
|
+
container_properties: dict[str, Any] = {}
|
|
293
|
+
if isinstance(prop, ViewCorePropertyRequest):
|
|
294
|
+
identifier = (prop.container, prop.container_property_identifier)
|
|
295
|
+
if identifier in container.properties_by_id:
|
|
296
|
+
container_properties = container.properties_by_id[identifier]
|
|
297
|
+
view_properties: dict[str, Any] = dict(
|
|
298
|
+
view=self._create_view_entity(view), view_property=prop_id, name=prop.name, description=prop.description
|
|
299
|
+
)
|
|
300
|
+
if connection := self._write_view_property_connection(prop):
|
|
301
|
+
view_properties["connection"] = connection
|
|
302
|
+
if view_value_type := self._write_view_property_value_type(prop):
|
|
303
|
+
view_properties["value_type"] = view_value_type
|
|
304
|
+
view_min_count = self._write_view_property_min_count(prop)
|
|
305
|
+
if view_min_count is not None:
|
|
306
|
+
view_properties["min_count"] = view_min_count
|
|
307
|
+
view_max_count = self._write_view_property_max_count(prop)
|
|
308
|
+
if view_max_count != "container":
|
|
309
|
+
view_properties["max_count"] = view_max_count
|
|
310
|
+
|
|
311
|
+
# Overwrite container properties with view properties where relevant.
|
|
312
|
+
args = container_properties | view_properties
|
|
313
|
+
return DMSProperty(**args)
|
|
314
|
+
|
|
315
|
+
def _write_view_property_connection(self, prop: ViewRequestProperty) -> ParsedEntity | None:
|
|
316
|
+
if isinstance(prop, ViewCorePropertyRequest):
|
|
317
|
+
# Use the container definition for connection
|
|
318
|
+
return None
|
|
319
|
+
elif isinstance(prop, EdgeProperty):
|
|
320
|
+
edge_properties: dict[str, str] = {}
|
|
321
|
+
if prop.direction != "outwards":
|
|
322
|
+
edge_properties["direction"] = prop.direction
|
|
323
|
+
if prop.edge_source is not None:
|
|
324
|
+
edge_properties["edgeSource"] = str(self._create_view_entity(prop.edge_source))
|
|
325
|
+
edge_properties["type"] = str(self._create_node_entity(prop.type))
|
|
326
|
+
return ParsedEntity("", "edge", properties=edge_properties)
|
|
327
|
+
elif isinstance(prop, ReverseDirectRelationProperty):
|
|
328
|
+
return ParsedEntity("", "reverse", properties={"property": prop.through.identifier})
|
|
329
|
+
else:
|
|
330
|
+
raise ValueError(f"Unknown view property type: {type(prop)}")
|
|
331
|
+
|
|
332
|
+
def _write_view_property_value_type(self, prop: ViewRequestProperty) -> ParsedEntity | None:
|
|
333
|
+
if isinstance(prop, ViewCorePropertyRequest):
|
|
334
|
+
if prop.source:
|
|
335
|
+
return self._create_view_entity(prop.source)
|
|
336
|
+
else:
|
|
337
|
+
# Use the container definition for value type
|
|
338
|
+
return None
|
|
339
|
+
elif isinstance(prop, ReverseDirectRelationProperty | EdgeProperty):
|
|
340
|
+
return self._create_view_entity(prop.source)
|
|
341
|
+
else:
|
|
342
|
+
raise ValueError(f"Unknown view property type: {type(prop)}")
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _write_view_property_min_count(prop: ViewRequestProperty) -> int | None:
|
|
346
|
+
if isinstance(prop, ViewCorePropertyRequest):
|
|
347
|
+
# Use the container definition for min count
|
|
348
|
+
return None
|
|
349
|
+
# Edges and reverse relations cannot be required.
|
|
350
|
+
return 0
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def _write_view_property_max_count(prop: ViewRequestProperty) -> int | None | Literal["container"]:
|
|
354
|
+
if isinstance(prop, ViewCorePropertyRequest):
|
|
355
|
+
# Use the container definition for max count
|
|
356
|
+
return "container"
|
|
357
|
+
elif isinstance(prop, SingleEdgeProperty | SingleReverseDirectRelationPropertyRequest):
|
|
358
|
+
return 1
|
|
359
|
+
elif isinstance(prop, MultiEdgeProperty | MultiReverseDirectRelationPropertyRequest):
|
|
360
|
+
return None
|
|
361
|
+
else:
|
|
362
|
+
raise ValueError(f"Unknown view property type: {type(prop)}")
|
|
363
|
+
|
|
364
|
+
### Node Sheet ###
|
|
365
|
+
|
|
366
|
+
def _write_node(self, prop: EdgeProperty) -> DMSNode:
|
|
367
|
+
return DMSNode(node=self._create_node_entity(prop.type))
|
|
368
|
+
|
|
369
|
+
## Entity Helpers ###
|
|
370
|
+
|
|
371
|
+
def _create_view_entity(self, view: ViewRequest | ViewReference) -> ParsedEntity:
|
|
372
|
+
prefix = view.space
|
|
373
|
+
properties = {"version": view.version}
|
|
374
|
+
if view.space == self.default_space:
|
|
375
|
+
prefix = ""
|
|
376
|
+
if view.version == self.default_version:
|
|
377
|
+
# Only use default version if space is also default.
|
|
378
|
+
properties = {}
|
|
379
|
+
return ParsedEntity(prefix=prefix, suffix=view.external_id, properties=properties)
|
|
380
|
+
|
|
381
|
+
def _create_container_entity(self, container: ContainerRequest | ContainerReference) -> ParsedEntity:
|
|
382
|
+
prefix = container.space
|
|
383
|
+
if container.space == self.default_space:
|
|
384
|
+
prefix = ""
|
|
385
|
+
return ParsedEntity(prefix=prefix, suffix=container.external_id, properties={})
|
|
386
|
+
|
|
387
|
+
def _create_node_entity(self, node: NodeReference) -> ParsedEntity:
|
|
388
|
+
prefix = node.space
|
|
389
|
+
if node.space == self.default_space:
|
|
390
|
+
prefix = ""
|
|
391
|
+
return ParsedEntity(prefix=prefix, suffix=node.external_id, properties={})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import ValidationError
|
|
4
|
+
|
|
5
|
+
from cognite.neat._client import NeatClient
|
|
6
|
+
from cognite.neat._data_model.importers._base import DMSImporter
|
|
7
|
+
from cognite.neat._data_model.models.dms import (
|
|
8
|
+
DataModelReference,
|
|
9
|
+
RequestSchema,
|
|
10
|
+
SpaceReference,
|
|
11
|
+
)
|
|
12
|
+
from cognite.neat._exceptions import CDFAPIException, DataModelImportException
|
|
13
|
+
from cognite.neat._issues import ModelSyntaxError
|
|
14
|
+
from cognite.neat._utils.http_client import FailedRequestMessage
|
|
15
|
+
from cognite.neat._utils.text import humanize_collection
|
|
16
|
+
from cognite.neat._utils.validation import humanize_validation_error
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DMSAPIImporter(DMSImporter):
|
|
20
|
+
"""Imports DMS in the API format."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, schema: RequestSchema | dict[str, Any]) -> None:
|
|
23
|
+
self._schema = schema
|
|
24
|
+
|
|
25
|
+
def to_data_model(self) -> RequestSchema:
|
|
26
|
+
if isinstance(self._schema, RequestSchema):
|
|
27
|
+
return self._schema
|
|
28
|
+
try:
|
|
29
|
+
return RequestSchema.model_validate(self._schema)
|
|
30
|
+
except ValidationError as e:
|
|
31
|
+
humanized_errors = humanize_validation_error(e)
|
|
32
|
+
errors = [ModelSyntaxError(message=error) for error in humanized_errors]
|
|
33
|
+
raise DataModelImportException(errors) from None
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_cdf(cls, data_model: DataModelReference, client: NeatClient) -> "DMSAPIImporter":
|
|
37
|
+
"""Create a DMSAPIImporter from a data model in CDF."""
|
|
38
|
+
data_models = client.data_models.retrieve([data_model])
|
|
39
|
+
if not data_models:
|
|
40
|
+
raise CDFAPIException(messages=[FailedRequestMessage(message=f"Data model {data_model} not found in CDF.")])
|
|
41
|
+
data_model = data_models[0]
|
|
42
|
+
views = client.views.retrieve(data_model.views or [])
|
|
43
|
+
if missing_views := set(data_model.views or []) - {view.as_reference() for view in views}:
|
|
44
|
+
raise CDFAPIException(
|
|
45
|
+
messages=[
|
|
46
|
+
FailedRequestMessage(
|
|
47
|
+
message=f"Views {humanize_collection(missing_views)} not found in CDF "
|
|
48
|
+
f"for data model {data_model}."
|
|
49
|
+
)
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
container_ids = list({container for view in views for container in view.mapped_containers})
|
|
53
|
+
containers = client.containers.retrieve(container_ids)
|
|
54
|
+
if missing_containers := set(container_ids) - {container.as_reference() for container in containers}:
|
|
55
|
+
raise CDFAPIException(
|
|
56
|
+
messages=[
|
|
57
|
+
FailedRequestMessage(
|
|
58
|
+
message=f"Containers {humanize_collection(missing_containers)} not found in CDF "
|
|
59
|
+
f"for data model {data_model}."
|
|
60
|
+
)
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
node_types = [nt for view in views for nt in view.node_types]
|
|
64
|
+
space_ids = list(
|
|
65
|
+
{data_model.space}
|
|
66
|
+
| {view.space for view in views}
|
|
67
|
+
| {container.space for container in containers}
|
|
68
|
+
| {nt.space for nt in node_types}
|
|
69
|
+
)
|
|
70
|
+
spaces = client.spaces.retrieve([SpaceReference(space=space_id) for space_id in space_ids])
|
|
71
|
+
if missing_spaces := set(space_ids) - {space.space for space in spaces}:
|
|
72
|
+
raise CDFAPIException(
|
|
73
|
+
messages=[
|
|
74
|
+
FailedRequestMessage(
|
|
75
|
+
message=f"Spaces {humanize_collection(missing_spaces)} not found in CDF "
|
|
76
|
+
f"for data model {data_model}."
|
|
77
|
+
)
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
return DMSAPIImporter(
|
|
81
|
+
RequestSchema(
|
|
82
|
+
dataModel=data_model.as_request(),
|
|
83
|
+
views=[view.as_request() for view in views],
|
|
84
|
+
containers=[container.as_request() for container in containers],
|
|
85
|
+
nodeTypes=node_types,
|
|
86
|
+
spaces=[space.as_request() for space in spaces],
|
|
87
|
+
)
|
|
88
|
+
)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from collections.abc import Mapping
|
|
2
|
-
from typing import Annotated, cast
|
|
2
|
+
from typing import Annotated, Literal, cast, get_args
|
|
3
3
|
|
|
4
|
-
from pydantic import AliasGenerator, BaseModel, BeforeValidator, Field, model_validator
|
|
4
|
+
from pydantic import AliasGenerator, BaseModel, BeforeValidator, Field, PlainSerializer, model_validator
|
|
5
5
|
from pydantic.alias_generators import to_camel
|
|
6
|
+
from pydantic.fields import FieldInfo
|
|
6
7
|
|
|
7
8
|
from cognite.neat._data_model.models.entities import ParsedEntity, parse_entities, parse_entity
|
|
8
9
|
from cognite.neat._utils.text import title_case
|
|
@@ -10,6 +11,8 @@ from cognite.neat._utils.useful_types import CellValueType
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def parse_entity_str(v: str) -> ParsedEntity:
|
|
14
|
+
if isinstance(v, ParsedEntity):
|
|
15
|
+
return v
|
|
13
16
|
try:
|
|
14
17
|
return parse_entity(v)
|
|
15
18
|
except ValueError as e:
|
|
@@ -17,23 +20,31 @@ def parse_entity_str(v: str) -> ParsedEntity:
|
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
def parse_entities_str(v: str) -> list[ParsedEntity] | None:
|
|
23
|
+
if isinstance(v, list) and all(isinstance(item, ParsedEntity) for item in v):
|
|
24
|
+
return v
|
|
20
25
|
try:
|
|
21
26
|
return parse_entities(v)
|
|
22
27
|
except ValueError as e:
|
|
23
28
|
raise ValueError(f"Invalid entity list syntax: {e}") from e
|
|
24
29
|
|
|
25
30
|
|
|
26
|
-
Entity = Annotated[ParsedEntity, BeforeValidator(parse_entity_str, str)]
|
|
27
|
-
EntityList = Annotated[
|
|
31
|
+
Entity = Annotated[ParsedEntity, BeforeValidator(parse_entity_str, str), PlainSerializer(func=str)]
|
|
32
|
+
EntityList = Annotated[
|
|
33
|
+
list[ParsedEntity],
|
|
34
|
+
BeforeValidator(parse_entities_str, str),
|
|
35
|
+
PlainSerializer(func=lambda v: ",".join([str(item) for item in v])),
|
|
36
|
+
]
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
class TableObj(
|
|
31
40
|
BaseModel,
|
|
32
41
|
extra="ignore",
|
|
33
42
|
alias_generator=AliasGenerator(
|
|
43
|
+
alias=to_camel,
|
|
34
44
|
validation_alias=title_case,
|
|
35
|
-
serialization_alias=
|
|
45
|
+
serialization_alias=title_case,
|
|
36
46
|
),
|
|
47
|
+
populate_by_name=True,
|
|
37
48
|
): ...
|
|
38
49
|
|
|
39
50
|
|
|
@@ -107,6 +118,35 @@ class TableDMS(TableObj):
|
|
|
107
118
|
return {title_case(k): v for k, v in data.items()}
|
|
108
119
|
return data
|
|
109
120
|
|
|
121
|
+
@classmethod
|
|
122
|
+
def get_sheet_columns(
|
|
123
|
+
cls, sheet_id: str, sheet: FieldInfo | None = None, *, column_type: Literal["all", "required"] = "required"
|
|
124
|
+
) -> list[str]:
|
|
125
|
+
if sheet_id not in cls.model_fields.keys():
|
|
126
|
+
raise KeyError(f"Invalid field id: {sheet_id}")
|
|
127
|
+
if sheet is None:
|
|
128
|
+
sheet = cls.model_fields[sheet_id]
|
|
129
|
+
return [
|
|
130
|
+
# We know all fields has validation_alias because of the alias_generator in TableDMS
|
|
131
|
+
cast(str, sheet_field.validation_alias)
|
|
132
|
+
# All the fields in the sheet's model are lists.
|
|
133
|
+
for sheet_field in get_args(sheet.annotation)[0].model_fields.values()
|
|
134
|
+
if sheet_field.is_required() or column_type == "all"
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def get_sheet_column_by_name(
|
|
139
|
+
cls, sheet_name: str, *, column_type: Literal["all", "required"] = "required"
|
|
140
|
+
) -> list[str]:
|
|
141
|
+
for field_id, field_ in cls.model_fields.items():
|
|
142
|
+
if cast(str, field_.validation_alias) == sheet_name:
|
|
143
|
+
return cls.get_sheet_columns(field_id, field_, column_type=column_type)
|
|
144
|
+
raise KeyError(f"Invalid field alias: {sheet_name}")
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def required_sheets(cls) -> set[str]:
|
|
148
|
+
return {cast(str, field_.validation_alias) for field_ in cls.model_fields.values() if field_.is_required()}
|
|
149
|
+
|
|
110
150
|
|
|
111
151
|
DMS_API_MAPPING: Mapping[str, Mapping[str, str]] = {
|
|
112
152
|
"Views": {
|
|
@@ -114,7 +154,7 @@ DMS_API_MAPPING: Mapping[str, Mapping[str, str]] = {
|
|
|
114
154
|
"externalId": "View",
|
|
115
155
|
"version": "View",
|
|
116
156
|
**{
|
|
117
|
-
cast(str, field_.
|
|
157
|
+
cast(str, field_.alias): cast(str, field_.validation_alias)
|
|
118
158
|
for field_id, field_ in DMSView.model_fields.items()
|
|
119
159
|
if field_id != "View"
|
|
120
160
|
},
|
|
@@ -123,7 +163,7 @@ DMS_API_MAPPING: Mapping[str, Mapping[str, str]] = {
|
|
|
123
163
|
"space": "Container",
|
|
124
164
|
"externalId": "Container",
|
|
125
165
|
**{
|
|
126
|
-
cast(str, field_.
|
|
166
|
+
cast(str, field_.alias): cast(str, field_.validation_alias)
|
|
127
167
|
for field_id, field_ in DMSContainer.model_fields.items()
|
|
128
168
|
if field_id != "Container"
|
|
129
169
|
},
|
|
@@ -133,7 +173,7 @@ DMS_API_MAPPING: Mapping[str, Mapping[str, str]] = {
|
|
|
133
173
|
"externalId": "View",
|
|
134
174
|
"property": "ViewProperty",
|
|
135
175
|
**{
|
|
136
|
-
cast(str, field_.
|
|
176
|
+
cast(str, field_.alias): cast(str, field_.validation_alias)
|
|
137
177
|
for field_id, field_ in DMSProperty.model_fields.items()
|
|
138
178
|
if field_id not in ("View", "ViewProperty")
|
|
139
179
|
},
|