cognite-neat 0.88.3__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 (75) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/constants.py +3 -0
  3. cognite/neat/graph/extractors/_mock_graph_generator.py +2 -1
  4. cognite/neat/issues/_base.py +2 -1
  5. cognite/neat/issues/errors/__init__.py +2 -1
  6. cognite/neat/issues/errors/_general.py +7 -0
  7. cognite/neat/issues/warnings/_models.py +1 -1
  8. cognite/neat/issues/warnings/user_modeling.py +1 -1
  9. cognite/neat/rules/_shared.py +49 -6
  10. cognite/neat/rules/analysis/_base.py +1 -1
  11. cognite/neat/rules/exporters/_base.py +7 -18
  12. cognite/neat/rules/exporters/_rules2dms.py +8 -18
  13. cognite/neat/rules/exporters/_rules2excel.py +5 -12
  14. cognite/neat/rules/exporters/_rules2ontology.py +9 -19
  15. cognite/neat/rules/exporters/_rules2yaml.py +3 -6
  16. cognite/neat/rules/importers/_base.py +7 -52
  17. cognite/neat/rules/importers/_dms2rules.py +171 -115
  18. cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +26 -18
  19. cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +14 -30
  20. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2classes.py +7 -3
  21. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2metadata.py +3 -3
  22. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2properties.py +18 -11
  23. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2rules.py +9 -18
  24. cognite/neat/rules/importers/_rdf/_inference2rules.py +10 -33
  25. cognite/neat/rules/importers/_rdf/_owl2rules/_owl2rules.py +9 -20
  26. cognite/neat/rules/importers/_rdf/_shared.py +1 -1
  27. cognite/neat/rules/importers/_spreadsheet2rules.py +22 -86
  28. cognite/neat/rules/importers/_yaml2rules.py +14 -41
  29. cognite/neat/rules/models/__init__.py +21 -5
  30. cognite/neat/rules/models/_base_input.py +162 -0
  31. cognite/neat/rules/models/{_base.py → _base_rules.py} +1 -12
  32. cognite/neat/rules/models/asset/__init__.py +5 -2
  33. cognite/neat/rules/models/asset/_rules.py +2 -20
  34. cognite/neat/rules/models/asset/_rules_input.py +40 -115
  35. cognite/neat/rules/models/asset/_validation.py +1 -1
  36. cognite/neat/rules/models/data_types.py +150 -44
  37. cognite/neat/rules/models/dms/__init__.py +19 -7
  38. cognite/neat/rules/models/dms/_exporter.py +72 -26
  39. cognite/neat/rules/models/dms/_rules.py +42 -155
  40. cognite/neat/rules/models/dms/_rules_input.py +186 -254
  41. cognite/neat/rules/models/dms/_serializer.py +44 -3
  42. cognite/neat/rules/models/dms/_validation.py +3 -4
  43. cognite/neat/rules/models/domain.py +52 -1
  44. cognite/neat/rules/models/entities/__init__.py +63 -0
  45. cognite/neat/rules/models/entities/_constants.py +73 -0
  46. cognite/neat/rules/models/entities/_loaders.py +76 -0
  47. cognite/neat/rules/models/entities/_multi_value.py +67 -0
  48. cognite/neat/rules/models/{entities.py → entities/_single_value.py} +74 -232
  49. cognite/neat/rules/models/entities/_types.py +86 -0
  50. cognite/neat/rules/models/{wrapped_entities.py → entities/_wrapped.py} +1 -1
  51. cognite/neat/rules/models/information/__init__.py +10 -2
  52. cognite/neat/rules/models/information/_rules.py +3 -14
  53. cognite/neat/rules/models/information/_rules_input.py +57 -204
  54. cognite/neat/rules/models/information/_validation.py +1 -1
  55. cognite/neat/rules/transformers/__init__.py +21 -0
  56. cognite/neat/rules/transformers/_base.py +69 -3
  57. cognite/neat/rules/{models/information/_converter.py → transformers/_converters.py} +216 -20
  58. cognite/neat/rules/transformers/_map_onto.py +97 -0
  59. cognite/neat/rules/transformers/_pipelines.py +61 -0
  60. cognite/neat/rules/transformers/_verification.py +136 -0
  61. cognite/neat/store/_provenance.py +10 -1
  62. cognite/neat/utils/cdf/data_classes.py +20 -0
  63. cognite/neat/utils/regex_patterns.py +6 -0
  64. cognite/neat/workflows/steps/lib/current/rules_exporter.py +106 -37
  65. cognite/neat/workflows/steps/lib/current/rules_importer.py +24 -22
  66. {cognite_neat-0.88.3.dist-info → cognite_neat-0.89.0.dist-info}/METADATA +1 -1
  67. {cognite_neat-0.88.3.dist-info → cognite_neat-0.89.0.dist-info}/RECORD +71 -66
  68. cognite/neat/rules/models/_constants.py +0 -2
  69. cognite/neat/rules/models/_types/__init__.py +0 -19
  70. cognite/neat/rules/models/asset/_converter.py +0 -4
  71. cognite/neat/rules/models/dms/_converter.py +0 -143
  72. /cognite/neat/rules/models/{_types/_field.py → _types.py} +0 -0
  73. {cognite_neat-0.88.3.dist-info → cognite_neat-0.89.0.dist-info}/LICENSE +0 -0
  74. {cognite_neat-0.88.3.dist-info → cognite_neat-0.89.0.dist-info}/WHEEL +0 -0
  75. {cognite_neat-0.88.3.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(),
@@ -4,7 +4,7 @@ from typing import cast
4
4
 
5
5
  from cognite.neat.issues import IssueList
6
6
  from cognite.neat.issues.errors import NeatValueError, ResourceNotDefinedError
7
- from cognite.neat.rules.models._base import DataModelType, SchemaCompleteness
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
 
@@ -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
+ ]
@@ -1,15 +1,81 @@
1
1
  from abc import ABC, abstractmethod
2
+ from collections.abc import MutableSequence
2
3
  from typing import Generic, TypeVar
3
4
 
4
- from cognite.neat.rules._shared import Rules
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
+ )
5
15
 
6
16
  T_RulesIn = TypeVar("T_RulesIn", bound=Rules)
7
17
  T_RulesOut = TypeVar("T_RulesOut", bound=Rules)
8
18
 
9
19
 
10
20
  class RulesTransformer(ABC, Generic[T_RulesIn, T_RulesOut]):
11
- """This is the base class for all rule transformers."""
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
+ """
12
26
 
13
27
  @abstractmethod
14
- def transform(self, rules: T_RulesIn) -> T_RulesOut:
28
+ def transform(self, rules: T_RulesIn | OutRules[T_RulesIn]) -> OutRules[T_RulesOut]:
29
+ """Transform the input rules into the output rules."""
15
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}")