cognite-neat 0.110.0__py3-none-any.whl → 0.111.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/_alpha.py +6 -0
- cognite/neat/_client/_api/schema.py +26 -0
- cognite/neat/_client/data_classes/schema.py +1 -1
- cognite/neat/_constants.py +4 -1
- cognite/neat/_graph/extractors/__init__.py +4 -0
- cognite/neat/_graph/extractors/_classic_cdf/_base.py +8 -16
- cognite/neat/_graph/extractors/_classic_cdf/_classic.py +39 -9
- cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +23 -17
- cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +15 -17
- cognite/neat/_graph/extractors/_dict.py +102 -0
- cognite/neat/_graph/extractors/_dms.py +27 -40
- cognite/neat/_graph/extractors/_dms_graph.py +30 -3
- cognite/neat/_graph/extractors/_raw.py +67 -0
- cognite/neat/_graph/loaders/_base.py +20 -4
- cognite/neat/_graph/loaders/_rdf2dms.py +243 -89
- cognite/neat/_graph/queries/_base.py +137 -43
- cognite/neat/_graph/transformers/_classic_cdf.py +6 -22
- cognite/neat/_issues/_factory.py +9 -1
- cognite/neat/_issues/errors/__init__.py +2 -0
- cognite/neat/_issues/errors/_external.py +7 -0
- cognite/neat/_issues/warnings/user_modeling.py +12 -0
- cognite/neat/_rules/_constants.py +3 -0
- cognite/neat/_rules/analysis/_base.py +29 -50
- cognite/neat/_rules/exporters/_rules2excel.py +1 -1
- cognite/neat/_rules/importers/_rdf/_inference2rules.py +16 -10
- cognite/neat/_rules/models/_base_rules.py +0 -2
- cognite/neat/_rules/models/data_types.py +7 -0
- cognite/neat/_rules/models/dms/_exporter.py +9 -8
- cognite/neat/_rules/models/dms/_rules.py +26 -1
- cognite/neat/_rules/models/dms/_rules_input.py +5 -1
- cognite/neat/_rules/models/dms/_validation.py +101 -1
- cognite/neat/_rules/models/entities/_single_value.py +8 -3
- cognite/neat/_rules/models/entities/_wrapped.py +2 -2
- cognite/neat/_rules/models/information/_rules_input.py +1 -0
- cognite/neat/_rules/models/information/_validation.py +64 -17
- cognite/neat/_rules/transformers/_converters.py +7 -2
- cognite/neat/_session/_base.py +2 -0
- cognite/neat/_session/_explore.py +39 -0
- cognite/neat/_session/_inspect.py +25 -6
- cognite/neat/_session/_read.py +67 -3
- cognite/neat/_session/_set.py +7 -1
- cognite/neat/_session/_state.py +6 -0
- cognite/neat/_session/_to.py +115 -8
- cognite/neat/_store/_graph_store.py +8 -4
- cognite/neat/_utils/rdf_.py +34 -3
- cognite/neat/_utils/text.py +72 -4
- cognite/neat/_utils/upload.py +2 -0
- cognite/neat/_version.py +2 -2
- {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/RECORD +53 -50
- {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/entry_points.txt +0 -0
|
@@ -130,6 +130,13 @@ class DataType(BaseModel):
|
|
|
130
130
|
def as_xml_uri_ref(cls) -> URIRef:
|
|
131
131
|
return XML_SCHEMA_NAMESPACE[cls.xsd]
|
|
132
132
|
|
|
133
|
+
@classmethod
|
|
134
|
+
def convert_value(cls, value: Any) -> Any:
|
|
135
|
+
if cls != Boolean:
|
|
136
|
+
return cls.python(value)
|
|
137
|
+
else:
|
|
138
|
+
return value.strip().lower() in {"true", "1", "yes"}
|
|
139
|
+
|
|
133
140
|
|
|
134
141
|
class Boolean(DataType):
|
|
135
142
|
python = bool
|
|
@@ -99,6 +99,7 @@ class _DMSExporter:
|
|
|
99
99
|
key=lambda x: x.as_tuple(), # type: ignore[union-attr]
|
|
100
100
|
)
|
|
101
101
|
spaces = self._create_spaces(rules.metadata, containers, views, data_model)
|
|
102
|
+
|
|
102
103
|
return DMSSchema(
|
|
103
104
|
spaces=spaces,
|
|
104
105
|
data_model=data_model,
|
|
@@ -114,14 +115,14 @@ class _DMSExporter:
|
|
|
114
115
|
views: ViewApplyDict,
|
|
115
116
|
data_model: dm.DataModelApply,
|
|
116
117
|
) -> SpaceApplyDict:
|
|
117
|
-
used_spaces =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
data_model.space
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
used_spaces = (
|
|
119
|
+
{container.space for container in containers.values()}
|
|
120
|
+
| {view.space for view in views.values()}
|
|
121
|
+
| {data_model.space}
|
|
122
|
+
| {metadata.space}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
spaces = SpaceApplyDict([dm.SpaceApply(space=space) for space in used_spaces])
|
|
125
126
|
if self.instance_space and self.instance_space not in spaces:
|
|
126
127
|
spaces[self.instance_space] = dm.SpaceApply(space=self.instance_space, name=self.instance_space)
|
|
127
128
|
return spaces
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from collections.abc import Hashable
|
|
2
3
|
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
|
3
4
|
|
|
@@ -8,6 +9,7 @@ from pydantic_core.core_schema import SerializationInfo, ValidationInfo
|
|
|
8
9
|
|
|
9
10
|
from cognite.neat._client.data_classes.schema import DMSSchema
|
|
10
11
|
from cognite.neat._issues.errors import NeatValueError
|
|
12
|
+
from cognite.neat._issues.warnings._general import NeatValueWarning
|
|
11
13
|
from cognite.neat._rules.models._base_rules import (
|
|
12
14
|
BaseMetadata,
|
|
13
15
|
BaseRules,
|
|
@@ -116,7 +118,7 @@ class DMSProperty(SheetRow):
|
|
|
116
118
|
description="Used to indicate whether the property holds single or multiple values (list). "
|
|
117
119
|
"Only applies to primitive types.",
|
|
118
120
|
)
|
|
119
|
-
default: str | int | dict | None = Field(
|
|
121
|
+
default: bool | str | int | float | dict | None = Field(
|
|
120
122
|
None, alias="Default", description="Specifies default value for the property."
|
|
121
123
|
)
|
|
122
124
|
container: ContainerEntityType | None = Field(
|
|
@@ -168,6 +170,29 @@ class DMSProperty(SheetRow):
|
|
|
168
170
|
raise ValueError(f"Reverse connection must have a value type that points to a view, got {value}")
|
|
169
171
|
return value
|
|
170
172
|
|
|
173
|
+
@field_validator("default", mode="after")
|
|
174
|
+
def set_proper_type_on_default(cls, value: Any, info: ValidationInfo) -> Any:
|
|
175
|
+
if not value:
|
|
176
|
+
return value
|
|
177
|
+
value_type = info.data.get("value_type")
|
|
178
|
+
if not isinstance(value_type, DataType):
|
|
179
|
+
warnings.filterwarnings("default")
|
|
180
|
+
warnings.warn(
|
|
181
|
+
NeatValueWarning(f"Default value {value} set to connection {value_type} will be ignored"),
|
|
182
|
+
stacklevel=2,
|
|
183
|
+
)
|
|
184
|
+
return None
|
|
185
|
+
else:
|
|
186
|
+
try:
|
|
187
|
+
return value_type.convert_value(value)
|
|
188
|
+
except ValueError:
|
|
189
|
+
warnings.filterwarnings("default")
|
|
190
|
+
warnings.warn(
|
|
191
|
+
NeatValueWarning(f"Could not convert {value} to {value_type}"),
|
|
192
|
+
stacklevel=2,
|
|
193
|
+
)
|
|
194
|
+
return None
|
|
195
|
+
|
|
171
196
|
@field_validator("container", "container_property", mode="after")
|
|
172
197
|
def container_set_correctly(cls, value: Any, info: ValidationInfo) -> Any:
|
|
173
198
|
if (connection := info.data.get("connection")) is None:
|
|
@@ -21,6 +21,7 @@ from cognite.neat._rules.models.entities import (
|
|
|
21
21
|
load_connection,
|
|
22
22
|
load_dms_value_type,
|
|
23
23
|
)
|
|
24
|
+
from cognite.neat._rules.models.entities._wrapped import DMSFilter
|
|
24
25
|
from cognite.neat._utils.rdf_ import uri_display_name
|
|
25
26
|
|
|
26
27
|
from ._rules import _DEFAULT_VERSION, DMSContainer, DMSEnum, DMSMetadata, DMSNode, DMSProperty, DMSRules, DMSView
|
|
@@ -69,7 +70,7 @@ class DMSInputMetadata(InputComponent[DMSMetadata]):
|
|
|
69
70
|
def _get_description_and_creator(cls, description_raw: str | None) -> tuple[str | None, list[str]]:
|
|
70
71
|
if description_raw and (description_match := re.search(r"Creator: (.+)", description_raw)):
|
|
71
72
|
creator = description_match.group(1).split(", ")
|
|
72
|
-
description = description_raw.replace(description_match
|
|
73
|
+
description = description_raw.replace(description_match[0], "").strip() or None
|
|
73
74
|
elif description_raw:
|
|
74
75
|
creator = ["MISSING"]
|
|
75
76
|
description = description_raw
|
|
@@ -211,6 +212,8 @@ class DMSInputView(InputComponent[DMSView]):
|
|
|
211
212
|
return ViewEntity.load(self.view, strict=True, space=default_space, version=default_version)
|
|
212
213
|
|
|
213
214
|
def _load_implements(self, default_space: str, default_version: str) -> list[ViewEntity] | None:
|
|
215
|
+
self.implements = self.implements.strip() if self.implements else None
|
|
216
|
+
|
|
214
217
|
return (
|
|
215
218
|
[
|
|
216
219
|
ViewEntity.load(implement, strict=True, space=default_space, version=default_version)
|
|
@@ -234,6 +237,7 @@ class DMSInputView(InputComponent[DMSView]):
|
|
|
234
237
|
implements=", ".join([str(ViewEntity.from_id(parent, _DEFAULT_VERSION)) for parent in view.implements])
|
|
235
238
|
or None,
|
|
236
239
|
in_model=in_model,
|
|
240
|
+
filter_=(str(DMSFilter.from_dms_filter(view.filter)) if view.filter else None),
|
|
237
241
|
)
|
|
238
242
|
|
|
239
243
|
|
|
@@ -27,10 +27,12 @@ from cognite.neat._issues.errors import (
|
|
|
27
27
|
ResourceNotFoundError,
|
|
28
28
|
ReversedConnectionNotFeasibleError,
|
|
29
29
|
)
|
|
30
|
+
from cognite.neat._issues.errors._external import CDFMissingResourcesError
|
|
30
31
|
from cognite.neat._issues.warnings import (
|
|
31
32
|
NotSupportedHasDataFilterLimitWarning,
|
|
32
33
|
NotSupportedViewContainerLimitWarning,
|
|
33
34
|
UndefinedViewWarning,
|
|
35
|
+
user_modeling,
|
|
34
36
|
)
|
|
35
37
|
from cognite.neat._issues.warnings.user_modeling import (
|
|
36
38
|
ContainerPropertyLimitWarning,
|
|
@@ -43,6 +45,7 @@ from cognite.neat._rules.models.entities._single_value import (
|
|
|
43
45
|
ViewEntity,
|
|
44
46
|
)
|
|
45
47
|
from cognite.neat._utils.spreadsheet import SpreadsheetRead
|
|
48
|
+
from cognite.neat._utils.text import humanize_collection
|
|
46
49
|
|
|
47
50
|
from ._rules import DMSProperty, DMSRules
|
|
48
51
|
|
|
@@ -113,6 +116,14 @@ class DMSValidation:
|
|
|
113
116
|
list(imported_containers), include_connected=True
|
|
114
117
|
)
|
|
115
118
|
|
|
119
|
+
missing_views = {view.as_id() for view in imported_views} - {view.as_id() for view in referenced_views}
|
|
120
|
+
missing_containers = {container.as_id() for container in imported_containers} - {
|
|
121
|
+
container.as_id() for container in referenced_containers
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if missing_views or missing_containers:
|
|
125
|
+
raise CDFMissingResourcesError(resources=f"{missing_views.union(missing_containers)}")
|
|
126
|
+
|
|
116
127
|
# Setup data structures for validation
|
|
117
128
|
dms_schema = self._rules.as_schema()
|
|
118
129
|
ref_view_by_id = {view.as_id(): view for view in referenced_views}
|
|
@@ -130,6 +141,10 @@ class DMSValidation:
|
|
|
130
141
|
parents_view_ids_by_child_id = self._parent_view_ids_by_child_id(all_views_by_id)
|
|
131
142
|
|
|
132
143
|
issue_list = IssueList()
|
|
144
|
+
|
|
145
|
+
# Validated for duplicated resource
|
|
146
|
+
issue_list.extend(self._duplicated_resources())
|
|
147
|
+
|
|
133
148
|
# Neat DMS classes Validation
|
|
134
149
|
# These are errors that can only happen due to the format of the Neat DMS classes
|
|
135
150
|
issue_list.extend(self._validate_raw_filter())
|
|
@@ -148,6 +163,91 @@ class DMSValidation:
|
|
|
148
163
|
)
|
|
149
164
|
issue_list.extend(self._validate_schema(dms_schema, all_views_by_id, all_containers_by_id))
|
|
150
165
|
issue_list.extend(self._validate_referenced_container_limits(dms_schema.views, view_properties_by_id))
|
|
166
|
+
issue_list.extend(self._same_space_views_and_data_model())
|
|
167
|
+
return issue_list
|
|
168
|
+
|
|
169
|
+
def _same_space_views_and_data_model(self) -> IssueList:
|
|
170
|
+
issue_list = IssueList()
|
|
171
|
+
|
|
172
|
+
schema = self._rules.as_schema(remove_cdf_spaces=True)
|
|
173
|
+
|
|
174
|
+
if schema.data_model and schema.views:
|
|
175
|
+
data_model_space = schema.data_model.space
|
|
176
|
+
views_spaces = {view.space for view in schema.views.values()}
|
|
177
|
+
|
|
178
|
+
if data_model_space not in views_spaces:
|
|
179
|
+
issue_list.append(
|
|
180
|
+
user_modeling.ViewsAndDataModelNotInSameSpaceWarning(
|
|
181
|
+
data_model_space=data_model_space,
|
|
182
|
+
views_spaces=humanize_collection(views_spaces),
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return issue_list
|
|
187
|
+
|
|
188
|
+
def _duplicated_resources(self) -> IssueList:
|
|
189
|
+
issue_list = IssueList()
|
|
190
|
+
|
|
191
|
+
properties_sheet = self._read_info_by_spreadsheet.get("Properties")
|
|
192
|
+
views_sheet = self._read_info_by_spreadsheet.get("Views")
|
|
193
|
+
containers_sheet = self._read_info_by_spreadsheet.get("Containers")
|
|
194
|
+
|
|
195
|
+
visited = defaultdict(list)
|
|
196
|
+
for row_no, property_ in enumerate(self._properties):
|
|
197
|
+
visited[property_._identifier()].append(
|
|
198
|
+
properties_sheet.adjusted_row_number(row_no) if properties_sheet else row_no + 1
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
for identifier, rows in visited.items():
|
|
202
|
+
if len(rows) == 1:
|
|
203
|
+
continue
|
|
204
|
+
issue_list.append(
|
|
205
|
+
ResourceDuplicatedError(
|
|
206
|
+
identifier[1],
|
|
207
|
+
"property",
|
|
208
|
+
(
|
|
209
|
+
f"the Properties sheet at row {humanize_collection(rows)} "
|
|
210
|
+
"if data model is read from a spreadsheet."
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
visited = defaultdict(list)
|
|
216
|
+
for row_no, view in enumerate(self._views):
|
|
217
|
+
visited[view._identifier()].append(views_sheet.adjusted_row_number(row_no) if views_sheet else row_no + 1)
|
|
218
|
+
|
|
219
|
+
for identifier, rows in visited.items():
|
|
220
|
+
if len(rows) == 1:
|
|
221
|
+
continue
|
|
222
|
+
issue_list.append(
|
|
223
|
+
ResourceDuplicatedError(
|
|
224
|
+
identifier[0],
|
|
225
|
+
"view",
|
|
226
|
+
(f"the Views sheet at row {humanize_collection(rows)} if data model is read from a spreadsheet."),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if self._containers:
|
|
231
|
+
visited = defaultdict(list)
|
|
232
|
+
for row_no, container in enumerate(self._containers):
|
|
233
|
+
visited[container._identifier()].append(
|
|
234
|
+
containers_sheet.adjusted_row_number(row_no) if containers_sheet else row_no + 1
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
for identifier, rows in visited.items():
|
|
238
|
+
if len(rows) == 1:
|
|
239
|
+
continue
|
|
240
|
+
issue_list.append(
|
|
241
|
+
ResourceDuplicatedError(
|
|
242
|
+
identifier[0],
|
|
243
|
+
"container",
|
|
244
|
+
(
|
|
245
|
+
f"the Containers sheet at row {humanize_collection(rows)} "
|
|
246
|
+
"if data model is read from a spreadsheet."
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
151
251
|
return issue_list
|
|
152
252
|
|
|
153
253
|
@staticmethod
|
|
@@ -595,7 +695,7 @@ class DMSValidation:
|
|
|
595
695
|
ResourceDuplicatedError(
|
|
596
696
|
view_id,
|
|
597
697
|
"view",
|
|
598
|
-
|
|
698
|
+
f"DMS {model.as_id()!r}",
|
|
599
699
|
)
|
|
600
700
|
)
|
|
601
701
|
|
|
@@ -35,6 +35,7 @@ else:
|
|
|
35
35
|
from cognite.neat._rules._constants import (
|
|
36
36
|
ENTITY_PATTERN,
|
|
37
37
|
SPLIT_ON_COMMA_PATTERN,
|
|
38
|
+
SPLIT_ON_EDGE_ENTITY_ARGS_PATTERN,
|
|
38
39
|
SPLIT_ON_EQUAL_PATTERN,
|
|
39
40
|
EntityTypes,
|
|
40
41
|
)
|
|
@@ -135,9 +136,13 @@ class Entity(BaseModel, extra="ignore"):
|
|
|
135
136
|
if content is None:
|
|
136
137
|
return dict(prefix=prefix, suffix=suffix)
|
|
137
138
|
try:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
if cls == EdgeEntity:
|
|
140
|
+
matches = SPLIT_ON_EDGE_ENTITY_ARGS_PATTERN.findall(content)
|
|
141
|
+
extra_args = {key: value for key, value in matches}
|
|
142
|
+
else:
|
|
143
|
+
extra_args = dict(
|
|
144
|
+
SPLIT_ON_EQUAL_PATTERN.split(pair.strip()) for pair in SPLIT_ON_COMMA_PATTERN.split(content)
|
|
145
|
+
)
|
|
141
146
|
except ValueError:
|
|
142
147
|
raise NeatValueError(f"Invalid {cls.type_.value} entity: {raw!r}") from None
|
|
143
148
|
expected_args = {
|
|
@@ -127,8 +127,8 @@ class DMSFilter(WrappedEntity):
|
|
|
127
127
|
if isinstance(entry, dict) and "space" in entry and "externalId" in entry
|
|
128
128
|
]
|
|
129
129
|
)
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
# fall back to raw filter to preserve the information
|
|
131
|
+
return RawFilter(filter=json.dumps(dumped))
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
class NodeTypeFilter(DMSFilter):
|
|
@@ -128,6 +128,7 @@ class InformationInputClass(InputComponent[InformationClass]):
|
|
|
128
128
|
output = super().dump()
|
|
129
129
|
parent: list[ClassEntity] | None = None
|
|
130
130
|
if isinstance(self.implements, str):
|
|
131
|
+
self.implements = self.implements.strip()
|
|
131
132
|
parent = [ClassEntity.load(parent, prefix=default_prefix) for parent in self.implements.split(",")]
|
|
132
133
|
elif isinstance(self.implements, list):
|
|
133
134
|
parent = [ClassEntity.load(parent_, prefix=default_prefix) for parent_ in self.implements]
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import itertools
|
|
2
|
-
from collections import Counter
|
|
2
|
+
from collections import Counter, defaultdict
|
|
3
3
|
|
|
4
4
|
from cognite.neat._issues import IssueList
|
|
5
5
|
from cognite.neat._issues.errors import NeatValueError
|
|
6
|
-
from cognite.neat._issues.errors._resources import
|
|
6
|
+
from cognite.neat._issues.errors._resources import (
|
|
7
|
+
ResourceDuplicatedError,
|
|
8
|
+
ResourceNotDefinedError,
|
|
9
|
+
)
|
|
7
10
|
from cognite.neat._issues.warnings._models import UndefinedClassWarning
|
|
8
11
|
from cognite.neat._issues.warnings._resources import (
|
|
9
12
|
ResourceNotDefinedWarning,
|
|
@@ -13,6 +16,7 @@ from cognite.neat._rules._constants import PATTERNS, EntityTypes
|
|
|
13
16
|
from cognite.neat._rules.models.entities import ClassEntity, UnknownEntity
|
|
14
17
|
from cognite.neat._rules.models.entities._multi_value import MultiValueTypeInfo
|
|
15
18
|
from cognite.neat._utils.spreadsheet import SpreadsheetRead
|
|
19
|
+
from cognite.neat._utils.text import humanize_collection
|
|
16
20
|
|
|
17
21
|
from ._rules import InformationRules
|
|
18
22
|
|
|
@@ -23,13 +27,14 @@ class InformationValidation:
|
|
|
23
27
|
|
|
24
28
|
def __init__(self, rules: InformationRules, read_info_by_spreadsheet: dict[str, SpreadsheetRead] | None = None):
|
|
25
29
|
self.rules = rules
|
|
26
|
-
self.
|
|
27
|
-
self.
|
|
28
|
-
self.
|
|
29
|
-
self.
|
|
30
|
+
self._read_info_by_spreadsheet = read_info_by_spreadsheet or {}
|
|
31
|
+
self._metadata = rules.metadata
|
|
32
|
+
self._properties = rules.properties
|
|
33
|
+
self._classes = rules.classes
|
|
30
34
|
self.issue_list = IssueList()
|
|
31
35
|
|
|
32
36
|
def validate(self) -> IssueList:
|
|
37
|
+
self._duplicated_resources()
|
|
33
38
|
self._namespaces_reassigned()
|
|
34
39
|
self._classes_without_properties()
|
|
35
40
|
self._undefined_classes()
|
|
@@ -40,9 +45,51 @@ class InformationValidation:
|
|
|
40
45
|
|
|
41
46
|
return self.issue_list
|
|
42
47
|
|
|
48
|
+
def _duplicated_resources(self) -> None:
|
|
49
|
+
properties_sheet = self._read_info_by_spreadsheet.get("Properties")
|
|
50
|
+
classes_sheet = self._read_info_by_spreadsheet.get("Classes")
|
|
51
|
+
|
|
52
|
+
visited = defaultdict(list)
|
|
53
|
+
for row_no, property_ in enumerate(self._properties):
|
|
54
|
+
visited[property_._identifier()].append(
|
|
55
|
+
properties_sheet.adjusted_row_number(row_no) if properties_sheet else row_no + 1
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
for identifier, rows in visited.items():
|
|
59
|
+
if len(rows) == 1:
|
|
60
|
+
continue
|
|
61
|
+
self.issue_list.append(
|
|
62
|
+
ResourceDuplicatedError(
|
|
63
|
+
identifier[1],
|
|
64
|
+
"property",
|
|
65
|
+
(
|
|
66
|
+
"the Properties sheet at row "
|
|
67
|
+
f"{humanize_collection(rows)}"
|
|
68
|
+
" if data model is read from a spreadsheet."
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
visited = defaultdict(list)
|
|
74
|
+
for row_no, class_ in enumerate(self._classes):
|
|
75
|
+
visited[class_._identifier()].append(
|
|
76
|
+
classes_sheet.adjusted_row_number(row_no) if classes_sheet else row_no + 1
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
for identifier, rows in visited.items():
|
|
80
|
+
if len(rows) == 1:
|
|
81
|
+
continue
|
|
82
|
+
self.issue_list.append(
|
|
83
|
+
ResourceDuplicatedError(
|
|
84
|
+
identifier[0],
|
|
85
|
+
"class",
|
|
86
|
+
(f"the Classes sheet at row {humanize_collection(rows)} if data model is read from a spreadsheet."),
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
43
90
|
def _classes_without_properties(self) -> None:
|
|
44
|
-
defined_classes = {class_.class_ for class_ in self.
|
|
45
|
-
referred_classes = {property_.class_ for property_ in self.
|
|
91
|
+
defined_classes = {class_.class_ for class_ in self._classes}
|
|
92
|
+
referred_classes = {property_.class_ for property_ in self._properties}
|
|
46
93
|
class_parent_pairs = self._class_parent_pairs()
|
|
47
94
|
|
|
48
95
|
if classes_without_properties := defined_classes.difference(referred_classes):
|
|
@@ -50,7 +97,7 @@ class InformationValidation:
|
|
|
50
97
|
# USE CASE: class has no direct properties and no parents with properties
|
|
51
98
|
# and it is a class in the prefix of data model, as long as it is in the
|
|
52
99
|
# same prefix, meaning same space
|
|
53
|
-
if not class_parent_pairs[class_] and class_.prefix == self.
|
|
100
|
+
if not class_parent_pairs[class_] and class_.prefix == self._metadata.prefix:
|
|
54
101
|
self.issue_list.append(
|
|
55
102
|
ResourceNotDefinedWarning(
|
|
56
103
|
resource_type="class",
|
|
@@ -60,8 +107,8 @@ class InformationValidation:
|
|
|
60
107
|
)
|
|
61
108
|
|
|
62
109
|
def _undefined_classes(self) -> None:
|
|
63
|
-
defined_classes = {class_.class_ for class_ in self.
|
|
64
|
-
referred_classes = {property_.class_ for property_ in self.
|
|
110
|
+
defined_classes = {class_.class_ for class_ in self._classes}
|
|
111
|
+
referred_classes = {property_.class_ for property_ in self._properties}
|
|
65
112
|
|
|
66
113
|
if undefined_classes := referred_classes.difference(defined_classes):
|
|
67
114
|
for class_ in undefined_classes:
|
|
@@ -81,7 +128,7 @@ class InformationValidation:
|
|
|
81
128
|
|
|
82
129
|
if undefined_parents := parents.difference(classes):
|
|
83
130
|
for parent in undefined_parents:
|
|
84
|
-
if parent.prefix != self.
|
|
131
|
+
if parent.prefix != self._metadata.prefix:
|
|
85
132
|
self.issue_list.append(UndefinedClassWarning(class_id=str(parent)))
|
|
86
133
|
else:
|
|
87
134
|
self.issue_list.append(
|
|
@@ -94,8 +141,8 @@ class InformationValidation:
|
|
|
94
141
|
|
|
95
142
|
def _referenced_classes_exist(self) -> None:
|
|
96
143
|
# needs to be complete for this validation to pass
|
|
97
|
-
defined_classes = {class_.class_ for class_ in self.
|
|
98
|
-
classes_with_explicit_properties = {property_.class_ for property_ in self.
|
|
144
|
+
defined_classes = {class_.class_ for class_ in self._classes}
|
|
145
|
+
classes_with_explicit_properties = {property_.class_ for property_ in self._properties}
|
|
99
146
|
|
|
100
147
|
# USE CASE: models are complete
|
|
101
148
|
if missing_classes := classes_with_explicit_properties.difference(defined_classes):
|
|
@@ -110,7 +157,7 @@ class InformationValidation:
|
|
|
110
157
|
|
|
111
158
|
def _referenced_value_types_exist(self) -> None:
|
|
112
159
|
# adding UnknownEntity to the set of defined classes to handle the case where a property references an unknown
|
|
113
|
-
defined_classes = {class_.class_ for class_ in self.
|
|
160
|
+
defined_classes = {class_.class_ for class_ in self._classes} | {UnknownEntity()}
|
|
114
161
|
referred_object_types = {
|
|
115
162
|
property_.value_type
|
|
116
163
|
for property_ in self.rules.properties
|
|
@@ -131,7 +178,7 @@ class InformationValidation:
|
|
|
131
178
|
def _regex_compliance_with_dms(self) -> None:
|
|
132
179
|
"""Check regex compliance with DMS of properties, classes and value types."""
|
|
133
180
|
|
|
134
|
-
for prop_ in self.
|
|
181
|
+
for prop_ in self._properties:
|
|
135
182
|
if not PATTERNS.dms_property_id_compliance.match(prop_.property_):
|
|
136
183
|
self.issue_list.append(
|
|
137
184
|
ResourceRegexViolationWarning(
|
|
@@ -181,7 +228,7 @@ class InformationValidation:
|
|
|
181
228
|
)
|
|
182
229
|
)
|
|
183
230
|
|
|
184
|
-
for class_ in self.
|
|
231
|
+
for class_ in self._classes:
|
|
185
232
|
if not PATTERNS.view_id_compliance.match(class_.class_.suffix):
|
|
186
233
|
self.issue_list.append(
|
|
187
234
|
ResourceRegexViolationWarning(
|
|
@@ -60,7 +60,7 @@ from cognite.neat._rules.models.entities import (
|
|
|
60
60
|
)
|
|
61
61
|
from cognite.neat._rules.models.information import InformationClass, InformationMetadata, InformationProperty
|
|
62
62
|
from cognite.neat._utils.rdf_ import get_inheritance_path
|
|
63
|
-
from cognite.neat._utils.text import NamingStandardization, to_camel_case
|
|
63
|
+
from cognite.neat._utils.text import NamingStandardization, title, to_camel_case, to_words
|
|
64
64
|
|
|
65
65
|
from ._base import RulesTransformer, T_VerifiedIn, T_VerifiedOut, VerifiedRulesTransformer
|
|
66
66
|
from ._verification import VerifyDMSRules
|
|
@@ -561,8 +561,9 @@ _T_Entity = TypeVar("_T_Entity", bound=ClassEntity | ViewEntity)
|
|
|
561
561
|
|
|
562
562
|
|
|
563
563
|
class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
564
|
-
def __init__(self, new_id: DataModelId | tuple[str, str, str]):
|
|
564
|
+
def __init__(self, new_id: DataModelId | tuple[str, str, str], name: str | None = None):
|
|
565
565
|
self.new_id = DataModelId.load(new_id)
|
|
566
|
+
self.name = name
|
|
566
567
|
|
|
567
568
|
@property
|
|
568
569
|
def description(self) -> str:
|
|
@@ -575,10 +576,14 @@ class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
|
575
576
|
dump["metadata"]["space"] = self.new_id.space
|
|
576
577
|
dump["metadata"]["external_id"] = self.new_id.external_id
|
|
577
578
|
dump["metadata"]["version"] = self.new_id.version
|
|
579
|
+
dump["metadata"]["name"] = self.name or self._generate_name()
|
|
578
580
|
# Serialize and deserialize to set the new space and external_id
|
|
579
581
|
# as the default values for the new model.
|
|
580
582
|
return DMSRules.model_validate(DMSInputRules.load(dump).dump())
|
|
581
583
|
|
|
584
|
+
def _generate_name(self) -> str:
|
|
585
|
+
return title(to_words(self.new_id.external_id))
|
|
586
|
+
|
|
582
587
|
|
|
583
588
|
class ToExtensionModel(VerifiedRulesTransformer[DMSRules, DMSRules], ABC):
|
|
584
589
|
type_: ClassVar[str]
|
cognite/neat/_session/_base.py
CHANGED
|
@@ -25,6 +25,7 @@ from cognite.neat._utils.auxiliary import local_import
|
|
|
25
25
|
from ._collector import _COLLECTOR, Collector
|
|
26
26
|
from ._create import CreateAPI
|
|
27
27
|
from ._drop import DropAPI
|
|
28
|
+
from ._explore import ExploreAPI
|
|
28
29
|
from ._fix import FixAPI
|
|
29
30
|
from ._inspect import InspectAPI
|
|
30
31
|
from ._mapping import MappingAPI
|
|
@@ -104,6 +105,7 @@ class NeatSession:
|
|
|
104
105
|
self.drop = DropAPI(self._state)
|
|
105
106
|
self.subset = SubsetAPI(self._state)
|
|
106
107
|
self.create = CreateAPI(self._state)
|
|
108
|
+
self._explore = ExploreAPI(self._state)
|
|
107
109
|
self.opt = OptAPI()
|
|
108
110
|
self.opt._display()
|
|
109
111
|
if load_engine != "skip" and (engine_version := load_neat_engine(client, load_engine)):
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from rdflib import URIRef
|
|
5
|
+
|
|
6
|
+
from cognite.neat._utils.rdf_ import remove_namespace_from_uri
|
|
7
|
+
from cognite.neat._utils.text import humanize_collection
|
|
8
|
+
|
|
9
|
+
from ._state import SessionState
|
|
10
|
+
from .exceptions import NeatSessionError, session_class_wrapper
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@session_class_wrapper
|
|
14
|
+
class ExploreAPI:
|
|
15
|
+
"""
|
|
16
|
+
Explore the instances in the session.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, state: SessionState):
|
|
20
|
+
self._state = state
|
|
21
|
+
|
|
22
|
+
def types(self) -> pd.DataFrame:
|
|
23
|
+
"""List all the types of instances in the session."""
|
|
24
|
+
return pd.DataFrame(self._state.instances.store.queries.types_with_instance_and_property_count())
|
|
25
|
+
|
|
26
|
+
def properties(self) -> pd.DataFrame:
|
|
27
|
+
"""List all the properties of a type of instances in the session."""
|
|
28
|
+
return pd.DataFrame(self._state.instances.store.queries.properties_with_count())
|
|
29
|
+
|
|
30
|
+
def instance_with_properties(self, type: str) -> dict[str, set[str]]:
|
|
31
|
+
"""List all the instances of a type with their properties."""
|
|
32
|
+
available_types = self._state.instances.store.queries.list_types(remove_namespace=False)
|
|
33
|
+
uri_by_type = {remove_namespace_from_uri(t[0]): t[0] for t in available_types}
|
|
34
|
+
if type not in uri_by_type:
|
|
35
|
+
raise NeatSessionError(
|
|
36
|
+
f"Type {type} not found. Available types are: {humanize_collection(uri_by_type.keys())}"
|
|
37
|
+
)
|
|
38
|
+
type_uri = cast(URIRef, uri_by_type[type])
|
|
39
|
+
return self._state.instances.store.queries.instances_with_properties(type_uri, remove_namespace=True)
|