cognite-neat 0.90.2__py3-none-any.whl → 0.92.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 (41) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/graph/extractors/__init__.py +5 -0
  3. cognite/neat/graph/extractors/_classic_cdf/_assets.py +8 -8
  4. cognite/neat/graph/extractors/_classic_cdf/_base.py +26 -1
  5. cognite/neat/graph/extractors/_classic_cdf/_classic.py +208 -0
  6. cognite/neat/graph/extractors/_classic_cdf/_data_sets.py +110 -0
  7. cognite/neat/graph/extractors/_classic_cdf/_events.py +30 -5
  8. cognite/neat/graph/extractors/_classic_cdf/_files.py +33 -8
  9. cognite/neat/graph/extractors/_classic_cdf/_labels.py +14 -6
  10. cognite/neat/graph/extractors/_classic_cdf/_relationships.py +38 -7
  11. cognite/neat/graph/extractors/_classic_cdf/_sequences.py +30 -5
  12. cognite/neat/graph/extractors/_classic_cdf/_timeseries.py +30 -5
  13. cognite/neat/graph/extractors/_dexpi.py +4 -4
  14. cognite/neat/graph/extractors/_iodd.py +160 -0
  15. cognite/neat/issues/_base.py +6 -2
  16. cognite/neat/rules/exporters/_rules2excel.py +3 -3
  17. cognite/neat/rules/exporters/_rules2yaml.py +5 -1
  18. cognite/neat/rules/models/__init__.py +2 -2
  19. cognite/neat/rules/models/_base_input.py +2 -2
  20. cognite/neat/rules/models/_base_rules.py +142 -142
  21. cognite/neat/rules/models/asset/_rules.py +1 -34
  22. cognite/neat/rules/models/dms/_rules.py +127 -46
  23. cognite/neat/rules/models/dms/_validation.py +2 -2
  24. cognite/neat/rules/models/domain.py +16 -19
  25. cognite/neat/rules/models/entities/_single_value.py +25 -11
  26. cognite/neat/rules/models/entities/_types.py +0 -10
  27. cognite/neat/rules/models/information/_rules.py +68 -43
  28. cognite/neat/rules/models/information/_validation.py +5 -5
  29. cognite/neat/rules/transformers/_converters.py +6 -8
  30. cognite/neat/rules/transformers/_pipelines.py +8 -4
  31. cognite/neat/store/_base.py +1 -1
  32. cognite/neat/utils/collection_.py +4 -3
  33. cognite/neat/utils/xml_.py +27 -12
  34. {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/METADATA +1 -1
  35. {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/RECORD +38 -38
  36. cognite/neat/rules/models/asset/_serializer.py +0 -73
  37. cognite/neat/rules/models/dms/_serializer.py +0 -157
  38. cognite/neat/rules/models/information/_serializer.py +0 -73
  39. {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/LICENSE +0 -0
  40. {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/WHEEL +0 -0
  41. {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/entry_points.txt +0 -0
@@ -1,13 +1,13 @@
1
1
  import math
2
2
  import sys
3
3
  import warnings
4
+ from collections.abc import Hashable
4
5
  from datetime import datetime
5
6
  from typing import TYPE_CHECKING, Any, ClassVar, Literal
6
7
 
7
8
  from cognite.client import data_modeling as dm
8
9
  from pydantic import Field, field_serializer, field_validator, model_validator
9
- from pydantic.main import IncEx
10
- from pydantic_core.core_schema import ValidationInfo
10
+ from pydantic_core.core_schema import SerializationInfo, ValidationInfo
11
11
 
12
12
  from cognite.neat.issues import MultiValueError
13
13
  from cognite.neat.issues.warnings import (
@@ -21,8 +21,8 @@ from cognite.neat.rules.models._base_rules import (
21
21
  ExtensionCategory,
22
22
  RoleTypes,
23
23
  SchemaCompleteness,
24
- SheetEntity,
25
24
  SheetList,
25
+ SheetRow,
26
26
  )
27
27
  from cognite.neat.rules.models._types import (
28
28
  ExternalIdType,
@@ -35,9 +35,11 @@ from cognite.neat.rules.models.entities import (
35
35
  ClassEntity,
36
36
  ContainerEntity,
37
37
  ContainerEntityList,
38
+ DMSEntity,
38
39
  DMSNodeEntity,
39
40
  DMSUnknownEntity,
40
41
  EdgeEntity,
42
+ Entity,
41
43
  HasDataFilter,
42
44
  NodeTypeFilter,
43
45
  RawFilter,
@@ -91,8 +93,7 @@ class DMSMetadata(BaseMetadata):
91
93
  return value
92
94
 
93
95
  @field_serializer("schema_", "extension", "data_model_type", when_used="always")
94
- @staticmethod
95
- def as_string(value: SchemaCompleteness | ExtensionCategory | DataModelType) -> str:
96
+ def as_string(self, value: SchemaCompleteness | ExtensionCategory | DataModelType) -> str:
96
97
  return str(value)
97
98
 
98
99
  @field_validator("schema_", mode="plain")
@@ -144,7 +145,13 @@ class DMSMetadata(BaseMetadata):
144
145
  return self.space
145
146
 
146
147
 
147
- class DMSProperty(SheetEntity):
148
+ def _metadata(context: Any) -> DMSMetadata | None:
149
+ if isinstance(context, dict) and isinstance(context.get("metadata"), DMSMetadata):
150
+ return context["metadata"]
151
+ return None
152
+
153
+
154
+ class DMSProperty(SheetRow):
148
155
  view: ViewEntity = Field(alias="View")
149
156
  view_property: str = Field(alias="View Property")
150
157
  name: str | None = Field(alias="Name", default=None)
@@ -163,6 +170,9 @@ class DMSProperty(SheetEntity):
163
170
  class_: ClassEntity = Field(alias="Class (linage)")
164
171
  property_: PropertyType = Field(alias="Property (linage)")
165
172
 
173
+ def _identifier(self) -> tuple[Hashable, ...]:
174
+ return self.view, self.view_property
175
+
166
176
  @field_validator("nullable")
167
177
  def direct_relation_must_be_nullable(cls, value: Any, info: ValidationInfo) -> None:
168
178
  if info.data.get("connection") == "direct" and value is False:
@@ -183,16 +193,46 @@ class DMSProperty(SheetEntity):
183
193
  raise ValueError(f"Reverse connection must have a value type that points to a view, got {value}")
184
194
  return value
185
195
 
196
+ @field_serializer("reference", when_used="always")
197
+ def set_reference(self, value: Any, info: SerializationInfo) -> str | None:
198
+ if isinstance(info.context, dict) and info.context.get("as_reference") is True:
199
+ return str(
200
+ ReferenceEntity(
201
+ prefix=self.view.prefix,
202
+ suffix=self.view.suffix,
203
+ version=self.view.version,
204
+ property=self.view_property,
205
+ )
206
+ )
207
+ return str(value) if value is not None else None
208
+
186
209
  @field_serializer("value_type", when_used="always")
187
- @staticmethod
188
- def as_dms_type(value_type: DataType | EdgeEntity | ViewEntity) -> str:
210
+ def as_dms_type(self, value_type: DataType | EdgeEntity | ViewEntity, info: SerializationInfo) -> str:
189
211
  if isinstance(value_type, DataType):
190
212
  return value_type._suffix_extra_args(value_type.dms._type)
191
- else:
192
- return str(value_type)
213
+ elif isinstance(value_type, EdgeEntity | ViewEntity) and (metadata := _metadata(info.context)):
214
+ return value_type.dump(space=metadata.space, version=metadata.version)
215
+ return str(value_type)
216
+
217
+ @field_serializer("view", "container", "class_", when_used="unless-none")
218
+ def remove_default_space(self, value: str, info: SerializationInfo) -> str:
219
+ if (metadata := _metadata(info.context)) and isinstance(value, Entity):
220
+ if info.field_name == "container" and info.context.get("as_reference") is True:
221
+ # When dumping as reference, the container should keep the default space for easy copying
222
+ # over to user sheets.
223
+ return value.dump()
224
+ return value.dump(prefix=metadata.space, version=metadata.version)
225
+ return str(value)
226
+
227
+ @field_serializer("connection", when_used="unless-none")
228
+ def remove_defaults(self, value: Any, info: SerializationInfo) -> str:
229
+ if isinstance(value, Entity) and (metadata := _metadata(info.context)):
230
+ default_type = f"{metadata.space}{self.view.external_id}.{self.view_property}"
231
+ return value.dump(space=metadata.space, version=metadata.version, type=default_type)
232
+ return str(value)
193
233
 
194
234
 
195
- class DMSContainer(SheetEntity):
235
+ class DMSContainer(SheetRow):
196
236
  container: ContainerEntity = Field(alias="Container")
197
237
  name: str | None = Field(alias="Name", default=None)
198
238
  description: str | None = Field(alias="Description", default=None)
@@ -201,6 +241,9 @@ class DMSContainer(SheetEntity):
201
241
  used_for: Literal["node", "edge", "all"] | None = Field("all", alias="Used For")
202
242
  class_: ClassEntity = Field(alias="Class (linage)")
203
243
 
244
+ def _identifier(self) -> tuple[Hashable, ...]:
245
+ return (self.container,)
246
+
204
247
  def as_container(self) -> dm.ContainerApply:
205
248
  container_id = self.container.as_id()
206
249
  constraints: dict[str, dm.Constraint] = {}
@@ -218,8 +261,34 @@ class DMSContainer(SheetEntity):
218
261
  used_for=self.used_for,
219
262
  )
220
263
 
264
+ @field_serializer("reference", when_used="always")
265
+ def set_reference(self, value: Any, info: SerializationInfo) -> str | None:
266
+ if isinstance(info.context, dict) and info.context.get("as_reference") is True:
267
+ return self.container.dump()
268
+ return str(value) if value is not None else None
269
+
270
+ @field_serializer("container", "class_", when_used="unless-none")
271
+ def remove_default_space(self, value: Any, info: SerializationInfo) -> str:
272
+ if metadata := _metadata(info.context):
273
+ if isinstance(value, DMSEntity):
274
+ return value.dump(space=metadata.space, version=metadata.version)
275
+ elif isinstance(value, Entity):
276
+ return value.dump(prefix=metadata.space, version=metadata.version)
277
+ return str(value)
278
+
279
+ @field_serializer("constraint", when_used="unless-none")
280
+ def remove_default_spaces(self, value: Any, info: SerializationInfo) -> str:
281
+ if isinstance(value, list) and (metadata := _metadata(info.context)):
282
+ return ",".join(
283
+ constraint.dump(space=metadata.space, version=metadata.version)
284
+ if isinstance(constraint, DMSEntity)
285
+ else str(constraint)
286
+ for constraint in value
287
+ )
288
+ return ",".join(str(value) for value in value)
289
+
221
290
 
222
- class DMSView(SheetEntity):
291
+ class DMSView(SheetRow):
223
292
  view: ViewEntity = Field(alias="View")
224
293
  name: str | None = Field(alias="Name", default=None)
225
294
  description: str | None = Field(alias="Description", default=None)
@@ -229,6 +298,32 @@ class DMSView(SheetEntity):
229
298
  in_model: bool = Field(True, alias="In Model")
230
299
  class_: ClassEntity = Field(alias="Class (linage)")
231
300
 
301
+ def _identifier(self) -> tuple[Hashable, ...]:
302
+ return (self.view,)
303
+
304
+ @field_serializer("reference", when_used="always")
305
+ def set_reference(self, value: Any, info: SerializationInfo) -> str | None:
306
+ if isinstance(info.context, dict) and info.context.get("as_reference") is True:
307
+ return self.view.dump()
308
+ return str(value) if value is not None else None
309
+
310
+ @field_serializer("view", "class_", when_used="unless-none")
311
+ def remove_default_space(self, value: Any, info: SerializationInfo) -> str:
312
+ if (metadata := _metadata(info.context)) and isinstance(value, Entity):
313
+ return value.dump(prefix=metadata.space, version=metadata.version)
314
+ return str(value)
315
+
316
+ @field_serializer("implements", when_used="unless-none")
317
+ def remove_default_spaces(self, value: Any, info: SerializationInfo) -> str:
318
+ if isinstance(value, list) and (metadata := _metadata(info.context)):
319
+ return ",".join(
320
+ parent.dump(space=metadata.space, version=metadata.version)
321
+ if isinstance(parent, DMSEntity)
322
+ else str(parent)
323
+ for parent in value
324
+ )
325
+ return ",".join(str(value) for value in value) if isinstance(value, list) else value
326
+
232
327
  def as_view(self) -> dm.ViewApply:
233
328
  view_id = self.view.as_id()
234
329
  implements = [parent.as_id() for parent in self.implements or []] or None
@@ -249,12 +344,15 @@ class DMSView(SheetEntity):
249
344
  )
250
345
 
251
346
 
252
- class DMSNode(SheetEntity):
347
+ class DMSNode(SheetRow):
253
348
  node: DMSNodeEntity = Field(alias="Node")
254
349
  usage: Literal["type", "collection"] = Field(alias="Usage")
255
350
  name: str | None = Field(alias="Name", default=None)
256
351
  description: str | None = Field(alias="Description", default=None)
257
352
 
353
+ def _identifier(self) -> tuple[Hashable, ...]:
354
+ return (self.node,)
355
+
258
356
  def as_node(self) -> dm.NodeApply:
259
357
  if self.usage == "type":
260
358
  return dm.NodeApply(space=self.node.space, external_id=self.node.external_id)
@@ -263,13 +361,28 @@ class DMSNode(SheetEntity):
263
361
  else:
264
362
  raise ValueError(f"Unknown usage {self.usage}")
265
363
 
364
+ @field_serializer("node", when_used="unless-none")
365
+ def remove_default_space(self, value: Any, info: SerializationInfo) -> str:
366
+ if isinstance(value, DMSEntity) and (metadata := _metadata(info.context)):
367
+ return value.dump(space=metadata.space, version=metadata.version)
368
+ return str(value)
369
+
266
370
 
267
- class DMSEnum(SheetEntity):
371
+ class DMSEnum(SheetRow):
268
372
  collection: ClassEntity = Field(alias="Collection")
269
373
  value: str = Field(alias="Value")
270
374
  name: str | None = Field(alias="Name", default=None)
271
375
  description: str | None = Field(alias="Description", default=None)
272
376
 
377
+ def _identifier(self) -> tuple[Hashable, ...]:
378
+ return self.collection, self.value
379
+
380
+ @field_serializer("collection", when_used="unless-none")
381
+ def remove_default_space(self, value: Any, info: SerializationInfo) -> str:
382
+ if isinstance(value, DMSEntity) and (metadata := _metadata(info.context)):
383
+ return value.dump(space=metadata.space, version=metadata.version)
384
+ return str(value)
385
+
273
386
 
274
387
  class DMSRules(BaseRules):
275
388
  metadata: DMSMetadata = Field(alias="Metadata")
@@ -331,38 +444,6 @@ class DMSRules(BaseRules):
331
444
  raise MultiValueError(issue_list.errors)
332
445
  return self
333
446
 
334
- def dump(
335
- self,
336
- mode: Literal["python", "json"] = "python",
337
- by_alias: bool = False,
338
- exclude: IncEx = None,
339
- exclude_none: bool = False,
340
- exclude_unset: bool = False,
341
- exclude_defaults: bool = False,
342
- as_reference: bool = False,
343
- ) -> dict[str, Any]:
344
- from ._serializer import _DMSRulesSerializer
345
-
346
- dumped = self.model_dump(
347
- mode=mode,
348
- by_alias=by_alias,
349
- exclude=exclude,
350
- exclude_none=exclude_none,
351
- exclude_unset=exclude_unset,
352
- exclude_defaults=exclude_defaults,
353
- )
354
- space, version = self.metadata.space, self.metadata.version
355
- serializer = _DMSRulesSerializer(by_alias, space, version)
356
- clean = serializer.clean(dumped, as_reference)
357
- last = "Last" if by_alias else "last"
358
- if last_dump := clean.get(last):
359
- clean[last] = serializer.clean(last_dump, False)
360
- reference = "Reference" if by_alias else "reference"
361
- if self.reference and (ref_dump := clean.get(reference)):
362
- space, version = self.reference.metadata.space, self.reference.metadata.version
363
- clean[reference] = _DMSRulesSerializer(by_alias, space, version).clean(ref_dump, True)
364
- return clean
365
-
366
447
  def as_schema(self, include_pipeline: bool = False, instance_space: str | None = None) -> DMSSchema:
367
448
  from ._exporter import _DMSExporter
368
449
 
@@ -201,7 +201,7 @@ class DMSPostValidation:
201
201
  for prop_no, prop in enumerate(self.properties):
202
202
  if prop.container and (container_id := prop.container.as_id()) not in defined_containers:
203
203
  errors.append(
204
- ResourceNotDefinedError[dm.ContainerId](
204
+ ResourceNotDefinedError(
205
205
  identifier=container_id,
206
206
  resource_type="container",
207
207
  location="Containers Sheet",
@@ -214,7 +214,7 @@ class DMSPostValidation:
214
214
  for constraint_no, constraint in enumerate(container.constraint or []):
215
215
  if constraint.as_id() not in defined_containers:
216
216
  errors.append(
217
- ResourceNotDefinedError[dm.ContainerId](
217
+ ResourceNotDefinedError(
218
218
  identifier=constraint.as_id(),
219
219
  resource_type="container",
220
220
  location="Containers Sheet",
@@ -1,9 +1,9 @@
1
1
  import math
2
+ from collections.abc import Hashable
2
3
  from dataclasses import dataclass, field
3
- from typing import Any, ClassVar
4
+ from typing import ClassVar
4
5
 
5
- from pydantic import Field, field_serializer, field_validator, model_serializer
6
- from pydantic_core.core_schema import SerializationInfo
6
+ from pydantic import Field, field_serializer, field_validator
7
7
 
8
8
  from cognite.neat.rules.models.data_types import DataType
9
9
  from cognite.neat.rules.models.entities import ClassEntity, ClassEntityList
@@ -13,8 +13,8 @@ from ._base_rules import (
13
13
  BaseMetadata,
14
14
  BaseRules,
15
15
  RoleTypes,
16
- SheetEntity,
17
16
  SheetList,
17
+ SheetRow,
18
18
  )
19
19
  from ._types import PropertyType, StrOrListType
20
20
 
@@ -30,7 +30,7 @@ class DomainMetadata(BaseMetadata):
30
30
  return "domain"
31
31
 
32
32
 
33
- class DomainProperty(SheetEntity):
33
+ class DomainProperty(SheetRow):
34
34
  class_: ClassEntity = Field(alias="Class")
35
35
  property_: PropertyType = Field(alias="Property")
36
36
  name: str | None = Field(alias="Name", default=None)
@@ -39,6 +39,9 @@ class DomainProperty(SheetEntity):
39
39
  min_count: int | None = Field(alias="Min Count", default=None)
40
40
  max_count: int | float | None = Field(alias="Max Count", default=None)
41
41
 
42
+ def _identifier(self) -> tuple[Hashable, ...]:
43
+ return self.class_, self.property_
44
+
42
45
  @field_serializer("max_count", when_used="json-unless-none")
43
46
  def serialize_max_count(self, value: int | float | None) -> int | float | None | str:
44
47
  if isinstance(value, float) and math.isinf(value):
@@ -52,12 +55,19 @@ class DomainProperty(SheetEntity):
52
55
  return value
53
56
 
54
57
 
55
- class DomainClass(SheetEntity):
58
+ class DomainClass(SheetRow):
56
59
  class_: ClassEntity = Field(alias="Class")
57
60
  name: str | None = Field(alias="Name", default=None)
58
61
  description: str | None = Field(None, alias="Description")
59
62
  parent: ClassEntityList | None = Field(alias="Parent Class")
60
63
 
64
+ def _identifier(self) -> tuple[Hashable, ...]:
65
+ return (self.class_,)
66
+
67
+ @field_serializer("parent", when_used="unless-none")
68
+ def serialize_parent(self, value: list[ClassEntity]) -> str:
69
+ return ",".join([str(entry) for entry in value])
70
+
61
71
 
62
72
  class DomainRules(BaseRules):
63
73
  metadata: DomainMetadata = Field(alias="Metadata")
@@ -66,19 +76,6 @@ class DomainRules(BaseRules):
66
76
  last: "DomainRules | None" = Field(None, alias="Last")
67
77
  reference: "DomainRules | None" = Field(None, alias="Reference")
68
78
 
69
- @model_serializer(mode="plain", when_used="always")
70
- def domain_rules_serializer(self, info: SerializationInfo) -> dict[str, Any]:
71
- kwargs = vars(info)
72
- output: dict[str, Any] = {
73
- "Metadata" if info.by_alias else "metadata": self.metadata.model_dump(**kwargs),
74
- "Properties" if info.by_alias else "properties": [prop.model_dump(**kwargs) for prop in self.properties],
75
- }
76
- if self.classes or not info.exclude_none:
77
- output["Classes" if info.by_alias else "classes"] = [
78
- cls.model_dump(**kwargs) for cls in self.classes or []
79
- ] or None
80
- return output
81
-
82
79
 
83
80
  @dataclass
84
81
  class DomainInputMetadata(InputComponent[DomainMetadata]):
@@ -58,7 +58,7 @@ class Entity(BaseModel, extra="ignore"):
58
58
  elif isinstance(data, str) and data == str(Unknown):
59
59
  return UnknownEntity(prefix=Undefined, suffix=Unknown)
60
60
  if defaults and isinstance(defaults, dict):
61
- # This is is a trick to pass in default values
61
+ # This is a trick to pass in default values
62
62
  return cls.model_validate({_PARSE: data, "defaults": defaults})
63
63
  else:
64
64
  return cls.model_validate(data)
@@ -123,8 +123,8 @@ class Entity(BaseModel, extra="ignore"):
123
123
  extra_args[key] = annotation.load(extra_args[key], **defaults) # type: ignore[union-attr, assignment]
124
124
  return dict(prefix=prefix, suffix=suffix, **extra_args)
125
125
 
126
- def dump(self) -> str:
127
- return str(self)
126
+ def dump(self, **defaults: Any) -> str:
127
+ return self._as_str(**defaults)
128
128
 
129
129
  def as_tuple(self) -> tuple[str, ...]:
130
130
  # We haver overwritten the serialization to str, so we need to do it manually
@@ -164,17 +164,22 @@ class Entity(BaseModel, extra="ignore"):
164
164
 
165
165
  @property
166
166
  def id(self) -> str:
167
+ return self._as_str()
168
+
169
+ def _as_str(self, **defaults: Any) -> str:
167
170
  # We have overwritten the serialization to str, so we need to do it manually
168
- model_dump = [
169
- (field.alias or field_name, v)
171
+ model_dump = {
172
+ field.alias or field_name: v.dump(**defaults) if isinstance(v, Entity) else v
170
173
  for field_name, field in self.model_fields.items()
171
174
  if (v := getattr(self, field_name)) is not None and field_name not in {"prefix", "suffix"}
172
- ]
173
- if len(model_dump) == 1:
174
- args = f"{model_dump[0][0]}={model_dump[0][1]}"
175
- else:
176
- args = ",".join([f"{k}={v}" for k, v in model_dump])
177
- if self.prefix == Undefined:
175
+ }
176
+ if isinstance(defaults, dict):
177
+ for key, value in defaults.items():
178
+ if key in model_dump and value == defaults.get(key):
179
+ del model_dump[key]
180
+
181
+ args = ",".join(f"{k}={v}" for k, v in model_dump.items())
182
+ if self.prefix == Undefined or (isinstance(defaults, dict) and self.prefix == defaults.get("prefix")):
178
183
  base_id = str(self.suffix)
179
184
  else:
180
185
  base_id = f"{self.prefix}:{self.suffix!s}"
@@ -269,6 +274,11 @@ class DMSEntity(Entity, Generic[T_ID], ABC):
269
274
  prefix: str = Field(alias="space")
270
275
  suffix: str = Field(alias="externalId")
271
276
 
277
+ def dump(self, **defaults: Any) -> str:
278
+ if isinstance(defaults, dict) and "space" in defaults:
279
+ defaults["prefix"] = defaults.pop("space")
280
+ return super().dump(**defaults)
281
+
272
282
  @classmethod
273
283
  def load(cls: "type[T_DMSEntity]", data: Any, **defaults) -> "T_DMSEntity | DMSUnknownEntity": # type: ignore[override]
274
284
  if isinstance(data, str) and data == str(Unknown):
@@ -407,6 +417,10 @@ class EdgeEntity(DMSEntity[None]):
407
417
  properties: ViewEntity | None = None
408
418
  direction: Literal["outwards", "inwards"] = "outwards"
409
419
 
420
+ def dump(self, **defaults: Any) -> str:
421
+ # Add default direction
422
+ return super().dump(**defaults, direction="outwards")
423
+
410
424
  def as_id(self) -> None:
411
425
  return None
412
426
 
@@ -41,11 +41,6 @@ def _generate_cdf_resource_list(v: Any) -> list[AssetEntity | RelationshipEntity
41
41
  ClassEntityList = Annotated[
42
42
  list[ClassEntity],
43
43
  BeforeValidator(_split_str),
44
- PlainSerializer(
45
- _join_str,
46
- return_type=str,
47
- when_used="unless-none",
48
- ),
49
44
  ]
50
45
 
51
46
 
@@ -63,11 +58,6 @@ CdfResourceEntityList = Annotated[
63
58
  ContainerEntityList = Annotated[
64
59
  list[ContainerEntity],
65
60
  BeforeValidator(_split_str),
66
- PlainSerializer(
67
- _join_str,
68
- return_type=str,
69
- when_used="unless-none",
70
- ),
71
61
  ]
72
62
 
73
63
  ViewEntityList = Annotated[
@@ -1,10 +1,11 @@
1
1
  import math
2
2
  import sys
3
+ from collections.abc import Hashable
3
4
  from datetime import datetime
4
- from typing import TYPE_CHECKING, Any, ClassVar, Literal
5
+ from typing import TYPE_CHECKING, Any, ClassVar
5
6
 
6
7
  from pydantic import Field, field_serializer, field_validator, model_validator
7
- from pydantic.main import IncEx
8
+ from pydantic_core.core_schema import SerializationInfo
8
9
  from rdflib import Namespace
9
10
 
10
11
  from cognite.neat.constants import get_default_prefixes
@@ -18,8 +19,8 @@ from cognite.neat.rules.models._base_rules import (
18
19
  MatchType,
19
20
  RoleTypes,
20
21
  SchemaCompleteness,
21
- SheetEntity,
22
22
  SheetList,
23
+ SheetRow,
23
24
  )
24
25
  from cognite.neat.rules.models._rdfpath import (
25
26
  RDFPath,
@@ -37,6 +38,7 @@ from cognite.neat.rules.models.data_types import DataType
37
38
  from cognite.neat.rules.models.entities import (
38
39
  ClassEntity,
39
40
  ClassEntityList,
41
+ Entity,
40
42
  EntityTypes,
41
43
  MultiValueTypeInfo,
42
44
  ReferenceEntity,
@@ -115,7 +117,13 @@ class InformationMetadata(BaseMetadata):
115
117
  return self.prefix
116
118
 
117
119
 
118
- class InformationClass(SheetEntity):
120
+ def _get_metadata(context: Any) -> InformationMetadata | None:
121
+ if isinstance(context, dict) and isinstance(context.get("metadata"), InformationMetadata):
122
+ return context["metadata"]
123
+ return None
124
+
125
+
126
+ class InformationClass(SheetRow):
119
127
  """
120
128
  Class is a category of things that share a common set of attributes and relationships.
121
129
 
@@ -135,8 +143,34 @@ class InformationClass(SheetEntity):
135
143
  match_type: MatchType | None = Field(alias="Match Type", default=None)
136
144
  comment: str | None = Field(alias="Comment", default=None)
137
145
 
138
-
139
- class InformationProperty(SheetEntity):
146
+ def _identifier(self) -> tuple[Hashable, ...]:
147
+ return (self.class_,)
148
+
149
+ @field_serializer("reference", when_used="always")
150
+ def set_reference(self, value: Any, info: SerializationInfo) -> str | None:
151
+ if isinstance(info.context, dict) and info.context.get("as_reference") is True:
152
+ return self.class_.dump()
153
+ return str(value) if value is not None else None
154
+
155
+ @field_serializer("class_", when_used="unless-none")
156
+ def remove_default_prefix(self, value: Any, info: SerializationInfo) -> str:
157
+ if (metadata := _get_metadata(info.context)) and isinstance(value, Entity):
158
+ return value.dump(prefix=metadata.prefix, version=metadata.version)
159
+ return str(value)
160
+
161
+ @field_serializer("parent", when_used="unless-none")
162
+ def remove_default_prefixes(self, value: Any, info: SerializationInfo) -> str:
163
+ if isinstance(value, list) and (metadata := _get_metadata(info.context)):
164
+ return ",".join(
165
+ parent.dump(prefix=metadata.prefix, version=metadata.version)
166
+ if isinstance(parent, Entity)
167
+ else str(parent)
168
+ for parent in value
169
+ )
170
+ return ",".join(str(value) for value in value)
171
+
172
+
173
+ class InformationProperty(SheetRow):
140
174
  """
141
175
  A property is a characteristic of a class. It is a named attribute of a class that describes a range of values
142
176
  or a relationship to another class.
@@ -171,15 +205,13 @@ class InformationProperty(SheetEntity):
171
205
  comment: str | None = Field(alias="Comment", default=None)
172
206
  inherited: bool = Field(
173
207
  default=False,
208
+ exclude=True,
174
209
  alias="Inherited",
175
210
  description="Flag to indicate if the property is inherited, only use for internal purposes",
176
211
  )
177
212
 
178
- @field_serializer("max_count", when_used="json-unless-none")
179
- def serialize_max_count(self, value: int | float | None) -> int | float | None | str:
180
- if isinstance(value, float) and math.isinf(value):
181
- return None
182
- return value
213
+ def _identifier(self) -> tuple[Hashable, ...]:
214
+ return self.class_, self.property_
183
215
 
184
216
  @field_validator("max_count", mode="before")
185
217
  def parse_max_count(cls, value: int | float | None) -> int | float | None:
@@ -233,6 +265,31 @@ class InformationProperty(SheetEntity):
233
265
  ) from None
234
266
  return self
235
267
 
268
+ @field_serializer("max_count", when_used="json-unless-none")
269
+ def serialize_max_count(self, value: int | float | None) -> int | float | None | str:
270
+ if isinstance(value, float) and math.isinf(value):
271
+ return None
272
+ return value
273
+
274
+ @field_serializer("reference", when_used="always")
275
+ def set_reference(self, value: Any, info: SerializationInfo) -> str | None:
276
+ # When rules as dumped as reference, we set the reference to the class
277
+ if isinstance(info.context, dict) and info.context.get("as_reference") is True:
278
+ return str(
279
+ ReferenceEntity(
280
+ prefix=str(self.class_.prefix),
281
+ suffix=self.class_.suffix,
282
+ property=self.property_,
283
+ )
284
+ )
285
+ return str(value) if value is not None else None
286
+
287
+ @field_serializer("class_", "value_type", when_used="unless-none")
288
+ def remove_default_prefix(self, value: Any, info: SerializationInfo) -> str:
289
+ if (metadata := _get_metadata(info.context)) and isinstance(value, Entity):
290
+ return value.dump(prefix=metadata.prefix, version=metadata.version)
291
+ return str(value)
292
+
236
293
  @property
237
294
  def type_(self) -> EntityTypes:
238
295
  """Type of property based on value type. Either data (attribute) or object (edge) property."""
@@ -307,38 +364,6 @@ class InformationRules(BaseRules):
307
364
  raise issue_list.as_exception()
308
365
  return self
309
366
 
310
- def dump(
311
- self,
312
- mode: Literal["python", "json"] = "python",
313
- by_alias: bool = False,
314
- exclude: IncEx = None,
315
- exclude_none: bool = False,
316
- exclude_unset: bool = False,
317
- exclude_defaults: bool = False,
318
- as_reference: bool = False,
319
- ) -> dict[str, Any]:
320
- from ._serializer import _InformationRulesSerializer
321
-
322
- dumped = self.model_dump(
323
- mode=mode,
324
- by_alias=by_alias,
325
- exclude=exclude,
326
- exclude_none=exclude_none,
327
- exclude_unset=exclude_unset,
328
- exclude_defaults=exclude_defaults,
329
- )
330
- prefix = self.metadata.prefix
331
- serializer = _InformationRulesSerializer(by_alias, prefix)
332
- cleaned = serializer.clean(dumped, as_reference)
333
- last = "Last" if by_alias else "last"
334
- if last_dump := cleaned.get(last):
335
- cleaned[last] = serializer.clean(last_dump, False)
336
- reference = "Reference" if by_alias else "reference"
337
- if self.reference and (ref_dump := cleaned.get(reference)):
338
- prefix = self.reference.metadata.prefix
339
- cleaned[reference] = _InformationRulesSerializer(by_alias, prefix).clean(ref_dump, True)
340
- return cleaned
341
-
342
367
  def as_dms_rules(self) -> "DMSRules":
343
368
  from cognite.neat.rules.transformers._converters import _InformationRulesConverter
344
369