cognite-neat 0.77.2__py3-none-any.whl → 0.77.4__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/analysis/_information_rules.py +2 -12
- cognite/neat/rules/exporters/_rules2excel.py +78 -89
- cognite/neat/rules/importers/_dms2rules.py +24 -11
- cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +1 -0
- cognite/neat/rules/importers/_spreadsheet2rules.py +21 -8
- cognite/neat/rules/issues/dms.py +19 -0
- cognite/neat/rules/issues/importing.py +16 -0
- cognite/neat/rules/issues/spreadsheet.py +60 -5
- cognite/neat/rules/models/_base.py +6 -0
- cognite/neat/rules/models/dms/_converter.py +25 -19
- cognite/neat/rules/models/dms/_exporter.py +2 -1
- cognite/neat/rules/models/dms/_rules.py +3 -2
- cognite/neat/rules/models/dms/_rules_input.py +4 -11
- cognite/neat/rules/models/dms/_schema.py +41 -5
- cognite/neat/rules/models/dms/_serializer.py +1 -1
- cognite/neat/rules/models/dms/_validation.py +27 -60
- cognite/neat/rules/models/information/__init__.py +8 -1
- cognite/neat/rules/models/information/_converter.py +27 -19
- cognite/neat/rules/models/information/_rules.py +41 -82
- cognite/neat/rules/models/information/_rules_input.py +266 -0
- cognite/neat/rules/models/information/_serializer.py +85 -0
- cognite/neat/rules/models/information/_validation.py +164 -0
- cognite/neat/utils/cdf.py +35 -0
- cognite/neat/workflows/steps/lib/current/rules_exporter.py +30 -7
- cognite/neat/workflows/steps/lib/current/rules_importer.py +21 -2
- {cognite_neat-0.77.2.dist-info → cognite_neat-0.77.4.dist-info}/METADATA +1 -1
- {cognite_neat-0.77.2.dist-info → cognite_neat-0.77.4.dist-info}/RECORD +31 -28
- {cognite_neat-0.77.2.dist-info → cognite_neat-0.77.4.dist-info}/LICENSE +0 -0
- {cognite_neat-0.77.2.dist-info → cognite_neat-0.77.4.dist-info}/WHEEL +0 -0
- {cognite_neat-0.77.2.dist-info → cognite_neat-0.77.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import math
|
|
2
2
|
import sys
|
|
3
|
-
import
|
|
3
|
+
from collections.abc import Callable
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from typing import TYPE_CHECKING, Any, ClassVar, cast
|
|
6
6
|
|
|
@@ -8,9 +8,9 @@ from pydantic import Field, field_serializer, field_validator, model_serializer,
|
|
|
8
8
|
from pydantic_core.core_schema import SerializationInfo
|
|
9
9
|
from rdflib import Namespace
|
|
10
10
|
|
|
11
|
-
import cognite.neat.rules.issues.spreadsheet
|
|
12
11
|
from cognite.neat.constants import PREFIXES
|
|
13
|
-
from cognite.neat.rules import exceptions
|
|
12
|
+
from cognite.neat.rules import exceptions, issues
|
|
13
|
+
from cognite.neat.rules.issues.base import MultiValueError
|
|
14
14
|
from cognite.neat.rules.models._base import (
|
|
15
15
|
BaseMetadata,
|
|
16
16
|
DataModelType,
|
|
@@ -44,17 +44,14 @@ from cognite.neat.rules.models.data_types import DataType
|
|
|
44
44
|
from cognite.neat.rules.models.domain import DomainRules
|
|
45
45
|
from cognite.neat.rules.models.entities import (
|
|
46
46
|
ClassEntity,
|
|
47
|
-
Entity,
|
|
48
47
|
EntityTypes,
|
|
49
48
|
ParentClassEntity,
|
|
50
49
|
ParentEntityList,
|
|
51
50
|
ReferenceEntity,
|
|
52
51
|
Undefined,
|
|
53
|
-
Unknown,
|
|
54
52
|
UnknownEntity,
|
|
55
53
|
URLEntity,
|
|
56
54
|
_UndefinedType,
|
|
57
|
-
_UnknownType,
|
|
58
55
|
)
|
|
59
56
|
|
|
60
57
|
if TYPE_CHECKING:
|
|
@@ -69,9 +66,10 @@ else:
|
|
|
69
66
|
|
|
70
67
|
class InformationMetadata(BaseMetadata):
|
|
71
68
|
role: ClassVar[RoleTypes] = RoleTypes.information_architect
|
|
72
|
-
data_model_type: DataModelType = Field(DataModelType.
|
|
73
|
-
schema_: SchemaCompleteness = Field(alias="schema")
|
|
69
|
+
data_model_type: DataModelType = Field(DataModelType.enterprise, alias="dataModelType")
|
|
70
|
+
schema_: SchemaCompleteness = Field(SchemaCompleteness.partial, alias="schema")
|
|
74
71
|
extension: ExtensionCategoryType | None = ExtensionCategory.addition
|
|
72
|
+
|
|
75
73
|
prefix: PrefixType
|
|
76
74
|
namespace: NamespaceType
|
|
77
75
|
|
|
@@ -97,6 +95,8 @@ class InformationMetadata(BaseMetadata):
|
|
|
97
95
|
"typically information architects are considered as contributors."
|
|
98
96
|
),
|
|
99
97
|
)
|
|
98
|
+
license: str | None = None
|
|
99
|
+
rights: str | None = None
|
|
100
100
|
|
|
101
101
|
@model_validator(mode="after")
|
|
102
102
|
def extension_none_but_schema_extend(self) -> Self:
|
|
@@ -105,6 +105,18 @@ class InformationMetadata(BaseMetadata):
|
|
|
105
105
|
return self
|
|
106
106
|
return self
|
|
107
107
|
|
|
108
|
+
@field_validator("schema_", mode="plain")
|
|
109
|
+
def as_enum_schema(cls, value: str) -> SchemaCompleteness:
|
|
110
|
+
return SchemaCompleteness(value)
|
|
111
|
+
|
|
112
|
+
@field_validator("extension", mode="plain")
|
|
113
|
+
def as_enum_extension(cls, value: str) -> ExtensionCategory:
|
|
114
|
+
return ExtensionCategory(value)
|
|
115
|
+
|
|
116
|
+
@field_validator("data_model_type", mode="plain")
|
|
117
|
+
def as_enum_model_type(cls, value: str) -> DataModelType:
|
|
118
|
+
return DataModelType(value)
|
|
119
|
+
|
|
108
120
|
|
|
109
121
|
class InformationClass(SheetEntity):
|
|
110
122
|
"""
|
|
@@ -282,77 +294,24 @@ class InformationRules(RuleModel):
|
|
|
282
294
|
return self
|
|
283
295
|
|
|
284
296
|
@model_validator(mode="after")
|
|
285
|
-
def
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
referred_types = {
|
|
294
|
-
str(property_.value_type)
|
|
295
|
-
for property_ in self.properties
|
|
296
|
-
if isinstance(property_.value_type, Entity)
|
|
297
|
-
and not isinstance(property_.value_type.suffix, _UnknownType)
|
|
298
|
-
}
|
|
299
|
-
if not referred_classes.issubset(defined_classes) or not referred_types.issubset(defined_classes):
|
|
300
|
-
missing_classes = referred_classes.difference(defined_classes).union(
|
|
301
|
-
referred_types.difference(defined_classes)
|
|
302
|
-
)
|
|
303
|
-
raise exceptions.IncompleteSchema(missing_classes).to_pydantic_custom_error()
|
|
304
|
-
|
|
297
|
+
def post_validation(self) -> "InformationRules":
|
|
298
|
+
from ._validation import InformationPostValidation
|
|
299
|
+
|
|
300
|
+
issue_list = InformationPostValidation(self).validate()
|
|
301
|
+
if issue_list.warnings:
|
|
302
|
+
issue_list.trigger_warnings()
|
|
303
|
+
if issue_list.has_errors:
|
|
304
|
+
raise MultiValueError([error for error in issue_list if isinstance(error, issues.NeatValidationError)])
|
|
305
305
|
return self
|
|
306
306
|
|
|
307
|
-
@
|
|
308
|
-
def
|
|
309
|
-
|
|
310
|
-
referred_classes = {property_.class_ for property_ in self.properties if property_.class_.suffix is not Unknown}
|
|
311
|
-
has_parent_classes = {class_.class_ for class_ in self.classes if class_.parent}
|
|
312
|
-
missing_classes = defined_classes.difference(referred_classes) - has_parent_classes
|
|
313
|
-
if missing_classes:
|
|
314
|
-
warnings.warn(
|
|
315
|
-
cognite.neat.rules.issues.spreadsheet.ClassNoPropertiesNoParentsWarning(
|
|
316
|
-
[missing.versioned_id for missing in missing_classes]
|
|
317
|
-
),
|
|
318
|
-
stacklevel=2,
|
|
319
|
-
)
|
|
320
|
-
return self
|
|
307
|
+
@model_serializer(mode="wrap", when_used="always")
|
|
308
|
+
def information_rules_serializer(self, handler: Callable, info: SerializationInfo) -> dict[str, Any]:
|
|
309
|
+
from ._serializer import _InformationRulesSerializer
|
|
321
310
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
field_names = ["Class", "Value Type"] if info.by_alias else ["class_", "value_type"]
|
|
328
|
-
properties = []
|
|
329
|
-
for prop in self.properties:
|
|
330
|
-
dumped = prop.model_dump(**kwargs)
|
|
331
|
-
for field_name in field_names:
|
|
332
|
-
if value := dumped.get(field_name):
|
|
333
|
-
dumped[field_name] = value.removeprefix(default_prefix)
|
|
334
|
-
properties.append(dumped)
|
|
335
|
-
|
|
336
|
-
field_names = ["Class"] if info.by_alias else ["class_"]
|
|
337
|
-
classes = []
|
|
338
|
-
parent_name = "Parent Class" if info.by_alias else "parent"
|
|
339
|
-
for cls in self.classes:
|
|
340
|
-
dumped = cls.model_dump(**kwargs)
|
|
341
|
-
for field_name in field_names:
|
|
342
|
-
if value := dumped.get(field_name):
|
|
343
|
-
dumped[field_name] = value.removeprefix(default_prefix)
|
|
344
|
-
if value := dumped.get(parent_name):
|
|
345
|
-
dumped[parent_name] = ",".join(
|
|
346
|
-
constraint.strip().removeprefix(default_prefix) for constraint in value.split(",")
|
|
347
|
-
)
|
|
348
|
-
classes.append(dumped)
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
"Metadata" if info.by_alias else "metadata": self.metadata.model_dump(**kwargs),
|
|
352
|
-
"Classes" if info.by_alias else "classes": classes,
|
|
353
|
-
"Properties" if info.by_alias else "properties": properties,
|
|
354
|
-
"prefixes": {key: str(value) for key, value in self.prefixes.items()},
|
|
355
|
-
}
|
|
311
|
+
dumped = cast(dict[str, Any], handler(self, info))
|
|
312
|
+
prefix = self.metadata.prefix
|
|
313
|
+
|
|
314
|
+
return _InformationRulesSerializer(info, prefix).clean(dumped)
|
|
356
315
|
|
|
357
316
|
def as_domain_rules(self) -> DomainRules:
|
|
358
317
|
from ._converter import _InformationRulesConverter
|
|
@@ -368,9 +327,9 @@ class InformationRules(RuleModel):
|
|
|
368
327
|
new_self = self.model_copy(deep=True)
|
|
369
328
|
for prop in new_self.properties:
|
|
370
329
|
prop.reference = ReferenceEntity(
|
|
371
|
-
prefix=
|
|
372
|
-
|
|
373
|
-
|
|
330
|
+
prefix=(
|
|
331
|
+
prop.class_.prefix if not isinstance(prop.class_.prefix, _UndefinedType) else self.metadata.prefix
|
|
332
|
+
),
|
|
374
333
|
suffix=prop.class_.suffix,
|
|
375
334
|
version=prop.class_.version,
|
|
376
335
|
property=prop.property_,
|
|
@@ -378,9 +337,9 @@ class InformationRules(RuleModel):
|
|
|
378
337
|
|
|
379
338
|
for cls_ in new_self.classes:
|
|
380
339
|
cls_.reference = ReferenceEntity(
|
|
381
|
-
prefix=
|
|
382
|
-
|
|
383
|
-
|
|
340
|
+
prefix=(
|
|
341
|
+
cls_.class_.prefix if not isinstance(cls_.class_.prefix, _UndefinedType) else self.metadata.prefix
|
|
342
|
+
),
|
|
384
343
|
suffix=cls_.class_.suffix,
|
|
385
344
|
version=cls_.class_.version,
|
|
386
345
|
)
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Literal, cast, overload
|
|
5
|
+
|
|
6
|
+
from rdflib import Namespace
|
|
7
|
+
|
|
8
|
+
from cognite.neat.rules.models._base import DataModelType, ExtensionCategory, SchemaCompleteness, _add_alias
|
|
9
|
+
from cognite.neat.rules.models.data_types import DataType
|
|
10
|
+
from cognite.neat.rules.models.entities import ClassEntity, ParentClassEntity, Unknown, UnknownEntity
|
|
11
|
+
|
|
12
|
+
from ._rules import InformationClass, InformationMetadata, InformationProperty, InformationRules
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class InformationMetadataInput:
|
|
17
|
+
schema_: Literal["complete", "partial", "extended"]
|
|
18
|
+
prefix: str
|
|
19
|
+
namespace: str
|
|
20
|
+
version: str
|
|
21
|
+
creator: str
|
|
22
|
+
data_model_type: Literal["solution", "enterprise"] = "enterprise"
|
|
23
|
+
extension: Literal["addition", "reshape", "rebuild"] = "addition"
|
|
24
|
+
name: str | None = None
|
|
25
|
+
description: str | None = None
|
|
26
|
+
created: datetime | str | None = None
|
|
27
|
+
updated: datetime | str | None = None
|
|
28
|
+
license: str | None = None
|
|
29
|
+
rights: str | None = None
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def load(cls, data: dict[str, Any] | None) -> "InformationMetadataInput | None":
|
|
33
|
+
if data is None:
|
|
34
|
+
return None
|
|
35
|
+
_add_alias(data, InformationMetadata)
|
|
36
|
+
return cls(
|
|
37
|
+
data_model_type=data.get("data_model_type", "enterprise"),
|
|
38
|
+
extension=data.get("extension", "addition"),
|
|
39
|
+
schema_=data.get("schema_", "partial"), # type: ignore[arg-type]
|
|
40
|
+
version=data.get("version"), # type: ignore[arg-type]
|
|
41
|
+
namespace=data.get("namespace"), # type: ignore[arg-type]
|
|
42
|
+
prefix=data.get("prefix"), # type: ignore[arg-type]
|
|
43
|
+
name=data.get("name"),
|
|
44
|
+
creator=data.get("creator"), # type: ignore[arg-type]
|
|
45
|
+
description=data.get("description"),
|
|
46
|
+
created=data.get("created"),
|
|
47
|
+
updated=data.get("updated"),
|
|
48
|
+
license=data.get("license"),
|
|
49
|
+
rights=data.get("rights"),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def dump(self) -> dict[str, Any]:
|
|
53
|
+
return dict(
|
|
54
|
+
dataModelType=DataModelType(self.data_model_type),
|
|
55
|
+
schema=SchemaCompleteness(self.schema_),
|
|
56
|
+
extension=ExtensionCategory(self.extension),
|
|
57
|
+
namespace=Namespace(self.namespace),
|
|
58
|
+
prefix=self.prefix,
|
|
59
|
+
version=self.version,
|
|
60
|
+
name=self.name,
|
|
61
|
+
creator=self.creator,
|
|
62
|
+
description=self.description,
|
|
63
|
+
created=self.created or datetime.now(),
|
|
64
|
+
updated=self.updated or datetime.now(),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class InformationPropertyInput:
|
|
70
|
+
class_: str
|
|
71
|
+
property_: str
|
|
72
|
+
value_type: str
|
|
73
|
+
name: str | None = None
|
|
74
|
+
description: str | None = None
|
|
75
|
+
comment: str | None = None
|
|
76
|
+
min_count: int | None = None
|
|
77
|
+
max_count: int | float | None = None
|
|
78
|
+
default: Any | None = None
|
|
79
|
+
reference: str | None = None
|
|
80
|
+
match_type: str | None = None
|
|
81
|
+
rule_type: str | None = None
|
|
82
|
+
rule: str | None = None
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
@overload
|
|
86
|
+
def load(cls, data: None) -> None: ...
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
@overload
|
|
90
|
+
def load(cls, data: dict[str, Any]) -> "InformationPropertyInput": ...
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
@overload
|
|
94
|
+
def load(cls, data: list[dict[str, Any]]) -> list["InformationPropertyInput"]: ...
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def load(
|
|
98
|
+
cls, data: dict[str, Any] | list[dict[str, Any]] | None
|
|
99
|
+
) -> "InformationPropertyInput | list[InformationPropertyInput] | None":
|
|
100
|
+
if data is None:
|
|
101
|
+
return None
|
|
102
|
+
if isinstance(data, list) or (isinstance(data, dict) and isinstance(data.get("data"), list)):
|
|
103
|
+
items = cast(list[dict[str, Any]], data.get("data") if isinstance(data, dict) else data)
|
|
104
|
+
return [loaded for item in items if (loaded := cls.load(item)) is not None]
|
|
105
|
+
|
|
106
|
+
_add_alias(data, InformationProperty)
|
|
107
|
+
return cls(
|
|
108
|
+
class_=data.get("class_"), # type: ignore[arg-type]
|
|
109
|
+
property_=data.get("property_"), # type: ignore[arg-type]
|
|
110
|
+
name=data.get("name", None),
|
|
111
|
+
description=data.get("description", None),
|
|
112
|
+
comment=data.get("comment", None),
|
|
113
|
+
value_type=data.get("value_type"), # type: ignore[arg-type]
|
|
114
|
+
min_count=data.get("min_count", None),
|
|
115
|
+
max_count=data.get("max_count", None),
|
|
116
|
+
default=data.get("default", None),
|
|
117
|
+
reference=data.get("reference", None),
|
|
118
|
+
match_type=data.get("match_type", None),
|
|
119
|
+
rule_type=data.get("rule_type", None),
|
|
120
|
+
rule=data.get("rule", None),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def dump(self, default_prefix: str) -> dict[str, Any]:
|
|
124
|
+
value_type: DataType | ClassEntity | UnknownEntity
|
|
125
|
+
|
|
126
|
+
# property holding xsd data type
|
|
127
|
+
if DataType.is_data_type(self.value_type):
|
|
128
|
+
value_type = DataType.load(self.value_type)
|
|
129
|
+
|
|
130
|
+
# unknown value type
|
|
131
|
+
elif self.value_type == str(Unknown):
|
|
132
|
+
value_type = UnknownEntity()
|
|
133
|
+
|
|
134
|
+
# property holding link to class
|
|
135
|
+
else:
|
|
136
|
+
value_type = ClassEntity.load(self.value_type, prefix=default_prefix)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"Class": ClassEntity.load(self.class_, prefix=default_prefix),
|
|
140
|
+
"Property": self.property_,
|
|
141
|
+
"Name": self.name,
|
|
142
|
+
"Description": self.description,
|
|
143
|
+
"Comment": self.comment,
|
|
144
|
+
"Value Type": value_type,
|
|
145
|
+
"Min Count": self.min_count,
|
|
146
|
+
"Max Count": self.max_count,
|
|
147
|
+
"Default": self.default,
|
|
148
|
+
"Reference": self.reference,
|
|
149
|
+
"Match Type": self.match_type,
|
|
150
|
+
"Rule Type": self.rule_type,
|
|
151
|
+
"Rule": self.rule,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class InformationClassInput:
|
|
157
|
+
class_: str
|
|
158
|
+
name: str | None = None
|
|
159
|
+
description: str | None = None
|
|
160
|
+
comment: str | None = None
|
|
161
|
+
parent: str | None = None
|
|
162
|
+
reference: str | None = None
|
|
163
|
+
match_type: str | None = None
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
@overload
|
|
167
|
+
def load(cls, data: None) -> None: ...
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
@overload
|
|
171
|
+
def load(cls, data: dict[str, Any]) -> "InformationClassInput": ...
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
@overload
|
|
175
|
+
def load(cls, data: list[dict[str, Any]]) -> list["InformationClassInput"]: ...
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def load(
|
|
179
|
+
cls, data: dict[str, Any] | list[dict[str, Any]] | None
|
|
180
|
+
) -> "InformationClassInput | list[InformationClassInput] | None":
|
|
181
|
+
if data is None:
|
|
182
|
+
return None
|
|
183
|
+
if isinstance(data, list) or (isinstance(data, dict) and isinstance(data.get("data"), list)):
|
|
184
|
+
items = cast(list[dict[str, Any]], data.get("data") if isinstance(data, dict) else data)
|
|
185
|
+
return [loaded for item in items if (loaded := cls.load(item)) is not None]
|
|
186
|
+
_add_alias(data, InformationClass)
|
|
187
|
+
return cls(
|
|
188
|
+
class_=data.get("class_"), # type: ignore[arg-type]
|
|
189
|
+
name=data.get("name", None),
|
|
190
|
+
description=data.get("description", None),
|
|
191
|
+
comment=data.get("comment", None),
|
|
192
|
+
parent=data.get("parent", None),
|
|
193
|
+
reference=data.get("reference", None),
|
|
194
|
+
match_type=data.get("match_type", None),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def dump(self, default_prefix: str) -> dict[str, Any]:
|
|
198
|
+
return {
|
|
199
|
+
"Class": ClassEntity.load(self.class_, prefix=default_prefix),
|
|
200
|
+
"Name": self.name,
|
|
201
|
+
"Description": self.description,
|
|
202
|
+
"Comment": self.comment,
|
|
203
|
+
"Reference": self.reference,
|
|
204
|
+
"Match Type": self.match_type,
|
|
205
|
+
"Parent Class": (
|
|
206
|
+
[ParentClassEntity.load(parent, prefix=default_prefix) for parent in self.parent.split(",")]
|
|
207
|
+
if self.parent
|
|
208
|
+
else None
|
|
209
|
+
),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class InformationRulesInput:
|
|
215
|
+
metadata: InformationMetadataInput
|
|
216
|
+
properties: Sequence[InformationPropertyInput]
|
|
217
|
+
classes: Sequence[InformationClassInput]
|
|
218
|
+
last: "InformationRulesInput | InformationRules | None" = None
|
|
219
|
+
reference: "InformationRulesInput | InformationRules | None" = None
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
@overload
|
|
223
|
+
def load(cls, data: dict[str, Any]) -> "InformationRulesInput": ...
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
@overload
|
|
227
|
+
def load(cls, data: None) -> None: ...
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def load(cls, data: dict | None) -> "InformationRulesInput | None":
|
|
231
|
+
if data is None:
|
|
232
|
+
return None
|
|
233
|
+
_add_alias(data, InformationRules)
|
|
234
|
+
|
|
235
|
+
return cls(
|
|
236
|
+
metadata=InformationMetadataInput.load(data.get("metadata")), # type: ignore[arg-type]
|
|
237
|
+
properties=InformationPropertyInput.load(data.get("properties")), # type: ignore[arg-type]
|
|
238
|
+
classes=InformationClassInput.load(data.get("classes")), # type: ignore[arg-type]
|
|
239
|
+
last=InformationRulesInput.load(data.get("last")),
|
|
240
|
+
reference=InformationRulesInput.load(data.get("reference")),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def as_rules(self) -> InformationRules:
|
|
244
|
+
return InformationRules.model_validate(self.dump())
|
|
245
|
+
|
|
246
|
+
def dump(self) -> dict[str, Any]:
|
|
247
|
+
default_prefix = self.metadata.prefix
|
|
248
|
+
reference: dict[str, Any] | None = None
|
|
249
|
+
if isinstance(self.reference, InformationRulesInput):
|
|
250
|
+
reference = self.reference.dump()
|
|
251
|
+
elif isinstance(self.reference, InformationRules):
|
|
252
|
+
# We need to load through the InformationRulesInput to set the correct default space and version
|
|
253
|
+
reference = InformationRulesInput.load(self.reference.model_dump()).dump()
|
|
254
|
+
last: dict[str, Any] | None = None
|
|
255
|
+
if isinstance(self.last, InformationRulesInput):
|
|
256
|
+
last = self.last.dump()
|
|
257
|
+
elif isinstance(self.last, InformationRules):
|
|
258
|
+
# We need to load through the InformationRulesInput to set the correct default space and version
|
|
259
|
+
last = InformationRulesInput.load(self.last.model_dump()).dump()
|
|
260
|
+
return dict(
|
|
261
|
+
Metadata=self.metadata.dump(),
|
|
262
|
+
Properties=[prop.dump(default_prefix) for prop in self.properties],
|
|
263
|
+
Classes=[class_.dump(default_prefix) for class_ in self.classes],
|
|
264
|
+
Last=last,
|
|
265
|
+
Reference=reference,
|
|
266
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from typing import Any, ClassVar, cast
|
|
2
|
+
|
|
3
|
+
from pydantic_core.core_schema import SerializationInfo
|
|
4
|
+
|
|
5
|
+
from cognite.neat.rules.models import InformationRules
|
|
6
|
+
from cognite.neat.rules.models.information import InformationClass, InformationProperty
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _InformationRulesSerializer:
|
|
10
|
+
# These are the fields that need to be cleaned from the default space and version
|
|
11
|
+
PROPERTIES_FIELDS: ClassVar[list[str]] = ["class_", "value_type"]
|
|
12
|
+
CLASSES_FIELDS: ClassVar[list[str]] = ["class_"]
|
|
13
|
+
|
|
14
|
+
def __init__(self, info: SerializationInfo, default_prefix: str) -> None:
|
|
15
|
+
self.default_prefix = f"{default_prefix}:"
|
|
16
|
+
|
|
17
|
+
self.properties_fields = self.PROPERTIES_FIELDS
|
|
18
|
+
self.classes_fields = self.CLASSES_FIELDS
|
|
19
|
+
|
|
20
|
+
self.prop_name = "properties"
|
|
21
|
+
self.class_name = "classes"
|
|
22
|
+
self.metadata_name = "metadata"
|
|
23
|
+
self.class_parent = "parent"
|
|
24
|
+
|
|
25
|
+
self.prop_property = "property_"
|
|
26
|
+
self.prop_class = "class_"
|
|
27
|
+
|
|
28
|
+
if info.by_alias:
|
|
29
|
+
self.properties_fields = [
|
|
30
|
+
InformationProperty.model_fields[field].alias or field for field in self.properties_fields
|
|
31
|
+
]
|
|
32
|
+
self.classes_fields = [InformationClass.model_fields[field].alias or field for field in self.classes_fields]
|
|
33
|
+
self.prop_name = InformationRules.model_fields[self.prop_name].alias or self.prop_name
|
|
34
|
+
self.class_name = InformationRules.model_fields[self.class_name].alias or self.class_name
|
|
35
|
+
self.class_parent = InformationClass.model_fields[self.class_parent].alias or self.class_parent
|
|
36
|
+
self.metadata_name = InformationRules.model_fields[self.metadata_name].alias or self.metadata_name
|
|
37
|
+
|
|
38
|
+
self.prop_property = InformationProperty.model_fields[self.prop_property].alias or self.prop_property
|
|
39
|
+
self.prop_class = InformationProperty.model_fields[self.prop_class].alias or self.prop_class
|
|
40
|
+
|
|
41
|
+
if isinstance(info.exclude, dict):
|
|
42
|
+
# Just for happy mypy
|
|
43
|
+
exclude = cast(dict, info.exclude)
|
|
44
|
+
self.metadata_exclude = exclude.get("metadata", set()) or set()
|
|
45
|
+
self.exclude_classes = exclude.get("classes", {}).get("__all__", set()) or set()
|
|
46
|
+
self.exclude_properties = exclude.get("properties", {}).get("__all__", set())
|
|
47
|
+
self.exclude_top = {k for k, v in exclude.items() if not v}
|
|
48
|
+
else:
|
|
49
|
+
self.exclude_top = set(info.exclude or {})
|
|
50
|
+
self.exclude_properties = set()
|
|
51
|
+
self.exclude_classes = set()
|
|
52
|
+
self.metadata_exclude = set()
|
|
53
|
+
|
|
54
|
+
def clean(self, dumped: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
# Sorting to get a deterministic order
|
|
56
|
+
dumped[self.prop_name] = sorted(
|
|
57
|
+
dumped[self.prop_name]["data"], key=lambda p: (p[self.prop_class], p[self.prop_property])
|
|
58
|
+
)
|
|
59
|
+
dumped[self.class_name] = sorted(dumped[self.class_name]["data"], key=lambda v: v[self.prop_class])
|
|
60
|
+
|
|
61
|
+
for prop in dumped[self.prop_name]:
|
|
62
|
+
for field_name in self.properties_fields:
|
|
63
|
+
if value := prop.get(field_name):
|
|
64
|
+
prop[field_name] = value.removeprefix(self.default_prefix)
|
|
65
|
+
|
|
66
|
+
if self.exclude_properties:
|
|
67
|
+
for field in self.exclude_properties:
|
|
68
|
+
prop.pop(field, None)
|
|
69
|
+
|
|
70
|
+
for class_ in dumped[self.class_name]:
|
|
71
|
+
for field_name in self.classes_fields:
|
|
72
|
+
if value := class_.get(field_name):
|
|
73
|
+
class_[field_name] = value.removeprefix(self.default_prefix)
|
|
74
|
+
|
|
75
|
+
if value := class_.get(self.class_parent):
|
|
76
|
+
class_[self.class_parent] = ",".join(
|
|
77
|
+
parent.strip().removeprefix(self.default_prefix) for parent in value.split(",")
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if self.metadata_exclude:
|
|
81
|
+
for field in self.metadata_exclude:
|
|
82
|
+
dumped[self.metadata_name].pop(field, None)
|
|
83
|
+
for field in self.exclude_top:
|
|
84
|
+
dumped.pop(field, None)
|
|
85
|
+
return dumped
|