cognite-neat 0.123.42__py3-none-any.whl → 0.124.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cognite-neat might be problematic. Click here for more details.
- cognite/neat/_data_model/models/conceptual/_data_model.py +51 -0
- cognite/neat/_data_model/models/conceptual/_property.py +3 -2
- cognite/neat/_data_model/models/dms/__init__.py +12 -1
- cognite/neat/_data_model/models/dms/_base.py +1 -1
- cognite/neat/_data_model/models/dms/_constraints.py +5 -2
- cognite/neat/_data_model/models/dms/_data_types.py +26 -10
- cognite/neat/_data_model/models/dms/_indexes.py +6 -3
- cognite/neat/_data_model/models/dms/_types.py +17 -0
- cognite/neat/_data_model/models/dms/_view_property.py +14 -25
- cognite/neat/_data_model/models/entities/__init__.py +2 -1
- cognite/neat/_data_model/models/entities/_parser.py +32 -0
- cognite/neat/_session/__init__.py +0 -0
- cognite/neat/_session/_session.py +33 -0
- cognite/neat/_session/_state_machine/__init__.py +23 -0
- cognite/neat/_session/_state_machine/_base.py +27 -0
- cognite/neat/_session/_state_machine/_states.py +150 -0
- cognite/neat/_utils/validation.py +58 -28
- cognite/neat/_version.py +1 -1
- cognite/neat/v0/core/_store/_instance.py +33 -0
- cognite/neat/v0/session/_base.py +2 -0
- cognite/neat/v0/session/_diff.py +51 -0
- {cognite_neat-0.123.42.dist-info → cognite_neat-0.124.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.123.42.dist-info → cognite_neat-0.124.0.dist-info}/RECORD +25 -17
- {cognite_neat-0.123.42.dist-info → cognite_neat-0.124.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.123.42.dist-info → cognite_neat-0.124.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, ValidationInfo, field_validator
|
|
4
|
+
|
|
5
|
+
from cognite.neat._data_model.models.entities._constants import PREFIX_PATTERN, SUFFIX_PATTERN, VERSION_PATTERN
|
|
6
|
+
from cognite.neat._utils.text import humanize_collection
|
|
7
|
+
from cognite.neat.v0.core._data_model.models.entities._single_value import ConceptEntity
|
|
8
|
+
|
|
9
|
+
from ._base import ResourceMetadata
|
|
10
|
+
from ._concept import Concept
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataModel(ResourceMetadata):
|
|
14
|
+
space: str = Field(
|
|
15
|
+
description="Id of the space that the concept belongs to.",
|
|
16
|
+
min_length=1,
|
|
17
|
+
max_length=43,
|
|
18
|
+
pattern=PREFIX_PATTERN,
|
|
19
|
+
alias="prefix",
|
|
20
|
+
)
|
|
21
|
+
external_id: str = Field(
|
|
22
|
+
description="External-id of the concept.",
|
|
23
|
+
min_length=1,
|
|
24
|
+
max_length=255,
|
|
25
|
+
pattern=SUFFIX_PATTERN,
|
|
26
|
+
alias="suffix",
|
|
27
|
+
)
|
|
28
|
+
version: str = Field(
|
|
29
|
+
description="Version of the concept.",
|
|
30
|
+
max_length=43,
|
|
31
|
+
pattern=VERSION_PATTERN,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
concepts: list[Concept] = Field(
|
|
35
|
+
description="References to the concepts from where this concept will inherit properties.",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@field_validator("concepts", mode="after")
|
|
39
|
+
def cannot_have_duplicates(cls, value: list[Concept], info: ValidationInfo) -> list[Concept]:
|
|
40
|
+
concept_ids = [
|
|
41
|
+
ConceptEntity(prefix=concept.space, suffix=concept.external_id, version=concept.version)
|
|
42
|
+
for concept in value
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
counts = Counter(concept_ids)
|
|
46
|
+
duplicates = {concept for concept, count in counts.items() if count > 1}
|
|
47
|
+
|
|
48
|
+
if duplicates:
|
|
49
|
+
raise ValueError(f"Duplicate concepts found: {humanize_collection(duplicates)}")
|
|
50
|
+
|
|
51
|
+
return value
|
|
@@ -78,11 +78,12 @@ class Property(ResourceMetadata):
|
|
|
78
78
|
return value
|
|
79
79
|
|
|
80
80
|
max_count = info.data.get("max_count")
|
|
81
|
+
min_count = info.data.get("min_count")
|
|
81
82
|
|
|
82
|
-
if max_count is None or max_count > 1:
|
|
83
|
+
if max_count is None or max_count > 1 or (min_count and min_count > 1):
|
|
83
84
|
raise ValueError(
|
|
84
85
|
"Setting default value is only supported for single-valued properties."
|
|
85
|
-
f" Property has
|
|
86
|
+
f" Property has min_count={info.data.get('min_count')} and max_count={info.data.get('max_count')}."
|
|
86
87
|
)
|
|
87
88
|
return value
|
|
88
89
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from cognite.neat._data_model.models.dms._base import Resource, WriteableResource
|
|
2
2
|
from cognite.neat._data_model.models.dms._constraints import (
|
|
3
3
|
Constraint,
|
|
4
|
+
ConstraintAdapter,
|
|
4
5
|
ConstraintDefinition,
|
|
5
6
|
RequiresConstraintDefinition,
|
|
6
7
|
UniquenessConstraintDefinition,
|
|
@@ -14,10 +15,14 @@ from cognite.neat._data_model.models.dms._container import (
|
|
|
14
15
|
from cognite.neat._data_model.models.dms._data_types import (
|
|
15
16
|
BooleanProperty,
|
|
16
17
|
DataType,
|
|
18
|
+
DataTypeAdapter,
|
|
17
19
|
DateProperty,
|
|
18
20
|
DirectNodeRelation,
|
|
19
21
|
EnumProperty,
|
|
22
|
+
EnumValue,
|
|
20
23
|
FileCDFExternalIdReference,
|
|
24
|
+
Float32Property,
|
|
25
|
+
Float64Property,
|
|
21
26
|
FloatProperty,
|
|
22
27
|
Int32Property,
|
|
23
28
|
Int64Property,
|
|
@@ -29,7 +34,7 @@ from cognite.neat._data_model.models.dms._data_types import (
|
|
|
29
34
|
TimeseriesCDFExternalIdReference,
|
|
30
35
|
TimestampProperty,
|
|
31
36
|
)
|
|
32
|
-
from cognite.neat._data_model.models.dms._indexes import BtreeIndex, Index, IndexDefinition, InvertedIndex
|
|
37
|
+
from cognite.neat._data_model.models.dms._indexes import BtreeIndex, Index, IndexAdapter, IndexDefinition, InvertedIndex
|
|
33
38
|
from cognite.neat._data_model.models.dms._space import Space, SpaceRequest, SpaceResponse
|
|
34
39
|
|
|
35
40
|
from ._data_model import DataModelRequest, DataModelResponse
|
|
@@ -68,6 +73,7 @@ __all__ = [
|
|
|
68
73
|
"BtreeIndex",
|
|
69
74
|
"ConnectionPropertyDefinition",
|
|
70
75
|
"Constraint",
|
|
76
|
+
"ConstraintAdapter",
|
|
71
77
|
"ConstraintDefinition",
|
|
72
78
|
"ConstraintOrIndexState",
|
|
73
79
|
"Container",
|
|
@@ -80,12 +86,17 @@ __all__ = [
|
|
|
80
86
|
"DataModelRequest",
|
|
81
87
|
"DataModelResponse",
|
|
82
88
|
"DataType",
|
|
89
|
+
"DataTypeAdapter",
|
|
83
90
|
"DateProperty",
|
|
84
91
|
"DirectNodeRelation",
|
|
85
92
|
"EnumProperty",
|
|
93
|
+
"EnumValue",
|
|
86
94
|
"FileCDFExternalIdReference",
|
|
95
|
+
"Float32Property",
|
|
96
|
+
"Float64Property",
|
|
87
97
|
"FloatProperty",
|
|
88
98
|
"Index",
|
|
99
|
+
"IndexAdapter",
|
|
89
100
|
"IndexDefinition",
|
|
90
101
|
"Int32Property",
|
|
91
102
|
"Int64Property",
|
|
@@ -20,7 +20,7 @@ class Resource(BaseModelObject):
|
|
|
20
20
|
T_Resource = TypeVar("T_Resource", bound=Resource)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class WriteableResource(Generic[T_Resource],
|
|
23
|
+
class WriteableResource(Resource, Generic[T_Resource], ABC):
|
|
24
24
|
@abstractmethod
|
|
25
25
|
def as_request(self) -> T_Resource:
|
|
26
26
|
"""Convert the response model to a request model by removing read-only fields."""
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from abc import ABC
|
|
2
2
|
from typing import Annotated, Literal
|
|
3
3
|
|
|
4
|
-
from pydantic import Field
|
|
4
|
+
from pydantic import Field, TypeAdapter
|
|
5
5
|
|
|
6
6
|
from ._base import BaseModelObject
|
|
7
7
|
from ._references import ContainerReference
|
|
8
|
+
from ._types import Bool
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class ConstraintDefinition(BaseModelObject, ABC):
|
|
@@ -16,7 +17,7 @@ class UniquenessConstraintDefinition(ConstraintDefinition):
|
|
|
16
17
|
properties: list[str] = Field(
|
|
17
18
|
description="List of properties included in the constraint.", min_length=1, max_length=10
|
|
18
19
|
)
|
|
19
|
-
by_space:
|
|
20
|
+
by_space: Bool | None = Field(default=None, description="Whether to make the constraint space-specific.")
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class RequiresConstraintDefinition(ConstraintDefinition):
|
|
@@ -28,3 +29,5 @@ Constraint = Annotated[
|
|
|
28
29
|
UniquenessConstraintDefinition | RequiresConstraintDefinition,
|
|
29
30
|
Field(discriminator="constraint_type"),
|
|
30
31
|
]
|
|
32
|
+
|
|
33
|
+
ConstraintAdapter: TypeAdapter[Constraint] = TypeAdapter(Constraint)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from abc import ABC
|
|
2
3
|
from typing import Annotated, Literal
|
|
3
4
|
|
|
4
|
-
from pydantic import Field, field_validator
|
|
5
|
-
|
|
6
|
-
from cognite.neat._utils.text import humanize_collection
|
|
5
|
+
from pydantic import Field, TypeAdapter, field_validator
|
|
7
6
|
|
|
8
7
|
from ._base import BaseModelObject
|
|
9
8
|
from ._constants import ENUM_VALUE_IDENTIFIER_PATTERN, FORBIDDEN_ENUM_VALUES, INSTANCE_ID_PATTERN
|
|
@@ -111,6 +110,7 @@ class DirectNodeRelation(ListablePropertyTypeDefinition):
|
|
|
111
110
|
|
|
112
111
|
class EnumValue(BaseModelObject):
|
|
113
112
|
name: str | None = Field(
|
|
113
|
+
None,
|
|
114
114
|
max_length=255,
|
|
115
115
|
description="The name of the enum value.",
|
|
116
116
|
)
|
|
@@ -121,6 +121,9 @@ class EnumValue(BaseModelObject):
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
_ENUM_KEY = re.compile(ENUM_VALUE_IDENTIFIER_PATTERN)
|
|
125
|
+
|
|
126
|
+
|
|
124
127
|
class EnumProperty(PropertyTypeDefinition):
|
|
125
128
|
type: Literal["enum"] = "enum"
|
|
126
129
|
unknown_value: str | None = Field(
|
|
@@ -129,22 +132,33 @@ class EnumProperty(PropertyTypeDefinition):
|
|
|
129
132
|
"provide forward-compatibility, Specifying what value to use if the client does not "
|
|
130
133
|
"recognize the returned value. It is not possible to ingest the unknown value, "
|
|
131
134
|
"but it must be part of the allowed values.",
|
|
135
|
+
min_length=1,
|
|
136
|
+
max_length=128,
|
|
137
|
+
pattern=ENUM_VALUE_IDENTIFIER_PATTERN,
|
|
132
138
|
)
|
|
133
139
|
values: dict[str, EnumValue] = Field(
|
|
134
140
|
description="A set of all possible values for the enum property.",
|
|
135
141
|
min_length=1,
|
|
136
142
|
max_length=32,
|
|
137
|
-
pattern=ENUM_VALUE_IDENTIFIER_PATTERN,
|
|
138
143
|
)
|
|
139
144
|
|
|
140
145
|
@field_validator("values", mode="after")
|
|
141
146
|
def _valid_enum_value(cls, val: dict[str, EnumValue]) -> dict[str, EnumValue]:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
errors: list[str] = []
|
|
148
|
+
for key in val.keys():
|
|
149
|
+
if not _ENUM_KEY.match(key):
|
|
150
|
+
errors.append(
|
|
151
|
+
f"Enum value {key!r} is not valid. Enum values must match "
|
|
152
|
+
f"the pattern: {ENUM_VALUE_IDENTIFIER_PATTERN}"
|
|
153
|
+
)
|
|
154
|
+
if len(key) > 128 or len(key) < 1:
|
|
155
|
+
errors.append(f"Enum value {key!r} must be between 1 and 128 characters long.")
|
|
156
|
+
if key.lower() in FORBIDDEN_ENUM_VALUES:
|
|
157
|
+
errors.append(
|
|
158
|
+
f"Enum value {key!r} cannot be any of the following reserved values: {FORBIDDEN_ENUM_VALUES}"
|
|
159
|
+
)
|
|
160
|
+
if errors:
|
|
161
|
+
raise ValueError(";".join(errors))
|
|
148
162
|
return val
|
|
149
163
|
|
|
150
164
|
|
|
@@ -165,3 +179,5 @@ DataType = Annotated[
|
|
|
165
179
|
| EnumProperty,
|
|
166
180
|
Field(discriminator="type"),
|
|
167
181
|
]
|
|
182
|
+
|
|
183
|
+
DataTypeAdapter: TypeAdapter[DataType] = TypeAdapter(DataType)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from abc import ABC
|
|
2
2
|
from typing import Annotated, Literal
|
|
3
3
|
|
|
4
|
-
from pydantic import Field
|
|
4
|
+
from pydantic import Field, TypeAdapter
|
|
5
5
|
|
|
6
6
|
from ._base import BaseModelObject
|
|
7
|
+
from ._types import Bool
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class IndexDefinition(BaseModelObject, ABC):
|
|
@@ -13,8 +14,8 @@ class IndexDefinition(BaseModelObject, ABC):
|
|
|
13
14
|
|
|
14
15
|
class BtreeIndex(IndexDefinition):
|
|
15
16
|
index_type: Literal["btree"] = "btree"
|
|
16
|
-
by_space:
|
|
17
|
-
cursorable:
|
|
17
|
+
by_space: Bool | None = Field(default=None, description="Whether to make the index space-specific.")
|
|
18
|
+
cursorable: Bool | None = Field(
|
|
18
19
|
default=None, description="Whether the index can be used for cursor-based pagination."
|
|
19
20
|
)
|
|
20
21
|
|
|
@@ -24,3 +25,5 @@ class InvertedIndex(IndexDefinition):
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
Index = Annotated[BtreeIndex | InvertedIndex, Field(discriminator="index_type")]
|
|
28
|
+
|
|
29
|
+
IndexAdapter: TypeAdapter[Index] = TypeAdapter(Index)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Annotated, Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BeforeValidator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def str_as_bool(value: Any) -> Any:
|
|
7
|
+
if isinstance(value, str):
|
|
8
|
+
val = value.lower()
|
|
9
|
+
if val in {"true", "1", "yes"}:
|
|
10
|
+
return True
|
|
11
|
+
if val in {"false", "0", "no"}:
|
|
12
|
+
return False
|
|
13
|
+
# All other cases are handled by Pydantic's built-in bool validator
|
|
14
|
+
return value
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
Bool = Annotated[bool, BeforeValidator(str_as_bool, str)]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from abc import ABC
|
|
2
2
|
from typing import Annotated, Literal
|
|
3
3
|
|
|
4
|
-
from pydantic import Field, Json
|
|
4
|
+
from pydantic import Field, Json, TypeAdapter
|
|
5
5
|
|
|
6
6
|
from ._base import BaseModelObject, Resource, WriteableResource
|
|
7
7
|
from ._constants import CONTAINER_AND_VIEW_PROPERTIES_IDENTIFIER_PATTERN
|
|
@@ -130,53 +130,40 @@ class ReverseDirectRelationProperty(ConnectionPropertyDefinition, ABC):
|
|
|
130
130
|
description="The node(s) containing the direct relation property can be read "
|
|
131
131
|
"through the view specified in 'source'."
|
|
132
132
|
)
|
|
133
|
+
through: ContainerDirectReference | ViewDirectReference = Field(
|
|
134
|
+
description="The view of the node containing the direct relation property."
|
|
135
|
+
)
|
|
133
136
|
|
|
134
137
|
|
|
135
138
|
class SingleReverseDirectRelationPropertyRequest(ReverseDirectRelationProperty):
|
|
136
139
|
connection_type: Literal["single_reverse_direct_relation"] = "single_reverse_direct_relation"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
description="The view of the node containing the direct relation property."
|
|
142
|
-
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class MultiReverseDirectRelationPropertyRequest(ReverseDirectRelationProperty):
|
|
143
|
+
connection_type: Literal["multi_reverse_direct_relation"] = "multi_reverse_direct_relation"
|
|
143
144
|
|
|
144
145
|
|
|
145
146
|
class SingleReverseDirectRelationPropertyResponse(
|
|
146
147
|
ReverseDirectRelationProperty, WriteableResource[SingleReverseDirectRelationPropertyRequest]
|
|
147
148
|
):
|
|
148
149
|
connection_type: Literal["single_reverse_direct_relation"] = "single_reverse_direct_relation"
|
|
149
|
-
|
|
150
|
-
description="
|
|
150
|
+
target_list: bool = Field(
|
|
151
|
+
description="Whether or not this reverse direct relation targets a list of direct relations.",
|
|
151
152
|
)
|
|
152
153
|
|
|
153
154
|
def as_request(self) -> SingleReverseDirectRelationPropertyRequest:
|
|
154
|
-
if isinstance(self.through, ViewDirectReference):
|
|
155
|
-
raise TypeError("Cannot convert to request when 'through' is a ViewDirectReference.")
|
|
156
155
|
return SingleReverseDirectRelationPropertyRequest.model_validate(self.model_dump(by_alias=True))
|
|
157
156
|
|
|
158
157
|
|
|
159
|
-
class MultiReverseDirectRelationPropertyRequest(ReverseDirectRelationProperty):
|
|
160
|
-
connection_type: Literal["multi_reverse_direct_relation"] = "multi_reverse_direct_relation"
|
|
161
|
-
# The API support through as either ViewDirectReference or ContainerDirectReference. However, in Neat
|
|
162
|
-
# we only use ContainerDirectReference. This is for simplicity and it improves performance as the server
|
|
163
|
-
# does not have to resolve the view to a container first.
|
|
164
|
-
through: ContainerDirectReference = Field(
|
|
165
|
-
description="The view of the node containing the direct relation property."
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
158
|
class MultiReverseDirectRelationPropertyResponse(
|
|
170
159
|
ReverseDirectRelationProperty, WriteableResource[MultiReverseDirectRelationPropertyRequest]
|
|
171
160
|
):
|
|
172
161
|
connection_type: Literal["multi_reverse_direct_relation"] = "multi_reverse_direct_relation"
|
|
173
|
-
|
|
174
|
-
description="
|
|
162
|
+
target_list: bool = Field(
|
|
163
|
+
description="Whether or not this reverse direct relation targets a list of direct relations.",
|
|
175
164
|
)
|
|
176
165
|
|
|
177
166
|
def as_request(self) -> MultiReverseDirectRelationPropertyRequest:
|
|
178
|
-
if isinstance(self.through, ViewDirectReference):
|
|
179
|
-
raise TypeError("Cannot convert to request when 'through' is a ViewDirectReference.")
|
|
180
167
|
return MultiReverseDirectRelationPropertyRequest.model_validate(self.model_dump(by_alias=True))
|
|
181
168
|
|
|
182
169
|
|
|
@@ -196,3 +183,5 @@ ViewResponseProperty = Annotated[
|
|
|
196
183
|
| ViewCorePropertyResponse,
|
|
197
184
|
Field(discriminator="connection_type"),
|
|
198
185
|
]
|
|
186
|
+
|
|
187
|
+
ViewRequestPropertyAdapter: TypeAdapter[ViewRequestProperty] = TypeAdapter(ViewRequestProperty)
|
|
@@ -19,7 +19,7 @@ from ._data_types import (
|
|
|
19
19
|
Timeseries,
|
|
20
20
|
)
|
|
21
21
|
from ._identifiers import URI, NameSpace
|
|
22
|
-
from ._parser import ParsedEntity, parse_entity
|
|
22
|
+
from ._parser import ParsedEntity, parse_entities, parse_entity
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
"URI",
|
|
@@ -45,5 +45,6 @@ __all__ = [
|
|
|
45
45
|
"Undefined",
|
|
46
46
|
"Unknown",
|
|
47
47
|
"UnknownEntity",
|
|
48
|
+
"parse_entities",
|
|
48
49
|
"parse_entity",
|
|
49
50
|
]
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from dataclasses import dataclass
|
|
3
|
+
from typing import Literal
|
|
2
4
|
|
|
3
5
|
SPECIAL_CHARACTERS = ":()=,"
|
|
4
6
|
|
|
@@ -11,6 +13,18 @@ class ParsedEntity:
|
|
|
11
13
|
suffix: str
|
|
12
14
|
properties: dict[str, str]
|
|
13
15
|
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
props_str = ""
|
|
18
|
+
if self.properties:
|
|
19
|
+
joined = ",".join(f"{k}={v}" for k, v in sorted(self.properties.items(), key=lambda x: x[0]))
|
|
20
|
+
props_str = f"({joined})"
|
|
21
|
+
if self.prefix:
|
|
22
|
+
return f"{self.prefix}:{self.suffix}{props_str}"
|
|
23
|
+
return f"{self.suffix}{props_str}"
|
|
24
|
+
|
|
25
|
+
def __hash__(self) -> int:
|
|
26
|
+
return hash(str(self))
|
|
27
|
+
|
|
14
28
|
|
|
15
29
|
class _EntityParser:
|
|
16
30
|
"""A parser for entity strings in the format 'prefix:suffix(prop1=val1,prop2=val2)'."""
|
|
@@ -192,3 +206,21 @@ def parse_entity(entity_string: str) -> ParsedEntity:
|
|
|
192
206
|
"""
|
|
193
207
|
parser = _EntityParser(entity_string)
|
|
194
208
|
return parser.parse()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def parse_entities(entities_str: str, separator: Literal[","] = ",") -> list[ParsedEntity] | None:
|
|
212
|
+
"""Parse a comma-separated string of entities.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
entities_str: A comma-separated string of entities.
|
|
216
|
+
separator: The separator used to split entities.
|
|
217
|
+
A list of `ParsedEntity` objects or None if the input string is empty.
|
|
218
|
+
"""
|
|
219
|
+
if not entities_str.strip():
|
|
220
|
+
return None
|
|
221
|
+
if separator != ",":
|
|
222
|
+
raise ValueError("Only ',' is supported as a separator currently.")
|
|
223
|
+
# Regex to split on the separator but ignore separators within parentheses
|
|
224
|
+
pattern = rf"{separator}(?![^()]*\))"
|
|
225
|
+
parts = re.split(pattern, entities_str)
|
|
226
|
+
return [parse_entity(part.strip()) for part in parts if part.strip()]
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from ._state_machine import EmptyState, ForbiddenState, State
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NeatSession:
|
|
5
|
+
"""A session is an interface for neat operations. It works as
|
|
6
|
+
a manager for handling user interactions and orchestrating
|
|
7
|
+
the state machine for data model and instance operations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.state: State = EmptyState()
|
|
12
|
+
|
|
13
|
+
def _execute_event(self, event: str) -> bool:
|
|
14
|
+
"""Place holder function for executing events and transitioning states.
|
|
15
|
+
It will be modified to include actual logic as we progress with v1 of neat.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
print(f"\n--- Executing event: '{event}' from {self.state} ---")
|
|
19
|
+
|
|
20
|
+
old_state = self.state
|
|
21
|
+
new_state = self.state.on_event(event)
|
|
22
|
+
|
|
23
|
+
# Handle ForbiddenState
|
|
24
|
+
if isinstance(new_state, ForbiddenState):
|
|
25
|
+
print(f"❌ Event '{event}' is FORBIDDEN from {old_state}")
|
|
26
|
+
# Return to previous state (as per your table logic)
|
|
27
|
+
self.state = new_state.on_event("undo")
|
|
28
|
+
print(f"↩️ Returned to: {self.state}")
|
|
29
|
+
return False
|
|
30
|
+
else:
|
|
31
|
+
self.state = new_state
|
|
32
|
+
print(f"✅ Transition successful: {old_state} → {self.state}")
|
|
33
|
+
return True
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ._base import State
|
|
2
|
+
from ._states import (
|
|
3
|
+
ConceptualPhysicalState,
|
|
4
|
+
ConceptualState,
|
|
5
|
+
EmptyState,
|
|
6
|
+
ForbiddenState,
|
|
7
|
+
InstancesConceptualPhysicalState,
|
|
8
|
+
InstancesConceptualState,
|
|
9
|
+
InstancesState,
|
|
10
|
+
PhysicalState,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ConceptualPhysicalState",
|
|
15
|
+
"ConceptualState",
|
|
16
|
+
"EmptyState",
|
|
17
|
+
"ForbiddenState",
|
|
18
|
+
"InstancesConceptualPhysicalState",
|
|
19
|
+
"InstancesConceptualState",
|
|
20
|
+
"InstancesState",
|
|
21
|
+
"PhysicalState",
|
|
22
|
+
"State",
|
|
23
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class State(ABC):
|
|
5
|
+
def __init__(self) -> None:
|
|
6
|
+
# this will be reference to the actual store in the session
|
|
7
|
+
# used to store data models and instances, here only as a placeholder
|
|
8
|
+
self._store = None
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def on_event(self, event: str) -> "State":
|
|
12
|
+
"""
|
|
13
|
+
Handle events that are delegated to this State.
|
|
14
|
+
"""
|
|
15
|
+
raise NotImplementedError("on_event() must be implemented by the subclass.")
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Leverages the __str__ method to describe the State.
|
|
20
|
+
"""
|
|
21
|
+
return self.__str__()
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Returns the name of the State.
|
|
26
|
+
"""
|
|
27
|
+
return self.__class__.__name__
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from ._base import State
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EmptyState(State):
|
|
5
|
+
"""
|
|
6
|
+
The initial state with empty NEAT store.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def on_event(self, event: str) -> State:
|
|
10
|
+
if event == "read_instances":
|
|
11
|
+
return InstancesState()
|
|
12
|
+
elif event == "read_conceptual":
|
|
13
|
+
return ConceptualState()
|
|
14
|
+
elif event == "read_physical":
|
|
15
|
+
return PhysicalState()
|
|
16
|
+
|
|
17
|
+
return ForbiddenState(self)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InstancesState(State):
|
|
21
|
+
"""
|
|
22
|
+
State with instances loaded to the store.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def on_event(self, event: str) -> State:
|
|
26
|
+
# One can keep on reading instances to stay in the same state
|
|
27
|
+
if event == "read_instances":
|
|
28
|
+
return InstancesState()
|
|
29
|
+
# We either read conceptual model or infer it from instances
|
|
30
|
+
# if read conceptual, we need to make sure that conceptual model is compatible with instances
|
|
31
|
+
elif event in ["infer_conceptual", "read_conceptual"]:
|
|
32
|
+
return InstancesConceptualState()
|
|
33
|
+
|
|
34
|
+
# transforming instances keeps us in the same state
|
|
35
|
+
elif event == "transform_instances":
|
|
36
|
+
return InstancesState()
|
|
37
|
+
# we should allow writing out instances in RDF format but not to CDF
|
|
38
|
+
elif event == "write_instances":
|
|
39
|
+
return InstancesState()
|
|
40
|
+
|
|
41
|
+
# all other operations are forbidden
|
|
42
|
+
return ForbiddenState(self)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConceptualState(State):
|
|
46
|
+
"""
|
|
47
|
+
State with conceptual model loaded.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def on_event(self, event: str) -> State:
|
|
51
|
+
# re-reading of model means transformation of
|
|
52
|
+
# the current model has been done outside of NeatSession
|
|
53
|
+
# requires checking that the new model is compatible with the existing
|
|
54
|
+
if event == "read_conceptual":
|
|
55
|
+
return ConceptualState()
|
|
56
|
+
|
|
57
|
+
# when reading: requires linking between models
|
|
58
|
+
# when converting: links are automatically created
|
|
59
|
+
elif event == "read_physical" or event == "convert_to_physical":
|
|
60
|
+
return ConceptualPhysicalState()
|
|
61
|
+
elif event == "transform_conceptual":
|
|
62
|
+
return ConceptualState()
|
|
63
|
+
elif event == "write_conceptual":
|
|
64
|
+
return ConceptualState()
|
|
65
|
+
|
|
66
|
+
return ForbiddenState(self)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PhysicalState(State):
|
|
70
|
+
"""
|
|
71
|
+
State with physical model loaded.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def on_event(self, event: str) -> State:
|
|
75
|
+
if event == "read_physical":
|
|
76
|
+
return PhysicalState()
|
|
77
|
+
elif event == "transform_physical":
|
|
78
|
+
return PhysicalState()
|
|
79
|
+
elif event == "write_physical":
|
|
80
|
+
return PhysicalState()
|
|
81
|
+
elif event == "convert_to_conceptual" or event == "read_conceptual":
|
|
82
|
+
return ConceptualPhysicalState()
|
|
83
|
+
|
|
84
|
+
return ForbiddenState(self)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class InstancesConceptualState(State):
|
|
88
|
+
"""
|
|
89
|
+
State with both instances and conceptual model loaded.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def on_event(self, event: str) -> State:
|
|
93
|
+
if event == "read_conceptual":
|
|
94
|
+
return InstancesConceptualState()
|
|
95
|
+
elif event == "transform_conceptual":
|
|
96
|
+
return InstancesConceptualState()
|
|
97
|
+
elif event in ["write_instances", "write_conceptual"]:
|
|
98
|
+
return InstancesConceptualState()
|
|
99
|
+
elif event in ["read_physical", "convert_to_physical"]:
|
|
100
|
+
return InstancesConceptualPhysicalState()
|
|
101
|
+
|
|
102
|
+
return ForbiddenState(self)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ConceptualPhysicalState(State):
|
|
106
|
+
"""
|
|
107
|
+
State with both conceptual and physical models loaded.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def on_event(self, event: str) -> State:
|
|
111
|
+
if event == "read_physical":
|
|
112
|
+
return ConceptualPhysicalState()
|
|
113
|
+
elif event == "transform_physical":
|
|
114
|
+
return ConceptualPhysicalState()
|
|
115
|
+
elif event in ["write_conceptual", "write_physical"]:
|
|
116
|
+
return ConceptualPhysicalState()
|
|
117
|
+
|
|
118
|
+
return ForbiddenState(self)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class InstancesConceptualPhysicalState(State):
|
|
122
|
+
"""
|
|
123
|
+
State with instances, conceptual, and physical models loaded.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def on_event(self, event: str) -> State:
|
|
127
|
+
if event == "read_physical":
|
|
128
|
+
return InstancesConceptualPhysicalState()
|
|
129
|
+
elif event == "transform_physical":
|
|
130
|
+
return InstancesConceptualPhysicalState()
|
|
131
|
+
elif event in ["write_instances", "write_conceptual", "write_physical"]:
|
|
132
|
+
return InstancesConceptualPhysicalState()
|
|
133
|
+
|
|
134
|
+
return ForbiddenState(self)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ForbiddenState(State):
|
|
138
|
+
"""
|
|
139
|
+
State representing forbidden transitions - returns to previous state.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, previous_state: State):
|
|
143
|
+
self.previous_state = previous_state
|
|
144
|
+
print(f"Forbidden action attempted. Returning to previous state: {previous_state}")
|
|
145
|
+
|
|
146
|
+
def on_event(self, event: str) -> State:
|
|
147
|
+
# only "undo" to trigger going back to previous state
|
|
148
|
+
if event.strip().lower() == "undo":
|
|
149
|
+
return self.previous_state
|
|
150
|
+
return self
|
|
@@ -1,23 +1,63 @@
|
|
|
1
|
+
from collections.abc import Callable, Mapping
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
1
4
|
from pydantic import ValidationError
|
|
2
5
|
from pydantic_core import ErrorDetails
|
|
3
6
|
|
|
4
7
|
|
|
5
|
-
def
|
|
8
|
+
def as_json_path(loc: tuple[str | int, ...]) -> str:
|
|
9
|
+
"""Converts a location tuple to a JSON path.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
loc: The location tuple to convert.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
A JSON path string.
|
|
16
|
+
"""
|
|
17
|
+
if not loc:
|
|
18
|
+
return ""
|
|
19
|
+
# +1 to convert from 0-based to 1-based indexing
|
|
20
|
+
prefix = ""
|
|
21
|
+
if isinstance(loc[0], int):
|
|
22
|
+
prefix = "item"
|
|
23
|
+
|
|
24
|
+
suffix = ".".join([str(x) if isinstance(x, str) else f"[{x + 1}]" for x in loc]).replace(".[", "[")
|
|
25
|
+
return f"{prefix}{suffix}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def humanize_validation_error(
|
|
29
|
+
error: ValidationError,
|
|
30
|
+
parent_loc: tuple[int | str, ...] = tuple(),
|
|
31
|
+
humanize_location: Callable[[tuple[int | str, ...]], str] = as_json_path,
|
|
32
|
+
field_name: Literal["field", "column", "value"] = "field",
|
|
33
|
+
field_renaming: Mapping[str, str] | None = None,
|
|
34
|
+
) -> list[str]:
|
|
6
35
|
"""Converts a ValidationError to a human-readable format.
|
|
7
36
|
|
|
8
37
|
This overwrites the default error messages from Pydantic to be better suited for Toolkit users.
|
|
9
38
|
|
|
10
39
|
Args:
|
|
11
40
|
error: The ValidationError to convert.
|
|
12
|
-
|
|
41
|
+
parent_loc: Optional location tuple to prepend to each error location.
|
|
42
|
+
This is useful when the error is for a nested model and you want to include the location
|
|
43
|
+
of the parent model.
|
|
44
|
+
humanize_location: A function that converts a location tuple to a human-readable string.
|
|
45
|
+
The default is `as_json_path`, which converts the location to a JSON path.
|
|
46
|
+
This can for example be replaced when the location comes from an Excel table.
|
|
47
|
+
field_name: The name use for "field" in error messages. Default is "field". This can be changed to
|
|
48
|
+
"column" or "value" to better fit the context.
|
|
49
|
+
field_renaming: Optional mapping of field names to source names.
|
|
50
|
+
This is useful when the field names in the model are different from the names in the source.
|
|
51
|
+
For example, if the model field is "asset_id" but the source column is "Asset ID",
|
|
52
|
+
you can provide a mapping {"asset_id": "Asset ID"} to have the error messages use the source names.
|
|
13
53
|
Returns:
|
|
14
54
|
A list of human-readable error messages.
|
|
15
55
|
"""
|
|
16
56
|
errors: list[str] = []
|
|
17
57
|
item: ErrorDetails
|
|
18
|
-
|
|
58
|
+
field_renaming = field_renaming or {}
|
|
19
59
|
for item in error.errors(include_input=True, include_url=False):
|
|
20
|
-
loc = item["loc"]
|
|
60
|
+
loc = (*parent_loc, *item["loc"])
|
|
21
61
|
error_type = item["type"]
|
|
22
62
|
if error_type == "missing":
|
|
23
63
|
msg = f"Missing required field: {loc[-1]!r}"
|
|
@@ -50,32 +90,22 @@ def humanize_validation_error(error: ValidationError) -> list[str]:
|
|
|
50
90
|
# This is hard to read, so we simplify it to just the field name.
|
|
51
91
|
loc = tuple(["dict" if isinstance(x, str) and "json-or-python" in x else x for x in loc])
|
|
52
92
|
|
|
93
|
+
error_suffix = f"{msg[:1].casefold()}{msg[1:]}"
|
|
53
94
|
if len(loc) > 1 and error_type in {"extra_forbidden", "missing"}:
|
|
54
|
-
|
|
55
|
-
|
|
95
|
+
if field_name == "column":
|
|
96
|
+
# This is a table so we modify the error message.
|
|
97
|
+
msg = (
|
|
98
|
+
f"In {humanize_location(loc[:-1])} the column {field_renaming.get(str(loc[-1]), loc[-1])!r} "
|
|
99
|
+
"cannot be empty."
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
# We skip the last element as this is in the message already
|
|
103
|
+
msg = f"In {humanize_location(loc[:-1])} {error_suffix.replace('field', field_name)}"
|
|
56
104
|
elif len(loc) > 1:
|
|
57
|
-
msg = f"In {
|
|
105
|
+
msg = f"In {humanize_location(loc)} {error_suffix}"
|
|
58
106
|
elif len(loc) == 1 and isinstance(loc[0], str) and error_type not in {"extra_forbidden", "missing"}:
|
|
59
|
-
msg = f"In
|
|
107
|
+
msg = f"In {field_name} {loc[0]!r}, {error_suffix}"
|
|
108
|
+
if not msg.endswith("."):
|
|
109
|
+
msg += "."
|
|
60
110
|
errors.append(msg)
|
|
61
111
|
return errors
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def as_json_path(loc: tuple[str | int, ...]) -> str:
|
|
65
|
-
"""Converts a location tuple to a JSON path.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
loc: The location tuple to convert.
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
A JSON path string.
|
|
72
|
-
"""
|
|
73
|
-
if not loc:
|
|
74
|
-
return ""
|
|
75
|
-
# +1 to convert from 0-based to 1-based indexing
|
|
76
|
-
prefix = ""
|
|
77
|
-
if isinstance(loc[0], int):
|
|
78
|
-
prefix = "item "
|
|
79
|
-
|
|
80
|
-
suffix = ".".join([str(x) if isinstance(x, str) else f"[{x + 1}]" for x in loc]).replace(".[", "[")
|
|
81
|
-
return f"{prefix}{suffix}"
|
cognite/neat/_version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.124.0"
|
|
2
2
|
__engine__ = "^2.0.4"
|
|
@@ -12,6 +12,7 @@ from rdflib import Dataset, Graph, Namespace, URIRef
|
|
|
12
12
|
from rdflib.graph import DATASET_DEFAULT_GRAPH_ID
|
|
13
13
|
from rdflib.plugins.stores.sparqlstore import SPARQLUpdateStore
|
|
14
14
|
|
|
15
|
+
from cognite.neat.v0.core._constants import NAMED_GRAPH_NAMESPACE
|
|
15
16
|
from cognite.neat.v0.core._instances._shared import quad_formats, rdflib_to_oxi_type
|
|
16
17
|
from cognite.neat.v0.core._instances.extractors import RdfFileExtractor, TripleExtractors
|
|
17
18
|
from cognite.neat.v0.core._instances.queries import Queries
|
|
@@ -450,3 +451,35 @@ class NeatInstanceStore:
|
|
|
450
451
|
def empty(self) -> bool:
|
|
451
452
|
"""Cheap way to check if the graph store is empty."""
|
|
452
453
|
return not self.queries.select.has_data()
|
|
454
|
+
|
|
455
|
+
def diff(self, current_named_graph: URIRef, new_named_graph: URIRef) -> None:
|
|
456
|
+
"""
|
|
457
|
+
Compare two named graphs and store diff results in dedicated named graphs.
|
|
458
|
+
|
|
459
|
+
Stores triples to add in DIFF_ADD and triples to delete in DIFF_DELETE.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
current_named_graph: URI of the current named graph
|
|
463
|
+
new_named_graph: URI of the new/updated named graph
|
|
464
|
+
|
|
465
|
+
Raises:
|
|
466
|
+
NeatValueError: If either named graph doesn't exist in the store
|
|
467
|
+
"""
|
|
468
|
+
if current_named_graph not in self.named_graphs:
|
|
469
|
+
raise NeatValueError(f"Current named graph not found: {current_named_graph}")
|
|
470
|
+
if new_named_graph not in self.named_graphs:
|
|
471
|
+
raise NeatValueError(f"New named graph not found: {new_named_graph}")
|
|
472
|
+
|
|
473
|
+
# Clear previous diff results using SPARQL
|
|
474
|
+
self.dataset.update(f"CLEAR SILENT GRAPH <{NAMED_GRAPH_NAMESPACE['DIFF_ADD']}>")
|
|
475
|
+
self.dataset.update(f"CLEAR SILENT GRAPH <{NAMED_GRAPH_NAMESPACE['DIFF_DELETE']}>")
|
|
476
|
+
|
|
477
|
+
# Store new diff results
|
|
478
|
+
self._add_triples(
|
|
479
|
+
self.queries.select.get_triples_to_add(current_named_graph, new_named_graph),
|
|
480
|
+
named_graph=NAMED_GRAPH_NAMESPACE["DIFF_ADD"],
|
|
481
|
+
)
|
|
482
|
+
self._add_triples(
|
|
483
|
+
self.queries.select.get_triples_to_delete(current_named_graph, new_named_graph),
|
|
484
|
+
named_graph=NAMED_GRAPH_NAMESPACE["DIFF_DELETE"],
|
|
485
|
+
)
|
cognite/neat/v0/session/_base.py
CHANGED
|
@@ -25,6 +25,7 @@ from cognite.neat.v0.core._store._data_model import DataModelEntity
|
|
|
25
25
|
from cognite.neat.v0.core._utils.auxiliary import local_import
|
|
26
26
|
|
|
27
27
|
from ._collector import _COLLECTOR, Collector
|
|
28
|
+
from ._diff import DiffAPI
|
|
28
29
|
from ._drop import DropAPI
|
|
29
30
|
from ._explore import ExploreAPI
|
|
30
31
|
from ._fix import FixAPI
|
|
@@ -110,6 +111,7 @@ class NeatSession:
|
|
|
110
111
|
self.template = TemplateAPI(self._state)
|
|
111
112
|
self._explore = ExploreAPI(self._state)
|
|
112
113
|
self.plugins = PluginAPI(self._state)
|
|
114
|
+
self._diff = DiffAPI(self._state)
|
|
113
115
|
self.opt = OptAPI()
|
|
114
116
|
self.opt._display()
|
|
115
117
|
if load_engine != "skip" and (engine_version := load_neat_engine(client, load_engine)):
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from rdflib.query import ResultRow
|
|
4
|
+
|
|
5
|
+
from cognite.neat.v0.core._constants import NAMED_GRAPH_NAMESPACE
|
|
6
|
+
|
|
7
|
+
from ._state import SessionState
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DiffAPI:
|
|
11
|
+
"""Compare RDF graphs (private API)."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, state: SessionState) -> None:
|
|
14
|
+
self._state = state
|
|
15
|
+
|
|
16
|
+
def instances(self, current_named_graph: str, new_named_graph: str) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Compare two named graphs and store diff results.
|
|
19
|
+
|
|
20
|
+
Results stored in DIFF_ADD and DIFF_DELETE named graphs.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
current_named_graph: Name of the current graph (e.g., "CURRENT")
|
|
24
|
+
new_named_graph: Name of the new graph (e.g., "NEW")
|
|
25
|
+
"""
|
|
26
|
+
current_uri = NAMED_GRAPH_NAMESPACE[current_named_graph]
|
|
27
|
+
new_uri = NAMED_GRAPH_NAMESPACE[new_named_graph]
|
|
28
|
+
|
|
29
|
+
self._state.instances.store.diff(current_uri, new_uri)
|
|
30
|
+
self._print_summary()
|
|
31
|
+
|
|
32
|
+
def _print_summary(self) -> None:
|
|
33
|
+
"""Print diff summary with triple counts."""
|
|
34
|
+
store = self._state.instances.store
|
|
35
|
+
|
|
36
|
+
add_query = (
|
|
37
|
+
f"SELECT (COUNT(*) as ?count) WHERE {{ GRAPH <{NAMED_GRAPH_NAMESPACE['DIFF_ADD']}> {{ ?s ?p ?o }} }}"
|
|
38
|
+
)
|
|
39
|
+
delete_query = (
|
|
40
|
+
f"SELECT (COUNT(*) as ?count) WHERE {{ GRAPH <{NAMED_GRAPH_NAMESPACE['DIFF_DELETE']}> {{ ?s ?p ?o }} }}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
add_result = cast(ResultRow, next(iter(store.dataset.query(add_query))))
|
|
44
|
+
delete_result = cast(ResultRow, next(iter(store.dataset.query(delete_query))))
|
|
45
|
+
|
|
46
|
+
add_count = int(add_result[0])
|
|
47
|
+
delete_count = int(delete_result[0])
|
|
48
|
+
|
|
49
|
+
print("Diff complete:")
|
|
50
|
+
print(f" {add_count} triples to add (stored in DIFF_ADD)")
|
|
51
|
+
print(f" {delete_count} triples to delete (stored in DIFF_DELETE)")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cognite-neat
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.124.0
|
|
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/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
cognite/neat/__init__.py,sha256=Lo4DbjDOwnhCYUoAgPp5RG1fDdF7OlnomalTe7n1ydw,211
|
|
2
2
|
cognite/neat/_issues.py,sha256=uv0fkkWwTKqNmTmHqyoBB3L6yMCh42EZpEkLGmIJYOY,812
|
|
3
|
-
cognite/neat/_version.py,sha256=
|
|
3
|
+
cognite/neat/_version.py,sha256=0GwMYSFb5nGMLM9dHslSjNFrEuvigs26DxXB6nxHwqk,46
|
|
4
4
|
cognite/neat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
cognite/neat/_data_model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
cognite/neat/_data_model/_constants.py,sha256=NGGvWHlQqhkkSBP_AqoofGYjNph3SiZX6QPINlMsy04,107
|
|
@@ -11,32 +11,39 @@ cognite/neat/_data_model/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
|
|
|
11
11
|
cognite/neat/_data_model/models/conceptual/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
cognite/neat/_data_model/models/conceptual/_base.py,sha256=SFkoBJDM51pqew_isHFJoB20OgfofpwVRnTrg-rKkNY,710
|
|
13
13
|
cognite/neat/_data_model/models/conceptual/_concept.py,sha256=0Pk4W2TJ_Y0Z7oPHpzely1kPXrAkmkyqw6a0n3il6LY,2248
|
|
14
|
+
cognite/neat/_data_model/models/conceptual/_data_model.py,sha256=mSX0z8i29ufcRUhvC_NPeo2xGidlK3B1n89kngY_SqQ,1695
|
|
14
15
|
cognite/neat/_data_model/models/conceptual/_properties.py,sha256=CpF37vJYBTLT4DH4ZOu2U-JyWtkb_27V8fw52qiaE_k,4007
|
|
15
|
-
cognite/neat/_data_model/models/conceptual/_property.py,sha256=
|
|
16
|
-
cognite/neat/_data_model/models/dms/__init__.py,sha256=
|
|
17
|
-
cognite/neat/_data_model/models/dms/_base.py,sha256=
|
|
16
|
+
cognite/neat/_data_model/models/conceptual/_property.py,sha256=blSZQxX52zaILAtjUkldPzPeysz7wnG-UGSNU5tacI8,4138
|
|
17
|
+
cognite/neat/_data_model/models/dms/__init__.py,sha256=ZIoXXe6I_0Z-x8K6n4UghzCQbt-KDlzAvXLYVd7dn8w,3829
|
|
18
|
+
cognite/neat/_data_model/models/dms/_base.py,sha256=F49CLKUmtjTaaW0NrQbjqGkpWNgnZMV8m8uomCU_FJU,719
|
|
18
19
|
cognite/neat/_data_model/models/dms/_constants.py,sha256=wBkLjAPwufPc2naxOfPA1XC0CM2RbDbo6Dpiv9dPrew,1344
|
|
19
|
-
cognite/neat/_data_model/models/dms/_constraints.py,sha256=
|
|
20
|
+
cognite/neat/_data_model/models/dms/_constraints.py,sha256=QxcsHzdaYe3E5_xTIdtQDfkLPOzKa2c-0D02QKLto9o,1064
|
|
20
21
|
cognite/neat/_data_model/models/dms/_container.py,sha256=SgrMCRo04A1gqayJsCia6oMhYT7q3NaeMBZMOQNI-lA,5967
|
|
21
22
|
cognite/neat/_data_model/models/dms/_data_model.py,sha256=GNa05JEj_eSRGoKB8vkR85-Qv-IhPlDR3gXZNQMfe6g,2537
|
|
22
|
-
cognite/neat/_data_model/models/dms/_data_types.py,sha256=
|
|
23
|
-
cognite/neat/_data_model/models/dms/_indexes.py,sha256=
|
|
23
|
+
cognite/neat/_data_model/models/dms/_data_types.py,sha256=0cNChruCvIVLpCfk81L2u4ZrOg7V4zOU3GOibk2X4Hw,5773
|
|
24
|
+
cognite/neat/_data_model/models/dms/_indexes.py,sha256=eM4VP6KtuPPSv4cuMRjBQI5TknmlSqwhW4sIPlExWfY,873
|
|
24
25
|
cognite/neat/_data_model/models/dms/_references.py,sha256=LJ7xlXATf7AP_Uj-8LlxatQk5GG1Tu4CHGK63cJqUU4,3078
|
|
25
26
|
cognite/neat/_data_model/models/dms/_schema.py,sha256=2JFLcm52smzPdtZ69Lf02UbYAD8I_hpRbI7ZAzdxJJs,641
|
|
26
27
|
cognite/neat/_data_model/models/dms/_space.py,sha256=-1LkRmQaAdIj2EYDcVv5Mcejl5uswgAEVv7DztpIUk4,1680
|
|
27
|
-
cognite/neat/_data_model/models/dms/
|
|
28
|
+
cognite/neat/_data_model/models/dms/_types.py,sha256=5-cgC53AG186OZUqkltv7pMjcGNLuH7Etbn8IUcgk1c,447
|
|
29
|
+
cognite/neat/_data_model/models/dms/_view_property.py,sha256=ROf2vYngQS4QPHt86VlwyQJBBOlKL-EylfePgX6Szwg,7422
|
|
28
30
|
cognite/neat/_data_model/models/dms/_views.py,sha256=afUZisM-6r0_IeSwn8ZVFAqWfodHBWrMcFnk_a-ewSI,6579
|
|
29
|
-
cognite/neat/_data_model/models/entities/__init__.py,sha256=
|
|
31
|
+
cognite/neat/_data_model/models/entities/__init__.py,sha256=7dDyES7fYl9LEREal59F038RdEvfGRpUOc6n_MtSgjU,836
|
|
30
32
|
cognite/neat/_data_model/models/entities/_base.py,sha256=PaNrD29iwxuqTpRWbmESMTxRhhKXmRyDF_cLZEC69dg,3927
|
|
31
33
|
cognite/neat/_data_model/models/entities/_constants.py,sha256=EK9Bus8UgFgxK5cVFMTAqWSl6aWkDe7d59hpUmlHlBs,517
|
|
32
34
|
cognite/neat/_data_model/models/entities/_data_types.py,sha256=DfdEWGek7gODro-_0SiiInhPGwul4zn-ASACQfn8HUY,2838
|
|
33
35
|
cognite/neat/_data_model/models/entities/_identifiers.py,sha256=uBiK4ot3V0b_LGXuJ7bfha6AEcFI3p2letr1z2iSvig,1923
|
|
34
|
-
cognite/neat/_data_model/models/entities/_parser.py,sha256=
|
|
36
|
+
cognite/neat/_data_model/models/entities/_parser.py,sha256=zef_pSDZYMZrJl4IKreFDR577KutfhtN1xpH3Ayjt2o,7669
|
|
37
|
+
cognite/neat/_session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
+
cognite/neat/_session/_session.py,sha256=A2RKC4EQjMaKDr-QUiIPJgBXFqst9p1RIV2_yJrfvuE,1240
|
|
39
|
+
cognite/neat/_session/_state_machine/__init__.py,sha256=8bStbS8ivqJbdyL2yR-_1K3OKOojsaKRrYcYTJac0ek,480
|
|
40
|
+
cognite/neat/_session/_state_machine/_base.py,sha256=gdk1uIj--f50v2_X9zxqFRYTXiCgqtmjwkaxJxAJW5k,773
|
|
41
|
+
cognite/neat/_session/_state_machine/_states.py,sha256=RX6C0YNNT9mX0C6c-ZTiUVh6pmMT5ZNXiOuSs80vVXU,4853
|
|
35
42
|
cognite/neat/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
43
|
cognite/neat/_utils/auxiliary.py,sha256=Cx-LP8dfN782R3iUcm--q26zdzQ0k_RFnVbJ0bwVZMI,1345
|
|
37
44
|
cognite/neat/_utils/text.py,sha256=1el5Ty1T0tdhTF0gp6L8AAzUxaFmAUAIFy_VvfG_kkM,1194
|
|
38
45
|
cognite/neat/_utils/useful_types.py,sha256=6Fpw_HlWFj8ZYjGPd0KzguBczuI8GFhj5wBceGsaeak,211
|
|
39
|
-
cognite/neat/_utils/validation.py,sha256=
|
|
46
|
+
cognite/neat/_utils/validation.py,sha256=Rwd-oABdnHyiwd2WR1H4F2JYTdshNIKzHqErbo-MDns,5113
|
|
40
47
|
cognite/neat/_utils/http_client/__init__.py,sha256=gJBrOH1tIzEzLforHbeakYimTn4RlelyANps-jtpREI,894
|
|
41
48
|
cognite/neat/_utils/http_client/_client.py,sha256=2RVwTbbPFlQ8eJVLKNUXwnc4Yq_783PkY44zwr6LlT8,11509
|
|
42
49
|
cognite/neat/_utils/http_client/_config.py,sha256=C8IF1JoijmVMjA_FEMgAkiD1buEV1cY5Og3t-Ecyfmk,756
|
|
@@ -183,7 +190,7 @@ cognite/neat/v0/core/_issues/warnings/_resources.py,sha256=L4iTuVYgfwcaCRTbTCVoo
|
|
|
183
190
|
cognite/neat/v0/core/_issues/warnings/user_modeling.py,sha256=neM9IJzLGWFcBiuo5p5CLFglXjrUXR61FNqvupNw7Y0,4147
|
|
184
191
|
cognite/neat/v0/core/_store/__init__.py,sha256=wpsF8xjIQ5V21NOh45XQV813n_EzgyPOt0VVinYjnDI,140
|
|
185
192
|
cognite/neat/v0/core/_store/_data_model.py,sha256=09JlHEkJVEPHCju8ixRUUsvRcZb0UrDE7wevB7tq4PI,19682
|
|
186
|
-
cognite/neat/v0/core/_store/_instance.py,sha256=
|
|
193
|
+
cognite/neat/v0/core/_store/_instance.py,sha256=NokBDdPkp1K6Ce53dwx3jsBePgGUX0ggXPCGC5HvnGw,18935
|
|
187
194
|
cognite/neat/v0/core/_store/_provenance.py,sha256=Q96wkVXRovO_uTlNvwCAOl6pAoWItTgFq1F79L_FqBk,7335
|
|
188
195
|
cognite/neat/v0/core/_store/exceptions.py,sha256=dTaBSt7IV7XWtS3EsE8lBX1Dv3tfWX1nIEgGHkluy3s,1668
|
|
189
196
|
cognite/neat/v0/core/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -207,8 +214,9 @@ cognite/neat/v0/plugins/_data_model.py,sha256=MSfb9Gtz-whsY3-usMaPYBxpc9-C5qN3gW
|
|
|
207
214
|
cognite/neat/v0/plugins/_issues.py,sha256=9lbLlxgPSVHjirOhEHyNQ0Pf3NH82Psj_DuTCcJDQQQ,943
|
|
208
215
|
cognite/neat/v0/plugins/_manager.py,sha256=Pmaw5yjobMJ64OYaODX3FTaH1Q7hW9rzbz0A8wyIZcI,3683
|
|
209
216
|
cognite/neat/v0/session/__init__.py,sha256=fxQ5URVlUnmEGYyB8Baw7IDq-uYacqkigbc4b-Pr9Fw,58
|
|
210
|
-
cognite/neat/v0/session/_base.py,sha256=
|
|
217
|
+
cognite/neat/v0/session/_base.py,sha256=cCGRKBqSQfkUbekvoIpGPUWPNk7K1yUCqczyCwhvcn8,13036
|
|
211
218
|
cognite/neat/v0/session/_collector.py,sha256=YCPeapbxBK-vAFMq7ekLy0Pn796M9AM7B8hxFtgsOUU,4236
|
|
219
|
+
cognite/neat/v0/session/_diff.py,sha256=F03wOfCwwKMS3ZW9U4Zcv_0QhQR7fBlSxVEuA3SoIk0,1773
|
|
212
220
|
cognite/neat/v0/session/_drop.py,sha256=8C9WDJglaO1lpej4jihOMTtEXjQn8wKL8JjZ1yxAvbE,4248
|
|
213
221
|
cognite/neat/v0/session/_experimental.py,sha256=0peZPZ9JpmzQE05wHbng2tWmPPLLTAVfWZEEUhdnI6o,1274
|
|
214
222
|
cognite/neat/v0/session/_explore.py,sha256=YQF8ubCm-dkpO5WuVfYGTkQjhF_XKHFcG6Ko5jwz-R0,1605
|
|
@@ -231,7 +239,7 @@ cognite/neat/v0/session/engine/__init__.py,sha256=D3MxUorEs6-NtgoICqtZ8PISQrjrr4
|
|
|
231
239
|
cognite/neat/v0/session/engine/_import.py,sha256=1QxA2_EK613lXYAHKQbZyw2yjo5P9XuiX4Z6_6-WMNQ,169
|
|
232
240
|
cognite/neat/v0/session/engine/_interface.py,sha256=3W-cYr493c_mW3P5O6MKN1xEQg3cA7NHR_ev3zdF9Vk,533
|
|
233
241
|
cognite/neat/v0/session/engine/_load.py,sha256=u0x7vuQCRoNcPt25KJBJRn8sJabonYK4vtSZpiTdP4k,5201
|
|
234
|
-
cognite_neat-0.
|
|
235
|
-
cognite_neat-0.
|
|
236
|
-
cognite_neat-0.
|
|
237
|
-
cognite_neat-0.
|
|
242
|
+
cognite_neat-0.124.0.dist-info/METADATA,sha256=uQiuJcVRVFPhAyKUSXFn7DyXW8kfBE75pCQgnZxU8Ec,9147
|
|
243
|
+
cognite_neat-0.124.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
244
|
+
cognite_neat-0.124.0.dist-info/licenses/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
|
|
245
|
+
cognite_neat-0.124.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|