cognite-neat 0.125.1__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.

Files changed (80) hide show
  1. cognite/neat/_client/__init__.py +4 -0
  2. cognite/neat/_client/api.py +8 -0
  3. cognite/neat/_client/client.py +19 -0
  4. cognite/neat/_client/config.py +40 -0
  5. cognite/neat/_client/containers_api.py +73 -0
  6. cognite/neat/_client/data_classes.py +10 -0
  7. cognite/neat/_client/data_model_api.py +63 -0
  8. cognite/neat/_client/spaces_api.py +67 -0
  9. cognite/neat/_client/views_api.py +82 -0
  10. cognite/neat/_data_model/_analysis.py +127 -0
  11. cognite/neat/_data_model/_constants.py +59 -0
  12. cognite/neat/_data_model/_shared.py +46 -0
  13. cognite/neat/_data_model/deployer/__init__.py +0 -0
  14. cognite/neat/_data_model/deployer/_differ.py +113 -0
  15. cognite/neat/_data_model/deployer/_differ_container.py +354 -0
  16. cognite/neat/_data_model/deployer/_differ_data_model.py +29 -0
  17. cognite/neat/_data_model/deployer/_differ_space.py +9 -0
  18. cognite/neat/_data_model/deployer/_differ_view.py +194 -0
  19. cognite/neat/_data_model/deployer/data_classes.py +176 -0
  20. cognite/neat/_data_model/exporters/__init__.py +4 -0
  21. cognite/neat/_data_model/exporters/_base.py +22 -0
  22. cognite/neat/_data_model/exporters/_table_exporter/__init__.py +0 -0
  23. cognite/neat/_data_model/exporters/_table_exporter/exporter.py +106 -0
  24. cognite/neat/_data_model/exporters/_table_exporter/workbook.py +414 -0
  25. cognite/neat/_data_model/exporters/_table_exporter/writer.py +391 -0
  26. cognite/neat/_data_model/importers/__init__.py +2 -1
  27. cognite/neat/_data_model/importers/_api_importer.py +88 -0
  28. cognite/neat/_data_model/importers/_table_importer/data_classes.py +48 -8
  29. cognite/neat/_data_model/importers/_table_importer/importer.py +102 -6
  30. cognite/neat/_data_model/importers/_table_importer/reader.py +860 -0
  31. cognite/neat/_data_model/models/dms/__init__.py +19 -1
  32. cognite/neat/_data_model/models/dms/_base.py +12 -8
  33. cognite/neat/_data_model/models/dms/_constants.py +1 -1
  34. cognite/neat/_data_model/models/dms/_constraints.py +2 -1
  35. cognite/neat/_data_model/models/dms/_container.py +5 -5
  36. cognite/neat/_data_model/models/dms/_data_model.py +3 -3
  37. cognite/neat/_data_model/models/dms/_data_types.py +8 -1
  38. cognite/neat/_data_model/models/dms/_http.py +18 -0
  39. cognite/neat/_data_model/models/dms/_indexes.py +2 -1
  40. cognite/neat/_data_model/models/dms/_references.py +17 -4
  41. cognite/neat/_data_model/models/dms/_space.py +11 -7
  42. cognite/neat/_data_model/models/dms/_view_property.py +7 -4
  43. cognite/neat/_data_model/models/dms/_views.py +16 -6
  44. cognite/neat/_data_model/validation/__init__.py +0 -0
  45. cognite/neat/_data_model/validation/_base.py +16 -0
  46. cognite/neat/_data_model/validation/dms/__init__.py +9 -0
  47. cognite/neat/_data_model/validation/dms/_orchestrator.py +68 -0
  48. cognite/neat/_data_model/validation/dms/_validators.py +139 -0
  49. cognite/neat/_exceptions.py +15 -3
  50. cognite/neat/_issues.py +39 -6
  51. cognite/neat/_session/__init__.py +3 -0
  52. cognite/neat/_session/_physical.py +88 -0
  53. cognite/neat/_session/_session.py +34 -25
  54. cognite/neat/_session/_wrappers.py +61 -0
  55. cognite/neat/_state_machine/__init__.py +10 -0
  56. cognite/neat/{_session/_state_machine → _state_machine}/_base.py +11 -1
  57. cognite/neat/_state_machine/_states.py +53 -0
  58. cognite/neat/_store/__init__.py +3 -0
  59. cognite/neat/_store/_provenance.py +55 -0
  60. cognite/neat/_store/_store.py +124 -0
  61. cognite/neat/_utils/_reader.py +194 -0
  62. cognite/neat/_utils/http_client/__init__.py +14 -20
  63. cognite/neat/_utils/http_client/_client.py +22 -61
  64. cognite/neat/_utils/http_client/_data_classes.py +167 -268
  65. cognite/neat/_utils/text.py +6 -0
  66. cognite/neat/_utils/useful_types.py +26 -2
  67. cognite/neat/_version.py +1 -1
  68. cognite/neat/v0/core/_data_model/importers/_rdf/_shared.py +2 -2
  69. cognite/neat/v0/core/_data_model/importers/_spreadsheet2data_model.py +2 -2
  70. cognite/neat/v0/core/_data_model/models/entities/_single_value.py +1 -1
  71. cognite/neat/v0/core/_data_model/models/physical/_unverified.py +1 -1
  72. cognite/neat/v0/core/_data_model/models/physical/_validation.py +2 -2
  73. cognite/neat/v0/core/_data_model/models/physical/_verified.py +3 -3
  74. cognite/neat/v0/core/_data_model/transformers/_converters.py +1 -1
  75. {cognite_neat-0.125.1.dist-info → cognite_neat-0.126.1.dist-info}/METADATA +1 -1
  76. {cognite_neat-0.125.1.dist-info → cognite_neat-0.126.1.dist-info}/RECORD +78 -40
  77. cognite/neat/_session/_state_machine/__init__.py +0 -23
  78. cognite/neat/_session/_state_machine/_states.py +0 -150
  79. {cognite_neat-0.125.1.dist-info → cognite_neat-0.126.1.dist-info}/WHEEL +0 -0
  80. {cognite_neat-0.125.1.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={})
@@ -1,4 +1,5 @@
1
+ from ._api_importer import DMSAPIImporter
1
2
  from ._base import DMSImporter
2
3
  from ._table_importer.importer import DMSTableImporter
3
4
 
4
- __all__ = ["DMSImporter", "DMSTableImporter"]
5
+ __all__ = ["DMSAPIImporter", "DMSImporter", "DMSTableImporter"]
@@ -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[list[ParsedEntity], BeforeValidator(parse_entities_str, str)]
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=to_camel,
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_.serialization_alias): cast(str, field_.validation_alias)
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_.serialization_alias): cast(str, field_.validation_alias)
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_.serialization_alias): cast(str, field_.validation_alias)
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
  },