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.
Files changed (31) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/rules/analysis/_information_rules.py +2 -12
  3. cognite/neat/rules/exporters/_rules2excel.py +10 -10
  4. cognite/neat/rules/exporters/_rules2yaml.py +3 -3
  5. cognite/neat/rules/importers/_dms2rules.py +15 -6
  6. cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +1 -0
  7. cognite/neat/rules/importers/_spreadsheet2rules.py +21 -8
  8. cognite/neat/rules/issues/spreadsheet.py +60 -5
  9. cognite/neat/rules/models/_base.py +29 -18
  10. cognite/neat/rules/models/dms/_converter.py +2 -0
  11. cognite/neat/rules/models/dms/_exporter.py +131 -17
  12. cognite/neat/rules/models/dms/_rules.py +43 -31
  13. cognite/neat/rules/models/dms/_rules_input.py +4 -11
  14. cognite/neat/rules/models/dms/_schema.py +5 -4
  15. cognite/neat/rules/models/dms/_serializer.py +26 -36
  16. cognite/neat/rules/models/dms/_validation.py +39 -61
  17. cognite/neat/rules/models/domain.py +2 -6
  18. cognite/neat/rules/models/information/__init__.py +8 -1
  19. cognite/neat/rules/models/information/_converter.py +53 -14
  20. cognite/neat/rules/models/information/_rules.py +63 -106
  21. cognite/neat/rules/models/information/_rules_input.py +266 -0
  22. cognite/neat/rules/models/information/_serializer.py +73 -0
  23. cognite/neat/rules/models/information/_validation.py +164 -0
  24. cognite/neat/utils/cdf.py +35 -0
  25. cognite/neat/workflows/steps/lib/current/rules_exporter.py +30 -7
  26. cognite/neat/workflows/steps/lib/current/rules_importer.py +21 -2
  27. {cognite_neat-0.77.3.dist-info → cognite_neat-0.77.5.dist-info}/METADATA +1 -1
  28. {cognite_neat-0.77.3.dist-info → cognite_neat-0.77.5.dist-info}/RECORD +31 -28
  29. {cognite_neat-0.77.3.dist-info → cognite_neat-0.77.5.dist-info}/LICENSE +0 -0
  30. {cognite_neat-0.77.3.dist-info → cognite_neat-0.77.5.dist-info}/WHEEL +0 -0
  31. {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="Is Reference",
273
- value="False",
274
- label="Export rules as reference rules. If provided, the rules will be exported as reference rules. ",
275
- options=["True", "False"],
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
- is_reference = self.configs.get("Is Reference") == "True"
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
- dump_as = "reference" if is_reference else "user"
296
- excel_exporter = exporters.ExcelExporter(styling=styling, output_role=output_role, dump_as=dump_as) # type: ignore[arg-type]
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
- dms_importer = importers.DMSImporter.from_data_model_id(cdf_client, datamodel_entity.as_id())
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cognite-neat
3
- Version: 0.77.3
3
+ Version: 0.77.5
4
4
  Summary: Knowledge graph transformation
5
5
  Home-page: https://cognite-neat.readthedocs-hosted.com/
6
6
  License: Apache-2.0