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.

Files changed (32) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/constants.py +3 -2
  3. cognite/neat/graph/extractor/_graph_capturing_sheet.py +14 -12
  4. cognite/neat/rules/examples/power-grid-containers.yaml +3 -0
  5. cognite/neat/rules/examples/power-grid-model.yaml +3 -0
  6. cognite/neat/rules/exporter/_rules2dms.py +3 -10
  7. cognite/neat/rules/exporter/_rules2excel.py +3 -2
  8. cognite/neat/rules/exporters/_rules2dms.py +3 -7
  9. cognite/neat/rules/exporters/_rules2excel.py +94 -4
  10. cognite/neat/rules/exporters/_rules2ontology.py +2 -0
  11. cognite/neat/rules/importer/_dms2rules.py +1 -4
  12. cognite/neat/rules/importers/_dms2rules.py +40 -11
  13. cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +15 -15
  14. cognite/neat/rules/importers/_owl2rules/_owl2properties.py +2 -0
  15. cognite/neat/rules/importers/_owl2rules/_owl2rules.py +1 -1
  16. cognite/neat/rules/importers/_spreadsheet2rules.py +52 -31
  17. cognite/neat/rules/issues/base.py +9 -1
  18. cognite/neat/rules/issues/dms.py +74 -0
  19. cognite/neat/rules/models/_rules/_types/_base.py +6 -16
  20. cognite/neat/rules/models/_rules/base.py +24 -2
  21. cognite/neat/rules/models/_rules/dms_architect_rules.py +181 -71
  22. cognite/neat/rules/models/_rules/dms_schema.py +5 -1
  23. cognite/neat/rules/models/_rules/domain_rules.py +14 -1
  24. cognite/neat/rules/models/_rules/information_rules.py +16 -2
  25. cognite/neat/utils/spreadsheet.py +2 -2
  26. cognite/neat/workflows/steps/lib/rules_exporter.py +0 -1
  27. {cognite_neat-0.72.2.dist-info → cognite_neat-0.73.0.dist-info}/METADATA +2 -2
  28. {cognite_neat-0.72.2.dist-info → cognite_neat-0.73.0.dist-info}/RECORD +31 -32
  29. cognite/neat/py.typed +0 -0
  30. {cognite_neat-0.72.2.dist-info → cognite_neat-0.73.0.dist-info}/LICENSE +0 -0
  31. {cognite_neat-0.72.2.dist-info → cognite_neat-0.73.0.dist-info}/WHEEL +0 -0
  32. {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) -> Rules | None:
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
- return rules
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
- user_rules: Rules | None = None
178
+ user_result: ReadResult | None = None
176
179
  if not is_reference:
177
- user_rules = SpreadsheetReader(issue_list, is_reference=False).read(self.filepath)
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
- reference_rules: Rules | None = None
184
+ reference_result: ReadResult | None = None
182
185
  if is_reference or (
183
- user_rules
184
- and user_rules.metadata.role != RoleTypes.domain_expert
185
- and cast(DMSRules | InformationRules, user_rules).metadata.schema_ == SchemaCompleteness.extended
186
+ user_result
187
+ and user_result.role != RoleTypes.domain_expert
188
+ and user_result.schema == SchemaCompleteness.extended
186
189
  ):
187
- reference_rules = SpreadsheetReader(issue_list, is_reference=True).read(self.filepath)
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 user_rules and reference_rules and user_rules.metadata.role != reference_rules.metadata.role:
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 user_rules and reference_rules:
196
- rules = user_rules
197
- rules.reference = reference_rules
198
- elif user_rules:
199
- rules = user_rules
200
- elif reference_rules:
201
- rules = reference_rules
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
- return [DefaultPydanticError.from_pydantic_error(error) for error in errors]
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)
@@ -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, standardize_casing: bool = True) -> ContainerId:
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
- external_id = to_pascal(self.external_id) if standardize_casing else self.external_id
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 BaseModel, ConfigDict, Field, HttpUrl, constr, field_validator, model_serializer, model_validator
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
+ ]