cognite-neat 0.123.26__py3-none-any.whl → 0.123.28__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 CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.123.26"
1
+ __version__ = "0.123.28"
2
2
  __engine__ = "^2.0.4"
@@ -43,6 +43,10 @@ class EntityTypes(StrEnum):
43
43
  prefix = "prefix"
44
44
  space = "space"
45
45
  container_index = "container_index"
46
+ concept_restriction = "conceptRestriction"
47
+ value_constraint = "valueConstraint"
48
+ cardinality_constraint = "cardinalityConstraint"
49
+ named_individual = "named_individual"
46
50
 
47
51
 
48
52
  def get_reserved_words(
@@ -100,8 +100,8 @@ class UnverifiedConceptualProperty(UnverifiedComponent[ConceptualProperty]):
100
100
 
101
101
  def dump(self, default_prefix: str, **kwargs) -> dict[str, Any]: # type: ignore
102
102
  output = super().dump()
103
- output["Concept"] = ConceptEntity.load(self.concept, prefix=default_prefix)
104
- output["Value Type"] = load_value_type(self.value_type, default_prefix)
103
+ output["Concept"] = ConceptEntity.load(self.concept, prefix=default_prefix, return_on_failure=True)
104
+ output["Value Type"] = load_value_type(self.value_type, default_prefix, return_on_failure=True)
105
105
  return output
106
106
 
107
107
  def copy(self, update: dict[str, Any], default_prefix: str) -> "UnverifiedConceptualProperty":
@@ -135,10 +135,16 @@ class UnverifiedConcept(UnverifiedComponent[Concept]):
135
135
  parent: list[ConceptEntity] | None = None
136
136
  if isinstance(self.implements, str):
137
137
  self.implements = self.implements.strip()
138
- parent = [ConceptEntity.load(parent, prefix=default_prefix) for parent in self.implements.split(",")]
138
+ parent = [
139
+ ConceptEntity.load(parent_str, prefix=default_prefix, return_on_failure=True)
140
+ for parent_str in self.implements.split(",")
141
+ ]
139
142
  elif isinstance(self.implements, list):
140
- parent = [ConceptEntity.load(parent_, prefix=default_prefix) for parent_ in self.implements]
141
- output["Concept"] = ConceptEntity.load(self.concept, prefix=default_prefix)
143
+ parent = [
144
+ ConceptEntity.load(parent_str, prefix=default_prefix, return_on_failure=True)
145
+ for parent_str in self.implements
146
+ ]
147
+ output["Concept"] = ConceptEntity.load(self.concept, prefix=default_prefix, return_on_failure=True)
142
148
  output["Implements"] = parent
143
149
  return output
144
150
 
@@ -1,6 +1,7 @@
1
1
  from ._constants import Undefined, Unknown
2
2
  from ._loaders import load_connection, load_dms_value_type, load_value_type
3
3
  from ._multi_value import MultiValueTypeInfo
4
+ from ._restrictions import ConceptPropertyCardinalityConstraint, ConceptPropertyValueConstraint, parse_restriction
4
5
  from ._single_value import (
5
6
  AssetEntity,
6
7
  AssetFields,
@@ -31,6 +32,8 @@ __all__ = [
31
32
  "CdfResourceEntityList",
32
33
  "ClassEntityList",
33
34
  "ConceptEntity",
35
+ "ConceptPropertyCardinalityConstraint",
36
+ "ConceptPropertyValueConstraint",
34
37
  "ConceptualEntity",
35
38
  "ContainerEntity",
36
39
  "ContainerEntityList",
@@ -61,4 +64,5 @@ __all__ = [
61
64
  "load_connection",
62
65
  "load_dms_value_type",
63
66
  "load_value_type",
67
+ "parse_restriction",
64
68
  ]
@@ -1,4 +1,4 @@
1
- from typing import Literal
1
+ from typing import Literal, overload
2
2
 
3
3
  from cognite.neat.core._data_model.models.data_types import DataType
4
4
  from cognite.neat.core._issues.errors import NeatTypeError
@@ -15,10 +15,38 @@ from ._single_value import (
15
15
  )
16
16
 
17
17
 
18
+ @overload
18
19
  def load_value_type(
19
20
  raw: str | MultiValueTypeInfo | DataType | ConceptEntity | UnknownEntity,
20
21
  default_prefix: str,
21
- ) -> MultiValueTypeInfo | DataType | ConceptEntity | UnknownEntity:
22
+ return_on_failure: Literal[False] = False,
23
+ ) -> MultiValueTypeInfo | DataType | ConceptEntity | UnknownEntity: ...
24
+
25
+
26
+ @overload
27
+ def load_value_type(
28
+ raw: str | MultiValueTypeInfo | DataType | ConceptEntity | UnknownEntity,
29
+ default_prefix: str,
30
+ return_on_failure: Literal[True],
31
+ ) -> MultiValueTypeInfo | DataType | ConceptEntity | UnknownEntity | None | str: ...
32
+
33
+
34
+ def load_value_type(
35
+ raw: str | MultiValueTypeInfo | DataType | ConceptEntity | UnknownEntity,
36
+ default_prefix: str,
37
+ return_on_failure: Literal[True, False] = False,
38
+ ) -> MultiValueTypeInfo | DataType | ConceptEntity | UnknownEntity | None | str:
39
+ """
40
+ Loads a value type from a raw string or entity.
41
+
42
+ Args:
43
+ raw: The raw value to load.
44
+ default_prefix: The default prefix to use if not specified in the raw value.
45
+ return_on_failure: If True, returns the raw value on parsing failure instead of raising an error.
46
+
47
+ Returns:
48
+ The loaded value type entity, or the raw value if loading fails and `return_on_failure` is True.
49
+ """
22
50
  if isinstance(raw, MultiValueTypeInfo | DataType | ConceptEntity | UnknownEntity):
23
51
  return raw
24
52
  elif isinstance(raw, str):
@@ -37,16 +65,47 @@ def load_value_type(
37
65
 
38
66
  # property holding link to class
39
67
  else:
40
- return ConceptEntity.load(raw, prefix=default_prefix)
68
+ return ConceptEntity.load(raw, prefix=default_prefix, return_on_failure=return_on_failure)
41
69
  else:
42
70
  raise NeatTypeError(f"Invalid value type: {type(raw)}")
43
71
 
44
72
 
73
+ @overload
74
+ def load_dms_value_type(
75
+ raw: str | DataType | ViewEntity | PhysicalUnknownEntity,
76
+ default_space: str,
77
+ default_version: str,
78
+ return_on_failure: Literal[False],
79
+ ) -> DataType | ViewEntity | PhysicalUnknownEntity: ...
80
+
81
+
82
+ @overload
83
+ def load_dms_value_type(
84
+ raw: str | DataType | ViewEntity | PhysicalUnknownEntity,
85
+ default_space: str,
86
+ default_version: str,
87
+ return_on_failure: Literal[True],
88
+ ) -> DataType | ViewEntity | PhysicalUnknownEntity | str: ...
89
+
90
+
45
91
  def load_dms_value_type(
46
92
  raw: str | DataType | ViewEntity | PhysicalUnknownEntity,
47
93
  default_space: str,
48
94
  default_version: str,
49
- ) -> DataType | ViewEntity | PhysicalUnknownEntity:
95
+ return_on_failure: Literal[True, False] = False,
96
+ ) -> DataType | ViewEntity | PhysicalUnknownEntity | str:
97
+ """
98
+ Loads a value type from a raw string or entity in the context of a data modeling service
99
+
100
+ Args:
101
+ raw: The raw value to load.
102
+ default_space: The default space to use if not specified in the raw value.
103
+ default_version: The default version to use if not specified in the raw value.
104
+ return_on_failure: If True, returns the raw value on parsing failure instead of raising an error.
105
+
106
+ Returns:
107
+ The loaded value type entity, or the raw value if loading fails and `return_on_failure` is True.
108
+ """
50
109
  if isinstance(raw, DataType | ViewEntity | PhysicalUnknownEntity):
51
110
  return raw
52
111
  elif isinstance(raw, str):
@@ -55,21 +114,42 @@ def load_dms_value_type(
55
114
  elif raw == str(Unknown):
56
115
  return PhysicalUnknownEntity()
57
116
  else:
58
- return ViewEntity.load(raw, space=default_space, version=default_version)
117
+ return ViewEntity.load(
118
+ raw, space=default_space, version=default_version, return_on_failure=return_on_failure
119
+ )
59
120
  raise NeatTypeError(f"Invalid value type: {type(raw)}")
60
121
 
61
122
 
123
+ @overload
124
+ def load_connection(
125
+ raw: Literal["direct"] | ReverseConnectionEntity | EdgeEntity | str | None,
126
+ default_space: str,
127
+ default_version: str,
128
+ return_on_failure: Literal[False] = False,
129
+ ) -> Literal["direct"] | ReverseConnectionEntity | EdgeEntity | None: ...
130
+
131
+
132
+ @overload
133
+ def load_connection(
134
+ raw: Literal["direct"] | ReverseConnectionEntity | EdgeEntity | str | None,
135
+ default_space: str,
136
+ default_version: str,
137
+ return_on_failure: Literal[True],
138
+ ) -> Literal["direct"] | ReverseConnectionEntity | EdgeEntity | None | str: ...
139
+
140
+
62
141
  def load_connection(
63
142
  raw: Literal["direct"] | ReverseConnectionEntity | EdgeEntity | str | None,
64
143
  default_space: str,
65
144
  default_version: str,
66
- ) -> Literal["direct"] | ReverseConnectionEntity | EdgeEntity | None:
145
+ return_on_failure: Literal[True, False] = False,
146
+ ) -> Literal["direct"] | ReverseConnectionEntity | EdgeEntity | None | str:
67
147
  if isinstance(raw, str) and raw.lower() == "direct":
68
148
  return "direct" # type: ignore[return-value]
69
149
  elif isinstance(raw, EdgeEntity | ReverseConnectionEntity) or raw is None:
70
150
  return raw # type: ignore[return-value]
71
151
  elif isinstance(raw, str) and raw.startswith("edge"):
72
- return EdgeEntity.load(raw, space=default_space, version=default_version) # type: ignore[return-value]
152
+ return EdgeEntity.load(raw, space=default_space, version=default_version, return_on_failure=return_on_failure) # type: ignore[return-value]
73
153
  elif isinstance(raw, str) and raw.startswith("reverse"):
74
- return ReverseConnectionEntity.load(raw) # type: ignore[return-value]
154
+ return ReverseConnectionEntity.load(raw, return_on_failure=return_on_failure) # type: ignore[return-value]
75
155
  raise NeatTypeError(f"Invalid connection: {type(raw)}")
@@ -0,0 +1,230 @@
1
+ import re
2
+ import sys
3
+ from abc import ABC
4
+ from typing import Any, ClassVar, Final, Literal, TypeVar, cast, get_args
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
7
+ from rdflib import Literal as RDFLiteral
8
+ from rdflib import URIRef
9
+
10
+ from cognite.neat.core._data_model._constants import EntityTypes
11
+ from cognite.neat.core._data_model.models.data_types import _XSD_TYPES, DataType
12
+ from cognite.neat.core._data_model.models.entities._constants import _PARSE
13
+ from cognite.neat.core._data_model.models.entities._single_value import ConceptEntity, ConceptualEntity
14
+ from cognite.neat.core._issues.errors._general import NeatValueError
15
+ from cognite.neat.core._utils.rdf_ import remove_namespace_from_uri
16
+
17
+ if sys.version_info <= (3, 11):
18
+ from typing_extensions import Self
19
+ else:
20
+ from typing import Self
21
+
22
+
23
+ ValueConstraints = Literal["allValuesFrom", "someValuesFrom", "hasValue"]
24
+ CardinalityConstraints = Literal["minCardinality", "maxCardinality", "cardinality", "qualifiedCardinality"]
25
+
26
+
27
+ # Constants for regex patterns - more maintainable
28
+ PROPERTY_PATTERN: Final[str] = r"[a-zA-Z0-9._~?@!$&'*+,;=%-]+"
29
+ VALUE_PATTERN: Final[str] = r".+"
30
+ CARDINALITY_VALUE_PATTERN: Final[str] = r"\d+"
31
+ ON_PATTERN: Final[str] = r"[^(]*"
32
+
33
+ VALUE_CONSTRAINT_REGEX = re.compile(
34
+ rf"^{EntityTypes.value_constraint}:(?P<property>{PROPERTY_PATTERN})\((?P<constraint>{'|'.join(get_args(ValueConstraints))}),(?P<value>{VALUE_PATTERN})\)$"
35
+ )
36
+
37
+ CARDINALITY_CONSTRAINT_REGEX = re.compile(
38
+ rf"^{EntityTypes.cardinality_constraint}:(?P<property>{PROPERTY_PATTERN})\((?P<constraint>{'|'.join(get_args(CardinalityConstraints))}),(?P<value>{CARDINALITY_VALUE_PATTERN})(?:,(?P<on>{ON_PATTERN}))?\)$"
39
+ )
40
+
41
+
42
+ class NamedIndividualEntity(ConceptualEntity):
43
+ type_: ClassVar[EntityTypes] = EntityTypes.named_individual
44
+
45
+ @model_validator(mode="after")
46
+ def reset_prefix(self) -> Self:
47
+ self.prefix = "ni"
48
+ return self
49
+
50
+
51
+ class ConceptPropertyRestriction(ABC, BaseModel):
52
+ model_config: ClassVar[ConfigDict] = ConfigDict(
53
+ str_strip_whitespace=True,
54
+ arbitrary_types_allowed=True,
55
+ strict=False,
56
+ extra="ignore",
57
+ )
58
+ type_: ClassVar[EntityTypes] = EntityTypes.concept_restriction
59
+ property_: str
60
+
61
+ @classmethod
62
+ def load(cls: "type[T_ConceptPropertyRestriction]", data: Any, **defaults: Any) -> "T_ConceptPropertyRestriction":
63
+ if isinstance(data, cls):
64
+ return data
65
+ elif not isinstance(data, str):
66
+ raise ValueError(f"Cannot load {cls.__name__} from {data}")
67
+
68
+ return cls.model_validate({_PARSE: data, "defaults": defaults})
69
+
70
+ @model_validator(mode="before")
71
+ def _load(cls, data: Any) -> dict:
72
+ defaults = {}
73
+ if isinstance(data, dict) and _PARSE in data:
74
+ defaults = data.get("defaults", {})
75
+ data = data[_PARSE]
76
+ if isinstance(data, dict):
77
+ data.update(defaults)
78
+ return data
79
+
80
+ if not isinstance(data, str):
81
+ raise ValueError(f"Cannot load {cls.__name__} from {data}")
82
+
83
+ return cls._parse(data, defaults)
84
+
85
+ @classmethod
86
+ def _parse(cls, data: str, defaults: dict) -> dict:
87
+ raise NotImplementedError(f"{cls.__name__} must implement _parse method")
88
+
89
+ def dump(self) -> str:
90
+ return self.__str__()
91
+
92
+ def as_tuple(self) -> tuple[str, ...]:
93
+ # We haver overwritten the serialization to str, so we need to do it manually
94
+ extra: tuple[str, ...] = tuple(
95
+ [
96
+ str(v or "")
97
+ for field_name in self.model_fields.keys()
98
+ if (v := getattr(self, field_name)) and field_name not in {"property_"}
99
+ ]
100
+ )
101
+
102
+ return self.property_, *extra
103
+
104
+ def __lt__(self, other: object) -> bool:
105
+ if not isinstance(other, ConceptPropertyRestriction):
106
+ return NotImplemented
107
+ return self.as_tuple() < other.as_tuple()
108
+
109
+ def __eq__(self, other: object) -> bool:
110
+ if not isinstance(other, ConceptPropertyRestriction):
111
+ return NotImplemented
112
+ return self.as_tuple() == other.as_tuple()
113
+
114
+ def __hash__(self) -> int:
115
+ return hash(str(self))
116
+
117
+
118
+ T_ConceptPropertyRestriction = TypeVar("T_ConceptPropertyRestriction", bound="ConceptPropertyRestriction")
119
+
120
+
121
+ class ConceptPropertyValueConstraint(ConceptPropertyRestriction):
122
+ type_: ClassVar[EntityTypes] = EntityTypes.value_constraint
123
+ constraint: ValueConstraints
124
+ value: RDFLiteral | ConceptEntity | NamedIndividualEntity
125
+
126
+ @field_validator("value")
127
+ def validate_value(cls, value: Any) -> Any:
128
+ if isinstance(value, RDFLiteral) and value.datatype is None:
129
+ raise NeatValueError("RDFLiteral must have a datatype set, which must be one of the XSD types.")
130
+ return value
131
+
132
+ def __str__(self) -> str:
133
+ value_str = (
134
+ f"{self.value.value}^^{remove_namespace_from_uri(cast(URIRef, self.value.datatype))}"
135
+ if isinstance(self.value, RDFLiteral)
136
+ else str(self.value)
137
+ )
138
+ return f"{self.type_}:{self.property_}({self.constraint},{value_str})"
139
+
140
+ @classmethod
141
+ def _parse(cls, data: str, defaults: dict) -> dict:
142
+ if not (result := VALUE_CONSTRAINT_REGEX.match(data)):
143
+ raise NeatValueError(f"Invalid value constraint format: {data}")
144
+
145
+ property_ = result.group("property")
146
+ constraint = result.group("constraint")
147
+ raw_value = result.group("value")
148
+
149
+ value: NamedIndividualEntity | RDFLiteral | ConceptEntity
150
+ # scenario 1: NamedIndividual as value restriction
151
+ if raw_value.startswith("ni:"):
152
+ value = NamedIndividualEntity.load(raw_value)
153
+ # scenario 2: Datatype as value restriction
154
+ elif "^^" in raw_value:
155
+ if len(value_components := raw_value.split("^^")) == 2 and value_components[1] in _XSD_TYPES:
156
+ value = RDFLiteral(value_components[0], datatype=DataType.load(value_components[1]).as_xml_uri_ref())
157
+ else:
158
+ raise NeatValueError(f"Invalid value format for datatype: {raw_value}")
159
+
160
+ # scenario 3: ConceptEntity as value restriction
161
+ else:
162
+ value = ConceptEntity.load(raw_value, **defaults)
163
+
164
+ return dict(property_=property_, constraint=constraint, value=value)
165
+
166
+
167
+ class ConceptPropertyCardinalityConstraint(ConceptPropertyRestriction):
168
+ type_: ClassVar[EntityTypes] = EntityTypes.cardinality_constraint
169
+ constraint: CardinalityConstraints
170
+ value: int = Field(ge=0)
171
+ on: DataType | ConceptEntity | None = None
172
+
173
+ def __str__(self) -> str:
174
+ on_str = f",{self.on}" if self.on else ""
175
+ return f"{self.type_}:{self.property_}({self.constraint},{self.value}{on_str})"
176
+
177
+ @classmethod
178
+ def _parse(cls, data: str, defaults: dict) -> dict:
179
+ if not (result := CARDINALITY_CONSTRAINT_REGEX.match(data)):
180
+ raise NeatValueError(f"Invalid cardinality constraint format: {data}")
181
+
182
+ property_ = result.group("property")
183
+ constraint = result.group("constraint")
184
+ value = result.group("value")
185
+ on = result.group("on")
186
+ if on:
187
+ if on in _XSD_TYPES:
188
+ on = DataType.load(on)
189
+ else:
190
+ on = cast(ConceptEntity, ConceptEntity.load(on, **defaults))
191
+
192
+ return dict(property_=property_, constraint=constraint, value=value, on=on)
193
+
194
+
195
+ def parse_restriction(data: str, **defaults: Any) -> ConceptPropertyRestriction:
196
+ """Parse a string to create either a value or cardinality restriction.
197
+
198
+ Args:
199
+ data: String representation of the restriction
200
+ **defaults: Default values to use when parsing
201
+
202
+ Returns:
203
+ Either a ConceptPropertyValueConstraint or ConceptPropertyCardinalityConstraint
204
+
205
+ Raises:
206
+ NeatValueError: If the string cannot be parsed as either restriction type
207
+ """
208
+ # Check for value constraint pattern first (more specific)
209
+ if VALUE_CONSTRAINT_REGEX.match(data):
210
+ try:
211
+ return ConceptPropertyValueConstraint.load(data, **defaults)
212
+ except Exception as e:
213
+ raise NeatValueError(f"Failed to parse value constraint: {data}") from e
214
+
215
+ # Check for cardinality constraint pattern
216
+ if CARDINALITY_CONSTRAINT_REGEX.match(data):
217
+ try:
218
+ return ConceptPropertyCardinalityConstraint.load(data, **defaults)
219
+ except Exception as e:
220
+ raise NeatValueError(f"Failed to parse cardinality constraint: {data}") from e
221
+
222
+ # If neither pattern matches, provide a clear error
223
+ raise NeatValueError(
224
+ f"Invalid restriction format: {data}. "
225
+ f"Expected format: '{EntityTypes.value_constraint}:property(constraint,value)' "
226
+ f"or '{EntityTypes.cardinality_constraint}:property(constraint,value[,on])'"
227
+ )
228
+
229
+
230
+ T_ConceptRestriction = TypeVar("T_ConceptRestriction", bound=ConceptPropertyRestriction)
@@ -59,7 +59,13 @@ class ConceptualEntity(BaseModel, extra="ignore"):
59
59
 
60
60
  @classmethod
61
61
  @overload
62
- def load(cls: "type[T_Entity]", data: Any, strict: Literal[True], **defaults: Any) -> "T_Entity": ...
62
+ def load(
63
+ cls: "type[T_Entity]",
64
+ data: Any,
65
+ strict: Literal[True],
66
+ return_on_failure: Literal[False] = False,
67
+ **defaults: Any,
68
+ ) -> "T_Entity": ...
63
69
 
64
70
  @classmethod
65
71
  @overload
@@ -67,25 +73,59 @@ class ConceptualEntity(BaseModel, extra="ignore"):
67
73
  cls: "type[T_Entity]",
68
74
  data: Any,
69
75
  strict: Literal[False] = False,
76
+ return_on_failure: Literal[False] = False,
70
77
  **defaults: Any,
71
78
  ) -> "T_Entity | UnknownEntity": ...
72
79
 
73
80
  @classmethod
74
- def load(cls: "type[T_Entity]", data: Any, strict: bool = False, **defaults: Any) -> "T_Entity | UnknownEntity":
81
+ @overload
82
+ def load(
83
+ cls: "type[T_Entity]",
84
+ data: Any,
85
+ strict: Literal[False] = False,
86
+ return_on_failure: Literal[True] = True,
87
+ **defaults: Any,
88
+ ) -> "T_Entity | Any": ...
89
+
90
+ @classmethod
91
+ @overload
92
+ def load(
93
+ cls: "type[T_Entity]",
94
+ data: Any,
95
+ strict: Literal[True] = True,
96
+ return_on_failure: Literal[True] = True,
97
+ **defaults: Any,
98
+ ) -> "T_Entity | Any": ...
99
+
100
+ @classmethod
101
+ def load(
102
+ cls: "type[T_Entity]", data: Any, strict: bool = False, return_on_failure: bool = False, **defaults: Any
103
+ ) -> "T_Entity | UnknownEntity | Any":
104
+ """Loads an entity from a string or dict representation.
105
+
106
+ Args:
107
+ data: The data to load the entity from. Can be a string or a dict.
108
+ strict: If True, will raise an error if the data is "unknown".
109
+ return_on_failure: If True, will return the input data if loading fails.
110
+ This is used when you want to defer error handling to a later stage.
111
+ defaults: Default values to use when loading the entity. These will be used if the corresponding
112
+ """
75
113
  if isinstance(data, cls):
76
114
  return data
77
115
  elif isinstance(data, str) and data == str(Unknown):
78
116
  if strict:
79
117
  raise NeatValueError(f"Failed to load entity {data!s}")
80
118
  return UnknownEntity(prefix=Undefined, suffix=Unknown)
81
- if defaults and isinstance(defaults, dict):
82
- # This is a trick to pass in default values
83
- try:
119
+ try:
120
+ if defaults and isinstance(defaults, dict):
121
+ # This is a trick to pass in default values
84
122
  return cls.model_validate({_PARSE: data, "defaults": defaults})
85
- except ValueError:
86
- raise
87
- else:
88
- return cls.model_validate(data)
123
+ else:
124
+ return cls.model_validate(data)
125
+ except ValueError:
126
+ if return_on_failure:
127
+ return data
128
+ raise
89
129
 
90
130
  @model_validator(mode="before")
91
131
  def _load(cls, data: Any) -> "dict | ConceptualEntity":
@@ -342,7 +382,13 @@ class PhysicalEntity(ConceptualEntity, Generic[T_ID], ABC):
342
382
 
343
383
  @classmethod # type: ignore[override]
344
384
  @overload
345
- def load(cls: "type[T_DMSEntity]", data: Any, strict: Literal[True], **defaults: Any) -> "T_DMSEntity": ...
385
+ def load(
386
+ cls: "type[T_DMSEntity]",
387
+ data: Any,
388
+ strict: Literal[True],
389
+ return_on_failure: Literal[False] = False,
390
+ **defaults: Any,
391
+ ) -> "T_DMSEntity": ...
346
392
 
347
393
  @classmethod
348
394
  @overload
@@ -350,18 +396,56 @@ class PhysicalEntity(ConceptualEntity, Generic[T_ID], ABC):
350
396
  cls: "type[T_DMSEntity]",
351
397
  data: Any,
352
398
  strict: Literal[False] = False,
399
+ return_on_failure: Literal[False] = False,
353
400
  **defaults: Any,
354
401
  ) -> "T_DMSEntity | PhysicalUnknownEntity": ...
355
402
 
356
403
  @classmethod
404
+ @overload
357
405
  def load(
358
- cls: "type[T_DMSEntity]", data: Any, strict: bool = False, **defaults: Any
359
- ) -> "T_DMSEntity | PhysicalUnknownEntity": # type: ignore
406
+ cls: "type[T_DMSEntity]",
407
+ data: Any,
408
+ strict: Literal[False] = False,
409
+ return_on_failure: Literal[True] = True,
410
+ **defaults: Any,
411
+ ) -> "T_DMSEntity | Any": ...
412
+
413
+ @classmethod
414
+ @overload
415
+ def load(
416
+ cls: "type[T_DMSEntity]",
417
+ data: Any,
418
+ strict: Literal[True] = True,
419
+ return_on_failure: Literal[True] = True,
420
+ **defaults: Any,
421
+ ) -> "T_DMSEntity | Any": ...
422
+
423
+ @classmethod
424
+ def load(
425
+ cls: "type[T_DMSEntity]",
426
+ data: Any,
427
+ strict: Literal[True, False] = False,
428
+ return_on_failure: Literal[True, False] = False,
429
+ **defaults: Any,
430
+ ) -> "T_DMSEntity | PhysicalUnknownEntity | Any":
431
+ """Loads a DMS entity from a string or dict representation.
432
+
433
+ Args:
434
+ data: The data to load the entity from. Can be a string or a dict.
435
+ strict: If True, will raise an error if the data is "unknown".
436
+ return_on_failure: If True, will return the input data if loading fails.
437
+ This is used when you want to defer error handling to a later stage.
438
+ defaults: Default values to use when loading the entity. These will be used if the corresponding
439
+ fields are missing in the input data.
440
+
441
+ """
360
442
  if isinstance(data, str) and data == str(Unknown):
361
443
  if strict:
362
444
  raise NeatValueError(f"Failed to load entity {data!s}")
363
445
  return PhysicalUnknownEntity.from_id(None)
364
- return cast(T_DMSEntity, super().load(data, **defaults))
446
+ if isinstance(data, UnknownEntity):
447
+ return PhysicalUnknownEntity.from_id(None)
448
+ return cast(T_DMSEntity, super().load(data, strict=False, return_on_failure=return_on_failure, **defaults))
365
449
 
366
450
  @property
367
451
  def space(self) -> str:
@@ -3,7 +3,7 @@ import sys
3
3
  import warnings
4
4
  from dataclasses import dataclass
5
5
  from datetime import datetime
6
- from typing import Any, Literal
6
+ from typing import Any, Literal, overload
7
7
 
8
8
  import pandas as pd
9
9
  from cognite.client import data_modeling as dm
@@ -161,19 +161,23 @@ class UnverifiedPhysicalProperty(UnverifiedComponent[PhysicalProperty]):
161
161
 
162
162
  def dump(self, default_space: str, default_version: str) -> dict[str, Any]: # type: ignore[override]
163
163
  output = super().dump()
164
- output["View"] = ViewEntity.load(self.view, space=default_space, version=default_version)
165
- output["Value Type"] = load_dms_value_type(self.value_type, default_space, default_version)
166
- output["Connection"] = load_connection(self.connection, default_space, default_version)
164
+ output["View"] = ViewEntity.load(
165
+ self.view, space=default_space, version=default_version, return_on_failure=True
166
+ )
167
+ output["Value Type"] = load_dms_value_type(
168
+ self.value_type, default_space, default_version, return_on_failure=True
169
+ )
170
+ output["Connection"] = load_connection(self.connection, default_space, default_version, return_on_failure=True)
167
171
  output["Container"] = (
168
- ContainerEntity.load(self.container, space=default_space, version=default_version)
172
+ ContainerEntity.load(self.container, space=default_space, version=default_version, return_on_failure=True)
169
173
  if self.container
170
174
  else None
171
175
  )
172
176
  if isinstance(self.index, ContainerIndexEntity) or (isinstance(self.index, str) and "," not in self.index):
173
- output["Index"] = [ContainerIndexEntity.load(self.index)]
177
+ output["Index"] = [ContainerIndexEntity.load(self.index, return_on_failure=True)]
174
178
  elif isinstance(self.index, str):
175
179
  output["Index"] = [
176
- ContainerIndexEntity.load(index.strip())
180
+ ContainerIndexEntity.load(index.strip(), return_on_failure=True)
177
181
  for index in SPLIT_ON_COMMA_PATTERN.split(self.index)
178
182
  if index.strip()
179
183
  ]
@@ -185,13 +189,11 @@ class UnverifiedPhysicalProperty(UnverifiedComponent[PhysicalProperty]):
185
189
  elif isinstance(index, str):
186
190
  index_list.extend(
187
191
  [
188
- ContainerIndexEntity.load(idx.strip())
192
+ ContainerIndexEntity.load(idx.strip(), return_on_failure=True)
189
193
  for idx in SPLIT_ON_COMMA_PATTERN.split(index)
190
194
  if idx.strip()
191
195
  ]
192
196
  )
193
- elif isinstance(index, str):
194
- index_list.append(ContainerIndexEntity.load(index.strip()))
195
197
  else:
196
198
  raise TypeError(f"Unexpected type for index: {type(index)}")
197
199
  output["Index"] = index_list
@@ -257,16 +259,29 @@ class UnverifiedPhysicalContainer(UnverifiedComponent[PhysicalContainer]):
257
259
 
258
260
  def dump(self, default_space: str) -> dict[str, Any]: # type: ignore[override]
259
261
  output = super().dump()
260
- output["Container"] = self.as_entity_id(default_space)
262
+ output["Container"] = self.as_entity_id(default_space, return_on_failure=True)
261
263
  output["Constraint"] = (
262
- [ContainerEntity.load(constraint.strip(), space=default_space) for constraint in self.constraint.split(",")]
264
+ [
265
+ ContainerEntity.load(constraint.strip(), space=default_space, return_on_failure=True)
266
+ for constraint in self.constraint.split(",")
267
+ ]
263
268
  if self.constraint
264
269
  else None
265
270
  )
266
271
  return output
267
272
 
268
- def as_entity_id(self, default_space: str) -> ContainerEntity:
269
- return ContainerEntity.load(self.container, strict=True, space=default_space)
273
+ @overload
274
+ def as_entity_id(self, default_space: str, return_on_failure: Literal[False] = False) -> ContainerEntity: ...
275
+
276
+ @overload
277
+ def as_entity_id(self, default_space: str, return_on_failure: Literal[True]) -> ContainerEntity | str: ...
278
+
279
+ def as_entity_id(
280
+ self, default_space: str, return_on_failure: Literal[True, False] = False
281
+ ) -> ContainerEntity | str:
282
+ return ContainerEntity.load(
283
+ self.container, strict=True, space=default_space, return_on_failure=return_on_failure
284
+ )
270
285
 
271
286
  @classmethod
272
287
  def from_container(cls, container: dm.ContainerApply) -> "UnverifiedPhysicalContainer":
@@ -306,19 +321,51 @@ class UnverifiedPhysicalView(UnverifiedComponent[PhysicalView]):
306
321
 
307
322
  def dump(self, default_space: str, default_version: str) -> dict[str, Any]: # type: ignore[override]
308
323
  output = super().dump()
309
- output["View"] = self.as_entity_id(default_space, default_version)
310
- output["Implements"] = self._load_implements(default_space, default_version)
324
+ output["View"] = self.as_entity_id(default_space, default_version, return_on_failure=True)
325
+ output["Implements"] = self._load_implements(default_space, default_version, return_on_failure=True)
311
326
  return output
312
327
 
313
- def as_entity_id(self, default_space: str, default_version: str) -> ViewEntity:
314
- return ViewEntity.load(self.view, strict=True, space=default_space, version=default_version)
328
+ @overload
329
+ def as_entity_id(
330
+ self, default_space: str, default_version: str, return_on_failure: Literal[False] = False
331
+ ) -> ViewEntity: ...
332
+
333
+ @overload
334
+ def as_entity_id(
335
+ self, default_space: str, default_version: str, return_on_failure: Literal[True]
336
+ ) -> ViewEntity | str: ...
337
+
338
+ def as_entity_id(
339
+ self, default_space: str, default_version: str, return_on_failure: Literal[True, False] = False
340
+ ) -> ViewEntity | str:
341
+ return ViewEntity.load(
342
+ self.view, strict=True, space=default_space, version=default_version, return_on_failure=return_on_failure
343
+ )
315
344
 
316
- def _load_implements(self, default_space: str, default_version: str) -> list[ViewEntity] | None:
345
+ @overload
346
+ def _load_implements(
347
+ self, default_space: str, default_version: str, return_on_failure: Literal[False] = False
348
+ ) -> list[ViewEntity] | None: ...
349
+
350
+ @overload
351
+ def _load_implements(
352
+ self, default_space: str, default_version: str, return_on_failure: Literal[True]
353
+ ) -> list[ViewEntity | str] | None: ...
354
+
355
+ def _load_implements(
356
+ self, default_space: str, default_version: str, return_on_failure: Literal[True, False] = False
357
+ ) -> list[ViewEntity] | list[ViewEntity | str] | None:
317
358
  self.implements = self.implements.strip() if self.implements else None
318
359
 
319
360
  return (
320
361
  [
321
- ViewEntity.load(implement, strict=True, space=default_space, version=default_version)
362
+ ViewEntity.load(
363
+ implement,
364
+ strict=True,
365
+ space=default_space,
366
+ version=default_version,
367
+ return_on_failure=return_on_failure,
368
+ )
322
369
  for implement in self.implements.split(",")
323
370
  ]
324
371
  if self.implements
@@ -361,7 +408,7 @@ class UnverifiedPhysicalNodeType(UnverifiedComponent[PhysicalNodeType]):
361
408
 
362
409
  def dump(self, default_space: str, **_) -> dict[str, Any]: # type: ignore
363
410
  output = super().dump()
364
- output["Node"] = DMSNodeEntity.load(self.node, space=default_space)
411
+ output["Node"] = DMSNodeEntity.load(self.node, space=default_space, return_on_failure=True)
365
412
  return output
366
413
 
367
414
 
@@ -20,7 +20,11 @@ class SpreadsheetError(NeatError, ValueError, ABC):
20
20
  spreadsheet_name = cast(str, location[0])
21
21
  if spreadsheet_name not in ERROR_CLS_BY_SPREADSHEET_NAME:
22
22
  # This happens for the metadata sheet, which are individual fields
23
- return MetadataValueError(error, field_name=spreadsheet_name)
23
+ if spreadsheet_name == "Metadata" and len(location) >= 2 and isinstance(location[1], str):
24
+ field_name = cast(str, location[1])
25
+ else:
26
+ field_name = spreadsheet_name
27
+ return MetadataValueError(error, field_name=field_name)
24
28
 
25
29
  error_cls = ERROR_CLS_BY_SPREADSHEET_NAME[spreadsheet_name]
26
30
  row, column = cast(tuple[int, str], location[2:4])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite-neat
3
- Version: 0.123.26
3
+ Version: 0.123.28
4
4
  Summary: Knowledge graph transformation
5
5
  Project-URL: Documentation, https://cognite-neat.readthedocs-hosted.com/
6
6
  Project-URL: Homepage, https://cognite-neat.readthedocs-hosted.com/
@@ -18,7 +18,7 @@ Requires-Dist: jsonpath-python<2.0.0,>=1.0.6
18
18
  Requires-Dist: mixpanel<5.0.0,>=4.10.1
19
19
  Requires-Dist: networkx<4.0.0,>=3.4.2
20
20
  Requires-Dist: openpyxl<4.0.0,>=3.0.10
21
- Requires-Dist: packaging<25.0,>=22.0
21
+ Requires-Dist: packaging>=22.0
22
22
  Requires-Dist: pandas<3.0.0,>=1.5.3
23
23
  Requires-Dist: pydantic<3.0.0,>=2.0.0
24
24
  Requires-Dist: pyvis<1.0.0,>=0.3.2
@@ -1,5 +1,5 @@
1
1
  cognite/neat/__init__.py,sha256=12StS1dzH9_MElqxGvLWrNsxCJl9Hv8A2a9D0E5OD_U,193
2
- cognite/neat/_version.py,sha256=ZRJQU5AqGl89hbyhyolzPHb6kKhu67DqV9zYl0ab7Nw,47
2
+ cognite/neat/_version.py,sha256=6seCV2thSh0iPLMJ5f4Ba6srBIKclOMKbKjgQpVgAiA,47
3
3
  cognite/neat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  cognite/neat/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  cognite/neat/core/_config.py,sha256=WT1BS8uADcFvGoUYOOfwFOVq_VBl472TisdoA3wLick,280
@@ -19,7 +19,7 @@ cognite/neat/core/_client/data_classes/neat_sequence.py,sha256=QZWSfWnwk6KlYJvsI
19
19
  cognite/neat/core/_client/data_classes/schema.py,sha256=SpkBGbC2SUJG38Ixf1vYJINI66i_OZaT03q4XKRtK54,25067
20
20
  cognite/neat/core/_client/data_classes/statistics.py,sha256=GU-u41cOTig0Y5pYhW5KqzCsuAUIX9tOmdizMEveYuw,4487
21
21
  cognite/neat/core/_data_model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- cognite/neat/core/_data_model/_constants.py,sha256=ssiOprhd4bamglQnLTNjmqYn9mCBW-VOUbn08qDBbsM,5857
22
+ cognite/neat/core/_data_model/_constants.py,sha256=37lvnq3UZu4bPSzjRRThXJGdvaxmA7KJzWL0YysYBs8,6040
23
23
  cognite/neat/core/_data_model/_shared.py,sha256=at3-TT6pkdwtsjuz94P1lj1W7dO6_Sb3nQ3JSDBNLCY,2088
24
24
  cognite/neat/core/_data_model/analysis/__init__.py,sha256=v3hSfz7AEEqcmdjL71I09tP8Hl-gPZYOiDYMp_CW4vg,70
25
25
  cognite/neat/core/_data_model/analysis/_base.py,sha256=VT1orVf0xSSZrKnJoc-cJR1F1mtMn9Ybjk7UDO71aFA,24448
@@ -53,14 +53,15 @@ cognite/neat/core/_data_model/models/_import_contexts.py,sha256=LAQbgwTbyIAY3at2
53
53
  cognite/neat/core/_data_model/models/_types.py,sha256=70E8fiLdZkVF2sDUGPuDhzXNA5niVECkVDI7YN0NF60,5488
54
54
  cognite/neat/core/_data_model/models/data_types.py,sha256=uQ_u9KxCetLjxo-VtFzOXSxQuuf97Kg-9lfTTGzY6hc,10150
55
55
  cognite/neat/core/_data_model/models/conceptual/__init__.py,sha256=9A6myEV8s0-LqdXejaljqPj8S0pIpUL75rNdRDZzyR8,585
56
- cognite/neat/core/_data_model/models/conceptual/_unverified.py,sha256=VswgnTSjSCRzBX3z5HvintBGaWBPexxIs-7z7S4J57c,6298
56
+ cognite/neat/core/_data_model/models/conceptual/_unverified.py,sha256=2Ju_LJS0xH8Ao-AxZMLwAZwHjg6ITyn4tBwTfzy2Wic,6524
57
57
  cognite/neat/core/_data_model/models/conceptual/_validation.py,sha256=CF1jYALTCQFAr5KUwplAzOLdxJGknIeJo-HpxVZsgMY,13859
58
58
  cognite/neat/core/_data_model/models/conceptual/_verified.py,sha256=BUB4Ur4kpBoWiwTf57tjxJ2l0tDTSbY7zGrg1g0yVNQ,13716
59
- cognite/neat/core/_data_model/models/entities/__init__.py,sha256=UsW-_6fwd-TW0WcnShPKf40h75l1elVn80VurUwRAic,1567
59
+ cognite/neat/core/_data_model/models/entities/__init__.py,sha256=qXrnTygYBgu4iE-ehNsHwmZ89pm9pTfU_Qe9rAq-ldg,1789
60
60
  cognite/neat/core/_data_model/models/entities/_constants.py,sha256=GXRzVfArwxF3C67VCkzy0JWTZRkRJUYXBQaaecrqcWc,351
61
- cognite/neat/core/_data_model/models/entities/_loaders.py,sha256=PkrVtGlZWYLvAVIRABrgVSgkMvJYpBqdrHBfz-H0Ut8,2783
61
+ cognite/neat/core/_data_model/models/entities/_loaders.py,sha256=vYRHID2SoseyJuzO4sdjjeWaRnv774SdKz9fCeuHTHU,5703
62
62
  cognite/neat/core/_data_model/models/entities/_multi_value.py,sha256=4507L7dr_CL4vo1En6EyMiHhUDOWPTDVR1aYZns6S38,2792
63
- cognite/neat/core/_data_model/models/entities/_single_value.py,sha256=DO_u8dLLD8xabVZFXilZELv_Fzs6dcMs6msoszw32iY,21034
63
+ cognite/neat/core/_data_model/models/entities/_restrictions.py,sha256=inztyR2EskFK9cYAdm39GbnjAod7Zmg0TL9NI0ZRleM,8925
64
+ cognite/neat/core/_data_model/models/entities/_single_value.py,sha256=HWANjeBNDo_o3s3oHxRWsxtidvo3d40GObcm7H_7hKo,23753
64
65
  cognite/neat/core/_data_model/models/entities/_types.py,sha256=MqrCovqI_nvpMB4UqiUk4eUlKANvr8P7wr8k3y8lXlQ,2183
65
66
  cognite/neat/core/_data_model/models/entities/_wrapped.py,sha256=hOvdyxCNFgv1UdfaasviKnbEN4yN09Iip0ggQiaXgB4,7993
66
67
  cognite/neat/core/_data_model/models/mapping/__init__.py,sha256=T68Hf7rhiXa7b03h4RMwarAmkGnB-Bbhc1H07b2PyC4,100
@@ -68,7 +69,7 @@ cognite/neat/core/_data_model/models/mapping/_classic2core.py,sha256=FRDpYP_CX-C
68
69
  cognite/neat/core/_data_model/models/mapping/_classic2core.yaml,sha256=ei-nuivNWVW9HmvzDBKIPF6ZdgaMq64XHw_rKm0CMxg,22584
69
70
  cognite/neat/core/_data_model/models/physical/__init__.py,sha256=ONE_xLw1cxfw88rICG_RtbjCYUZm8yS2kBQ4Di3EGnA,987
70
71
  cognite/neat/core/_data_model/models/physical/_exporter.py,sha256=DPOytV-sIzpGJtfDEfi7G4RWnSCVNRLWe1KzY26ewmc,30083
71
- cognite/neat/core/_data_model/models/physical/_unverified.py,sha256=VyI-JULAu6kHJygUclDPH1JYjhf_XcO58tI9BkXORC0,18430
72
+ cognite/neat/core/_data_model/models/physical/_unverified.py,sha256=8Sg0-tfSJ6glc5h1yNDlY29sajAOv3xoi9j-Q-Md6ZY,20116
72
73
  cognite/neat/core/_data_model/models/physical/_validation.py,sha256=AuBAecOTAVRbGh9VXZ6W91HsU7-B75ko7oaxlX4Mmqw,41140
73
74
  cognite/neat/core/_data_model/models/physical/_verified.py,sha256=4_7XUj6-x74DhL8qe-duXhlNnq6ANmShB7UpICjbQW4,26783
74
75
  cognite/neat/core/_data_model/transformers/__init__.py,sha256=N6yRBplAkrwwxoTAre_1BE_fdSZL5jihr7xTQjW3KnM,1876
@@ -130,7 +131,7 @@ cognite/neat/core/_issues/errors/_external.py,sha256=AaKwO5-AvX01d7Hd83vqYl1qNmM
130
131
  cognite/neat/core/_issues/errors/_general.py,sha256=QEgTp_bvzGjmpRtr09Lj_SBeD9IVdql5_JmP02P7PfM,1391
131
132
  cognite/neat/core/_issues/errors/_properties.py,sha256=ZR2_j-TkxT8Zn5NGMNNOuKQ_bKeciaMOGZkRKg1YCvw,2924
132
133
  cognite/neat/core/_issues/errors/_resources.py,sha256=lBK65tJZMhV3z3_xi8zJeo7Nt_agXsOklH_RPKQu28s,4002
133
- cognite/neat/core/_issues/errors/_wrapper.py,sha256=clhuSwUuHy-FQXQopFIQRY8c_NZM5u-QB9ncoc6Hrbo,2320
134
+ cognite/neat/core/_issues/errors/_wrapper.py,sha256=8kJXp9ONNrP7BvXAr8CUicoN4K_9SToR0Iwde3lja24,2533
134
135
  cognite/neat/core/_issues/warnings/__init__.py,sha256=3MQS_elyRD3SG3iEWMWLRJDabth7upT3oX4WD0xxOh4,3263
135
136
  cognite/neat/core/_issues/warnings/_external.py,sha256=w-1R7ea6DXTIWqwlwMMjY0YxKDMSJ8gKAbp_nIIM1AI,1324
136
137
  cognite/neat/core/_issues/warnings/_general.py,sha256=_6dAFaMz-LIv7GsBBIBq2d-kmbuxVXKvU4jZeb7tjAo,972
@@ -195,7 +196,7 @@ cognite/neat/session/engine/__init__.py,sha256=D3MxUorEs6-NtgoICqtZ8PISQrjrr4dvc
195
196
  cognite/neat/session/engine/_import.py,sha256=1QxA2_EK613lXYAHKQbZyw2yjo5P9XuiX4Z6_6-WMNQ,169
196
197
  cognite/neat/session/engine/_interface.py,sha256=3W-cYr493c_mW3P5O6MKN1xEQg3cA7NHR_ev3zdF9Vk,533
197
198
  cognite/neat/session/engine/_load.py,sha256=g52uYakQM03VqHt_RDHtpHso1-mFFifH5M4T2ScuH8A,5198
198
- cognite_neat-0.123.26.dist-info/METADATA,sha256=_X-TDE3Cg0RJyvXEA2xzG6Dld67JV1-p7VDXuqDkFFw,9172
199
- cognite_neat-0.123.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
200
- cognite_neat-0.123.26.dist-info/licenses/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
201
- cognite_neat-0.123.26.dist-info/RECORD,,
199
+ cognite_neat-0.123.28.dist-info/METADATA,sha256=UScQChiQFnF4EpPSGPtFyHZ-JgQuiIiPMlRA_CUHLdA,9166
200
+ cognite_neat-0.123.28.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
201
+ cognite_neat-0.123.28.dist-info/licenses/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
202
+ cognite_neat-0.123.28.dist-info/RECORD,,