cognite-neat 0.72.2__py3-none-any.whl → 0.73.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/_version.py +1 -1
- cognite/neat/constants.py +3 -2
- cognite/neat/graph/extractor/_graph_capturing_sheet.py +14 -12
- cognite/neat/rules/examples/power-grid-containers.yaml +3 -0
- cognite/neat/rules/examples/power-grid-model.yaml +3 -0
- cognite/neat/rules/exporter/_rules2dms.py +3 -10
- cognite/neat/rules/exporter/_rules2excel.py +3 -2
- cognite/neat/rules/exporters/_rules2dms.py +3 -7
- cognite/neat/rules/exporters/_rules2excel.py +94 -4
- cognite/neat/rules/exporters/_rules2ontology.py +2 -0
- cognite/neat/rules/importer/_dms2rules.py +1 -4
- cognite/neat/rules/importers/_dms2rules.py +40 -11
- cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +15 -15
- cognite/neat/rules/importers/_owl2rules/_owl2properties.py +2 -0
- cognite/neat/rules/importers/_owl2rules/_owl2rules.py +1 -1
- cognite/neat/rules/importers/_spreadsheet2rules.py +52 -31
- cognite/neat/rules/issues/base.py +9 -1
- cognite/neat/rules/issues/dms.py +74 -0
- cognite/neat/rules/models/_rules/_types/_base.py +6 -16
- cognite/neat/rules/models/_rules/base.py +24 -2
- cognite/neat/rules/models/_rules/dms_architect_rules.py +181 -71
- cognite/neat/rules/models/_rules/dms_schema.py +5 -1
- cognite/neat/rules/models/_rules/domain_rules.py +14 -1
- cognite/neat/rules/models/_rules/information_rules.py +16 -2
- cognite/neat/utils/spreadsheet.py +2 -2
- cognite/neat/workflows/steps/lib/rules_exporter.py +0 -1
- {cognite_neat-0.72.2.dist-info → cognite_neat-0.73.0.dist-info}/METADATA +2 -2
- {cognite_neat-0.72.2.dist-info → cognite_neat-0.73.0.dist-info}/RECORD +31 -32
- cognite/neat/py.typed +0 -0
- {cognite_neat-0.72.2.dist-info → cognite_neat-0.73.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.72.2.dist-info → cognite_neat-0.73.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.72.2.dist-info → cognite_neat-0.73.0.dist-info}/entry_points.txt +0 -0
|
@@ -4,6 +4,7 @@ generating a list of rules based on which nodes that form the graph are made.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from collections import UserDict, defaultdict
|
|
7
|
+
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Literal, cast, overload
|
|
9
10
|
|
|
@@ -49,6 +50,12 @@ class MetadataRaw(UserDict):
|
|
|
49
50
|
def has_schema_field(self) -> bool:
|
|
50
51
|
return self.get("schema") in [schema.value for schema in SchemaCompleteness.__members__.values()]
|
|
51
52
|
|
|
53
|
+
@property
|
|
54
|
+
def schema(self) -> SchemaCompleteness | None:
|
|
55
|
+
if not self.has_schema_field:
|
|
56
|
+
return None
|
|
57
|
+
return SchemaCompleteness(self["schema"])
|
|
58
|
+
|
|
52
59
|
def is_valid(self, issue_list: IssueList, filepath: Path) -> bool:
|
|
53
60
|
if not self.has_role_field:
|
|
54
61
|
issue_list.append(issues.spreadsheet_file.RoleMissingOrUnsupportedError(filepath))
|
|
@@ -61,6 +68,14 @@ class MetadataRaw(UserDict):
|
|
|
61
68
|
return True
|
|
62
69
|
|
|
63
70
|
|
|
71
|
+
@dataclass
|
|
72
|
+
class ReadResult:
|
|
73
|
+
sheets: dict[str, dict | list]
|
|
74
|
+
read_info_by_sheet: dict[str, SpreadsheetRead]
|
|
75
|
+
role: RoleTypes
|
|
76
|
+
schema: SchemaCompleteness | None
|
|
77
|
+
|
|
78
|
+
|
|
64
79
|
class SpreadsheetReader:
|
|
65
80
|
def __init__(self, issue_list: IssueList, is_reference: bool = False):
|
|
66
81
|
self.issue_list = issue_list
|
|
@@ -79,7 +94,7 @@ class SpreadsheetReader:
|
|
|
79
94
|
def to_reference_sheet(cls, sheet_name: str) -> str:
|
|
80
95
|
return f"Ref{sheet_name}"
|
|
81
96
|
|
|
82
|
-
def read(self, filepath: Path) ->
|
|
97
|
+
def read(self, filepath: Path) -> None | ReadResult:
|
|
83
98
|
with pd.ExcelFile(filepath) as excel_file:
|
|
84
99
|
if self.metadata_sheet_name not in excel_file.sheet_names:
|
|
85
100
|
self.issue_list.append(
|
|
@@ -95,21 +110,10 @@ class SpreadsheetReader:
|
|
|
95
110
|
return None
|
|
96
111
|
|
|
97
112
|
sheets, read_info_by_sheet = self._read_sheets(metadata, excel_file)
|
|
98
|
-
if self.issue_list.has_errors:
|
|
99
|
-
return None
|
|
100
|
-
|
|
101
|
-
rules_cls = RULES_PER_ROLE[metadata.role]
|
|
102
|
-
with _handle_issues(
|
|
103
|
-
self.issue_list,
|
|
104
|
-
error_cls=issues.spreadsheet.InvalidSheetError,
|
|
105
|
-
error_args={"read_info_by_sheet": read_info_by_sheet},
|
|
106
|
-
) as future:
|
|
107
|
-
rules = rules_cls.model_validate(sheets) # type: ignore[attr-defined]
|
|
108
|
-
|
|
109
|
-
if future.result == "failure" or self.issue_list.has_errors:
|
|
113
|
+
if sheets is None or self.issue_list.has_errors:
|
|
110
114
|
return None
|
|
111
115
|
|
|
112
|
-
|
|
116
|
+
return ReadResult(sheets, read_info_by_sheet, metadata.role, metadata.schema)
|
|
113
117
|
|
|
114
118
|
def _read_sheets(
|
|
115
119
|
self, metadata: MetadataRaw, excel_file: ExcelFile
|
|
@@ -167,43 +171,60 @@ class ExcelImporter(BaseImporter):
|
|
|
167
171
|
is_reference: bool = False,
|
|
168
172
|
) -> tuple[Rules | None, IssueList] | Rules:
|
|
169
173
|
issue_list = IssueList(title=f"'{self.filepath.name}'")
|
|
170
|
-
|
|
171
174
|
if not self.filepath.exists():
|
|
172
175
|
issue_list.append(issues.spreadsheet_file.SpreadsheetNotFoundError(self.filepath))
|
|
173
176
|
return self._return_or_raise(issue_list, errors)
|
|
174
177
|
|
|
175
|
-
|
|
178
|
+
user_result: ReadResult | None = None
|
|
176
179
|
if not is_reference:
|
|
177
|
-
|
|
178
|
-
if issue_list.has_errors:
|
|
180
|
+
user_result = SpreadsheetReader(issue_list, is_reference=False).read(self.filepath)
|
|
181
|
+
if user_result is None or issue_list.has_errors:
|
|
179
182
|
return self._return_or_raise(issue_list, errors)
|
|
180
183
|
|
|
181
|
-
|
|
184
|
+
reference_result: ReadResult | None = None
|
|
182
185
|
if is_reference or (
|
|
183
|
-
|
|
184
|
-
and
|
|
185
|
-
and
|
|
186
|
+
user_result
|
|
187
|
+
and user_result.role != RoleTypes.domain_expert
|
|
188
|
+
and user_result.schema == SchemaCompleteness.extended
|
|
186
189
|
):
|
|
187
|
-
|
|
190
|
+
reference_result = SpreadsheetReader(issue_list, is_reference=True).read(self.filepath)
|
|
188
191
|
if issue_list.has_errors:
|
|
189
192
|
return self._return_or_raise(issue_list, errors)
|
|
190
193
|
|
|
191
|
-
if
|
|
194
|
+
if user_result and reference_result and user_result.role != reference_result.role:
|
|
192
195
|
issue_list.append(issues.spreadsheet_file.RoleMismatchError(self.filepath))
|
|
193
196
|
return self._return_or_raise(issue_list, errors)
|
|
194
197
|
|
|
195
|
-
if
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
if user_result and reference_result:
|
|
199
|
+
user_result.sheets["reference"] = reference_result.sheets
|
|
200
|
+
sheets = user_result.sheets
|
|
201
|
+
original_role = user_result.role
|
|
202
|
+
read_info_by_sheet = user_result.read_info_by_sheet
|
|
203
|
+
read_info_by_sheet.update(reference_result.read_info_by_sheet)
|
|
204
|
+
elif user_result:
|
|
205
|
+
sheets = user_result.sheets
|
|
206
|
+
original_role = user_result.role
|
|
207
|
+
read_info_by_sheet = user_result.read_info_by_sheet
|
|
208
|
+
elif reference_result:
|
|
209
|
+
sheets = reference_result.sheets
|
|
210
|
+
original_role = reference_result.role
|
|
211
|
+
read_info_by_sheet = reference_result.read_info_by_sheet
|
|
202
212
|
else:
|
|
203
213
|
raise ValueError(
|
|
204
214
|
"No rules were generated. This should have been caught earlier. " f"Bug in {type(self).__name__}."
|
|
205
215
|
)
|
|
206
216
|
|
|
217
|
+
rules_cls = RULES_PER_ROLE[original_role]
|
|
218
|
+
with _handle_issues(
|
|
219
|
+
issue_list,
|
|
220
|
+
error_cls=issues.spreadsheet.InvalidSheetError,
|
|
221
|
+
error_args={"read_info_by_sheet": read_info_by_sheet},
|
|
222
|
+
) as future:
|
|
223
|
+
rules = rules_cls.model_validate(sheets) # type: ignore[attr-defined]
|
|
224
|
+
|
|
225
|
+
if future.result == "failure" or issue_list.has_errors:
|
|
226
|
+
return self._return_or_raise(issue_list, errors)
|
|
227
|
+
|
|
207
228
|
return self._to_output(
|
|
208
229
|
rules,
|
|
209
230
|
issue_list,
|
|
@@ -68,7 +68,15 @@ class NeatValidationError(ValidationIssue, ABC):
|
|
|
68
68
|
|
|
69
69
|
This is intended to be overridden in subclasses to handle specific error types.
|
|
70
70
|
"""
|
|
71
|
-
|
|
71
|
+
all_errors = []
|
|
72
|
+
for error in errors:
|
|
73
|
+
if isinstance(ctx := error.get("ctx"), dict) and isinstance(
|
|
74
|
+
multi_error := ctx.get("error"), MultiValueError
|
|
75
|
+
):
|
|
76
|
+
all_errors.extend(multi_error.errors)
|
|
77
|
+
else:
|
|
78
|
+
all_errors.append(DefaultPydanticError.from_pydantic_error(error))
|
|
79
|
+
return all_errors
|
|
72
80
|
|
|
73
81
|
|
|
74
82
|
@dataclass(frozen=True)
|
cognite/neat/rules/issues/dms.py
CHANGED
|
@@ -26,6 +26,8 @@ __all__ = [
|
|
|
26
26
|
"MultipleReferenceWarning",
|
|
27
27
|
"HasDataFilterOnNoPropertiesViewWarning",
|
|
28
28
|
"NodeTypeFilterOnParentViewWarning",
|
|
29
|
+
"ChangingContainerError",
|
|
30
|
+
"ChangingViewError",
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
|
|
@@ -225,6 +227,78 @@ class ContainerPropertyUsedMultipleTimesError(DMSSchemaError):
|
|
|
225
227
|
return output
|
|
226
228
|
|
|
227
229
|
|
|
230
|
+
@dataclass(frozen=True)
|
|
231
|
+
class ChangingContainerError(DMSSchemaError):
|
|
232
|
+
description = "You are adding to an existing model. "
|
|
233
|
+
fix = "Keep the container the same"
|
|
234
|
+
error_name: ClassVar[str] = "ChangingContainerError"
|
|
235
|
+
container_id: dm.ContainerId
|
|
236
|
+
changed_properties: list[str] | None = None
|
|
237
|
+
changed_attributes: list[str] | None = None
|
|
238
|
+
|
|
239
|
+
def __post_init__(self):
|
|
240
|
+
# Sorting for deterministic output
|
|
241
|
+
if self.changed_properties:
|
|
242
|
+
self.changed_properties.sort()
|
|
243
|
+
if self.changed_attributes:
|
|
244
|
+
self.changed_attributes.sort()
|
|
245
|
+
|
|
246
|
+
def message(self) -> str:
|
|
247
|
+
if self.changed_properties:
|
|
248
|
+
changed = f" properties {self.changed_properties}."
|
|
249
|
+
elif self.changed_attributes:
|
|
250
|
+
changed = f" attributes {self.changed_attributes}."
|
|
251
|
+
else:
|
|
252
|
+
changed = "."
|
|
253
|
+
return (
|
|
254
|
+
f"The container {self.container_id} has changed{changed}"
|
|
255
|
+
"When extending model with extension set to addition or reshape, the container "
|
|
256
|
+
"properties must remain the same"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def dump(self) -> dict[str, Any]:
|
|
260
|
+
output = super().dump()
|
|
261
|
+
output["container_id"] = self.container_id.dump()
|
|
262
|
+
output["changed_properties"] = self.changed_properties
|
|
263
|
+
return output
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass(frozen=True)
|
|
267
|
+
class ChangingViewError(DMSSchemaError):
|
|
268
|
+
description = "You are adding to an existing model. "
|
|
269
|
+
fix = "Keep the view the same"
|
|
270
|
+
error_name: ClassVar[str] = "ChangingViewError"
|
|
271
|
+
view_id: dm.ViewId
|
|
272
|
+
changed_properties: list[str] | None = None
|
|
273
|
+
changed_attributes: list[str] | None = None
|
|
274
|
+
|
|
275
|
+
def __post_init__(self):
|
|
276
|
+
# Sorting for deterministic output
|
|
277
|
+
if self.changed_properties:
|
|
278
|
+
self.changed_properties.sort()
|
|
279
|
+
if self.changed_attributes:
|
|
280
|
+
self.changed_attributes.sort()
|
|
281
|
+
|
|
282
|
+
def message(self) -> str:
|
|
283
|
+
if self.changed_properties:
|
|
284
|
+
changed = f" properties {self.changed_properties}."
|
|
285
|
+
elif self.changed_attributes:
|
|
286
|
+
changed = f" attributes {self.changed_attributes}."
|
|
287
|
+
else:
|
|
288
|
+
changed = "."
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
f"The view {self.view_id} has changed{changed}"
|
|
292
|
+
"When extending model with extension set to addition, the view properties must remain the same"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def dump(self) -> dict[str, Any]:
|
|
296
|
+
output = super().dump()
|
|
297
|
+
output["view_id"] = self.view_id.dump()
|
|
298
|
+
output["difference"] = self.changed_properties
|
|
299
|
+
return output
|
|
300
|
+
|
|
301
|
+
|
|
228
302
|
@dataclass(frozen=True)
|
|
229
303
|
class DirectRelationListWarning(DMSSchemaWarning):
|
|
230
304
|
description = "The container property is set to a direct relation list, which is not supported by the CDF API"
|
|
@@ -14,7 +14,6 @@ from cognite.neat.rules.models.rdfpath import (
|
|
|
14
14
|
TransformationRuleType,
|
|
15
15
|
parse_rule,
|
|
16
16
|
)
|
|
17
|
-
from cognite.neat.utils.text import to_pascal
|
|
18
17
|
|
|
19
18
|
if sys.version_info >= (3, 11):
|
|
20
19
|
from enum import StrEnum
|
|
@@ -197,15 +196,14 @@ class ContainerEntity(Entity):
|
|
|
197
196
|
def from_id(cls, container_id: ContainerId) -> "ContainerEntity":
|
|
198
197
|
return ContainerEntity(prefix=container_id.space, suffix=container_id.external_id)
|
|
199
198
|
|
|
200
|
-
def as_id(self, default_space: str | None
|
|
199
|
+
def as_id(self, default_space: str | None) -> ContainerId:
|
|
201
200
|
if self.space is Undefined and default_space is None:
|
|
202
201
|
raise ValueError("Space is Undefined! Set default_space!")
|
|
203
202
|
|
|
204
|
-
external_id = to_pascal(self.external_id) if standardize_casing else self.external_id
|
|
205
203
|
if self.space is Undefined:
|
|
206
|
-
return ContainerId(space=cast(str, default_space), external_id=external_id)
|
|
204
|
+
return ContainerId(space=cast(str, default_space), external_id=self.external_id)
|
|
207
205
|
else:
|
|
208
|
-
return ContainerId(space=self.space, external_id=external_id)
|
|
206
|
+
return ContainerId(space=self.space, external_id=self.external_id)
|
|
209
207
|
|
|
210
208
|
|
|
211
209
|
class ViewEntity(Entity):
|
|
@@ -233,7 +231,6 @@ class ViewEntity(Entity):
|
|
|
233
231
|
allow_none: Literal[False] = False,
|
|
234
232
|
default_space: str | None = None,
|
|
235
233
|
default_version: str | None = None,
|
|
236
|
-
standardize_casing: bool = True,
|
|
237
234
|
) -> ViewId:
|
|
238
235
|
...
|
|
239
236
|
|
|
@@ -243,7 +240,6 @@ class ViewEntity(Entity):
|
|
|
243
240
|
allow_none: Literal[True],
|
|
244
241
|
default_space: str | None = None,
|
|
245
242
|
default_version: str | None = None,
|
|
246
|
-
standardize_casing: bool = True,
|
|
247
243
|
) -> ViewId | None:
|
|
248
244
|
...
|
|
249
245
|
|
|
@@ -252,7 +248,6 @@ class ViewEntity(Entity):
|
|
|
252
248
|
allow_none: bool = False,
|
|
253
249
|
default_space: str | None = None,
|
|
254
250
|
default_version: str | None = None,
|
|
255
|
-
standardize_casing: bool = True,
|
|
256
251
|
) -> ViewId | None:
|
|
257
252
|
if self.suffix is Unknown and allow_none:
|
|
258
253
|
return None
|
|
@@ -265,8 +260,7 @@ class ViewEntity(Entity):
|
|
|
265
260
|
raise ValueError("space is required")
|
|
266
261
|
if version is None:
|
|
267
262
|
raise ValueError("version is required")
|
|
268
|
-
|
|
269
|
-
return ViewId(space=space, external_id=external_id, version=version)
|
|
263
|
+
return ViewId(space=space, external_id=self.external_id, version=version)
|
|
270
264
|
|
|
271
265
|
|
|
272
266
|
class ViewPropEntity(ViewEntity):
|
|
@@ -303,14 +297,10 @@ class ViewPropEntity(ViewEntity):
|
|
|
303
297
|
property_=prop_id.property,
|
|
304
298
|
)
|
|
305
299
|
|
|
306
|
-
def as_prop_id(
|
|
307
|
-
self, default_space: str | None = None, default_version: str | None = None, standardize_casing: bool = True
|
|
308
|
-
) -> PropertyId:
|
|
300
|
+
def as_prop_id(self, default_space: str | None = None, default_version: str | None = None) -> PropertyId:
|
|
309
301
|
if self.property_ is None:
|
|
310
302
|
raise ValueError("property is required to create PropertyId")
|
|
311
|
-
return PropertyId(
|
|
312
|
-
source=self.as_id(False, default_space, default_version, standardize_casing), property=self.property_
|
|
313
|
-
)
|
|
303
|
+
return PropertyId(source=self.as_id(False, default_space, default_version), property=self.property_)
|
|
314
304
|
|
|
315
305
|
@property
|
|
316
306
|
def versioned_id(self) -> str:
|
|
@@ -10,10 +10,21 @@ import types
|
|
|
10
10
|
from abc import abstractmethod
|
|
11
11
|
from collections.abc import Callable, Iterator
|
|
12
12
|
from functools import wraps
|
|
13
|
-
from typing import Any, ClassVar, Generic, TypeAlias, TypeVar
|
|
13
|
+
from typing import Annotated, Any, ClassVar, Generic, TypeAlias, TypeVar
|
|
14
14
|
|
|
15
15
|
import pandas as pd
|
|
16
|
-
from pydantic import
|
|
16
|
+
from pydantic import (
|
|
17
|
+
BaseModel,
|
|
18
|
+
BeforeValidator,
|
|
19
|
+
ConfigDict,
|
|
20
|
+
Field,
|
|
21
|
+
HttpUrl,
|
|
22
|
+
PlainSerializer,
|
|
23
|
+
constr,
|
|
24
|
+
field_validator,
|
|
25
|
+
model_serializer,
|
|
26
|
+
model_validator,
|
|
27
|
+
)
|
|
17
28
|
from pydantic.fields import FieldInfo
|
|
18
29
|
|
|
19
30
|
from cognite.neat.rules.models._rules._types import ClassType
|
|
@@ -318,3 +329,14 @@ class SheetList(BaseModel, Generic[T_Entity]):
|
|
|
318
329
|
def mandatory_fields(cls, use_alias=False) -> set[str]:
|
|
319
330
|
"""Returns a set of mandatory fields for the model."""
|
|
320
331
|
return _get_required_fields(cls, use_alias)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
ExtensionCategoryType = Annotated[
|
|
335
|
+
ExtensionCategory,
|
|
336
|
+
PlainSerializer(
|
|
337
|
+
lambda v: v.value if isinstance(v, ExtensionCategory) else v,
|
|
338
|
+
return_type=str,
|
|
339
|
+
when_used="unless-none",
|
|
340
|
+
),
|
|
341
|
+
BeforeValidator(lambda v: ExtensionCategory(v) if isinstance(v, str) else v),
|
|
342
|
+
]
|