cognite-neat 0.75.9__py3-none-any.whl → 0.76.0__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/_version.py +1 -1
- cognite/neat/rules/exporters/_models.py +3 -0
- cognite/neat/rules/exporters/_rules2dms.py +46 -4
- cognite/neat/rules/exporters/_rules2excel.py +0 -9
- cognite/neat/rules/importers/_base.py +6 -0
- cognite/neat/rules/importers/_dms2rules.py +319 -125
- cognite/neat/rules/importers/_spreadsheet2rules.py +14 -8
- cognite/neat/rules/issues/base.py +3 -0
- cognite/neat/rules/issues/dms.py +142 -54
- cognite/neat/rules/issues/fileread.py +41 -0
- cognite/neat/rules/issues/importing.py +155 -0
- cognite/neat/rules/issues/spreadsheet.py +12 -9
- cognite/neat/rules/models/entities.py +29 -6
- cognite/neat/rules/models/rules/_base.py +5 -6
- cognite/neat/rules/models/rules/_dms_architect_rules.py +492 -332
- cognite/neat/rules/models/rules/_dms_rules_write.py +32 -30
- cognite/neat/rules/models/rules/_dms_schema.py +112 -22
- cognite/neat/rules/models/rules/_domain_rules.py +5 -0
- cognite/neat/rules/models/rules/_information_rules.py +13 -6
- cognite/neat/rules/models/wrapped_entities.py +166 -0
- {cognite_neat-0.75.9.dist-info → cognite_neat-0.76.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.75.9.dist-info → cognite_neat-0.76.0.dist-info}/RECORD +25 -24
- {cognite_neat-0.75.9.dist-info → cognite_neat-0.76.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.75.9.dist-info → cognite_neat-0.76.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.75.9.dist-info → cognite_neat-0.76.0.dist-info}/entry_points.txt +0 -0
cognite/neat/rules/issues/dms.py
CHANGED
|
@@ -16,15 +16,16 @@ __all__ = [
|
|
|
16
16
|
"MissingParentViewError",
|
|
17
17
|
"MissingSourceViewError",
|
|
18
18
|
"MissingEdgeViewError",
|
|
19
|
-
"DuplicatedViewInDataModelError",
|
|
20
19
|
"DirectRelationMissingSourceWarning",
|
|
20
|
+
"ViewModelVersionNotMatchingWarning",
|
|
21
|
+
"ViewModelSpaceNotMatchingWarning",
|
|
22
|
+
"DuplicatedViewInDataModelError",
|
|
21
23
|
"ContainerPropertyUsedMultipleTimesError",
|
|
22
|
-
"DirectRelationListWarning",
|
|
23
|
-
"ReverseOfDirectRelationListWarning",
|
|
24
24
|
"EmptyContainerWarning",
|
|
25
|
-
"
|
|
25
|
+
"UnsupportedConnectionWarning",
|
|
26
26
|
"MultipleReferenceWarning",
|
|
27
27
|
"HasDataFilterOnNoPropertiesViewWarning",
|
|
28
|
+
"ReverseRelationMissingOtherSideWarning",
|
|
28
29
|
"NodeTypeFilterOnParentViewWarning",
|
|
29
30
|
"ChangingContainerError",
|
|
30
31
|
"ChangingViewError",
|
|
@@ -202,6 +203,53 @@ class DirectRelationMissingSourceWarning(DMSSchemaWarning):
|
|
|
202
203
|
return output
|
|
203
204
|
|
|
204
205
|
|
|
206
|
+
@dataclass(frozen=True)
|
|
207
|
+
class ViewModelVersionNotMatchingWarning(DMSSchemaWarning):
|
|
208
|
+
description = "The view model version does not match the data model version"
|
|
209
|
+
fix = "Update the view model version to match the data model version"
|
|
210
|
+
error_name: ClassVar[str] = "ViewModelVersionNotMatching"
|
|
211
|
+
view_ids: list[dm.ViewId]
|
|
212
|
+
data_model_version: str
|
|
213
|
+
|
|
214
|
+
def message(self) -> str:
|
|
215
|
+
return (
|
|
216
|
+
f"The version in the views {self.view_ids} does not match the version in the data model "
|
|
217
|
+
f"{self.data_model_version}. This is not recommended as it easily leads to confusion and errors. "
|
|
218
|
+
f"Views are very cheap and we recommend you update the version of the views to match the data"
|
|
219
|
+
f" model version, irrespective of whether the views have changed or not."
|
|
220
|
+
" The approach of having same version of model components as the model itself "
|
|
221
|
+
" is a globally recognized data modeling practice."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def dump(self) -> dict[str, Any]:
|
|
225
|
+
output = super().dump()
|
|
226
|
+
output["view_id"] = [view_id.dump() for view_id in self.view_ids]
|
|
227
|
+
output["data_model_version"] = self.data_model_version
|
|
228
|
+
return output
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass(frozen=True)
|
|
232
|
+
class ViewModelSpaceNotMatchingWarning(DMSSchemaWarning):
|
|
233
|
+
description = "The view model space does not match the data model space"
|
|
234
|
+
fix = "Update the view model space to match the data model space"
|
|
235
|
+
error_name: ClassVar[str] = "ViewModelSpaceNotMatching"
|
|
236
|
+
view_ids: list[dm.ViewId]
|
|
237
|
+
data_model_space: str
|
|
238
|
+
|
|
239
|
+
def message(self) -> str:
|
|
240
|
+
return (
|
|
241
|
+
f"The space in the views {self.view_ids} does not match the space in the data model "
|
|
242
|
+
f"{self.data_model_space}. This is not recommended as it easily leads to confusion and errors. "
|
|
243
|
+
f"Views are very cheap and we recommend you always have views in the same space as the data model."
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def dump(self) -> dict[str, Any]:
|
|
247
|
+
output = super().dump()
|
|
248
|
+
output["view_id"] = [view_id.dump() for view_id in self.view_ids]
|
|
249
|
+
output["data_model_space"] = self.data_model_space
|
|
250
|
+
return output
|
|
251
|
+
|
|
252
|
+
|
|
205
253
|
@dataclass(frozen=True)
|
|
206
254
|
class ContainerPropertyUsedMultipleTimesError(DMSSchemaError):
|
|
207
255
|
description = "The container property is used multiple times by the same view property"
|
|
@@ -298,92 +346,62 @@ class ChangingViewError(DMSSchemaError):
|
|
|
298
346
|
|
|
299
347
|
|
|
300
348
|
@dataclass(frozen=True)
|
|
301
|
-
class
|
|
302
|
-
description = "The container
|
|
303
|
-
fix = "
|
|
304
|
-
error_name: ClassVar[str] = "
|
|
305
|
-
view_id: dm.ViewId
|
|
349
|
+
class EmptyContainerWarning(DMSSchemaWarning):
|
|
350
|
+
description = "The container is empty"
|
|
351
|
+
fix = "Add data to the container"
|
|
352
|
+
error_name: ClassVar[str] = "EmptyContainerWarning"
|
|
306
353
|
container_id: dm.ContainerId
|
|
307
|
-
property: str
|
|
308
354
|
|
|
309
355
|
def message(self) -> str:
|
|
310
356
|
return (
|
|
311
|
-
f"The
|
|
312
|
-
|
|
313
|
-
f"the view {self.view_id}.{self.property} instead"
|
|
357
|
+
f"The container {self.container_id} is empty. Is this intended? Skipping this container "
|
|
358
|
+
"in the data model."
|
|
314
359
|
)
|
|
315
360
|
|
|
316
361
|
def dump(self) -> dict[str, Any]:
|
|
317
362
|
output = super().dump()
|
|
318
|
-
output["view_id"] = self.view_id.dump()
|
|
319
363
|
output["container_id"] = self.container_id.dump()
|
|
320
|
-
output["property"] = self.property
|
|
321
364
|
return output
|
|
322
365
|
|
|
323
366
|
|
|
324
367
|
@dataclass(frozen=True)
|
|
325
|
-
class
|
|
326
|
-
description =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
fix = "Make the property into a multiedge connection instead"
|
|
330
|
-
error_name: ClassVar[str] = "ReverseOfDirectRelationListWarning"
|
|
368
|
+
class UnsupportedConnectionWarning(DMSSchemaWarning):
|
|
369
|
+
description = "The connection type is not supported by neat"
|
|
370
|
+
fix = "Change the connection to a supported type"
|
|
371
|
+
error_name: ClassVar[str] = "UnsupportedConnectionWarning"
|
|
331
372
|
view_id: dm.ViewId
|
|
332
373
|
property: str
|
|
374
|
+
connection: str
|
|
333
375
|
|
|
334
376
|
def message(self) -> str:
|
|
335
377
|
return (
|
|
336
|
-
f"The
|
|
337
|
-
|
|
338
|
-
"will be converted from a reverse direct relation to an MultiEdgeConnection instead"
|
|
378
|
+
f"The connection {self.connection} in {self.view_id}.{self.property} is not supported."
|
|
379
|
+
"This property will be ignored."
|
|
339
380
|
)
|
|
340
381
|
|
|
341
382
|
def dump(self) -> dict[str, Any]:
|
|
342
383
|
output = super().dump()
|
|
343
384
|
output["view_id"] = self.view_id.dump()
|
|
344
385
|
output["property"] = self.property
|
|
386
|
+
output["connection"] = self.connection
|
|
345
387
|
return output
|
|
346
388
|
|
|
347
389
|
|
|
348
390
|
@dataclass(frozen=True)
|
|
349
|
-
class
|
|
350
|
-
description = "The
|
|
351
|
-
fix = "Add
|
|
352
|
-
error_name: ClassVar[str] = "
|
|
353
|
-
container_id: dm.ContainerId
|
|
354
|
-
|
|
355
|
-
def message(self) -> str:
|
|
356
|
-
return (
|
|
357
|
-
f"The container {self.container_id} is empty. Is this intended? Skipping this container "
|
|
358
|
-
"in the data model."
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
def dump(self) -> dict[str, Any]:
|
|
362
|
-
output = super().dump()
|
|
363
|
-
output["container_id"] = self.container_id.dump()
|
|
364
|
-
return output
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
@dataclass(frozen=True)
|
|
368
|
-
class UnsupportedRelationWarning(DMSSchemaWarning):
|
|
369
|
-
description = "The relatio type is not supported by neat"
|
|
370
|
-
fix = "Change the relation to a supported type"
|
|
371
|
-
error_name: ClassVar[str] = "UnsupportedRelationWarning"
|
|
391
|
+
class ReverseRelationMissingOtherSideWarning(DMSSchemaWarning):
|
|
392
|
+
description = "The relation is missing the other side"
|
|
393
|
+
fix = "Add the other side of the relation"
|
|
394
|
+
error_name: ClassVar[str] = "ReverseRelationMissingOtherSideWarning"
|
|
372
395
|
view_id: dm.ViewId
|
|
373
396
|
property: str
|
|
374
|
-
relation: str
|
|
375
397
|
|
|
376
398
|
def message(self) -> str:
|
|
377
|
-
return
|
|
378
|
-
f"The relation {self.relation} in {self.view_id}.{self.property} is not supported."
|
|
379
|
-
"This property will be ignored."
|
|
380
|
-
)
|
|
399
|
+
return f"The reverse relation specified in {self.view_id}.{self.property} is missing the other side."
|
|
381
400
|
|
|
382
401
|
def dump(self) -> dict[str, Any]:
|
|
383
402
|
output = super().dump()
|
|
384
403
|
output["view_id"] = self.view_id.dump()
|
|
385
404
|
output["property"] = self.property
|
|
386
|
-
output["relation"] = self.relation
|
|
387
405
|
return output
|
|
388
406
|
|
|
389
407
|
|
|
@@ -427,7 +445,7 @@ class HasDataFilterOnNoPropertiesViewWarning(DMSSchemaWarning):
|
|
|
427
445
|
@dataclass(frozen=True)
|
|
428
446
|
class NodeTypeFilterOnParentViewWarning(DMSSchemaWarning):
|
|
429
447
|
description = (
|
|
430
|
-
"Setting a node type filter on a parent view. This is
|
|
448
|
+
"Setting a node type filter on a parent view. This is not "
|
|
431
449
|
"recommended as parent views are typically used for multiple type of nodes."
|
|
432
450
|
)
|
|
433
451
|
fix = "Use a HasData filter instead"
|
|
@@ -444,3 +462,73 @@ class NodeTypeFilterOnParentViewWarning(DMSSchemaWarning):
|
|
|
444
462
|
output = super().dump()
|
|
445
463
|
output["view_id"] = self.view_id.dump()
|
|
446
464
|
return output
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@dataclass(frozen=True)
|
|
468
|
+
class HasDataFilterOnViewWithReferencesWarning(DMSSchemaWarning):
|
|
469
|
+
description = (
|
|
470
|
+
"Setting a hasData filter on a solution view which reference other containers is not recommended."
|
|
471
|
+
"This will lead to no nodes being returned when querying the solution view."
|
|
472
|
+
)
|
|
473
|
+
fix = "Use a node type filter instead"
|
|
474
|
+
error_name: ClassVar[str] = "HasDataFilterOnReferencedViewWarning"
|
|
475
|
+
|
|
476
|
+
view_id: dm.ViewId
|
|
477
|
+
references: list[dm.ViewId]
|
|
478
|
+
|
|
479
|
+
def message(self) -> str:
|
|
480
|
+
return (
|
|
481
|
+
f"Setting a hasData filter on view {self.view_id} which references other views {self.references}. "
|
|
482
|
+
"This is not recommended as it will lead to no nodes being returned when querying the solution view."
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def dump(self) -> dict[str, Any]:
|
|
486
|
+
output = super().dump()
|
|
487
|
+
output["view_id"] = self.view_id.dump()
|
|
488
|
+
output["references"] = [view.dump() for view in sorted(self.references, key=lambda x: x.as_tuple())]
|
|
489
|
+
return output
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@dataclass(frozen=True)
|
|
493
|
+
class OtherDataModelsInSpaceWarning(DMSSchemaWarning):
|
|
494
|
+
description = "The space contains other data models"
|
|
495
|
+
fix = "Move the data models to their respective spaces."
|
|
496
|
+
error_name: ClassVar[str] = "OtherDataModelsInSpaceWarning"
|
|
497
|
+
space: str
|
|
498
|
+
data_models: list[dm.DataModelId]
|
|
499
|
+
|
|
500
|
+
def message(self) -> str:
|
|
501
|
+
return (
|
|
502
|
+
f"The space {self.space} contains data models from other spaces: {self.data_models}. {self.fix}"
|
|
503
|
+
" It is recommended to only have one data model per space. This avoid potential conflicts and "
|
|
504
|
+
"makes it easier to manage the data models."
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def dump(self) -> dict[str, Any]:
|
|
508
|
+
output = super().dump()
|
|
509
|
+
output["space"] = self.space
|
|
510
|
+
output["data_models"] = [data_model.dump() for data_model in self.data_models]
|
|
511
|
+
return output
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@dataclass(frozen=True)
|
|
515
|
+
class SolutionOnTopOfSolutionModelWarning(DMSSchemaWarning):
|
|
516
|
+
description = "The data model is a solution on top of another solution"
|
|
517
|
+
fix = "Use the base solution as the data model"
|
|
518
|
+
error_name: ClassVar[str] = "SolutionOnTopOfSolutionModelWarning"
|
|
519
|
+
data_model: dm.DataModelId
|
|
520
|
+
base_data_model: dm.DataModelId
|
|
521
|
+
|
|
522
|
+
def message(self) -> str:
|
|
523
|
+
return (
|
|
524
|
+
f"The data model {self.data_model} is a solution model on top of another solution {self.base_data_model} "
|
|
525
|
+
"model. This is not recommended as it can lead to confusion and errors. It is very hard to "
|
|
526
|
+
"maintain a nested structure of solution models. Instead, only build solution models on "
|
|
527
|
+
"top of enterprise models."
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
def dump(self) -> dict[str, Any]:
|
|
531
|
+
output = super().dump()
|
|
532
|
+
output["data_model"] = self.data_model.dump()
|
|
533
|
+
output["base_data_model"] = self.base_data_model.dump()
|
|
534
|
+
return output
|
|
@@ -9,10 +9,13 @@ __all__ = [
|
|
|
9
9
|
"InvalidFileFormatWarning",
|
|
10
10
|
"UnsupportedSpecWarning",
|
|
11
11
|
"UnknownItemWarning",
|
|
12
|
+
"FailedLoadWarning",
|
|
12
13
|
"BugInImporterWarning",
|
|
13
14
|
"FileReadError",
|
|
14
15
|
"FileNotFoundError",
|
|
15
16
|
"FileNotAFileError",
|
|
17
|
+
"InvalidFileFormatError",
|
|
18
|
+
"FailedStringLoadError",
|
|
16
19
|
]
|
|
17
20
|
|
|
18
21
|
|
|
@@ -96,6 +99,26 @@ class UnknownItemWarning(FileReadWarning):
|
|
|
96
99
|
return output
|
|
97
100
|
|
|
98
101
|
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class FailedLoadWarning(FileReadWarning):
|
|
104
|
+
description = "The file content is invalid"
|
|
105
|
+
fix = "Check if the file content is valid"
|
|
106
|
+
|
|
107
|
+
expected_format: str
|
|
108
|
+
error_message: str
|
|
109
|
+
|
|
110
|
+
def message(self) -> str:
|
|
111
|
+
return (
|
|
112
|
+
f"Failed to load {self.filepath.name}. Expected format: {self.expected_format}. Error: {self.error_message}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def dump(self) -> dict[str, str | None]:
|
|
116
|
+
output = super().dump()
|
|
117
|
+
output["expected_format"] = self.expected_format
|
|
118
|
+
output["error_message"] = self.error_message
|
|
119
|
+
return output
|
|
120
|
+
|
|
121
|
+
|
|
99
122
|
@dataclass(frozen=True)
|
|
100
123
|
class BugInImporterWarning(FileReadWarning):
|
|
101
124
|
description = "A bug was raised during reading."
|
|
@@ -143,6 +166,24 @@ class InvalidFileFormatError(FileReadError):
|
|
|
143
166
|
return f"{self.filepath} is not in the expected format. Expected format: {self.expected_format}."
|
|
144
167
|
|
|
145
168
|
|
|
169
|
+
@dataclass(frozen=True)
|
|
170
|
+
class FailedStringLoadError(NeatValidationError):
|
|
171
|
+
description = "The file content is invalid"
|
|
172
|
+
fix = "Check if the file content is valid"
|
|
173
|
+
|
|
174
|
+
expected_format: str
|
|
175
|
+
error_message: str
|
|
176
|
+
|
|
177
|
+
def message(self) -> str:
|
|
178
|
+
return f"Failed to load string. Expected format: {self.expected_format}. Error: {self.error_message}"
|
|
179
|
+
|
|
180
|
+
def dump(self) -> dict[str, str | None]:
|
|
181
|
+
output = super().dump()
|
|
182
|
+
output["expected_format"] = self.expected_format
|
|
183
|
+
output["error_message"] = self.error_message
|
|
184
|
+
return output
|
|
185
|
+
|
|
186
|
+
|
|
146
187
|
@dataclass(frozen=True)
|
|
147
188
|
class NoFilesFoundError(FileReadError):
|
|
148
189
|
description = "No files were found in the directory"
|
|
@@ -10,11 +10,19 @@ __all__ = [
|
|
|
10
10
|
"IgnoredComponentWarning",
|
|
11
11
|
"UnknownPropertyWarning",
|
|
12
12
|
"UnknownValueTypeWarning",
|
|
13
|
+
"MissingContainerWarning",
|
|
14
|
+
"MissingContainerPropertyWarning",
|
|
15
|
+
"MultipleDataModelsWarning",
|
|
16
|
+
"UnknownPropertyTypeWarning",
|
|
17
|
+
"FailedToInferValueTypeWarning",
|
|
18
|
+
"UnknownContainerConstraintWarning",
|
|
19
|
+
"NoDataModelError",
|
|
13
20
|
"ModelImportError",
|
|
14
21
|
"InvalidComponentError",
|
|
15
22
|
"MissingParentDefinitionError",
|
|
16
23
|
"MissingIdentifierError",
|
|
17
24
|
"UnsupportedPropertyTypeError",
|
|
25
|
+
"APIError",
|
|
18
26
|
]
|
|
19
27
|
|
|
20
28
|
|
|
@@ -132,12 +140,159 @@ class UnknownValueTypeWarning(ModelImportWarning):
|
|
|
132
140
|
)
|
|
133
141
|
|
|
134
142
|
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class MultipleDataModelsWarning(ModelImportWarning):
|
|
145
|
+
description = "Multiple data models detected. This is not supported."
|
|
146
|
+
fix = "Remove the extra data models."
|
|
147
|
+
|
|
148
|
+
data_models: list[str]
|
|
149
|
+
|
|
150
|
+
def message(self) -> str:
|
|
151
|
+
return f"Multiple data models detected: {self.data_models}. Will only import the first one."
|
|
152
|
+
|
|
153
|
+
def dump(self) -> dict[str, list[str]]:
|
|
154
|
+
return {"data_models": self.data_models}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class MissingContainerWarning(ModelImportWarning):
|
|
159
|
+
description = "Missing container definition."
|
|
160
|
+
fix = "Add a container definition."
|
|
161
|
+
|
|
162
|
+
view_id: str
|
|
163
|
+
property_: str
|
|
164
|
+
container_id: str
|
|
165
|
+
|
|
166
|
+
def message(self) -> str:
|
|
167
|
+
return (
|
|
168
|
+
f"Container '{self.container_id}' is missing. "
|
|
169
|
+
f"Will skip property '{self.property_}' of view '{self.view_id}'."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def dump(self) -> dict[str, str]:
|
|
173
|
+
return {"view_id": self.view_id, "property": self.property_, "container_id": self.container_id}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True)
|
|
177
|
+
class MissingContainerPropertyWarning(ModelImportWarning):
|
|
178
|
+
description = "Missing container property definition."
|
|
179
|
+
fix = "Add a container property definition."
|
|
180
|
+
|
|
181
|
+
view_id: str
|
|
182
|
+
property_: str
|
|
183
|
+
container_id: str
|
|
184
|
+
|
|
185
|
+
def message(self) -> str:
|
|
186
|
+
return (
|
|
187
|
+
f"Container '{self.container_id}' is missing property '{self.property_}'. "
|
|
188
|
+
f"This property will be skipped for view '{self.view_id}'."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def dump(self) -> dict[str, str]:
|
|
192
|
+
return {"view_id": self.view_id, "property": self.property_, "container_id": self.container_id}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class UnknownPropertyTypeWarning(ModelImportWarning):
|
|
197
|
+
description = "Unknown property type. This will be ignored in the imports."
|
|
198
|
+
fix = "Set to a supported property type."
|
|
199
|
+
view_id: str
|
|
200
|
+
property_id: str
|
|
201
|
+
property_type: str
|
|
202
|
+
|
|
203
|
+
def message(self) -> str:
|
|
204
|
+
return (
|
|
205
|
+
f"Unknown property type '{self.property_type}' for property '{self.property_id}' "
|
|
206
|
+
f"of view '{self.view_id}'. This will be ignored in the imports."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def dump(self) -> dict[str, str]:
|
|
210
|
+
return {"view_id": self.view_id, "property_id": self.property_id, "property_type": self.property_type}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True)
|
|
214
|
+
class UnknownContainerConstraintWarning(ModelImportWarning):
|
|
215
|
+
description = "Unknown container constraint. This will be ignored in the imports."
|
|
216
|
+
fix = "Set to a supported container constraint."
|
|
217
|
+
container_id: str
|
|
218
|
+
property_id: str
|
|
219
|
+
constraint: str
|
|
220
|
+
|
|
221
|
+
def message(self) -> str:
|
|
222
|
+
return (
|
|
223
|
+
f"Unknown container constraint '{self.constraint}' for property '{self.property_id}' of container "
|
|
224
|
+
f"'{self.container_id}'. This will be ignored in the imports."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def dump(self) -> dict[str, str]:
|
|
228
|
+
return {"container_id": self.container_id, "property_id": self.property_id, "constraint": self.constraint}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass(frozen=True)
|
|
232
|
+
class FailedToInferValueTypeWarning(ModelImportWarning):
|
|
233
|
+
description = "Failed to infer value type. This will be ignored in the imports."
|
|
234
|
+
fix = "Set to a supported value type."
|
|
235
|
+
view_id: str
|
|
236
|
+
property_id: str
|
|
237
|
+
|
|
238
|
+
def message(self) -> str:
|
|
239
|
+
return (
|
|
240
|
+
f"Failed to infer value type for property '{self.property_id}' of view '{self.view_id}'. "
|
|
241
|
+
f"This will be ignored in the imports."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def dump(self) -> dict[str, str]:
|
|
245
|
+
return {"view_id": self.view_id, "property_id": self.property_id}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass(frozen=True)
|
|
249
|
+
class APIWarning(ModelImportWarning):
|
|
250
|
+
description = "An error was raised."
|
|
251
|
+
fix = "No fix is available."
|
|
252
|
+
|
|
253
|
+
error_message: str
|
|
254
|
+
|
|
255
|
+
def message(self) -> str:
|
|
256
|
+
return self.error_message
|
|
257
|
+
|
|
258
|
+
def dump(self) -> dict[str, str]:
|
|
259
|
+
return {"error_message": self.error_message}
|
|
260
|
+
|
|
261
|
+
|
|
135
262
|
@dataclass(frozen=True)
|
|
136
263
|
class ModelImportError(NeatValidationError, ABC):
|
|
137
264
|
description = "An error was raised during importing."
|
|
138
265
|
fix = "No fix is available."
|
|
139
266
|
|
|
140
267
|
|
|
268
|
+
@dataclass(frozen=True)
|
|
269
|
+
class NoDataModelError(ModelImportError):
|
|
270
|
+
description = "No data model found.."
|
|
271
|
+
fix = "Check if the data model exists in the source."
|
|
272
|
+
|
|
273
|
+
error_message: str
|
|
274
|
+
|
|
275
|
+
def message(self) -> str:
|
|
276
|
+
return self.error_message
|
|
277
|
+
|
|
278
|
+
def dump(self) -> dict[str, str]:
|
|
279
|
+
return {"error_message": self.error_message}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass(frozen=True)
|
|
283
|
+
class APIError(ModelImportError):
|
|
284
|
+
description = "An error was raised during importing."
|
|
285
|
+
fix = "No fix is available."
|
|
286
|
+
|
|
287
|
+
error_message: str
|
|
288
|
+
|
|
289
|
+
def message(self) -> str:
|
|
290
|
+
return self.error_message
|
|
291
|
+
|
|
292
|
+
def dump(self) -> dict[str, str]:
|
|
293
|
+
return {"error_message": self.error_message}
|
|
294
|
+
|
|
295
|
+
|
|
141
296
|
@dataclass(frozen=True)
|
|
142
297
|
class InvalidComponentError(ModelImportError, ABC):
|
|
143
298
|
description = "This is a base class for all errors related invalid component definitions"
|
|
@@ -176,15 +176,18 @@ class InvalidRowUnknownSheetError(InvalidRowError):
|
|
|
176
176
|
) -> Self:
|
|
177
177
|
sheet_name, _, row, column, *__ = error["loc"]
|
|
178
178
|
reader = (read_info_by_sheet or {}).get(str(sheet_name), SpreadsheetRead())
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
try:
|
|
180
|
+
return cls(
|
|
181
|
+
column=str(column),
|
|
182
|
+
row=reader.adjusted_row_number(int(row)),
|
|
183
|
+
actual_sheet_name=str(sheet_name),
|
|
184
|
+
type=error["type"],
|
|
185
|
+
msg=error["msg"],
|
|
186
|
+
input=error.get("input"),
|
|
187
|
+
url=str(url) if (url := error.get("url")) else None,
|
|
188
|
+
)
|
|
189
|
+
except ValueError:
|
|
190
|
+
return DefaultPydanticError.from_pydantic_error(error) # type: ignore[return-value]
|
|
188
191
|
|
|
189
192
|
def dump(self) -> dict[str, Any]:
|
|
190
193
|
output = super().dump()
|
|
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
from functools import total_ordering
|
|
5
5
|
from typing import Annotated, Any, ClassVar, Generic, TypeVar, cast
|
|
6
6
|
|
|
7
|
-
from cognite.client.data_classes.data_modeling.ids import ContainerId, DataModelId, PropertyId, ViewId
|
|
7
|
+
from cognite.client.data_classes.data_modeling.ids import ContainerId, DataModelId, NodeId, PropertyId, ViewId
|
|
8
8
|
from pydantic import AnyHttpUrl, BaseModel, BeforeValidator, Field, PlainSerializer, model_serializer, model_validator
|
|
9
9
|
|
|
10
10
|
if sys.version_info >= (3, 11):
|
|
@@ -30,6 +30,7 @@ class EntityTypes(StrEnum):
|
|
|
30
30
|
data_value_type = "data_value_type" # these are strings, floats, ...
|
|
31
31
|
xsd_value_type = "xsd_value_type"
|
|
32
32
|
dms_value_type = "dms_value_type"
|
|
33
|
+
dms_node = "dms_node"
|
|
33
34
|
view = "view"
|
|
34
35
|
reference_entity = "reference_entity"
|
|
35
36
|
container = "container"
|
|
@@ -248,7 +249,7 @@ class UnknownEntity(ClassEntity):
|
|
|
248
249
|
return str(Unknown)
|
|
249
250
|
|
|
250
251
|
|
|
251
|
-
T_ID = TypeVar("T_ID", bound=ContainerId | ViewId | DataModelId | PropertyId | None)
|
|
252
|
+
T_ID = TypeVar("T_ID", bound=ContainerId | ViewId | DataModelId | PropertyId | NodeId | None)
|
|
252
253
|
|
|
253
254
|
|
|
254
255
|
class DMSEntity(Entity, Generic[T_ID], ABC):
|
|
@@ -302,7 +303,9 @@ class ContainerEntity(DMSEntity[ContainerId]):
|
|
|
302
303
|
class DMSVersionedEntity(DMSEntity[T_ID], ABC):
|
|
303
304
|
version: str
|
|
304
305
|
|
|
305
|
-
def as_class(self) -> ClassEntity:
|
|
306
|
+
def as_class(self, skip_version: bool = False) -> ClassEntity:
|
|
307
|
+
if skip_version:
|
|
308
|
+
return ClassEntity(prefix=self.space, suffix=self.external_id)
|
|
306
309
|
return ClassEntity(prefix=self.space, suffix=self.external_id, version=self.version)
|
|
307
310
|
|
|
308
311
|
|
|
@@ -315,10 +318,13 @@ class ViewEntity(DMSVersionedEntity[ViewId]):
|
|
|
315
318
|
return ViewId(space=self.space, external_id=self.external_id, version=self.version)
|
|
316
319
|
|
|
317
320
|
@classmethod
|
|
318
|
-
def from_id(cls, id: ViewId) -> "ViewEntity":
|
|
319
|
-
if id.version is None:
|
|
321
|
+
def from_id(cls, id: ViewId, default_version: str | None = None) -> "ViewEntity":
|
|
322
|
+
if id.version is not None:
|
|
323
|
+
return cls(space=id.space, externalId=id.external_id, version=id.version)
|
|
324
|
+
elif default_version is not None:
|
|
325
|
+
return cls(space=id.space, externalId=id.external_id, version=default_version)
|
|
326
|
+
else:
|
|
320
327
|
raise ValueError("Version must be specified")
|
|
321
|
-
return cls(space=id.space, externalId=id.external_id, version=id.version)
|
|
322
328
|
|
|
323
329
|
|
|
324
330
|
class DMSUnknownEntity(DMSEntity[None]):
|
|
@@ -376,6 +382,17 @@ class DataModelEntity(DMSVersionedEntity[DataModelId]):
|
|
|
376
382
|
return cls(space=id.space, externalId=id.external_id, version=id.version)
|
|
377
383
|
|
|
378
384
|
|
|
385
|
+
class DMSNodeEntity(DMSEntity[NodeId]):
|
|
386
|
+
type_: ClassVar[EntityTypes] = EntityTypes.dms_node
|
|
387
|
+
|
|
388
|
+
def as_id(self) -> NodeId:
|
|
389
|
+
return NodeId(space=self.space, external_id=self.external_id)
|
|
390
|
+
|
|
391
|
+
@classmethod
|
|
392
|
+
def from_id(cls, id: NodeId) -> "DMSNodeEntity":
|
|
393
|
+
return cls(space=id.space, externalId=id.external_id)
|
|
394
|
+
|
|
395
|
+
|
|
379
396
|
class ReferenceEntity(ClassEntity):
|
|
380
397
|
type_: ClassVar[EntityTypes] = EntityTypes.reference_entity
|
|
381
398
|
prefix: str
|
|
@@ -391,6 +408,12 @@ class ReferenceEntity(ClassEntity):
|
|
|
391
408
|
raise ValueError("Property is not defined or prefix is not defined or suffix is unknown")
|
|
392
409
|
return PropertyId(source=self.as_view_id(), property=self.property_)
|
|
393
410
|
|
|
411
|
+
def as_node_id(self) -> NodeId:
|
|
412
|
+
return NodeId(space=self.prefix, external_id=self.suffix)
|
|
413
|
+
|
|
414
|
+
def as_node_entity(self) -> DMSNodeEntity:
|
|
415
|
+
return DMSNodeEntity(space=self.prefix, externalId=self.suffix)
|
|
416
|
+
|
|
394
417
|
def as_class_entity(self) -> ClassEntity:
|
|
395
418
|
return ClassEntity(prefix=self.prefix, suffix=self.suffix, version=self.version)
|
|
396
419
|
|
|
@@ -27,8 +27,6 @@ from pydantic import (
|
|
|
27
27
|
)
|
|
28
28
|
from pydantic.fields import FieldInfo
|
|
29
29
|
|
|
30
|
-
from cognite.neat.rules.models.entities import ClassEntity
|
|
31
|
-
|
|
32
30
|
if sys.version_info >= (3, 11):
|
|
33
31
|
from enum import StrEnum
|
|
34
32
|
from typing import Self
|
|
@@ -140,6 +138,11 @@ class ExtensionCategory(StrEnum):
|
|
|
140
138
|
rebuild = "rebuild"
|
|
141
139
|
|
|
142
140
|
|
|
141
|
+
class DataModelType(StrEnum):
|
|
142
|
+
solution = "solution"
|
|
143
|
+
enterprise = "enterprise"
|
|
144
|
+
|
|
145
|
+
|
|
143
146
|
class RoleTypes(StrEnum):
|
|
144
147
|
domain_expert = "domain expert"
|
|
145
148
|
information_architect = "information architect"
|
|
@@ -274,10 +277,6 @@ class BaseRules(RuleModel):
|
|
|
274
277
|
|
|
275
278
|
# An sheet entity is either a class or a property.
|
|
276
279
|
class SheetEntity(RuleModel):
|
|
277
|
-
class_: ClassEntity = Field(alias="Class")
|
|
278
|
-
name: str | None = Field(alias="Name", default=None)
|
|
279
|
-
description: str | None = Field(alias="Description", default=None)
|
|
280
|
-
|
|
281
280
|
@field_validator("*", mode="before")
|
|
282
281
|
def strip_string(cls, value: Any) -> Any:
|
|
283
282
|
if isinstance(value, str):
|