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