cognite-neat 0.104.0__py3-none-any.whl → 0.105.1__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 (143) hide show
  1. cognite/neat/_client/_api/data_modeling_loaders.py +83 -23
  2. cognite/neat/_client/_api/schema.py +2 -1
  3. cognite/neat/_client/data_classes/neat_sequence.py +261 -0
  4. cognite/neat/_client/data_classes/schema.py +5 -1
  5. cognite/neat/_client/testing.py +33 -0
  6. cognite/neat/_constants.py +57 -0
  7. cognite/neat/_graph/extractors/_classic_cdf/_base.py +6 -5
  8. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +225 -11
  9. cognite/neat/_graph/extractors/_mock_graph_generator.py +1 -1
  10. cognite/neat/_graph/loaders/_rdf2dms.py +31 -5
  11. cognite/neat/_graph/transformers/__init__.py +3 -1
  12. cognite/neat/_graph/transformers/_classic_cdf.py +39 -51
  13. cognite/neat/_graph/transformers/_rdfpath.py +14 -15
  14. cognite/neat/_graph/transformers/_value_type.py +72 -0
  15. cognite/neat/_issues/__init__.py +0 -2
  16. cognite/neat/_issues/_base.py +19 -35
  17. cognite/neat/_issues/warnings/__init__.py +6 -1
  18. cognite/neat/_issues/warnings/_general.py +7 -0
  19. cognite/neat/_issues/warnings/_properties.py +11 -0
  20. cognite/neat/_issues/warnings/_resources.py +11 -0
  21. cognite/neat/_rules/exporters/_rules2dms.py +35 -1
  22. cognite/neat/_rules/exporters/_rules2excel.py +2 -2
  23. cognite/neat/_rules/importers/_dms2rules.py +66 -55
  24. cognite/neat/_rules/models/_base_rules.py +4 -1
  25. cognite/neat/_rules/models/entities/_wrapped.py +10 -5
  26. cognite/neat/_rules/models/mapping/_classic2core.yaml +239 -38
  27. cognite/neat/_rules/transformers/__init__.py +8 -2
  28. cognite/neat/_rules/transformers/_converters.py +271 -188
  29. cognite/neat/_rules/transformers/_mapping.py +75 -59
  30. cognite/neat/_rules/transformers/_verification.py +2 -3
  31. cognite/neat/_session/_inspect.py +3 -1
  32. cognite/neat/_session/_prepare.py +112 -24
  33. cognite/neat/_session/_read.py +33 -70
  34. cognite/neat/_session/_state.py +2 -2
  35. cognite/neat/_session/_to.py +2 -2
  36. cognite/neat/_store/_rules_store.py +4 -8
  37. cognite/neat/_utils/reader/_base.py +27 -0
  38. cognite/neat/_version.py +1 -1
  39. {cognite_neat-0.104.0.dist-info → cognite_neat-0.105.1.dist-info}/METADATA +4 -3
  40. cognite_neat-0.105.1.dist-info/RECORD +179 -0
  41. {cognite_neat-0.104.0.dist-info → cognite_neat-0.105.1.dist-info}/WHEEL +1 -1
  42. cognite/neat/_app/api/__init__.py +0 -0
  43. cognite/neat/_app/api/asgi/metrics.py +0 -4
  44. cognite/neat/_app/api/configuration.py +0 -98
  45. cognite/neat/_app/api/context_manager/__init__.py +0 -3
  46. cognite/neat/_app/api/context_manager/manager.py +0 -16
  47. cognite/neat/_app/api/data_classes/__init__.py +0 -0
  48. cognite/neat/_app/api/data_classes/rest.py +0 -59
  49. cognite/neat/_app/api/explorer.py +0 -66
  50. cognite/neat/_app/api/routers/configuration.py +0 -25
  51. cognite/neat/_app/api/routers/crud.py +0 -102
  52. cognite/neat/_app/api/routers/metrics.py +0 -10
  53. cognite/neat/_app/api/routers/workflows.py +0 -224
  54. cognite/neat/_app/api/utils/__init__.py +0 -0
  55. cognite/neat/_app/api/utils/data_mapping.py +0 -17
  56. cognite/neat/_app/api/utils/logging.py +0 -26
  57. cognite/neat/_app/api/utils/query_templates.py +0 -92
  58. cognite/neat/_app/main.py +0 -17
  59. cognite/neat/_app/monitoring/__init__.py +0 -0
  60. cognite/neat/_app/monitoring/metrics.py +0 -69
  61. cognite/neat/_app/ui/index.html +0 -1
  62. cognite/neat/_app/ui/neat-app/.gitignore +0 -23
  63. cognite/neat/_app/ui/neat-app/README.md +0 -70
  64. cognite/neat/_app/ui/neat-app/build/asset-manifest.json +0 -14
  65. cognite/neat/_app/ui/neat-app/build/favicon.ico +0 -0
  66. cognite/neat/_app/ui/neat-app/build/img/architect-icon.svg +0 -116
  67. cognite/neat/_app/ui/neat-app/build/img/developer-icon.svg +0 -112
  68. cognite/neat/_app/ui/neat-app/build/img/sme-icon.svg +0 -34
  69. cognite/neat/_app/ui/neat-app/build/index.html +0 -1
  70. cognite/neat/_app/ui/neat-app/build/logo192.png +0 -0
  71. cognite/neat/_app/ui/neat-app/build/manifest.json +0 -25
  72. cognite/neat/_app/ui/neat-app/build/robots.txt +0 -3
  73. cognite/neat/_app/ui/neat-app/build/static/css/main.72e3d92e.css +0 -2
  74. cognite/neat/_app/ui/neat-app/build/static/css/main.72e3d92e.css.map +0 -1
  75. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js +0 -3
  76. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js.LICENSE.txt +0 -88
  77. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js.map +0 -1
  78. cognite/neat/_app/ui/neat-app/build/static/media/logo.8093b84df9ed36a174c629d6fe0b730d.svg +0 -1
  79. cognite/neat/_app/ui/neat-app/package-lock.json +0 -18306
  80. cognite/neat/_app/ui/neat-app/package.json +0 -62
  81. cognite/neat/_app/ui/neat-app/public/favicon.ico +0 -0
  82. cognite/neat/_app/ui/neat-app/public/img/architect-icon.svg +0 -116
  83. cognite/neat/_app/ui/neat-app/public/img/developer-icon.svg +0 -112
  84. cognite/neat/_app/ui/neat-app/public/img/sme-icon.svg +0 -34
  85. cognite/neat/_app/ui/neat-app/public/index.html +0 -43
  86. cognite/neat/_app/ui/neat-app/public/logo192.png +0 -0
  87. cognite/neat/_app/ui/neat-app/public/manifest.json +0 -25
  88. cognite/neat/_app/ui/neat-app/public/robots.txt +0 -3
  89. cognite/neat/_app/ui/neat-app/src/App.css +0 -38
  90. cognite/neat/_app/ui/neat-app/src/App.js +0 -17
  91. cognite/neat/_app/ui/neat-app/src/App.test.js +0 -8
  92. cognite/neat/_app/ui/neat-app/src/MainContainer.tsx +0 -70
  93. cognite/neat/_app/ui/neat-app/src/components/JsonViewer.tsx +0 -43
  94. cognite/neat/_app/ui/neat-app/src/components/LocalUploader.tsx +0 -124
  95. cognite/neat/_app/ui/neat-app/src/components/OverviewComponentEditorDialog.tsx +0 -63
  96. cognite/neat/_app/ui/neat-app/src/components/StepEditorDialog.tsx +0 -511
  97. cognite/neat/_app/ui/neat-app/src/components/TabPanel.tsx +0 -36
  98. cognite/neat/_app/ui/neat-app/src/components/Utils.tsx +0 -56
  99. cognite/neat/_app/ui/neat-app/src/components/WorkflowDeleteDialog.tsx +0 -60
  100. cognite/neat/_app/ui/neat-app/src/components/WorkflowExecutionReport.tsx +0 -112
  101. cognite/neat/_app/ui/neat-app/src/components/WorkflowImportExportDialog.tsx +0 -67
  102. cognite/neat/_app/ui/neat-app/src/components/WorkflowMetadataDialog.tsx +0 -79
  103. cognite/neat/_app/ui/neat-app/src/index.css +0 -13
  104. cognite/neat/_app/ui/neat-app/src/index.js +0 -13
  105. cognite/neat/_app/ui/neat-app/src/logo.svg +0 -1
  106. cognite/neat/_app/ui/neat-app/src/reportWebVitals.js +0 -13
  107. cognite/neat/_app/ui/neat-app/src/setupTests.js +0 -5
  108. cognite/neat/_app/ui/neat-app/src/types/WorkflowTypes.ts +0 -388
  109. cognite/neat/_app/ui/neat-app/src/views/AboutView.tsx +0 -61
  110. cognite/neat/_app/ui/neat-app/src/views/ConfigView.tsx +0 -184
  111. cognite/neat/_app/ui/neat-app/src/views/GlobalConfigView.tsx +0 -180
  112. cognite/neat/_app/ui/neat-app/src/views/WorkflowView.tsx +0 -570
  113. cognite/neat/_app/ui/neat-app/tsconfig.json +0 -27
  114. cognite/neat/_workflows/__init__.py +0 -17
  115. cognite/neat/_workflows/base.py +0 -590
  116. cognite/neat/_workflows/cdf_store.py +0 -393
  117. cognite/neat/_workflows/examples/Export_DMS/workflow.yaml +0 -89
  118. cognite/neat/_workflows/examples/Export_Semantic_Data_Model/workflow.yaml +0 -66
  119. cognite/neat/_workflows/examples/Import_DMS/workflow.yaml +0 -65
  120. cognite/neat/_workflows/examples/Validate_Rules/workflow.yaml +0 -67
  121. cognite/neat/_workflows/examples/Validate_Solution_Model/workflow.yaml +0 -64
  122. cognite/neat/_workflows/manager.py +0 -292
  123. cognite/neat/_workflows/model.py +0 -203
  124. cognite/neat/_workflows/steps/__init__.py +0 -0
  125. cognite/neat/_workflows/steps/data_contracts.py +0 -109
  126. cognite/neat/_workflows/steps/lib/__init__.py +0 -0
  127. cognite/neat/_workflows/steps/lib/current/__init__.py +0 -6
  128. cognite/neat/_workflows/steps/lib/current/graph_extractor.py +0 -100
  129. cognite/neat/_workflows/steps/lib/current/graph_loader.py +0 -51
  130. cognite/neat/_workflows/steps/lib/current/graph_store.py +0 -48
  131. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +0 -537
  132. cognite/neat/_workflows/steps/lib/current/rules_importer.py +0 -323
  133. cognite/neat/_workflows/steps/lib/current/rules_validator.py +0 -106
  134. cognite/neat/_workflows/steps/lib/io/__init__.py +0 -1
  135. cognite/neat/_workflows/steps/lib/io/io_steps.py +0 -393
  136. cognite/neat/_workflows/steps/step_model.py +0 -79
  137. cognite/neat/_workflows/steps_registry.py +0 -218
  138. cognite/neat/_workflows/tasks.py +0 -18
  139. cognite/neat/_workflows/triggers.py +0 -169
  140. cognite/neat/_workflows/utils.py +0 -19
  141. cognite_neat-0.104.0.dist-info/RECORD +0 -276
  142. {cognite_neat-0.104.0.dist-info → cognite_neat-0.105.1.dist-info}/LICENSE +0 -0
  143. {cognite_neat-0.104.0.dist-info → cognite_neat-0.105.1.dist-info}/entry_points.txt +0 -0
@@ -425,6 +425,10 @@ class NeatIssueList(list, Sequence[T_NeatIssue], ABC):
425
425
  """Return True if this list contains any errors of the given type."""
426
426
  return any(isinstance(issue, error_type) for issue in self)
427
427
 
428
+ def has_warning_type(self, warning_type: type[NeatWarning]) -> bool:
429
+ """Return True if this list contains any warnings of the given type."""
430
+ return any(isinstance(issue, warning_type) for issue in self)
431
+
428
432
  def as_errors(self, operation: str = "Operation failed") -> ExceptionGroup:
429
433
  """Return an ExceptionGroup with all the errors in this list."""
430
434
  return ExceptionGroup(
@@ -505,55 +509,35 @@ def _get_subclasses(cls_: type[T_Cls], include_base: bool = False) -> Iterable[t
505
509
 
506
510
 
507
511
  @contextmanager
508
- def catch_warnings(
509
- issues: IssueList | None = None,
510
- warning_cls: type[NeatWarning] = DefaultWarning,
511
- ) -> Iterator[None]:
512
+ def catch_warnings() -> Iterator[IssueList]:
512
513
  """Catch warnings and append them to the issues list."""
514
+ issues = IssueList()
513
515
  with warnings.catch_warnings(record=True) as warning_logger:
514
516
  warnings.simplefilter("always")
515
517
  try:
516
- yield None
518
+ yield issues
517
519
  finally:
518
- if warning_logger and issues is not None:
519
- issues.extend([warning_cls.from_warning(warning) for warning in warning_logger]) # type: ignore[misc]
520
-
521
-
522
- class FutureResult:
523
- def __init__(self) -> None:
524
- self._result: Literal["success", "failure", "pending"] = "pending"
525
-
526
- @property
527
- def result(self) -> Literal["success", "failure", "pending"]:
528
- return self._result
520
+ if warning_logger:
521
+ issues.extend([NeatWarning.from_warning(warning) for warning in warning_logger]) # type: ignore[misc]
529
522
 
530
523
 
531
524
  @contextmanager
532
- def catch_issues(
533
- issues: IssueList,
534
- error_cls: type[NeatError] = NeatError,
535
- warning_cls: type[NeatWarning] = NeatWarning,
536
- error_args: dict[str, Any] | None = None,
537
- ) -> Iterator[FutureResult]:
525
+ def catch_issues(error_args: dict[str, Any] | None = None) -> Iterator[IssueList]:
538
526
  """This is an internal help function to handle issues and warnings.
539
527
 
540
528
  Args:
541
- issues: The issues list to append to.
542
- error_cls: The class used to convert errors to issues.
543
- warning_cls: The class used to convert warnings to issues.
529
+ error_args: Additional arguments to pass to the error class. The only use case as of (2025-01-03) is to pass
530
+ the read_info_by_sheet to the error class such that the row numbers can be adjusted to match the source
531
+ spreadsheet.
544
532
 
545
533
  Returns:
546
- FutureResult: A future result object that can be used to check the result of the context manager.
534
+ IssueList: The list of issues.
535
+
547
536
  """
548
- with catch_warnings(issues, warning_cls):
549
- future_result = FutureResult()
537
+ with catch_warnings() as issues:
550
538
  try:
551
- yield future_result
539
+ yield issues
552
540
  except ValidationError as e:
553
- issues.extend(error_cls.from_errors(e.errors(), **(error_args or {}))) # type: ignore[arg-type]
554
- future_result._result = "failure"
541
+ issues.extend(NeatError.from_errors(e.errors(), **(error_args or {}))) # type: ignore[arg-type]
555
542
  except (NeatError, MultiValueError) as e:
556
- issues.extend(error_cls.from_errors([e], **(error_args or {}))) # type: ignore[arg-type, list-item]
557
- future_result._result = "failure"
558
- else:
559
- future_result._result = "success"
543
+ issues.extend(NeatError.from_errors([e], **(error_args or {}))) # type: ignore[arg-type, list-item]
@@ -13,7 +13,7 @@ from ._external import (
13
13
  FileReadWarning,
14
14
  FileTypeUnexpectedWarning,
15
15
  )
16
- from ._general import NeatValueWarning, NotSupportedWarning, RegexViolationWarning
16
+ from ._general import MissingCogniteClientWarning, NeatValueWarning, NotSupportedWarning, RegexViolationWarning
17
17
  from ._models import (
18
18
  BreakingModelingPrincipleWarning,
19
19
  CDFNotSupportedWarning,
@@ -28,6 +28,7 @@ from ._models import (
28
28
  from ._properties import (
29
29
  PropertyDataTypeConversionWarning,
30
30
  PropertyDefinitionDuplicatedWarning,
31
+ PropertyDirectRelationLimitWarning,
31
32
  PropertyNotFoundWarning,
32
33
  PropertyOverwritingWarning,
33
34
  PropertySkippedWarning,
@@ -41,6 +42,7 @@ from ._resources import (
41
42
  ResourceRetrievalWarning,
42
43
  ResourcesDuplicatedWarning,
43
44
  ResourceTypeNotSupportedWarning,
45
+ ResourceUnknownWarning,
44
46
  )
45
47
 
46
48
  __all__ = [
@@ -53,6 +55,7 @@ __all__ = [
53
55
  "FileMissingRequiredFieldWarning",
54
56
  "FileReadWarning",
55
57
  "FileTypeUnexpectedWarning",
58
+ "MissingCogniteClientWarning",
56
59
  "NeatValueWarning",
57
60
  "NotSupportedHasDataFilterLimitWarning",
58
61
  "NotSupportedViewContainerLimitWarning",
@@ -62,6 +65,7 @@ __all__ = [
62
65
  "PrincipleSolutionBuildsOnEnterpriseWarning",
63
66
  "PropertyDataTypeConversionWarning",
64
67
  "PropertyDefinitionDuplicatedWarning",
68
+ "PropertyDirectRelationLimitWarning",
65
69
  "PropertyNotFoundWarning",
66
70
  "PropertyOverwritingWarning",
67
71
  "PropertySkippedWarning",
@@ -73,6 +77,7 @@ __all__ = [
73
77
  "ResourceRegexViolationWarning",
74
78
  "ResourceRetrievalWarning",
75
79
  "ResourceTypeNotSupportedWarning",
80
+ "ResourceUnknownWarning",
76
81
  "ResourcesDuplicatedWarning",
77
82
  "UndefinedViewWarning",
78
83
  "UserModelingWarning",
@@ -27,3 +27,10 @@ class RegexViolationWarning(NeatWarning):
27
27
  identifier: str
28
28
  pattern_name: str
29
29
  motivation: str | None = None
30
+
31
+
32
+ @dataclass(unsafe_hash=True)
33
+ class MissingCogniteClientWarning(NeatWarning):
34
+ """Missing Cognite Client required for {functionality}"""
35
+
36
+ functionality: str
@@ -1,6 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Generic
3
3
 
4
+ from cognite.neat._constants import DMS_DIRECT_RELATION_LIST_LIMIT
4
5
  from cognite.neat._issues._base import ResourceType
5
6
 
6
7
  from ._resources import ResourceNeatWarning, T_Identifier, T_ReferenceIdentifier
@@ -77,3 +78,13 @@ class PropertyDataTypeConversionWarning(PropertyWarning[T_Identifier]):
77
78
  """The {resource_type} with identifier {identifier} failed to convert the property {property_name}: {error}"""
78
79
 
79
80
  error: str
81
+
82
+
83
+ @dataclass(unsafe_hash=True)
84
+ class PropertyDirectRelationLimitWarning(PropertyWarning[T_Identifier]):
85
+ """The listable direct relation property {property_name} in the {resource_type} with identifier {identifier}
86
+ has more than {limit} relations. The relations will be sorted by (space, externalId) and truncated."""
87
+
88
+ resource_type = "view"
89
+
90
+ limit: int = DMS_DIRECT_RELATION_LIST_LIMIT
@@ -39,6 +39,17 @@ class ResourceNotFoundWarning(ResourceNeatWarning, Generic[T_Identifier, T_Refer
39
39
  referred_type: str
40
40
 
41
41
 
42
+ @dataclass(unsafe_hash=True)
43
+ class ResourceUnknownWarning(ResourceNeatWarning, Generic[T_Identifier, T_ReferenceIdentifier]):
44
+ """The {resource_type} with identifier {identifier} referred by {referred_type} {referred_by} is unknown.
45
+ Will continue, but the model is incomplete."""
46
+
47
+ referred_by: T_ReferenceIdentifier
48
+ referred_type: str
49
+
50
+ fix = "You can maybe retrieve the resource from the CDF."
51
+
52
+
42
53
  @dataclass(unsafe_hash=True)
43
54
  class ResourceNotDefinedWarning(ResourceNeatWarning, Generic[T_Identifier, T_ReferenceIdentifier]):
44
55
  """The {resource_type} {identifier} is not defined in the {location}"""
@@ -4,6 +4,7 @@ from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
5
  from typing import Generic, Literal
6
6
 
7
+ from cognite.client import data_modeling as dm
7
8
  from cognite.client.data_classes._base import (
8
9
  T_CogniteResourceList,
9
10
  T_WritableCogniteResource,
@@ -19,7 +20,7 @@ from cognite.client.exceptions import CogniteAPIError
19
20
 
20
21
  from cognite.neat._client import DataModelingLoader, NeatClient
21
22
  from cognite.neat._client._api.data_modeling_loaders import MultiCogniteAPIError, T_WritableCogniteResourceList
22
- from cognite.neat._client.data_classes.data_modeling import Component
23
+ from cognite.neat._client.data_classes.data_modeling import Component, ViewApplyDict
23
24
  from cognite.neat._client.data_classes.schema import DMSSchema
24
25
  from cognite.neat._issues import IssueList
25
26
  from cognite.neat._issues.warnings import (
@@ -199,6 +200,10 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
199
200
  ) -> Iterable[UploadResult]:
200
201
  schema = self.export(rules)
201
202
 
203
+ # The CDF UI does not deal well with a child view overwriting a parent property with the same name
204
+ # This is a workaround to remove the duplicated properties
205
+ self._remove_duplicated_properties(schema.views, client)
206
+
202
207
  categorized_items_by_loader = self._categorize_by_loader(client, schema)
203
208
 
204
209
  is_failing = self.existing == "fail" and any(
@@ -376,3 +381,32 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
376
381
  for data_model in data_models
377
382
  if (data_model.space, data_model.external_id) != (space, external_id)
378
383
  ]
384
+
385
+ @staticmethod
386
+ def _remove_duplicated_properties(views: ViewApplyDict, client: NeatClient) -> None:
387
+ parent_view_ids = {parent for view in views.values() for parent in view.implements}
388
+ parent_view_list = client.data_modeling.views.retrieve(
389
+ list(parent_view_ids), include_inherited_properties=False
390
+ )
391
+ parent_view_by_id = {view.as_id(): view.as_write() for view in parent_view_list}
392
+ for view in views.values():
393
+ if view.implements is None:
394
+ continue
395
+ for parent_id in view.implements:
396
+ if not (parent_view := parent_view_by_id.get(parent_id)):
397
+ continue
398
+ for shared_prop_id in set(view.properties or {}) & set(parent_view.properties or {}):
399
+ if view.properties is None or parent_view.properties is None:
400
+ continue
401
+ prop = view.properties[shared_prop_id]
402
+ parent_prop = parent_view.properties[shared_prop_id]
403
+ if (
404
+ isinstance(prop, dm.MappedPropertyApply)
405
+ and isinstance(parent_prop, dm.MappedPropertyApply)
406
+ and (
407
+ prop.container_property_identifier == parent_prop.container_property_identifier
408
+ and prop.container == parent_prop.container
409
+ and prop.source == parent_prop.source
410
+ )
411
+ ):
412
+ view.properties.pop(shared_prop_id)
@@ -229,8 +229,8 @@ class ExcelExporter(BaseExporter[VerifiedRules, Workbook]):
229
229
  if isinstance(selected_column, MergedCell):
230
230
  selected_column = column_cells[1]
231
231
 
232
- current = sheet.column_dimensions[selected_column.column_letter].width or (max_length + 0.5)
233
- sheet.column_dimensions[selected_column.column_letter].width = min(
232
+ current = sheet.column_dimensions[selected_column.column_letter].width or (max_length + 0.5) # type: ignore[union-attr]
233
+ sheet.column_dimensions[selected_column.column_letter].width = min( # type: ignore[union-attr]
234
234
  max(current, max_length + 0.5), MAX_COLUMN_WIDTH
235
235
  )
236
236
  return None
@@ -1,6 +1,6 @@
1
- from collections import Counter, defaultdict
1
+ from collections import defaultdict
2
2
  from collections.abc import Collection, Iterable, Sequence
3
- from datetime import datetime, timezone
3
+ from datetime import datetime
4
4
  from pathlib import Path
5
5
  from typing import Literal, cast
6
6
 
@@ -20,12 +20,19 @@ from cognite.client.utils import ms_to_datetime
20
20
 
21
21
  from cognite.neat._client import NeatClient
22
22
  from cognite.neat._issues import IssueList, MultiValueError, NeatIssue
23
- from cognite.neat._issues.errors import FileTypeUnexpectedError, ResourceMissingIdentifierError, ResourceRetrievalError
23
+ from cognite.neat._issues.errors import (
24
+ FileTypeUnexpectedError,
25
+ NeatValueError,
26
+ ResourceMissingIdentifierError,
27
+ ResourceRetrievalError,
28
+ )
24
29
  from cognite.neat._issues.warnings import (
30
+ MissingCogniteClientWarning,
25
31
  PropertyNotFoundWarning,
26
32
  PropertyTypeNotSupportedWarning,
27
33
  ResourceNotFoundWarning,
28
34
  ResourcesDuplicatedWarning,
35
+ ResourceUnknownWarning,
29
36
  )
30
37
  from cognite.neat._rules._shared import ReadRules
31
38
  from cognite.neat._rules.importers._base import BaseImporter, _handle_issues
@@ -60,7 +67,6 @@ class DMSImporter(BaseImporter[DMSInputRules]):
60
67
  schema: The schema containing the data model.
61
68
  read_issues: A list of issues that occurred during the import.
62
69
  metadata: Metadata for the data model.
63
- ref_metadata: Metadata for the reference data model.
64
70
 
65
71
  """
66
72
 
@@ -69,30 +75,23 @@ class DMSImporter(BaseImporter[DMSInputRules]):
69
75
  schema: DMSSchema,
70
76
  read_issues: Sequence[NeatIssue] | None = None,
71
77
  metadata: DMSInputMetadata | None = None,
72
- ref_metadata: DMSInputMetadata | None = None,
78
+ referenced_containers: Iterable[dm.ContainerApply] | None = None,
73
79
  ):
74
- # Calling this root schema to distinguish it from
75
- # * User Schema
76
- # * Reference Schema
77
- self.root_schema = schema
80
+ self.schema = schema
78
81
  self.metadata = metadata
79
- self.ref_metadata = ref_metadata
80
82
  self.issue_list = IssueList(read_issues)
81
83
  self._all_containers_by_id = schema.containers.copy()
82
84
  self._all_views_by_id = schema.views.copy()
83
-
84
- def update_referenced_containers(self, containers: Iterable[dm.ContainerApply]) -> None:
85
- """Update the referenced containers. This is useful to add Cognite containers identified after the root schema
86
- is read"""
87
- for container in containers:
88
- if container.as_id() in self._all_containers_by_id:
89
- continue
90
- self._all_containers_by_id[container.as_id()] = container
85
+ if referenced_containers is not None:
86
+ for container in referenced_containers:
87
+ if container.as_id() in self._all_containers_by_id:
88
+ continue
89
+ self._all_containers_by_id[container.as_id()] = container
91
90
 
92
91
  @property
93
92
  def description(self) -> str:
94
- if self.root_schema.data_model is not None:
95
- identifier = f"{self.root_schema.data_model.as_id().as_tuple()!s}"
93
+ if self.schema.data_model is not None:
94
+ identifier = f"{self.schema.data_model.as_id().as_tuple()!s}"
96
95
  else:
97
96
  identifier = "Unknown"
98
97
  return f"DMS Data model {identifier} read as unverified data model"
@@ -107,8 +106,6 @@ class DMSImporter(BaseImporter[DMSInputRules]):
107
106
 
108
107
  Args:
109
108
  client: Instantiated CogniteClient to retrieve data model.
110
- reference_model_id: The reference data model to retrieve. This is the data model that
111
- the given data model is built on top of, typically, an enterprise data model.
112
109
  data_model_id: Data Model to retrieve.
113
110
 
114
111
  Returns:
@@ -141,7 +138,12 @@ class DMSImporter(BaseImporter[DMSInputRules]):
141
138
 
142
139
  metadata = cls._create_metadata_from_model(user_model)
143
140
 
144
- return cls(schema, issue_list, metadata, None)
141
+ return cls(
142
+ schema,
143
+ issue_list,
144
+ metadata,
145
+ referenced_containers=cls._lookup_referenced_containers(schema, issue_list, client),
146
+ )
145
147
 
146
148
  @classmethod
147
149
  def _find_model_in_list(
@@ -182,15 +184,17 @@ class DMSImporter(BaseImporter[DMSInputRules]):
182
184
  )
183
185
 
184
186
  @classmethod
185
- def from_directory(cls, directory: str | Path) -> "DMSImporter":
187
+ def from_directory(cls, directory: str | Path, client: NeatClient | None = None) -> "DMSImporter":
186
188
  issue_list = IssueList()
187
189
  with _handle_issues(issue_list) as _:
188
190
  schema = DMSSchema.from_directory(directory)
189
191
  # If there were errors during the import, the to_rules
190
- return cls(schema, issue_list)
192
+ return cls(
193
+ schema, issue_list, referenced_containers=cls._lookup_referenced_containers(schema, issue_list, client)
194
+ )
191
195
 
192
196
  @classmethod
193
- def from_zip_file(cls, zip_file: str | Path) -> "DMSImporter":
197
+ def from_zip_file(cls, zip_file: str | Path, client: NeatClient | None = None) -> "DMSImporter":
194
198
  if Path(zip_file).suffix != ".zip":
195
199
  return cls(
196
200
  DMSSchema(),
@@ -199,30 +203,49 @@ class DMSImporter(BaseImporter[DMSInputRules]):
199
203
  issue_list = IssueList()
200
204
  with _handle_issues(issue_list) as _:
201
205
  schema = DMSSchema.from_zip(zip_file)
202
- return cls(schema, issue_list)
206
+ return cls(
207
+ schema, issue_list, referenced_containers=cls._lookup_referenced_containers(schema, issue_list, client)
208
+ )
209
+
210
+ @classmethod
211
+ def _lookup_referenced_containers(
212
+ cls, schema: DMSSchema, issue_list: IssueList, client: NeatClient | None = None
213
+ ) -> Iterable[dm.ContainerApply]:
214
+ ref_containers = schema.externally_referenced_containers()
215
+ if not ref_containers:
216
+ return []
217
+ elif client is None:
218
+ id_ = ""
219
+ if schema.data_model:
220
+ id_ = f" {schema.data_model.as_id()!r}"
221
+ issue_list.append(MissingCogniteClientWarning(f"importing full DMS model{id_}"))
222
+ return []
223
+ return client.loaders.containers.retrieve(list(ref_containers), format="write")
224
+
225
+ @classmethod
226
+ def from_path(cls, path: Path, client: NeatClient | None = None) -> "DMSImporter":
227
+ if path.is_file():
228
+ return cls.from_zip_file(path, client)
229
+ elif path.is_dir():
230
+ return cls.from_directory(path, client)
231
+ else:
232
+ raise NeatValueError(f"Unsupported YAML format: {format}")
203
233
 
204
234
  def to_rules(self) -> ReadRules[DMSInputRules]:
205
235
  if self.issue_list.has_errors:
206
236
  # In case there were errors during the import, the to_rules method will return None
207
- self._end = datetime.now(timezone.utc)
208
237
  self.issue_list.trigger_warnings()
209
238
  raise MultiValueError(self.issue_list.errors)
210
239
 
211
- if not self.root_schema.data_model:
212
- self.issue_list.append(ResourceMissingIdentifierError("data model", type(self.root_schema).__name__))
213
- self._end = datetime.now(timezone.utc)
240
+ if not self.schema.data_model:
241
+ self.issue_list.append(ResourceMissingIdentifierError("data model", type(self.schema).__name__))
214
242
  self.issue_list.trigger_warnings()
215
243
  raise MultiValueError(self.issue_list.errors)
216
244
 
217
- model = self.root_schema.data_model
245
+ model = self.schema.data_model
218
246
 
219
- user_rules = self._create_rule_components(
220
- model,
221
- self.root_schema,
222
- self.metadata,
223
- )
247
+ user_rules = self._create_rule_components(model, self.schema, self.metadata)
224
248
 
225
- self._end = datetime.now(timezone.utc)
226
249
  self.issue_list.trigger_warnings()
227
250
  if self.issue_list.has_errors:
228
251
  raise MultiValueError(self.issue_list.errors)
@@ -234,7 +257,7 @@ class DMSImporter(BaseImporter[DMSInputRules]):
234
257
  schema: DMSSchema,
235
258
  metadata: DMSInputMetadata | None = None,
236
259
  ) -> DMSInputRules:
237
- enum_by_container_property = self._create_enum_collections(schema.containers.values())
260
+ enum_by_container_property = self._create_enum_collections(self._all_containers_by_id.values())
238
261
  enum_collection_by_container_property = {
239
262
  key: enum_list[0].collection for key, enum_list in enum_by_container_property.items() if enum_list
240
263
  }
@@ -267,21 +290,6 @@ class DMSImporter(BaseImporter[DMSInputRules]):
267
290
  enum=[enum for enum_list in enum_by_container_property.values() for enum in enum_list] or None,
268
291
  )
269
292
 
270
- @classmethod
271
- def _create_default_metadata(
272
- cls, views: Sequence[dm.View | dm.ViewApply], is_ref: bool = False
273
- ) -> DMSInputMetadata:
274
- now = datetime.now().replace(microsecond=0)
275
- space = Counter(view.space for view in views).most_common(1)[0][0]
276
- return DMSInputMetadata(
277
- space=space,
278
- external_id="Unknown",
279
- version="0.1.0",
280
- creator="Unknown",
281
- created=now,
282
- updated=now,
283
- )
284
-
285
293
  def _create_dms_property(
286
294
  self,
287
295
  prop_id: str,
@@ -384,8 +392,11 @@ class DMSImporter(BaseImporter[DMSInputRules]):
384
392
  elif isinstance(prop, dm.MappedPropertyApply):
385
393
  container_prop = self._container_prop_unsafe(cast(dm.MappedPropertyApply, prop))
386
394
  if isinstance(container_prop.type, dm.DirectRelation):
387
- if prop.source is None or prop.source not in self._all_views_by_id:
395
+ if prop.source is None:
388
396
  return DMSUnknownEntity()
397
+ elif prop.source not in self._all_views_by_id:
398
+ self.issue_list.append(ResourceUnknownWarning(prop.source, "view", view_entity.as_id(), "view"))
399
+ return ViewEntity.from_id(prop.source)
389
400
  else:
390
401
  return ViewEntity.from_id(prop.source)
391
402
  elif isinstance(container_prop.type, PropertyTypeWithUnit) and container_prop.type.unit:
@@ -398,7 +398,10 @@ class SheetList(list, MutableSequence[T_SheetRow]):
398
398
 
399
399
  def _repr_html_(self) -> str:
400
400
  """Returns HTML representation of ResourceDict."""
401
- return self.to_pandas(drop_na_columns=True)._repr_html_() # type: ignore[operator]
401
+ df = self.to_pandas(drop_na_columns=True)
402
+ if "neatId" in df.columns:
403
+ df = df.drop(columns=["neatId"])
404
+ return df._repr_html_() # type: ignore[operator]
402
405
 
403
406
  # Implemented to get correct type hints
404
407
  def __iter__(self) -> Iterator[T_SheetRow]:
@@ -9,7 +9,7 @@ from cognite.client import data_modeling as dm
9
9
  from cognite.client.data_classes.data_modeling import ContainerId, NodeId
10
10
  from pydantic import BaseModel, model_serializer, model_validator
11
11
 
12
- from ._single_value import ContainerEntity, DMSNodeEntity, Entity
12
+ from ._single_value import ContainerEntity, DMSNodeEntity, Entity, ViewEntity
13
13
 
14
14
 
15
15
  @total_ordering
@@ -121,6 +121,8 @@ class DMSFilter(WrappedEntity):
121
121
  return HasDataFilter(
122
122
  inner=[
123
123
  ContainerEntity(space=entry["space"], externalId=entry["externalId"])
124
+ if entry["type"] == "container"
125
+ else ViewEntity(space=entry["space"], externalId=entry["externalId"], version=entry["version"])
124
126
  for entry in body
125
127
  if isinstance(entry, dict) and "space" in entry and "externalId" in entry
126
128
  ]
@@ -161,21 +163,24 @@ class NodeTypeFilter(DMSFilter):
161
163
 
162
164
  class HasDataFilter(DMSFilter):
163
165
  name: ClassVar[str] = "hasData"
164
- _inner_cls: ClassVar[type[ContainerEntity]] = ContainerEntity
165
- inner: list[ContainerEntity] | None = None # type: ignore[assignment]
166
+ _inner_cls: ClassVar[type[ContainerEntity | ViewEntity]] = ContainerEntity
167
+ inner: list[ContainerEntity | ViewEntity] | None = None # type: ignore[assignment]
166
168
 
167
169
  def as_dms_filter(self, default: Collection[ContainerId] | None = None) -> dm.Filter:
168
170
  containers: list[ContainerId]
169
171
  if self.inner:
170
- containers = [container.as_id() for container in self.inner]
172
+ containers = [item.as_id() for item in self.inner if isinstance(item, ContainerEntity)]
173
+ views = [item.as_id() for item in self.inner if isinstance(item, ViewEntity)]
171
174
  elif default:
172
175
  containers = list(default)
176
+ views = []
173
177
  else:
174
178
  raise ValueError("Empty hasData filter, please provide a default containers.")
175
179
 
176
180
  return dm.filters.HasData(
177
181
  # Sorting to ensure deterministic order
178
- containers=sorted(containers, key=lambda container: container.as_tuple()) # type: ignore[union-attr]
182
+ containers=sorted(containers, key=lambda container: container.as_tuple()), # type: ignore[union-attr]
183
+ views=sorted(views, key=lambda view: view.as_tuple()), # type: ignore[union-attr]
179
184
  )
180
185
 
181
186