cognite-neat 0.77.3__py3-none-any.whl → 0.77.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cognite-neat might be problematic. Click here for more details.

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