cognite-neat 0.88.2__py3-none-any.whl → 0.89.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.

Files changed (129) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/constants.py +3 -0
  3. cognite/neat/graph/__init__.py +0 -3
  4. cognite/neat/graph/extractors/_mock_graph_generator.py +2 -1
  5. cognite/neat/graph/loaders/_base.py +3 -3
  6. cognite/neat/graph/loaders/_rdf2asset.py +24 -25
  7. cognite/neat/graph/loaders/_rdf2dms.py +20 -15
  8. cognite/neat/issues/__init__.py +1 -3
  9. cognite/neat/issues/_base.py +261 -71
  10. cognite/neat/issues/errors/__init__.py +73 -0
  11. cognite/neat/issues/errors/_external.py +67 -0
  12. cognite/neat/issues/errors/_general.py +35 -0
  13. cognite/neat/issues/errors/_properties.py +62 -0
  14. cognite/neat/issues/errors/_resources.py +111 -0
  15. cognite/neat/issues/errors/_workflow.py +36 -0
  16. cognite/neat/issues/formatters.py +1 -1
  17. cognite/neat/issues/warnings/__init__.py +66 -0
  18. cognite/neat/issues/warnings/_external.py +40 -0
  19. cognite/neat/issues/warnings/_general.py +29 -0
  20. cognite/neat/issues/warnings/_models.py +92 -0
  21. cognite/neat/issues/warnings/_properties.py +44 -0
  22. cognite/neat/issues/warnings/_resources.py +55 -0
  23. cognite/neat/issues/warnings/user_modeling.py +113 -0
  24. cognite/neat/rules/_shared.py +53 -2
  25. cognite/neat/rules/analysis/_base.py +1 -1
  26. cognite/neat/rules/exporters/_base.py +7 -18
  27. cognite/neat/rules/exporters/_rules2dms.py +17 -20
  28. cognite/neat/rules/exporters/_rules2excel.py +9 -16
  29. cognite/neat/rules/exporters/_rules2ontology.py +77 -64
  30. cognite/neat/rules/exporters/_rules2yaml.py +6 -9
  31. cognite/neat/rules/exporters/_validation.py +11 -96
  32. cognite/neat/rules/importers/_base.py +9 -58
  33. cognite/neat/rules/importers/_dms2rules.py +188 -135
  34. cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +48 -35
  35. cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +36 -45
  36. cognite/neat/rules/importers/_dtdl2rules/spec.py +7 -0
  37. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2classes.py +8 -4
  38. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2metadata.py +3 -3
  39. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2properties.py +18 -11
  40. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2rules.py +12 -19
  41. cognite/neat/rules/importers/_rdf/_inference2rules.py +14 -37
  42. cognite/neat/rules/importers/_rdf/_owl2rules/_owl2classes.py +1 -0
  43. cognite/neat/rules/importers/_rdf/_owl2rules/_owl2properties.py +1 -0
  44. cognite/neat/rules/importers/_rdf/_owl2rules/_owl2rules.py +9 -20
  45. cognite/neat/rules/importers/_rdf/_shared.py +4 -4
  46. cognite/neat/rules/importers/_spreadsheet2rules.py +46 -97
  47. cognite/neat/rules/importers/_yaml2rules.py +32 -58
  48. cognite/neat/rules/models/__init__.py +21 -5
  49. cognite/neat/rules/models/_base_input.py +162 -0
  50. cognite/neat/rules/models/{_base.py → _base_rules.py} +1 -12
  51. cognite/neat/rules/models/_rdfpath.py +4 -4
  52. cognite/neat/rules/models/{_types/_field.py → _types.py} +5 -10
  53. cognite/neat/rules/models/asset/__init__.py +5 -2
  54. cognite/neat/rules/models/asset/_rules.py +3 -23
  55. cognite/neat/rules/models/asset/_rules_input.py +40 -115
  56. cognite/neat/rules/models/asset/_validation.py +14 -10
  57. cognite/neat/rules/models/data_types.py +150 -44
  58. cognite/neat/rules/models/dms/__init__.py +19 -7
  59. cognite/neat/rules/models/dms/_exporter.py +102 -34
  60. cognite/neat/rules/models/dms/_rules.py +65 -162
  61. cognite/neat/rules/models/dms/_rules_input.py +186 -254
  62. cognite/neat/rules/models/dms/_schema.py +87 -78
  63. cognite/neat/rules/models/dms/_serializer.py +44 -3
  64. cognite/neat/rules/models/dms/_validation.py +106 -68
  65. cognite/neat/rules/models/domain.py +52 -1
  66. cognite/neat/rules/models/entities/__init__.py +63 -0
  67. cognite/neat/rules/models/entities/_constants.py +73 -0
  68. cognite/neat/rules/models/entities/_loaders.py +76 -0
  69. cognite/neat/rules/models/entities/_multi_value.py +67 -0
  70. cognite/neat/rules/models/{entities.py → entities/_single_value.py} +74 -232
  71. cognite/neat/rules/models/entities/_types.py +86 -0
  72. cognite/neat/rules/models/{wrapped_entities.py → entities/_wrapped.py} +1 -1
  73. cognite/neat/rules/models/information/__init__.py +10 -2
  74. cognite/neat/rules/models/information/_rules.py +10 -22
  75. cognite/neat/rules/models/information/_rules_input.py +57 -204
  76. cognite/neat/rules/models/information/_validation.py +48 -25
  77. cognite/neat/rules/transformers/__init__.py +21 -0
  78. cognite/neat/rules/transformers/_base.py +81 -0
  79. cognite/neat/rules/{models/information/_converter.py → transformers/_converters.py} +217 -21
  80. cognite/neat/rules/transformers/_map_onto.py +97 -0
  81. cognite/neat/rules/transformers/_pipelines.py +61 -0
  82. cognite/neat/rules/transformers/_verification.py +136 -0
  83. cognite/neat/{graph/stores → store}/_provenance.py +10 -1
  84. cognite/neat/utils/auxiliary.py +2 -35
  85. cognite/neat/utils/cdf/data_classes.py +20 -0
  86. cognite/neat/utils/regex_patterns.py +6 -0
  87. cognite/neat/utils/text.py +17 -0
  88. cognite/neat/workflows/base.py +4 -4
  89. cognite/neat/workflows/cdf_store.py +3 -3
  90. cognite/neat/workflows/steps/data_contracts.py +1 -1
  91. cognite/neat/workflows/steps/lib/current/graph_extractor.py +3 -3
  92. cognite/neat/workflows/steps/lib/current/graph_loader.py +2 -2
  93. cognite/neat/workflows/steps/lib/current/graph_store.py +1 -1
  94. cognite/neat/workflows/steps/lib/current/rules_exporter.py +116 -47
  95. cognite/neat/workflows/steps/lib/current/rules_importer.py +30 -28
  96. cognite/neat/workflows/steps/lib/current/rules_validator.py +5 -6
  97. cognite/neat/workflows/steps/lib/io/io_steps.py +5 -5
  98. cognite/neat/workflows/steps_registry.py +4 -5
  99. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/METADATA +1 -1
  100. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/RECORD +105 -106
  101. cognite/neat/exceptions.py +0 -145
  102. cognite/neat/graph/exceptions.py +0 -90
  103. cognite/neat/issues/errors/external.py +0 -21
  104. cognite/neat/issues/errors/properties.py +0 -75
  105. cognite/neat/issues/errors/resources.py +0 -123
  106. cognite/neat/issues/errors/schema.py +0 -0
  107. cognite/neat/issues/neat_warnings/__init__.py +0 -2
  108. cognite/neat/issues/neat_warnings/identifier.py +0 -27
  109. cognite/neat/issues/neat_warnings/models.py +0 -22
  110. cognite/neat/issues/neat_warnings/properties.py +0 -77
  111. cognite/neat/issues/neat_warnings/resources.py +0 -125
  112. cognite/neat/rules/issues/__init__.py +0 -22
  113. cognite/neat/rules/issues/base.py +0 -63
  114. cognite/neat/rules/issues/dms.py +0 -549
  115. cognite/neat/rules/issues/fileread.py +0 -197
  116. cognite/neat/rules/issues/ontology.py +0 -298
  117. cognite/neat/rules/issues/spreadsheet.py +0 -563
  118. cognite/neat/rules/issues/spreadsheet_file.py +0 -151
  119. cognite/neat/rules/issues/tables.py +0 -72
  120. cognite/neat/rules/models/_constants.py +0 -1
  121. cognite/neat/rules/models/_types/__init__.py +0 -19
  122. cognite/neat/rules/models/asset/_converter.py +0 -4
  123. cognite/neat/rules/models/dms/_converter.py +0 -145
  124. cognite/neat/workflows/_exceptions.py +0 -41
  125. /cognite/neat/{graph/stores → store}/__init__.py +0 -0
  126. /cognite/neat/{graph/stores → store}/_base.py +0 -0
  127. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/LICENSE +0 -0
  128. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/WHEEL +0 -0
  129. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/entry_points.txt +0 -0
@@ -1,22 +1,16 @@
1
- from collections.abc import Sequence
2
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
3
2
  from datetime import datetime
4
- from typing import Any, Literal, cast, overload
3
+ from typing import Any, Literal
5
4
 
6
5
  from rdflib import Namespace
7
6
 
8
- from cognite.neat.rules.models._base import (
9
- DataModelType,
10
- ExtensionCategory,
11
- SchemaCompleteness,
12
- _add_alias,
13
- )
7
+ from cognite.neat.rules.models._base_input import InputComponent, InputRules
14
8
  from cognite.neat.rules.models.data_types import DataType
15
9
  from cognite.neat.rules.models.entities import (
16
10
  ClassEntity,
17
11
  MultiValueTypeInfo,
18
- Unknown,
19
12
  UnknownEntity,
13
+ load_value_type,
20
14
  )
21
15
 
22
16
  from ._rules import (
@@ -28,7 +22,7 @@ from ._rules import (
28
22
 
29
23
 
30
24
  @dataclass
31
- class InformationMetadataInput:
25
+ class InformationInputMetadata(InputComponent[InformationMetadata]):
32
26
  schema_: Literal["complete", "partial", "extended"]
33
27
  prefix: str
34
28
  namespace: str
@@ -44,47 +38,23 @@ class InformationMetadataInput:
44
38
  rights: str | None = None
45
39
 
46
40
  @classmethod
47
- def load(cls, data: dict[str, Any] | None) -> "InformationMetadataInput | None":
48
- if data is None:
49
- return None
50
- _add_alias(data, InformationMetadata)
51
- return cls(
52
- data_model_type=data.get("data_model_type", "enterprise"),
53
- extension=data.get("extension", "addition"),
54
- schema_=data.get("schema_", "partial"), # type: ignore[arg-type]
55
- version=data.get("version"), # type: ignore[arg-type]
56
- namespace=data.get("namespace"), # type: ignore[arg-type]
57
- prefix=data.get("prefix"), # type: ignore[arg-type]
58
- name=data.get("name"),
59
- creator=data.get("creator"), # type: ignore[arg-type]
60
- description=data.get("description"),
61
- created=data.get("created"),
62
- updated=data.get("updated"),
63
- license=data.get("license"),
64
- rights=data.get("rights"),
65
- )
41
+ def _get_verified_cls(cls) -> type[InformationMetadata]:
42
+ return InformationMetadata
66
43
 
67
- def dump(self) -> dict[str, Any]:
68
- return dict(
69
- dataModelType=DataModelType(self.data_model_type),
70
- schema=SchemaCompleteness(self.schema_),
71
- extension=ExtensionCategory(self.extension),
72
- namespace=Namespace(self.namespace),
73
- prefix=self.prefix,
74
- version=self.version,
75
- name=self.name,
76
- creator=self.creator,
77
- description=self.description,
78
- created=self.created or datetime.now(),
79
- updated=self.updated or datetime.now(),
80
- )
44
+ def dump(self, **kwargs) -> dict[str, Any]:
45
+ output = super().dump()
46
+ if self.created is None:
47
+ output["created"] = datetime.now()
48
+ if self.updated is None:
49
+ output["updated"] = datetime.now()
50
+ return output
81
51
 
82
52
 
83
53
  @dataclass
84
- class InformationPropertyInput:
85
- class_: str
54
+ class InformationInputProperty(InputComponent[InformationProperty]):
55
+ class_: ClassEntity | str
86
56
  property_: str
87
- value_type: str
57
+ value_type: DataType | ClassEntity | MultiValueTypeInfo | UnknownEntity | str
88
58
  name: str | None = None
89
59
  description: str | None = None
90
60
  comment: str | None = None
@@ -94,194 +64,77 @@ class InformationPropertyInput:
94
64
  reference: str | None = None
95
65
  match_type: str | None = None
96
66
  transformation: str | None = None
67
+ # Only used internally
68
+ inherited: bool = False
97
69
 
98
70
  @classmethod
99
- @overload
100
- def load(cls, data: None) -> None: ...
101
-
102
- @classmethod
103
- @overload
104
- def load(cls, data: dict[str, Any]) -> "InformationPropertyInput": ...
105
-
106
- @classmethod
107
- @overload
108
- def load(cls, data: list[dict[str, Any]]) -> list["InformationPropertyInput"]: ...
109
-
110
- @classmethod
111
- def load(
112
- cls, data: dict[str, Any] | list[dict[str, Any]] | None
113
- ) -> "InformationPropertyInput | list[InformationPropertyInput] | None":
114
- if data is None:
115
- return None
116
- if isinstance(data, list) or (isinstance(data, dict) and isinstance(data.get("data"), list)):
117
- items = cast(
118
- list[dict[str, Any]],
119
- data.get("data") if isinstance(data, dict) else data,
120
- )
121
- return [loaded for item in items if (loaded := cls.load(item)) is not None]
122
-
123
- _add_alias(data, InformationProperty)
124
- return cls(
125
- class_=data.get("class_"), # type: ignore[arg-type]
126
- property_=data.get("property_"), # type: ignore[arg-type]
127
- name=data.get("name", None),
128
- description=data.get("description", None),
129
- comment=data.get("comment", None),
130
- value_type=data.get("value_type"), # type: ignore[arg-type]
131
- min_count=data.get("min_count", None),
132
- max_count=data.get("max_count", None),
133
- default=data.get("default", None),
134
- reference=data.get("reference", None),
135
- match_type=data.get("match_type", None),
136
- transformation=data.get("transformation", None),
137
- )
138
-
139
- def dump(self, default_prefix: str) -> dict[str, Any]:
140
- value_type: MultiValueTypeInfo | DataType | ClassEntity | UnknownEntity
141
-
142
- # property holding xsd data type
143
- # check if it is multi value type
144
- if "|" in self.value_type:
145
- value_type = MultiValueTypeInfo.load(self.value_type)
146
- value_type.set_default_prefix(default_prefix)
147
-
148
- elif DataType.is_data_type(self.value_type):
149
- value_type = DataType.load(self.value_type)
71
+ def _get_verified_cls(cls) -> type[InformationProperty]:
72
+ return InformationProperty
150
73
 
151
- # unknown value type
152
- elif self.value_type == str(Unknown):
153
- value_type = UnknownEntity()
154
-
155
- # property holding link to class
156
- else:
157
- value_type = ClassEntity.load(self.value_type, prefix=default_prefix)
158
-
159
- return {
160
- "Class": ClassEntity.load(self.class_, prefix=default_prefix),
161
- "Property": self.property_,
162
- "Name": self.name,
163
- "Description": self.description,
164
- "Comment": self.comment,
165
- "Value Type": value_type,
166
- "Min Count": self.min_count,
167
- "Max Count": self.max_count,
168
- "Default": self.default,
169
- "Reference": self.reference,
170
- "Match Type": self.match_type,
171
- "Transformation": self.transformation,
172
- }
74
+ def dump(self, default_prefix: str, **kwargs) -> dict[str, Any]: # type: ignore[override]
75
+ output = super().dump()
76
+ output["Class"] = ClassEntity.load(self.class_, prefix=default_prefix)
77
+ output["Value Type"] = load_value_type(self.value_type, default_prefix)
78
+ return output
173
79
 
174
80
 
175
81
  @dataclass
176
- class InformationClassInput:
177
- class_: str
82
+ class InformationInputClass(InputComponent[InformationClass]):
83
+ class_: ClassEntity | str
178
84
  name: str | None = None
179
85
  description: str | None = None
180
86
  comment: str | None = None
181
- parent: str | None = None
87
+ parent: str | list[ClassEntity] | None = None
182
88
  reference: str | None = None
183
89
  match_type: str | None = None
184
90
 
185
91
  @classmethod
186
- @overload
187
- def load(cls, data: None) -> None: ...
188
-
189
- @classmethod
190
- @overload
191
- def load(cls, data: dict[str, Any]) -> "InformationClassInput": ...
92
+ def _get_verified_cls(cls) -> type[InformationClass]:
93
+ return InformationClass
192
94
 
193
- @classmethod
194
- @overload
195
- def load(cls, data: list[dict[str, Any]]) -> list["InformationClassInput"]: ...
196
-
197
- @classmethod
198
- def load(
199
- cls, data: dict[str, Any] | list[dict[str, Any]] | None
200
- ) -> "InformationClassInput | list[InformationClassInput] | None":
201
- if data is None:
202
- return None
203
- if isinstance(data, list) or (isinstance(data, dict) and isinstance(data.get("data"), list)):
204
- items = cast(
205
- list[dict[str, Any]],
206
- data.get("data") if isinstance(data, dict) else data,
207
- )
208
- return [loaded for item in items if (loaded := cls.load(item)) is not None]
209
- _add_alias(data, InformationClass)
210
- return cls(
211
- class_=data.get("class_"), # type: ignore[arg-type]
212
- name=data.get("name", None),
213
- description=data.get("description", None),
214
- comment=data.get("comment", None),
215
- parent=data.get("parent", None),
216
- reference=data.get("reference", None),
217
- match_type=data.get("match_type", None),
218
- )
95
+ @property
96
+ def class_str(self) -> str:
97
+ return str(self.class_)
219
98
 
220
- def dump(self, default_prefix: str) -> dict[str, Any]:
221
- return {
222
- "Class": ClassEntity.load(self.class_, prefix=default_prefix),
223
- "Name": self.name,
224
- "Description": self.description,
225
- "Comment": self.comment,
226
- "Reference": self.reference,
227
- "Match Type": self.match_type,
228
- "Parent Class": (
229
- [ClassEntity.load(parent, prefix=default_prefix) for parent in self.parent.split(",")]
230
- if self.parent
231
- else None
232
- ),
233
- }
99
+ def dump(self, default_prefix: str, **kwargs) -> dict[str, Any]: # type: ignore[override]
100
+ output = super().dump()
101
+ parent: list[ClassEntity] | None = None
102
+ if isinstance(self.parent, str):
103
+ parent = [ClassEntity.load(parent, prefix=default_prefix) for parent in self.parent.split(",")]
104
+ elif isinstance(self.parent, list):
105
+ parent = [ClassEntity.load(parent_, prefix=default_prefix) for parent_ in self.parent]
106
+ output["Class"] = ClassEntity.load(self.class_, prefix=default_prefix)
107
+ output["Parent Class"] = parent
108
+ return output
234
109
 
235
110
 
236
111
  @dataclass
237
- class InformationRulesInput:
238
- metadata: InformationMetadataInput
239
- properties: Sequence[InformationPropertyInput]
240
- classes: Sequence[InformationClassInput]
241
- prefixes: "dict[str, Namespace] | None" = None
242
- last: "InformationRulesInput | InformationRules | None" = None
243
- reference: "InformationRulesInput | InformationRules | None" = None
112
+ class InformationInputRules(InputRules[InformationRules]):
113
+ metadata: InformationInputMetadata
114
+ properties: list[InformationInputProperty] = field(default_factory=list)
115
+ classes: list[InformationInputClass] = field(default_factory=list)
116
+ prefixes: dict[str, Namespace] | None = None
117
+ last: "InformationInputRules | None" = None
118
+ reference: "InformationInputRules | None" = None
244
119
 
245
120
  @classmethod
246
- @overload
247
- def load(cls, data: dict[str, Any]) -> "InformationRulesInput": ...
248
-
249
- @classmethod
250
- @overload
251
- def load(cls, data: None) -> None: ...
252
-
253
- @classmethod
254
- def load(cls, data: dict | None) -> "InformationRulesInput | None":
255
- if data is None:
256
- return None
257
- _add_alias(data, InformationRules)
258
-
259
- return cls(
260
- metadata=InformationMetadataInput.load(data.get("metadata")), # type: ignore[arg-type]
261
- properties=InformationPropertyInput.load(data.get("properties")), # type: ignore[arg-type]
262
- classes=InformationClassInput.load(data.get("classes")), # type: ignore[arg-type]
263
- prefixes=data.get("prefixes"),
264
- last=InformationRulesInput.load(data.get("last")),
265
- reference=InformationRulesInput.load(data.get("reference")),
266
- )
267
-
268
- def as_rules(self) -> InformationRules:
269
- return InformationRules.model_validate(self.dump())
121
+ def _get_verified_cls(cls) -> type[InformationRules]:
122
+ return InformationRules
270
123
 
271
124
  def dump(self) -> dict[str, Any]:
272
125
  default_prefix = self.metadata.prefix
273
126
  reference: dict[str, Any] | None = None
274
- if isinstance(self.reference, InformationRulesInput):
127
+ if isinstance(self.reference, InformationInputRules):
275
128
  reference = self.reference.dump()
276
129
  elif isinstance(self.reference, InformationRules):
277
130
  # We need to load through the InformationRulesInput to set the correct default space and version
278
- reference = InformationRulesInput.load(self.reference.model_dump()).dump()
131
+ reference = InformationInputRules.load(self.reference.model_dump()).dump()
279
132
  last: dict[str, Any] | None = None
280
- if isinstance(self.last, InformationRulesInput):
133
+ if isinstance(self.last, InformationInputRules):
281
134
  last = self.last.dump()
282
135
  elif isinstance(self.last, InformationRules):
283
136
  # We need to load through the InformationRulesInput to set the correct default space and version
284
- last = InformationRulesInput.load(self.last.model_dump()).dump()
137
+ last = InformationInputRules.load(self.last.model_dump()).dump()
285
138
 
286
139
  return dict(
287
140
  Metadata=self.metadata.dump(),
@@ -3,8 +3,8 @@ from collections import Counter
3
3
  from typing import cast
4
4
 
5
5
  from cognite.neat.issues import IssueList
6
- from cognite.neat.rules import issues
7
- from cognite.neat.rules.models._base import DataModelType, SchemaCompleteness
6
+ from cognite.neat.issues.errors import NeatValueError, ResourceNotDefinedError
7
+ from cognite.neat.rules.models._base_rules import DataModelType, SchemaCompleteness
8
8
  from cognite.neat.rules.models.entities import ClassEntity, EntityTypes, UnknownEntity
9
9
  from cognite.neat.utils.rdf_ import get_inheritance_path
10
10
 
@@ -58,9 +58,9 @@ class InformationPostValidation:
58
58
  ):
59
59
  dangling_classes.add(class_)
60
60
 
61
- if dangling_classes:
61
+ for class_ in dangling_classes:
62
62
  self.issue_list.append(
63
- issues.spreadsheet.ClassNoPropertiesNoParentError([class_.versioned_id for class_ in dangling_classes])
63
+ NeatValueError(f"Class {class_} has no properties and is not a parent of any class with properties")
64
64
  )
65
65
 
66
66
  def _referenced_parent_classes_exist(self) -> None:
@@ -70,9 +70,15 @@ class InformationPostValidation:
70
70
  parents = set(itertools.chain.from_iterable(class_parent_pairs.values()))
71
71
 
72
72
  if undefined_parents := parents.difference(classes):
73
- self.issue_list.append(
74
- issues.spreadsheet.ParentClassesNotDefinedError([missing.versioned_id for missing in undefined_parents])
75
- )
73
+ for parent in undefined_parents:
74
+ # Todo: include row and column number
75
+ self.issue_list.append(
76
+ ResourceNotDefinedError[ClassEntity](
77
+ resource_type="class",
78
+ identifier=parent,
79
+ location="Classes sheet",
80
+ )
81
+ )
76
82
 
77
83
  def _referenced_classes_exist(self) -> None:
78
84
  # needs to be complete for this validation to pass
@@ -83,21 +89,27 @@ class InformationPostValidation:
83
89
  if self.metadata.schema_ == SchemaCompleteness.complete and (
84
90
  missing_classes := referred_classes.difference(defined_classes)
85
91
  ):
86
- self.issue_list.append(
87
- issues.spreadsheet.PropertiesDefinedForUndefinedClassesError(
88
- [missing.versioned_id for missing in missing_classes]
92
+ for class_ in missing_classes:
93
+ self.issue_list.append(
94
+ ResourceNotDefinedError[ClassEntity](
95
+ resource_type="class",
96
+ identifier=class_,
97
+ location="Classes sheet",
98
+ )
89
99
  )
90
- )
91
100
 
92
101
  # USE CASE: models are extended (user + last = complete)
93
102
  if self.metadata.schema_ == SchemaCompleteness.extended:
94
103
  defined_classes |= {class_.class_ for class_ in cast(InformationRules, self.rules.last).classes}
95
104
  if missing_classes := referred_classes.difference(defined_classes):
96
- self.issue_list.append(
97
- issues.spreadsheet.PropertiesDefinedForUndefinedClassesError(
98
- [missing.versioned_id for missing in missing_classes]
105
+ for class_ in missing_classes:
106
+ self.issue_list.append(
107
+ ResourceNotDefinedError[ClassEntity](
108
+ resource_type="class",
109
+ identifier=class_,
110
+ location="Classes sheet",
111
+ )
99
112
  )
100
- )
101
113
 
102
114
  def _referenced_value_types_exist(self) -> None:
103
115
  # adding UnknownEntity to the set of defined classes to handle the case where a property references an unknown
@@ -112,21 +124,29 @@ class InformationPostValidation:
112
124
  if self.metadata.schema_ == SchemaCompleteness.complete and (
113
125
  missing_value_types := referred_object_types.difference(defined_classes)
114
126
  ):
115
- self.issue_list.append(
116
- issues.spreadsheet.ValueTypeNotDefinedError(
117
- [cast(ClassEntity, missing).versioned_id for missing in missing_value_types]
127
+ # Todo: include row and column number
128
+ for missing in missing_value_types:
129
+ self.issue_list.append(
130
+ ResourceNotDefinedError[ClassEntity](
131
+ resource_type="class",
132
+ identifier=cast(ClassEntity, missing),
133
+ location="Classes sheet",
134
+ )
118
135
  )
119
- )
120
136
 
121
137
  # USE CASE: models are extended (user + last = complete)
122
138
  if self.metadata.schema_ == SchemaCompleteness.extended:
123
139
  defined_classes |= {class_.class_ for class_ in cast(InformationRules, self.rules.last).classes}
124
140
  if missing_value_types := referred_object_types.difference(defined_classes):
125
- self.issue_list.append(
126
- issues.spreadsheet.ValueTypeNotDefinedError(
127
- [cast(ClassEntity, missing).versioned_id for missing in missing_value_types]
141
+ # Todo: include row and column number
142
+ for missing in missing_value_types:
143
+ self.issue_list.append(
144
+ ResourceNotDefinedError(
145
+ resource_type="class",
146
+ identifier=cast(ClassEntity, missing),
147
+ location="Classes sheet",
148
+ )
128
149
  )
129
- )
130
150
 
131
151
  def _class_parent_pairs(self) -> dict[ClassEntity, list[ClassEntity]]:
132
152
  class_subclass_pairs: dict[ClassEntity, list[ClassEntity]] = {}
@@ -173,7 +193,10 @@ class InformationPostValidation:
173
193
  reused_namespaces = [value for value, count in Counter(prefixes.values()).items() if count > 1]
174
194
  impacted_prefixes = [key for key, value in prefixes.items() if value in reused_namespaces]
175
195
  self.issue_list.append(
176
- issues.spreadsheet.PrefixNamespaceCollisionError(
177
- prefixes=impacted_prefixes, namespaces=reused_namespaces
196
+ NeatValueError(
197
+ "Namespace collision detected. The following prefixes "
198
+ f"are assigned to the same namespace: {impacted_prefixes}"
199
+ f"\nImpacted namespaces: {reused_namespaces}"
200
+ "\nMake sure that each unique namespace is assigned to a unique prefix"
178
201
  )
179
202
  )
@@ -0,0 +1,21 @@
1
+ from ._base import RulesPipeline, RulesTransformer
2
+ from ._converters import AssetToInformation, ConvertToRules, DMSToInformation, InformationToAsset, InformationToDMS
3
+ from ._map_onto import MapOneToOne
4
+ from ._pipelines import ImporterPipeline
5
+ from ._verification import VerifyAnyRules, VerifyAssetRules, VerifyDMSRules, VerifyInformationRules
6
+
7
+ __all__ = [
8
+ "ImporterPipeline",
9
+ "RulesTransformer",
10
+ "RulesPipeline",
11
+ "InformationToDMS",
12
+ "InformationToAsset",
13
+ "ConvertToRules",
14
+ "AssetToInformation",
15
+ "DMSToInformation",
16
+ "VerifyAssetRules",
17
+ "VerifyDMSRules",
18
+ "VerifyInformationRules",
19
+ "VerifyAnyRules",
20
+ "MapOneToOne",
21
+ ]
@@ -0,0 +1,81 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import MutableSequence
3
+ from typing import Generic, TypeVar
4
+
5
+ from cognite.neat.issues import IssueList, NeatError
6
+ from cognite.neat.issues.errors import NeatTypeError, NeatValueError
7
+ from cognite.neat.rules._shared import (
8
+ InputRules,
9
+ JustRules,
10
+ MaybeRules,
11
+ OutRules,
12
+ Rules,
13
+ VerifiedRules,
14
+ )
15
+
16
+ T_RulesIn = TypeVar("T_RulesIn", bound=Rules)
17
+ T_RulesOut = TypeVar("T_RulesOut", bound=Rules)
18
+
19
+
20
+ class RulesTransformer(ABC, Generic[T_RulesIn, T_RulesOut]):
21
+ """This is the base class for all rule transformers.
22
+
23
+ Note transformers follow the functional pattern Monad
24
+ https://en.wikipedia.org/wiki/Monad_(functional_programming)
25
+ """
26
+
27
+ @abstractmethod
28
+ def transform(self, rules: T_RulesIn | OutRules[T_RulesIn]) -> OutRules[T_RulesOut]:
29
+ """Transform the input rules into the output rules."""
30
+ raise NotImplementedError()
31
+
32
+ def try_transform(self, rules: MaybeRules[T_RulesIn]) -> MaybeRules[T_RulesOut]:
33
+ """Try to transform the input rules into the output rules."""
34
+ try:
35
+ result = self.transform(rules)
36
+ except NeatError:
37
+ # Any error caught during transformation will be returned as issues
38
+ return MaybeRules(None, rules.issues)
39
+ issues = IssueList(rules.issues, title=rules.issues.title)
40
+ if isinstance(result, MaybeRules):
41
+ issues.extend(result.issues)
42
+ return MaybeRules(result.get_rules(), issues)
43
+
44
+ @classmethod
45
+ def _to_rules(cls, rules: T_RulesIn | OutRules[T_RulesIn]) -> T_RulesIn:
46
+ if isinstance(rules, JustRules):
47
+ return rules.rules
48
+ elif isinstance(rules, MaybeRules):
49
+ if rules.rules is None:
50
+ raise NeatValueError("Rules is missing cannot convert")
51
+ return rules.rules
52
+ elif isinstance(rules, VerifiedRules | InputRules):
53
+ return rules # type: ignore[return-value]
54
+ else:
55
+ raise NeatTypeError(f"Unsupported type: {type(rules)}")
56
+
57
+
58
+ class RulesPipeline(list, MutableSequence[RulesTransformer], Generic[T_RulesIn, T_RulesOut]):
59
+ def transform(self, rules: T_RulesIn | OutRules[T_RulesIn]) -> OutRules[T_RulesOut]:
60
+ """Transform the input rules into the output rules."""
61
+ for transformer in self:
62
+ rules = transformer.transform(rules)
63
+ return rules # type: ignore[return-value]
64
+
65
+ def try_transform(self, rules: MaybeRules[T_RulesIn]) -> MaybeRules[T_RulesOut]:
66
+ """Try to transform the input rules into the output rules."""
67
+ for transformer in self:
68
+ rules = transformer.try_transform(rules)
69
+ return rules # type: ignore[return-value]
70
+
71
+ def run(self, rules: T_RulesIn | OutRules[T_RulesIn]) -> T_RulesOut:
72
+ """Run the pipeline from the input rules to the output rules."""
73
+ output = self.transform(rules)
74
+ if isinstance(output, MaybeRules):
75
+ if output.rules is None:
76
+ raise NeatValueError(f"Rule transformation failed: {output.issues}")
77
+ return output.rules
78
+ elif isinstance(output, JustRules):
79
+ return output.rules
80
+ else:
81
+ raise NeatTypeError(f"Rule transformation failed: {output}")