cognite-neat 0.75.7__py3-none-any.whl → 0.75.9__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 (103) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/app/api/configuration.py +4 -9
  3. cognite/neat/app/api/routers/configuration.py +2 -1
  4. cognite/neat/app/api/routers/crud.py +5 -5
  5. cognite/neat/app/api/routers/data_exploration.py +3 -1
  6. cognite/neat/app/api/routers/rules.py +3 -3
  7. cognite/neat/app/api/routers/workflows.py +3 -3
  8. cognite/neat/app/ui/neat-app/build/asset-manifest.json +3 -3
  9. cognite/neat/app/ui/neat-app/build/index.html +1 -1
  10. cognite/neat/app/ui/neat-app/build/static/js/{main.4345d42f.js → main.ec7f72e2.js} +3 -3
  11. cognite/neat/app/ui/neat-app/build/static/js/{main.4345d42f.js.map → main.ec7f72e2.js.map} +1 -1
  12. cognite/neat/config.py +147 -12
  13. cognite/neat/constants.py +1 -0
  14. cognite/neat/graph/exceptions.py +1 -2
  15. cognite/neat/graph/extractors/_mock_graph_generator.py +6 -5
  16. cognite/neat/legacy/graph/exceptions.py +1 -2
  17. cognite/neat/legacy/graph/extractors/_mock_graph_generator.py +1 -2
  18. cognite/neat/legacy/graph/loaders/_asset_loader.py +8 -13
  19. cognite/neat/legacy/graph/loaders/_base.py +2 -4
  20. cognite/neat/legacy/graph/loaders/_exceptions.py +1 -3
  21. cognite/neat/legacy/graph/loaders/core/rdf_to_assets.py +4 -8
  22. cognite/neat/legacy/graph/loaders/core/rdf_to_relationships.py +2 -4
  23. cognite/neat/legacy/graph/loaders/rdf_to_dms.py +2 -4
  24. cognite/neat/legacy/graph/loaders/validator.py +1 -1
  25. cognite/neat/legacy/graph/transformations/transformer.py +1 -2
  26. cognite/neat/legacy/rules/exporters/_rules2dms.py +1 -2
  27. cognite/neat/legacy/rules/exporters/_validation.py +4 -8
  28. cognite/neat/legacy/rules/importers/_base.py +0 -4
  29. cognite/neat/legacy/rules/importers/_dms2rules.py +0 -2
  30. cognite/neat/legacy/rules/models/rdfpath.py +1 -2
  31. cognite/neat/legacy/workflows/examples/Export_DMS/workflow.yaml +89 -0
  32. cognite/neat/legacy/workflows/examples/Export_Rules_to_Ontology/workflow.yaml +152 -0
  33. cognite/neat/legacy/workflows/examples/Extract_DEXPI_Graph_and_Export_Rules/workflow.yaml +139 -0
  34. cognite/neat/legacy/workflows/examples/Extract_RDF_Graph_and_Generate_Assets/workflow.yaml +270 -0
  35. cognite/neat/legacy/workflows/examples/Import_DMS/workflow.yaml +65 -0
  36. cognite/neat/legacy/workflows/examples/Ontology_to_Data_Model/workflow.yaml +116 -0
  37. cognite/neat/legacy/workflows/examples/Validate_Rules/workflow.yaml +67 -0
  38. cognite/neat/legacy/workflows/examples/Validate_Solution_Model/workflow.yaml +64 -0
  39. cognite/neat/legacy/workflows/examples/Visualize_Data_Model_Using_Mock_Graph/workflow.yaml +95 -0
  40. cognite/neat/legacy/workflows/examples/Visualize_Semantic_Data_Model/workflow.yaml +111 -0
  41. cognite/neat/rules/analysis/_base.py +1 -1
  42. cognite/neat/rules/analysis/_information_rules.py +4 -4
  43. cognite/neat/rules/exporters/_rules2excel.py +2 -2
  44. cognite/neat/rules/exporters/_rules2ontology.py +20 -17
  45. cognite/neat/rules/exporters/_validation.py +8 -10
  46. cognite/neat/rules/importers/_base.py +2 -4
  47. cognite/neat/rules/importers/_dms2rules.py +16 -19
  48. cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +21 -19
  49. cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +2 -4
  50. cognite/neat/rules/importers/_dtdl2rules/spec.py +3 -5
  51. cognite/neat/rules/importers/_owl2rules/_owl2rules.py +4 -6
  52. cognite/neat/rules/importers/_spreadsheet2rules.py +10 -9
  53. cognite/neat/rules/importers/_yaml2rules.py +10 -6
  54. cognite/neat/rules/issues/dms.py +3 -5
  55. cognite/neat/rules/issues/formatters.py +3 -1
  56. cognite/neat/rules/models/data_types.py +54 -31
  57. cognite/neat/rules/models/entities.py +184 -42
  58. cognite/neat/rules/models/rdfpath.py +112 -13
  59. cognite/neat/rules/models/rules/_base.py +2 -2
  60. cognite/neat/rules/models/rules/_dms_architect_rules.py +119 -189
  61. cognite/neat/rules/models/rules/_dms_rules_write.py +344 -0
  62. cognite/neat/rules/models/rules/_dms_schema.py +3 -3
  63. cognite/neat/rules/models/rules/_domain_rules.py +6 -3
  64. cognite/neat/rules/models/rules/_information_rules.py +68 -61
  65. cognite/neat/rules/models/rules/_types/__init__.py +0 -47
  66. cognite/neat/rules/models/rules/_types/_base.py +1 -309
  67. cognite/neat/rules/models/rules/_types/_field.py +0 -225
  68. cognite/neat/utils/cdf_loaders/_data_modeling.py +3 -1
  69. cognite/neat/utils/cdf_loaders/_ingestion.py +2 -4
  70. cognite/neat/utils/spreadsheet.py +2 -4
  71. cognite/neat/utils/utils.py +2 -4
  72. cognite/neat/workflows/base.py +5 -5
  73. cognite/neat/workflows/manager.py +32 -22
  74. cognite/neat/workflows/model.py +3 -3
  75. cognite/neat/workflows/steps/lib/__init__.py +0 -7
  76. cognite/neat/workflows/steps/lib/current/__init__.py +6 -0
  77. cognite/neat/workflows/steps/lib/{rules_exporter.py → current/rules_exporter.py} +8 -8
  78. cognite/neat/workflows/steps/lib/{rules_importer.py → current/rules_importer.py} +7 -7
  79. cognite/neat/workflows/steps/lib/io/__init__.py +1 -0
  80. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_contextualization.py +2 -2
  81. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_extractor.py +9 -9
  82. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_loader.py +9 -9
  83. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_store.py +4 -4
  84. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_transformer.py +2 -2
  85. cognite/neat/workflows/steps/lib/{v1 → legacy}/rules_exporter.py +15 -17
  86. cognite/neat/workflows/steps/lib/{v1 → legacy}/rules_importer.py +7 -7
  87. cognite/neat/workflows/steps/step_model.py +5 -9
  88. cognite/neat/workflows/steps_registry.py +20 -11
  89. {cognite_neat-0.75.7.dist-info → cognite_neat-0.75.9.dist-info}/METADATA +1 -1
  90. {cognite_neat-0.75.7.dist-info → cognite_neat-0.75.9.dist-info}/RECORD +100 -90
  91. cognite/neat/app/api/data_classes/configuration.py +0 -121
  92. cognite/neat/rules/models/_entity.py +0 -142
  93. cognite/neat/rules/models/rules/_types/_value.py +0 -159
  94. /cognite/neat/app/ui/neat-app/build/static/js/{main.4345d42f.js.LICENSE.txt → main.ec7f72e2.js.LICENSE.txt} +0 -0
  95. /cognite/neat/workflows/steps/lib/{graph_extractor.py → current/graph_extractor.py} +0 -0
  96. /cognite/neat/workflows/steps/lib/{graph_loader.py → current/graph_loader.py} +0 -0
  97. /cognite/neat/workflows/steps/lib/{graph_store.py → current/graph_store.py} +0 -0
  98. /cognite/neat/workflows/steps/lib/{rules_validator.py → current/rules_validator.py} +0 -0
  99. /cognite/neat/workflows/steps/lib/{io_steps.py → io/io_steps.py} +0 -0
  100. /cognite/neat/workflows/steps/lib/{v1 → legacy}/__init__.py +0 -0
  101. {cognite_neat-0.75.7.dist-info → cognite_neat-0.75.9.dist-info}/LICENSE +0 -0
  102. {cognite_neat-0.75.7.dist-info → cognite_neat-0.75.9.dist-info}/WHEEL +0 -0
  103. {cognite_neat-0.75.7.dist-info → cognite_neat-0.75.9.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,11 @@
1
1
  import re
2
2
  import sys
3
- import threading
4
3
  from abc import ABC, abstractmethod
5
4
  from functools import total_ordering
6
- from typing import Annotated, Any, ClassVar, Generic, TypeVar
5
+ from typing import Annotated, Any, ClassVar, Generic, TypeVar, cast
7
6
 
8
7
  from cognite.client.data_classes.data_modeling.ids import ContainerId, DataModelId, PropertyId, ViewId
9
- from pydantic import BaseModel, BeforeValidator, Field, PlainSerializer, model_serializer, model_validator
8
+ from pydantic import AnyHttpUrl, BaseModel, BeforeValidator, Field, PlainSerializer, model_serializer, model_validator
10
9
 
11
10
  if sys.version_info >= (3, 11):
12
11
  from enum import StrEnum
@@ -58,45 +57,69 @@ _PROPERTY_ID_REGEX = rf"\((?P<{EntityTypes.property_}>{_ENTITY_ID_REGEX})\)"
58
57
  _ENTITY_PATTERN = re.compile(r"^(?P<prefix>.*?):?(?P<suffix>[^(:]*)(\((?P<content>[^)]+)\))?$")
59
58
 
60
59
 
61
- class _Undefined(BaseModel):
62
- ...
60
+ class _UndefinedType(BaseModel): ...
63
61
 
64
62
 
65
- class _Unknown(BaseModel):
63
+ class _UnknownType(BaseModel):
66
64
  def __str__(self) -> str:
67
65
  return "#N/A"
68
66
 
69
67
 
70
68
  # This is a trick to make Undefined and Unknown singletons
71
- Undefined = _Undefined()
72
- Unknown = _Unknown()
69
+ Undefined = _UndefinedType()
70
+ Unknown = _UnknownType()
71
+ _PARSE = object()
73
72
 
74
73
 
75
74
  @total_ordering
76
- class Entity(BaseModel):
75
+ class Entity(BaseModel, extra="ignore"):
77
76
  """Entity is a class or property in OWL/RDF sense."""
78
77
 
79
78
  type_: ClassVar[EntityTypes] = EntityTypes.undefined
80
- prefix: str | _Undefined = Undefined
81
- suffix: str | _Unknown
79
+ prefix: str | _UndefinedType = Undefined
80
+ suffix: str
82
81
 
83
82
  @classmethod
84
- def load(cls, data: Any) -> Self:
85
- return cls.model_validate(data)
83
+ def load(cls: "type[T_Entity]", data: Any, **defaults) -> "T_Entity | UnknownEntity":
84
+ if isinstance(data, cls):
85
+ return data
86
+ elif isinstance(data, str) and data == str(Unknown):
87
+ return UnknownEntity(prefix=Undefined, suffix=Unknown)
88
+ if defaults and isinstance(defaults, dict):
89
+ # This is trick to pass in default values
90
+ return cls.model_validate({_PARSE: data, "defaults": defaults})
91
+ else:
92
+ return cls.model_validate(data)
86
93
 
87
94
  @model_validator(mode="before")
88
- def _load(cls, data: Any) -> dict:
89
- if isinstance(data, cls):
90
- return data.model_dump()
91
- elif isinstance(data, dict):
95
+ def _load(cls, data: Any) -> "dict | Entity":
96
+ defaults = {}
97
+ if isinstance(data, dict) and _PARSE in data:
98
+ defaults = data.get("defaults", {})
99
+ data = data[_PARSE]
100
+ if isinstance(data, dict):
101
+ data.update(defaults)
92
102
  return data
93
103
  elif hasattr(data, "versioned_id"):
94
104
  # Todo: Remove. Is here for backwards compatibility
95
105
  data = data.versioned_id
96
106
  elif not isinstance(data, str):
97
107
  raise ValueError(f"Cannot load {cls.__name__} from {data}")
98
-
99
- return cls._parse(data)
108
+ elif data == str(Unknown) and cls.type_ == EntityTypes.undefined:
109
+ return dict(prefix=Undefined, suffix=Unknown) # type: ignore[arg-type]
110
+ elif data == str(Unknown):
111
+ raise ValueError(f"Unknown is not allowed for {cls.type_} entity")
112
+
113
+ result = cls._parse(data)
114
+ output = defaults.copy()
115
+ # Populate by alias
116
+ for field_name, field_ in cls.model_fields.items():
117
+ name = field_.alias or field_name
118
+ if (field_value := result.get(field_name)) and not (field_value in [Unknown, Undefined] and name in output):
119
+ output[name] = result.pop(field_name)
120
+ elif name not in output and name in result:
121
+ output[name] = result.pop(name)
122
+ return output
100
123
 
101
124
  @model_serializer(when_used="unless-none", return_type=str)
102
125
  def as_str(self) -> str:
@@ -131,7 +154,7 @@ class Entity(BaseModel):
131
154
  if isinstance(v := getattr(self, field_name), str | None) and field_name not in {"prefix", "suffix"}
132
155
  ]
133
156
  )
134
- if isinstance(self.prefix, _Undefined):
157
+ if isinstance(self.prefix, _UndefinedType):
135
158
  return str(self.suffix), *extra
136
159
  else:
137
160
  return self.prefix, str(self.suffix), *extra
@@ -188,10 +211,25 @@ class Entity(BaseModel):
188
211
  return f"{self.prefix}:{self.suffix!s}"
189
212
 
190
213
 
214
+ T_Entity = TypeVar("T_Entity", bound=Entity)
215
+
216
+
191
217
  class ClassEntity(Entity):
192
218
  type_: ClassVar[EntityTypes] = EntityTypes.class_
193
219
  version: str | None = None
194
220
 
221
+ def as_view_entity(self, default_space: str, default_version) -> "ViewEntity":
222
+ if self.version is None:
223
+ version = default_version
224
+ else:
225
+ version = self.version
226
+ space = default_space if isinstance(self.prefix, _UndefinedType) else self.prefix
227
+ return ViewEntity(space=space, externalId=str(self.suffix), version=version)
228
+
229
+ def as_container_entity(self, default_space: str) -> "ContainerEntity":
230
+ space = default_space if isinstance(self.prefix, _UndefinedType) else self.prefix
231
+ return ContainerEntity(space=space, externalId=str(self.suffix))
232
+
195
233
 
196
234
  class ParentClassEntity(ClassEntity):
197
235
  type_: ClassVar[EntityTypes] = EntityTypes.parent_class
@@ -200,25 +238,34 @@ class ParentClassEntity(ClassEntity):
200
238
  return ClassEntity(prefix=self.prefix, suffix=self.suffix, version=self.version)
201
239
 
202
240
 
203
- T_ID = TypeVar("T_ID", bound=ContainerId | ViewId | DataModelId | PropertyId)
241
+ class UnknownEntity(ClassEntity):
242
+ type_: ClassVar[EntityTypes] = EntityTypes.undefined
243
+ prefix: _UndefinedType = Undefined
244
+ suffix: _UnknownType = Unknown # type: ignore[assignment]
245
+
246
+ @property
247
+ def id(self) -> str:
248
+ return str(Unknown)
249
+
250
+
251
+ T_ID = TypeVar("T_ID", bound=ContainerId | ViewId | DataModelId | PropertyId | None)
204
252
 
205
253
 
206
254
  class DMSEntity(Entity, Generic[T_ID], ABC):
207
255
  type_: ClassVar[EntityTypes] = EntityTypes.undefined
208
- default_space_by_thread: ClassVar[dict[threading.Thread, str]] = {}
209
- suffix: str
256
+ prefix: str = Field(alias="space")
257
+ suffix: str = Field(alias="externalId")
210
258
 
211
259
  @classmethod
212
- def set_default_space(cls, space: str) -> None:
213
- cls.default_space_by_thread[threading.current_thread()] = space
260
+ def load(cls: "type[T_DMSEntity]", data: Any, **defaults) -> "T_DMSEntity | DMSUnknownEntity": # type: ignore[override]
261
+ if isinstance(data, str) and data == str(Unknown):
262
+ return DMSUnknownEntity.from_id(None)
263
+ return cast(T_DMSEntity, super().load(data, **defaults))
214
264
 
215
265
  @property
216
266
  def space(self) -> str:
217
267
  """Returns entity space in CDF."""
218
- if isinstance(self.prefix, _Undefined):
219
- return self.default_space_by_thread.get(threading.current_thread(), "MISSING")
220
- else:
221
- return self.prefix
268
+ return self.prefix
222
269
 
223
270
  @property
224
271
  def external_id(self) -> str:
@@ -229,6 +276,17 @@ class DMSEntity(Entity, Generic[T_ID], ABC):
229
276
  def as_id(self) -> T_ID:
230
277
  raise NotImplementedError("Method as_id must be implemented in subclasses")
231
278
 
279
+ @classmethod
280
+ @abstractmethod
281
+ def from_id(cls, id: T_ID) -> Self:
282
+ raise NotImplementedError("Method from_id must be implemented in subclasses")
283
+
284
+ def as_class(self) -> ClassEntity:
285
+ return ClassEntity(prefix=self.space, suffix=self.external_id)
286
+
287
+
288
+ T_DMSEntity = TypeVar("T_DMSEntity", bound=DMSEntity)
289
+
232
290
 
233
291
  class ContainerEntity(DMSEntity[ContainerId]):
234
292
  type_: ClassVar[EntityTypes] = EntityTypes.container
@@ -236,16 +294,16 @@ class ContainerEntity(DMSEntity[ContainerId]):
236
294
  def as_id(self) -> ContainerId:
237
295
  return ContainerId(space=self.space, external_id=self.external_id)
238
296
 
297
+ @classmethod
298
+ def from_id(cls, id: ContainerId) -> "ContainerEntity":
299
+ return cls(space=id.space, externalId=id.external_id)
300
+
239
301
 
240
302
  class DMSVersionedEntity(DMSEntity[T_ID], ABC):
241
- version: str | None = None
242
- default_version_by_thread: ClassVar[dict[threading.Thread, str]] = {}
303
+ version: str
243
304
 
244
- @property
245
- def version_with_fallback(self) -> str:
246
- if self.version is not None:
247
- return self.version
248
- return self.default_version_by_thread.get(threading.current_thread(), "MISSING")
305
+ def as_class(self) -> ClassEntity:
306
+ return ClassEntity(prefix=self.space, suffix=self.external_id, version=self.version)
249
307
 
250
308
 
251
309
  class ViewEntity(DMSVersionedEntity[ViewId]):
@@ -254,16 +312,54 @@ class ViewEntity(DMSVersionedEntity[ViewId]):
254
312
  def as_id(
255
313
  self,
256
314
  ) -> ViewId:
257
- return ViewId(space=self.space, external_id=self.external_id, version=self.version_with_fallback)
315
+ return ViewId(space=self.space, external_id=self.external_id, version=self.version)
316
+
317
+ @classmethod
318
+ def from_id(cls, id: ViewId) -> "ViewEntity":
319
+ if id.version is None:
320
+ raise ValueError("Version must be specified")
321
+ return cls(space=id.space, externalId=id.external_id, version=id.version)
322
+
323
+
324
+ class DMSUnknownEntity(DMSEntity[None]):
325
+ """This is a special entity that represents an unknown entity.
326
+
327
+ The use case is for direct relations where the source is not known."""
328
+
329
+ type_: ClassVar[EntityTypes] = EntityTypes.undefined
330
+ prefix: _UndefinedType = Field(Undefined, alias="space") # type: ignore[assignment]
331
+ suffix: _UnknownType = Field(Unknown, alias="externalId") # type: ignore[assignment]
332
+
333
+ def as_id(self) -> None:
334
+ return None
258
335
 
336
+ @classmethod
337
+ def from_id(cls, id: None) -> "DMSUnknownEntity":
338
+ return cls(space=Undefined, externalId=Unknown)
259
339
 
260
- class PropertyEntity(DMSVersionedEntity[PropertyId]):
340
+ @property
341
+ def id(self) -> str:
342
+ return str(Unknown)
343
+
344
+
345
+ class ViewPropertyEntity(DMSVersionedEntity[PropertyId]):
261
346
  type_: ClassVar[EntityTypes] = EntityTypes.property_
262
347
  property_: str = Field(alias="property")
263
348
 
264
349
  def as_id(self) -> PropertyId:
265
- return PropertyId(
266
- source=ViewId(self.space, self.external_id, self.version_with_fallback), property=self.property_
350
+ return PropertyId(source=ViewId(self.space, self.external_id, self.version), property=self.property_)
351
+
352
+ def as_view_id(self) -> ViewId:
353
+ return ViewId(space=self.space, external_id=self.external_id, version=self.version)
354
+
355
+ @classmethod
356
+ def from_id(cls, id: PropertyId) -> "ViewPropertyEntity":
357
+ if isinstance(id.source, ContainerId):
358
+ raise ValueError("Only view source are supported")
359
+ if id.source.version is None:
360
+ raise ValueError("Version must be specified")
361
+ return cls(
362
+ space=id.source.space, externalId=id.source.external_id, version=id.source.version, property=id.property
267
363
  )
268
364
 
269
365
 
@@ -271,11 +367,32 @@ class DataModelEntity(DMSVersionedEntity[DataModelId]):
271
367
  type_: ClassVar[EntityTypes] = EntityTypes.datamodel
272
368
 
273
369
  def as_id(self) -> DataModelId:
274
- return DataModelId(space=self.space, external_id=self.external_id, version=self.version_with_fallback)
370
+ return DataModelId(space=self.space, external_id=self.external_id, version=self.version)
371
+
372
+ @classmethod
373
+ def from_id(cls, id: DataModelId) -> "DataModelEntity":
374
+ if id.version is None:
375
+ raise ValueError("Version must be specified")
376
+ return cls(space=id.space, externalId=id.external_id, version=id.version)
275
377
 
276
378
 
277
- class ReferenceEntity(PropertyEntity):
379
+ class ReferenceEntity(ClassEntity):
278
380
  type_: ClassVar[EntityTypes] = EntityTypes.reference_entity
381
+ prefix: str
382
+ property_: str | None = Field(None, alias="property")
383
+
384
+ def as_view_id(self) -> ViewId:
385
+ if isinstance(self.prefix, _UndefinedType) or isinstance(self.suffix, _UnknownType):
386
+ raise ValueError("Prefix is not defined or suffix is unknown")
387
+ return ViewId(space=self.prefix, external_id=self.suffix, version=self.version)
388
+
389
+ def as_view_property_id(self) -> PropertyId:
390
+ if self.property_ is None or self.prefix is Undefined or self.suffix is Unknown:
391
+ raise ValueError("Property is not defined or prefix is not defined or suffix is unknown")
392
+ return PropertyId(source=self.as_view_id(), property=self.property_)
393
+
394
+ def as_class_entity(self) -> ClassEntity:
395
+ return ClassEntity(prefix=self.prefix, suffix=self.suffix, version=self.version)
279
396
 
280
397
 
281
398
  def _split_str(v: Any) -> list[str]:
@@ -297,3 +414,28 @@ ParentEntityList = Annotated[
297
414
  when_used="unless-none",
298
415
  ),
299
416
  ]
417
+
418
+ ContainerEntityList = Annotated[
419
+ list[ContainerEntity],
420
+ BeforeValidator(_split_str),
421
+ PlainSerializer(
422
+ _join_str,
423
+ return_type=str,
424
+ when_used="unless-none",
425
+ ),
426
+ ]
427
+
428
+ ViewEntityList = Annotated[
429
+ list[ViewEntity],
430
+ BeforeValidator(_split_str),
431
+ PlainSerializer(
432
+ _join_str,
433
+ return_type=str,
434
+ when_used="unless-none",
435
+ ),
436
+ ]
437
+
438
+ URLEntity = Annotated[
439
+ AnyHttpUrl,
440
+ PlainSerializer(lambda v: str(v), return_type=str, when_used="unless-none"),
441
+ ]
@@ -1,25 +1,15 @@
1
- """
2
- """
1
+ """ """
3
2
 
4
3
  import re
5
4
  import sys
6
5
  from collections import Counter
7
- from typing import Literal
6
+ from functools import total_ordering
7
+ from typing import ClassVar, Literal
8
8
 
9
9
  from pydantic import BaseModel, field_validator
10
10
 
11
11
  from cognite.neat.rules import exceptions
12
12
 
13
- from ._entity import (
14
- CLASS_ID_REGEX,
15
- CLASS_ID_REGEX_COMPILED,
16
- ENTITY_ID_REGEX,
17
- PROPERTY_ID_REGEX,
18
- SUFFIX_REGEX,
19
- Entity,
20
- EntityTypes,
21
- )
22
-
23
13
  if sys.version_info >= (3, 11):
24
14
  from enum import StrEnum
25
15
  from typing import Self
@@ -40,6 +30,26 @@ class Lookup(StrEnum):
40
30
  value = "value" # type: ignore
41
31
 
42
32
 
33
+ class EntityTypes(StrEnum):
34
+ class_ = "class"
35
+ property_ = "property"
36
+ undefined = "undefined"
37
+
38
+
39
+ # FOR PARSING STRINGS:
40
+ PREFIX_REGEX = r"[a-zA-Z]+[a-zA-Z0-9-_.]*[a-zA-Z0-9]+"
41
+ SUFFIX_REGEX = r"[a-zA-Z0-9-_.]+[a-zA-Z0-9]|[-_.]*[a-zA-Z0-9]+"
42
+ VERSION_REGEX = r"[a-zA-Z0-9]([.a-zA-Z0-9_-]{0,41}[a-zA-Z0-9])?"
43
+
44
+ ENTITY_ID_REGEX = rf"{PREFIX_REGEX}:({SUFFIX_REGEX})"
45
+ ENTITY_ID_REGEX_COMPILED = re.compile(rf"^(?P<prefix>{PREFIX_REGEX}):(?P<suffix>{SUFFIX_REGEX})$")
46
+ VERSIONED_ENTITY_REGEX_COMPILED = re.compile(
47
+ rf"^(?P<prefix>{PREFIX_REGEX}):(?P<suffix>{SUFFIX_REGEX})\(version=(?P<version>{VERSION_REGEX})\)$"
48
+ )
49
+ CLASS_ID_REGEX = rf"(?P<{EntityTypes.class_}>{ENTITY_ID_REGEX})"
50
+ CLASS_ID_REGEX_COMPILED = re.compile(rf"^{CLASS_ID_REGEX}$")
51
+ PROPERTY_ID_REGEX = rf"\((?P<{EntityTypes.property_}>{ENTITY_ID_REGEX})\)"
52
+
43
53
  # traversal direction
44
54
  DIRECTION_REGEX = r"(?P<direction>(->|<-))"
45
55
 
@@ -73,6 +83,95 @@ TABLE_REGEX_COMPILED = re.compile(
73
83
  StepDirection = Literal["source", "target", "origin"]
74
84
  _direction_by_symbol: dict[str, StepDirection] = {"->": "target", "<-": "source"}
75
85
 
86
+ Undefined = type(object())
87
+ Unknown = type(object())
88
+
89
+
90
+ # mypy does not like the sentinel value, and it is not possible to ignore only the line with it below.
91
+ # so we ignore all errors beyond this point.
92
+ # mypy: ignore-errors
93
+ @total_ordering
94
+ class Entity(BaseModel, arbitrary_types_allowed=True):
95
+ """Entity is a class or property in OWL/RDF sense."""
96
+
97
+ type_: ClassVar[EntityTypes] = EntityTypes.undefined
98
+ prefix: str | Undefined = Undefined
99
+ suffix: str | Unknown
100
+ version: str | None = None
101
+ name: str | None = None
102
+ description: str | None = None
103
+
104
+ def __lt__(self, other: object) -> bool:
105
+ if type(self) is not type(other) or not isinstance(other, Entity):
106
+ return NotImplemented
107
+ return self.versioned_id < other.versioned_id
108
+
109
+ def __eq__(self, other: object) -> bool:
110
+ if type(self) is not type(other) or not isinstance(other, Entity):
111
+ return NotImplemented
112
+ return self.versioned_id == other.versioned_id
113
+
114
+ def __hash__(self) -> int:
115
+ return hash(self.versioned_id)
116
+
117
+ def as_non_versioned_entity(self) -> Self:
118
+ return self.from_string(f"{self.prefix}:{self.suffix}")
119
+
120
+ @property
121
+ def id(self) -> str:
122
+ if self.suffix is Unknown:
123
+ return "#N/A"
124
+ elif self.prefix is Undefined:
125
+ return self.suffix
126
+ else:
127
+ return f"{self.prefix}:{self.suffix}"
128
+
129
+ @property
130
+ def versioned_id(self) -> str:
131
+ if self.version is None:
132
+ return self.id
133
+ else:
134
+ return f"{self.id}(version={self.version})"
135
+
136
+ @property
137
+ def space(self) -> str:
138
+ """Returns entity space in CDF."""
139
+ return self.prefix
140
+
141
+ @property
142
+ def external_id(self) -> str:
143
+ """Returns entity external id in CDF."""
144
+ return self.suffix
145
+
146
+ def __repr__(self):
147
+ return self.versioned_id
148
+
149
+ def __str__(self):
150
+ return self.versioned_id
151
+
152
+ @classmethod
153
+ def from_string(cls, entity_string: str, base_prefix: str | None = None) -> Self:
154
+ if entity_string == "#N/A":
155
+ return cls(prefix=Undefined, suffix=Unknown)
156
+ elif result := VERSIONED_ENTITY_REGEX_COMPILED.match(entity_string):
157
+ return cls(
158
+ prefix=result.group("prefix"),
159
+ suffix=result.group("suffix"),
160
+ version=result.group("version"),
161
+ )
162
+ elif result := ENTITY_ID_REGEX_COMPILED.match(entity_string):
163
+ return cls(prefix=result.group("prefix"), suffix=result.group("suffix"))
164
+ elif base_prefix and re.match(SUFFIX_REGEX, entity_string) and re.match(PREFIX_REGEX, base_prefix):
165
+ return cls(prefix=base_prefix, suffix=entity_string)
166
+ else:
167
+ raise ValueError(f"{cls.__name__} is expected to be prefix:suffix, got {entity_string}")
168
+
169
+ @classmethod
170
+ def from_list(cls, entity_strings: list[str], base_prefix: str | None = None) -> list[Self]:
171
+ return [
172
+ cls.from_string(entity_string=entity_string, base_prefix=base_prefix) for entity_string in entity_strings
173
+ ]
174
+
76
175
 
77
176
  class Step(BaseModel):
78
177
  class_: Entity
@@ -27,7 +27,7 @@ from pydantic import (
27
27
  )
28
28
  from pydantic.fields import FieldInfo
29
29
 
30
- from cognite.neat.rules.models.rules._types import ClassType
30
+ from cognite.neat.rules.models.entities import ClassEntity
31
31
 
32
32
  if sys.version_info >= (3, 11):
33
33
  from enum import StrEnum
@@ -274,7 +274,7 @@ class BaseRules(RuleModel):
274
274
 
275
275
  # An sheet entity is either a class or a property.
276
276
  class SheetEntity(RuleModel):
277
- class_: ClassType = Field(alias="Class")
277
+ class_: ClassEntity = Field(alias="Class")
278
278
  name: str | None = Field(alias="Name", default=None)
279
279
  description: str | None = Field(alias="Description", default=None)
280
280