cognite-neat 0.77.3__py3-none-any.whl → 0.77.5__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.
- cognite/neat/_version.py +1 -1
- cognite/neat/rules/analysis/_information_rules.py +2 -12
- cognite/neat/rules/exporters/_rules2excel.py +10 -10
- cognite/neat/rules/exporters/_rules2yaml.py +3 -3
- cognite/neat/rules/importers/_dms2rules.py +15 -6
- cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +1 -0
- cognite/neat/rules/importers/_spreadsheet2rules.py +21 -8
- cognite/neat/rules/issues/spreadsheet.py +60 -5
- cognite/neat/rules/models/_base.py +29 -18
- cognite/neat/rules/models/dms/_converter.py +2 -0
- cognite/neat/rules/models/dms/_exporter.py +131 -17
- cognite/neat/rules/models/dms/_rules.py +43 -31
- cognite/neat/rules/models/dms/_rules_input.py +4 -11
- cognite/neat/rules/models/dms/_schema.py +5 -4
- cognite/neat/rules/models/dms/_serializer.py +26 -36
- cognite/neat/rules/models/dms/_validation.py +39 -61
- cognite/neat/rules/models/domain.py +2 -6
- cognite/neat/rules/models/information/__init__.py +8 -1
- cognite/neat/rules/models/information/_converter.py +53 -14
- cognite/neat/rules/models/information/_rules.py +63 -106
- cognite/neat/rules/models/information/_rules_input.py +266 -0
- cognite/neat/rules/models/information/_serializer.py +73 -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.3.dist-info → cognite_neat-0.77.5.dist-info}/METADATA +1 -1
- {cognite_neat-0.77.3.dist-info → cognite_neat-0.77.5.dist-info}/RECORD +31 -28
- {cognite_neat-0.77.3.dist-info → cognite_neat-0.77.5.dist-info}/LICENSE +0 -0
- {cognite_neat-0.77.3.dist-info → cognite_neat-0.77.5.dist-info}/WHEEL +0 -0
- {cognite_neat-0.77.3.dist-info → cognite_neat-0.77.5.dist-info}/entry_points.txt +0 -0
|
@@ -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,73 @@
|
|
|
1
|
+
from typing import Any, ClassVar
|
|
2
|
+
|
|
3
|
+
from cognite.neat.rules.models import InformationRules
|
|
4
|
+
from cognite.neat.rules.models.entities import ClassEntity, ReferenceEntity
|
|
5
|
+
from cognite.neat.rules.models.information import InformationClass, InformationProperty
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _InformationRulesSerializer:
|
|
9
|
+
# These are the fields that need to be cleaned from the default space and version
|
|
10
|
+
PROPERTIES_FIELDS: ClassVar[list[str]] = ["class_", "value_type"]
|
|
11
|
+
CLASSES_FIELDS: ClassVar[list[str]] = ["class_"]
|
|
12
|
+
|
|
13
|
+
def __init__(self, by_alias: bool, default_prefix: str) -> None:
|
|
14
|
+
self.default_prefix = f"{default_prefix}:"
|
|
15
|
+
|
|
16
|
+
self.properties_fields = self.PROPERTIES_FIELDS
|
|
17
|
+
self.classes_fields = self.CLASSES_FIELDS
|
|
18
|
+
|
|
19
|
+
self.prop_name = "properties"
|
|
20
|
+
self.class_name = "classes"
|
|
21
|
+
self.metadata_name = "metadata"
|
|
22
|
+
self.class_parent = "parent"
|
|
23
|
+
|
|
24
|
+
self.prop_property = "property_"
|
|
25
|
+
self.prop_class = "class_"
|
|
26
|
+
|
|
27
|
+
self.reference = "Reference" if by_alias else "reference"
|
|
28
|
+
if 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
|
+
def clean(self, dumped: dict[str, Any], as_reference: bool) -> dict[str, Any]:
|
|
42
|
+
# Sorting to get a deterministic order
|
|
43
|
+
dumped[self.prop_name] = sorted(
|
|
44
|
+
dumped[self.prop_name]["data"], key=lambda p: (p[self.prop_class], p[self.prop_property])
|
|
45
|
+
)
|
|
46
|
+
dumped[self.class_name] = sorted(dumped[self.class_name]["data"], key=lambda v: v[self.prop_class])
|
|
47
|
+
|
|
48
|
+
for prop in dumped[self.prop_name]:
|
|
49
|
+
if as_reference:
|
|
50
|
+
class_entity = ClassEntity.load(prop[self.prop_class])
|
|
51
|
+
prop[self.reference] = str(
|
|
52
|
+
ReferenceEntity(
|
|
53
|
+
prefix=str(class_entity.prefix), suffix=class_entity.suffix, property=prop[self.prop_property]
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
for field_name in self.properties_fields:
|
|
58
|
+
if value := prop.get(field_name):
|
|
59
|
+
prop[field_name] = value.removeprefix(self.default_prefix)
|
|
60
|
+
|
|
61
|
+
for class_ in dumped[self.class_name]:
|
|
62
|
+
if as_reference:
|
|
63
|
+
class_[self.reference] = class_[self.prop_class]
|
|
64
|
+
for field_name in self.classes_fields:
|
|
65
|
+
if value := class_.get(field_name):
|
|
66
|
+
class_[field_name] = value.removeprefix(self.default_prefix)
|
|
67
|
+
|
|
68
|
+
if value := class_.get(self.class_parent):
|
|
69
|
+
class_[self.class_parent] = ",".join(
|
|
70
|
+
parent.strip().removeprefix(self.default_prefix) for parent in value.split(",")
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return dumped
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from cognite.neat.rules import issues
|
|
5
|
+
from cognite.neat.rules.issues import IssueList
|
|
6
|
+
from cognite.neat.rules.models._base import DataModelType, SchemaCompleteness
|
|
7
|
+
from cognite.neat.rules.models.entities import ClassEntity, EntityTypes, UnknownEntity
|
|
8
|
+
from cognite.neat.utils.utils import get_inheritance_path
|
|
9
|
+
|
|
10
|
+
from ._rules import InformationRules
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InformationPostValidation:
|
|
14
|
+
"""This class does all the validation of the Information rules that have dependencies
|
|
15
|
+
between components."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, rules: InformationRules):
|
|
18
|
+
self.rules = rules
|
|
19
|
+
self.metadata = rules.metadata
|
|
20
|
+
self.properties = rules.properties
|
|
21
|
+
self.classes = rules.classes
|
|
22
|
+
self.issue_list = IssueList()
|
|
23
|
+
|
|
24
|
+
def validate(self) -> IssueList:
|
|
25
|
+
if self.metadata.schema_ == SchemaCompleteness.partial:
|
|
26
|
+
return self.issue_list
|
|
27
|
+
|
|
28
|
+
if self.metadata.data_model_type == DataModelType.solution and not self.rules.reference:
|
|
29
|
+
raise ValueError("Reference data model is missing")
|
|
30
|
+
|
|
31
|
+
if self.metadata.schema_ == SchemaCompleteness.extended and not self.rules.last:
|
|
32
|
+
raise ValueError("Last version is missing")
|
|
33
|
+
|
|
34
|
+
self._dangling_classes()
|
|
35
|
+
self._referenced_parent_classes_exist()
|
|
36
|
+
self._referenced_classes_exist()
|
|
37
|
+
self._referenced_value_types_exist()
|
|
38
|
+
|
|
39
|
+
return self.issue_list
|
|
40
|
+
|
|
41
|
+
def _dangling_classes(self) -> None:
|
|
42
|
+
# needs to be complete for this validation to pass
|
|
43
|
+
defined_classes = {class_.class_ for class_ in self.classes}
|
|
44
|
+
referred_classes = {property_.class_ for property_ in self.properties}
|
|
45
|
+
class_parent_pairs = self._class_parent_pairs()
|
|
46
|
+
dangling_classes = set()
|
|
47
|
+
|
|
48
|
+
if classes_without_properties := defined_classes.difference(referred_classes):
|
|
49
|
+
for class_ in classes_without_properties:
|
|
50
|
+
# USE CASE: class has no direct properties and no parents
|
|
51
|
+
if class_ not in class_parent_pairs:
|
|
52
|
+
dangling_classes.add(class_)
|
|
53
|
+
# USE CASE: class has no direct properties and no parents with properties
|
|
54
|
+
elif class_ not in class_parent_pairs and not any(
|
|
55
|
+
parent in referred_classes for parent in get_inheritance_path(class_, class_parent_pairs)
|
|
56
|
+
):
|
|
57
|
+
dangling_classes.add(class_)
|
|
58
|
+
|
|
59
|
+
if dangling_classes:
|
|
60
|
+
self.issue_list.append(
|
|
61
|
+
issues.spreadsheet.ClassNoPropertiesNoParentError([class_.versioned_id for class_ in dangling_classes])
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _referenced_parent_classes_exist(self) -> None:
|
|
65
|
+
# needs to be complete for this validation to pass
|
|
66
|
+
class_parent_pairs = self._class_parent_pairs()
|
|
67
|
+
classes = set(class_parent_pairs.keys())
|
|
68
|
+
parents = set(itertools.chain.from_iterable(class_parent_pairs.values()))
|
|
69
|
+
|
|
70
|
+
if undefined_parents := parents.difference(classes):
|
|
71
|
+
self.issue_list.append(
|
|
72
|
+
issues.spreadsheet.ParentClassesNotDefinedError([missing.versioned_id for missing in undefined_parents])
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def _referenced_classes_exist(self) -> None:
|
|
76
|
+
# needs to be complete for this validation to pass
|
|
77
|
+
defined_classes = {class_.class_ for class_ in self.classes}
|
|
78
|
+
referred_classes = {property_.class_ for property_ in self.properties}
|
|
79
|
+
|
|
80
|
+
# USE CASE: models are complete
|
|
81
|
+
if self.metadata.schema_ == SchemaCompleteness.complete and (
|
|
82
|
+
missing_classes := referred_classes.difference(defined_classes)
|
|
83
|
+
):
|
|
84
|
+
self.issue_list.append(
|
|
85
|
+
issues.spreadsheet.PropertiesDefinedForUndefinedClassesError(
|
|
86
|
+
[missing.versioned_id for missing in missing_classes]
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# USE CASE: models are extended (user + last = complete)
|
|
91
|
+
if self.metadata.schema_ == SchemaCompleteness.extended:
|
|
92
|
+
defined_classes |= {class_.class_ for class_ in cast(InformationRules, self.rules.last).classes}
|
|
93
|
+
if missing_classes := referred_classes.difference(defined_classes):
|
|
94
|
+
self.issue_list.append(
|
|
95
|
+
issues.spreadsheet.PropertiesDefinedForUndefinedClassesError(
|
|
96
|
+
[missing.versioned_id for missing in missing_classes]
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _referenced_value_types_exist(self) -> None:
|
|
101
|
+
# adding UnknownEntity to the set of defined classes to handle the case where a property references an unknown
|
|
102
|
+
defined_classes = {class_.class_ for class_ in self.classes} | {UnknownEntity()}
|
|
103
|
+
referred_object_types = {
|
|
104
|
+
property_.value_type
|
|
105
|
+
for property_ in self.rules.properties
|
|
106
|
+
if property_.type_ == EntityTypes.object_property
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# USE CASE: models are complete
|
|
110
|
+
if self.metadata.schema_ == SchemaCompleteness.complete and (
|
|
111
|
+
missing_value_types := referred_object_types.difference(defined_classes)
|
|
112
|
+
):
|
|
113
|
+
self.issue_list.append(
|
|
114
|
+
issues.spreadsheet.ValueTypeNotDefinedError(
|
|
115
|
+
[cast(ClassEntity, missing).versioned_id for missing in missing_value_types]
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# USE CASE: models are extended (user + last = complete)
|
|
120
|
+
if self.metadata.schema_ == SchemaCompleteness.extended:
|
|
121
|
+
defined_classes |= {class_.class_ for class_ in cast(InformationRules, self.rules.last).classes}
|
|
122
|
+
if missing_value_types := referred_object_types.difference(defined_classes):
|
|
123
|
+
self.issue_list.append(
|
|
124
|
+
issues.spreadsheet.ValueTypeNotDefinedError(
|
|
125
|
+
[cast(ClassEntity, missing).versioned_id for missing in missing_value_types]
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _class_parent_pairs(self) -> dict[ClassEntity, list[ClassEntity]]:
|
|
130
|
+
class_subclass_pairs: dict[ClassEntity, list[ClassEntity]] = {}
|
|
131
|
+
|
|
132
|
+
classes = self.rules.model_copy(deep=True).classes.data
|
|
133
|
+
|
|
134
|
+
# USE CASE: Solution model being extended (user + last + reference = complete)
|
|
135
|
+
if (
|
|
136
|
+
self.metadata.schema_ == SchemaCompleteness.extended
|
|
137
|
+
and self.metadata.data_model_type == DataModelType.solution
|
|
138
|
+
):
|
|
139
|
+
classes += (
|
|
140
|
+
cast(InformationRules, self.rules.last).model_copy(deep=True).classes.data
|
|
141
|
+
+ cast(InformationRules, self.rules.reference).model_copy(deep=True).classes.data
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# USE CASE: Solution model being created from scratch (user + reference = complete)
|
|
145
|
+
elif (
|
|
146
|
+
self.metadata.schema_ == SchemaCompleteness.complete
|
|
147
|
+
and self.metadata.data_model_type == DataModelType.solution
|
|
148
|
+
):
|
|
149
|
+
classes += cast(InformationRules, self.rules.reference).model_copy(deep=True).classes.data
|
|
150
|
+
|
|
151
|
+
# USE CASE: Enterprise model being extended (user + last = complete)
|
|
152
|
+
elif (
|
|
153
|
+
self.metadata.schema_ == SchemaCompleteness.extended
|
|
154
|
+
and self.metadata.data_model_type == DataModelType.enterprise
|
|
155
|
+
):
|
|
156
|
+
classes += cast(InformationRules, self.rules.last).model_copy(deep=True).classes.data
|
|
157
|
+
|
|
158
|
+
for class_ in classes:
|
|
159
|
+
class_subclass_pairs[class_.class_] = []
|
|
160
|
+
if class_.parent is None:
|
|
161
|
+
continue
|
|
162
|
+
class_subclass_pairs[class_.class_].extend([parent.as_class_entity() for parent in class_.parent])
|
|
163
|
+
|
|
164
|
+
return class_subclass_pairs
|
cognite/neat/utils/cdf.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from cognite.client import CogniteClient
|
|
2
|
+
from cognite.client.data_classes import filters
|
|
1
3
|
from pydantic import BaseModel, field_validator
|
|
2
4
|
|
|
3
5
|
|
|
@@ -22,3 +24,36 @@ class InteractiveCogniteClient(CogniteClientConfig):
|
|
|
22
24
|
class ServiceCogniteClient(CogniteClientConfig):
|
|
23
25
|
token_url: str = "https://login.microsoftonline.com/common/oauth2/token"
|
|
24
26
|
client_secret: str = "secret"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def clean_space(client: CogniteClient, space: str) -> None:
|
|
30
|
+
"""Deletes all data in a space.
|
|
31
|
+
|
|
32
|
+
This means all nodes, edges, views, containers, and data models located in the given space.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
client: Connected CogniteClient
|
|
36
|
+
space: The space to delete.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
edges = client.data_modeling.instances.list("edge", limit=-1, filter=filters.Equals(["edge", "space"], space))
|
|
40
|
+
if edges:
|
|
41
|
+
instances = client.data_modeling.instances.delete(edges=edges.as_ids())
|
|
42
|
+
print(f"Deleted {len(instances.edges)} edges")
|
|
43
|
+
nodes = client.data_modeling.instances.list("node", limit=-1, filter=filters.Equals(["node", "space"], space))
|
|
44
|
+
if nodes:
|
|
45
|
+
instances = client.data_modeling.instances.delete(nodes=nodes.as_ids())
|
|
46
|
+
print(f"Deleted {len(instances.nodes)} nodes")
|
|
47
|
+
views = client.data_modeling.views.list(limit=-1, space=space)
|
|
48
|
+
if views:
|
|
49
|
+
deleted_views = client.data_modeling.views.delete(views.as_ids())
|
|
50
|
+
print(f"Deleted {len(deleted_views)} views")
|
|
51
|
+
containers = client.data_modeling.containers.list(limit=-1, space=space)
|
|
52
|
+
if containers:
|
|
53
|
+
deleted_containers = client.data_modeling.containers.delete(containers.as_ids())
|
|
54
|
+
print(f"Deleted {len(deleted_containers)} containers")
|
|
55
|
+
if data_models := client.data_modeling.data_models.list(limit=-1, space=space):
|
|
56
|
+
deleted_data_models = client.data_modeling.data_models.delete(data_models.as_ids())
|
|
57
|
+
print(f"Deleted {len(deleted_data_models)} data models")
|
|
58
|
+
deleted_space = client.data_modeling.spaces.delete(space)
|
|
59
|
+
print(f"Deleted space {deleted_space}")
|
|
@@ -269,10 +269,19 @@ class RulesToExcel(Step):
|
|
|
269
269
|
options=["input", *RoleTypes.__members__.keys()],
|
|
270
270
|
),
|
|
271
271
|
Configurable(
|
|
272
|
-
name="
|
|
273
|
-
value="
|
|
274
|
-
label="
|
|
275
|
-
|
|
272
|
+
name="Dump Format",
|
|
273
|
+
value="user",
|
|
274
|
+
label="How to dump the rules to the Excel file.\n"
|
|
275
|
+
"'user' - just as is.\n'reference' - enterprise model used as basis for a solution model\n"
|
|
276
|
+
"'last' - used when updating a data model.",
|
|
277
|
+
options=list(exporters.ExcelExporter.dump_options),
|
|
278
|
+
),
|
|
279
|
+
Configurable(
|
|
280
|
+
name="New Data Model ID",
|
|
281
|
+
value="",
|
|
282
|
+
label="If you chose Dump Format 'reference', the provided ID will be use in the new medata sheet. "
|
|
283
|
+
"Expected format 'sp_space:my_external_id'.",
|
|
284
|
+
options=list(exporters.ExcelExporter.dump_options),
|
|
276
285
|
),
|
|
277
286
|
Configurable(
|
|
278
287
|
name="File path",
|
|
@@ -285,15 +294,29 @@ class RulesToExcel(Step):
|
|
|
285
294
|
if self.configs is None or self.data_store_path is None:
|
|
286
295
|
raise StepNotInitialized(type(self).__name__)
|
|
287
296
|
|
|
288
|
-
|
|
297
|
+
dump_format = self.configs.get("Dump Format", "user")
|
|
289
298
|
styling = cast(exporters.ExcelExporter.Style, self.configs.get("Styling", "default"))
|
|
290
299
|
role = self.configs.get("Output role format")
|
|
291
300
|
output_role = None
|
|
292
301
|
if role != "input" and role is not None:
|
|
293
302
|
output_role = RoleTypes[role]
|
|
294
303
|
|
|
295
|
-
|
|
296
|
-
|
|
304
|
+
new_model_str = self.configs.get("New Data Model ID")
|
|
305
|
+
new_model_id: tuple[str, str] | None = None
|
|
306
|
+
if new_model_str and ":" in new_model_str:
|
|
307
|
+
new_model_id = tuple(new_model_str.split(":", 1)) # type: ignore[assignment]
|
|
308
|
+
elif new_model_str:
|
|
309
|
+
return FlowMessage(
|
|
310
|
+
error_text="New Data Model ID must be in the format 'sp_space:my_external_id'!",
|
|
311
|
+
step_execution_status=StepExecutionStatus.ABORT_AND_FAIL,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
excel_exporter = exporters.ExcelExporter(
|
|
315
|
+
styling=styling,
|
|
316
|
+
output_role=output_role,
|
|
317
|
+
dump_as=dump_format, # type: ignore[arg-type]
|
|
318
|
+
new_model_id=new_model_id,
|
|
319
|
+
)
|
|
297
320
|
|
|
298
321
|
rule_instance: Rules
|
|
299
322
|
if rules.domain:
|
|
@@ -3,6 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
from typing import ClassVar
|
|
4
4
|
|
|
5
5
|
from cognite.client import CogniteClient
|
|
6
|
+
from cognite.client.data_classes.data_modeling import DataModelId
|
|
6
7
|
|
|
7
8
|
from cognite.neat.rules import importers
|
|
8
9
|
from cognite.neat.rules.issues.formatters import FORMATTER_BY_NAME
|
|
@@ -184,6 +185,13 @@ class DMSToRules(Step):
|
|
|
184
185
|
type="string",
|
|
185
186
|
required=True,
|
|
186
187
|
),
|
|
188
|
+
Configurable(
|
|
189
|
+
name="Reference data model id",
|
|
190
|
+
value=None,
|
|
191
|
+
label="The ID of the Reference Data Model to import. Written at 'my_space:my_data_model(version=1)'. "
|
|
192
|
+
"This is typically an enterprise data model when you want to import a solution model",
|
|
193
|
+
type="string",
|
|
194
|
+
),
|
|
187
195
|
Configurable(
|
|
188
196
|
name="Report formatter",
|
|
189
197
|
value=next(iter(FORMATTER_BY_NAME.keys())),
|
|
@@ -214,8 +222,19 @@ class DMSToRules(Step):
|
|
|
214
222
|
f"or 'my_space:my_data_model', failed to parse space from {datamodel_id_str}"
|
|
215
223
|
)
|
|
216
224
|
return FlowMessage(error_text=error_text, step_execution_status=StepExecutionStatus.ABORT_AND_FAIL)
|
|
217
|
-
|
|
218
|
-
|
|
225
|
+
ref_datamodel_str = self.configs.get("Reference data model id", "")
|
|
226
|
+
ref_model_id: DataModelId | None = None
|
|
227
|
+
if ref_datamodel_str:
|
|
228
|
+
ref_model = DataModelEntity.load(ref_datamodel_str)
|
|
229
|
+
if isinstance(ref_model, DMSUnknownEntity):
|
|
230
|
+
error_text = (
|
|
231
|
+
f"Reference data model id should be in the format 'my_space:my_data_model(version=1)' "
|
|
232
|
+
f"or 'my_space:my_data_model', failed to parse space from {ref_datamodel_str}"
|
|
233
|
+
)
|
|
234
|
+
return FlowMessage(error_text=error_text, step_execution_status=StepExecutionStatus.ABORT_AND_FAIL)
|
|
235
|
+
ref_model_id = ref_model.as_id()
|
|
236
|
+
|
|
237
|
+
dms_importer = importers.DMSImporter.from_data_model_id(cdf_client, datamodel_entity.as_id(), ref_model_id)
|
|
219
238
|
|
|
220
239
|
# if role is None, it will be inferred from the rules file
|
|
221
240
|
role = self.configs.get("Role")
|