cognite-neat 0.98.0__py3-none-any.whl → 0.99.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 (72) hide show
  1. cognite/neat/_client/__init__.py +4 -0
  2. cognite/neat/_client/_api/data_modeling_loaders.py +512 -0
  3. cognite/neat/_client/_api/schema.py +50 -0
  4. cognite/neat/_client/_api_client.py +17 -0
  5. cognite/neat/_client/data_classes/__init__.py +0 -0
  6. cognite/neat/{_utils/cdf/data_classes.py → _client/data_classes/data_modeling.py} +8 -135
  7. cognite/neat/{_rules/models/dms/_schema.py → _client/data_classes/schema.py} +21 -281
  8. cognite/neat/_graph/_shared.py +14 -15
  9. cognite/neat/_graph/extractors/_classic_cdf/_assets.py +14 -154
  10. cognite/neat/_graph/extractors/_classic_cdf/_base.py +154 -7
  11. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +23 -12
  12. cognite/neat/_graph/extractors/_classic_cdf/_data_sets.py +17 -92
  13. cognite/neat/_graph/extractors/_classic_cdf/_events.py +13 -162
  14. cognite/neat/_graph/extractors/_classic_cdf/_files.py +15 -179
  15. cognite/neat/_graph/extractors/_classic_cdf/_labels.py +32 -100
  16. cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +27 -178
  17. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +14 -139
  18. cognite/neat/_graph/extractors/_classic_cdf/_timeseries.py +15 -173
  19. cognite/neat/_graph/extractors/_rdf_file.py +6 -7
  20. cognite/neat/_graph/queries/_base.py +17 -1
  21. cognite/neat/_graph/transformers/_classic_cdf.py +50 -134
  22. cognite/neat/_graph/transformers/_prune_graph.py +1 -1
  23. cognite/neat/_graph/transformers/_rdfpath.py +1 -1
  24. cognite/neat/_issues/warnings/__init__.py +6 -0
  25. cognite/neat/_issues/warnings/_external.py +8 -0
  26. cognite/neat/_issues/warnings/_properties.py +16 -0
  27. cognite/neat/_rules/_constants.py +7 -6
  28. cognite/neat/_rules/analysis/_base.py +8 -4
  29. cognite/neat/_rules/exporters/_base.py +3 -4
  30. cognite/neat/_rules/exporters/_rules2dms.py +29 -40
  31. cognite/neat/_rules/importers/_dms2rules.py +4 -5
  32. cognite/neat/_rules/importers/_rdf/_inference2rules.py +25 -33
  33. cognite/neat/_rules/models/__init__.py +1 -1
  34. cognite/neat/_rules/models/_base_rules.py +22 -12
  35. cognite/neat/_rules/models/dms/__init__.py +2 -2
  36. cognite/neat/_rules/models/dms/_exporter.py +15 -20
  37. cognite/neat/_rules/models/dms/_rules.py +48 -3
  38. cognite/neat/_rules/models/dms/_rules_input.py +52 -8
  39. cognite/neat/_rules/models/dms/_validation.py +10 -5
  40. cognite/neat/_rules/models/entities/_single_value.py +32 -4
  41. cognite/neat/_rules/models/information/_rules.py +0 -8
  42. cognite/neat/_rules/models/mapping/__init__.py +2 -3
  43. cognite/neat/_rules/models/mapping/_classic2core.py +36 -146
  44. cognite/neat/_rules/models/mapping/_classic2core.yaml +339 -0
  45. cognite/neat/_rules/transformers/__init__.py +2 -2
  46. cognite/neat/_rules/transformers/_converters.py +110 -11
  47. cognite/neat/_rules/transformers/_mapping.py +105 -30
  48. cognite/neat/_rules/transformers/_verification.py +5 -2
  49. cognite/neat/_session/_base.py +49 -8
  50. cognite/neat/_session/_drop.py +35 -0
  51. cognite/neat/_session/_inspect.py +17 -5
  52. cognite/neat/_session/_mapping.py +39 -0
  53. cognite/neat/_session/_prepare.py +218 -23
  54. cognite/neat/_session/_read.py +49 -12
  55. cognite/neat/_session/_to.py +3 -3
  56. cognite/neat/_store/_base.py +27 -24
  57. cognite/neat/_utils/rdf_.py +28 -1
  58. cognite/neat/_version.py +1 -1
  59. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +8 -3
  60. cognite/neat/_workflows/steps/lib/current/rules_importer.py +4 -1
  61. cognite/neat/_workflows/steps/lib/current/rules_validator.py +3 -2
  62. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/METADATA +3 -3
  63. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/RECORD +67 -64
  64. cognite/neat/_rules/models/mapping/_base.py +0 -131
  65. cognite/neat/_utils/cdf/loaders/__init__.py +0 -25
  66. cognite/neat/_utils/cdf/loaders/_base.py +0 -54
  67. cognite/neat/_utils/cdf/loaders/_data_modeling.py +0 -339
  68. cognite/neat/_utils/cdf/loaders/_ingestion.py +0 -167
  69. /cognite/neat/{_utils/cdf → _client/_api}/__init__.py +0 -0
  70. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/LICENSE +0 -0
  71. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/WHEEL +0 -0
  72. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/entry_points.txt +0 -0
@@ -4,7 +4,6 @@ from datetime import datetime, timezone
4
4
  from pathlib import Path
5
5
  from typing import Literal, cast
6
6
 
7
- from cognite.client import CogniteClient
8
7
  from cognite.client import data_modeling as dm
9
8
  from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier
10
9
  from cognite.client.data_classes.data_modeling.containers import BTreeIndex, InvertedIndex
@@ -19,6 +18,7 @@ from cognite.client.data_classes.data_modeling.views import (
19
18
  )
20
19
  from cognite.client.utils import ms_to_datetime
21
20
 
21
+ from cognite.neat._client import NeatClient
22
22
  from cognite.neat._issues import IssueList, NeatIssue
23
23
  from cognite.neat._issues.errors import FileTypeUnexpectedError, ResourceMissingIdentifierError, ResourceRetrievalError
24
24
  from cognite.neat._issues.warnings import (
@@ -95,7 +95,7 @@ class DMSImporter(BaseImporter[DMSInputRules]):
95
95
  @classmethod
96
96
  def from_data_model_id(
97
97
  cls,
98
- client: CogniteClient,
98
+ client: NeatClient,
99
99
  data_model_id: DataModelIdentifier,
100
100
  reference_model_id: DataModelIdentifier | None = None,
101
101
  ) -> "DMSImporter":
@@ -147,12 +147,12 @@ class DMSImporter(BaseImporter[DMSInputRules]):
147
147
 
148
148
  issue_list = IssueList()
149
149
  with _handle_issues(issue_list) as result:
150
- schema = DMSSchema.from_data_model(client, user_model, ref_model)
150
+ schema = DMSSchema.from_data_model(NeatClient(client), user_model, ref_model)
151
151
 
152
152
  if result.result == "failure" or issue_list.has_errors:
153
153
  return cls(DMSSchema(), issue_list)
154
154
 
155
- metadata = cls._create_metadata_from_model(user_model, has_reference=ref_model is not None)
155
+ metadata = cls._create_metadata_from_model(user_model)
156
156
  ref_metadata = cls._create_metadata_from_model(ref_model) if ref_model else None
157
157
 
158
158
  return cls(schema, issue_list, metadata, ref_metadata)
@@ -174,7 +174,6 @@ class DMSImporter(BaseImporter[DMSInputRules]):
174
174
  def _create_metadata_from_model(
175
175
  cls,
176
176
  model: dm.DataModel[dm.View] | dm.DataModelApply,
177
- has_reference: bool = False,
178
177
  ) -> DMSInputMetadata:
179
178
  description, creator = DMSInputMetadata._get_description_and_creator(model.description)
180
179
 
@@ -1,6 +1,6 @@
1
1
  from collections import Counter, defaultdict
2
2
  from collections.abc import Mapping
3
- from datetime import datetime
3
+ from datetime import datetime, timezone
4
4
  from pathlib import Path
5
5
  from typing import ClassVar, cast
6
6
 
@@ -8,8 +8,7 @@ from cognite.client import data_modeling as dm
8
8
  from rdflib import RDF, Namespace, URIRef
9
9
  from rdflib import Literal as RdfLiteral
10
10
 
11
- from cognite.neat._constants import DEFAULT_NAMESPACE
12
- from cognite.neat._issues.warnings import PropertyValueTypeUndefinedWarning
11
+ from cognite.neat._issues.warnings import PropertySkippedWarning, PropertyValueTypeUndefinedWarning
13
12
  from cognite.neat._rules.models import data_types
14
13
  from cognite.neat._rules.models.data_types import AnyURI
15
14
  from cognite.neat._rules.models.entities._single_value import UnknownEntity
@@ -156,9 +155,21 @@ class InferenceImporter(BaseRDFImporter):
156
155
  # this is to skip rdf:type property
157
156
  if property_uri == RDF.type:
158
157
  continue
158
+ property_id = remove_namespace_from_uri(property_uri)
159
+ if property_id in {"external_id", "externalId"}:
160
+ skip_issue = PropertySkippedWarning(
161
+ resource_type="Property",
162
+ identifier=f"{class_id}:{property_id}",
163
+ property_name=property_id,
164
+ reason="External ID is assumed to be the unique identifier of the instance "
165
+ "and is not part of the data model schema.",
166
+ )
167
+ if skip_issue not in self.issue_list:
168
+ self.issue_list.append(skip_issue)
169
+ continue
159
170
 
160
171
  self._add_uri_namespace_to_prefixes(cast(URIRef, property_uri), prefixes)
161
- property_id = remove_namespace_from_uri(property_uri)
172
+
162
173
  if isinstance(data_type_uri, URIRef):
163
174
  data_type_uri = self.overwrite_data_types.get(data_type_uri, data_type_uri)
164
175
 
@@ -198,11 +209,12 @@ class InferenceImporter(BaseRDFImporter):
198
209
 
199
210
  # USE CASE 1: If property is not present in properties
200
211
  if id_ not in properties:
212
+ definition["value_type"] = {definition["value_type"]}
201
213
  properties[id_] = definition
202
214
 
203
215
  # USE CASE 2: first time redefinition, value type change to multi
204
216
  elif id_ in properties and definition["value_type"] not in properties[id_]["value_type"]:
205
- properties[id_]["value_type"] = properties[id_]["value_type"] + " | " + definition["value_type"]
217
+ properties[id_]["value_type"].add(definition["value_type"])
206
218
 
207
219
  # USE CASE 3: existing but max count is different
208
220
  elif (
@@ -212,32 +224,12 @@ class InferenceImporter(BaseRDFImporter):
212
224
  ):
213
225
  properties[id_]["max_count"] = max(properties[id_]["max_count"], definition["max_count"])
214
226
 
215
- # Add comments
216
- for id_, property_ in properties.items():
217
- if id_ not in count_by_value_type_by_property:
218
- continue
219
-
220
- count_by_value_type = count_by_value_type_by_property[id_]
221
- count_list = sorted(count_by_value_type.items(), key=lambda item: item[1], reverse=True)
222
- # Make the comment more readable by adapting to the number of value types
223
- base_string = "<{value_type}> which occurs <{count}> times"
224
- if len(count_list) == 1:
225
- type_, count = count_list[0]
226
- counts_str = f"with value type {base_string.format(value_type=type_, count=count)} in the graph"
227
- elif len(count_list) == 2:
228
- first = base_string.format(value_type=count_list[0][0], count=count_list[0][1])
229
- second = base_string.format(value_type=count_list[1][0], count=count_list[1][1])
230
- counts_str = f"with value types {first} and {second} in the graph"
227
+ # Create multi-value properties otherwise single value
228
+ for property_ in properties.values():
229
+ if len(property_["value_type"]) > 1:
230
+ property_["value_type"] = " | ".join([str(t) for t in property_["value_type"]])
231
231
  else:
232
- first_part = ", ".join(
233
- base_string.format(value_type=type_, count=count) for type_, count in count_list[:-1]
234
- )
235
- last = base_string.format(value_type=count_list[-1][0], count=count_list[-1][1])
236
- counts_str = f"with value types {first_part} and {last} in the graph"
237
-
238
- class_id = property_["class_"]
239
- property_id = property_["property_"]
240
- property_["comment"] = f"Class <{class_id}> has property <{property_id}> {counts_str}"
232
+ property_["value_type"] = next(iter(property_["value_type"]))
241
233
 
242
234
  return {
243
235
  "metadata": self._default_metadata().model_dump(),
@@ -247,14 +239,14 @@ class InferenceImporter(BaseRDFImporter):
247
239
  }
248
240
 
249
241
  def _default_metadata(self):
242
+ now = datetime.now(timezone.utc)
250
243
  return InformationMetadata(
251
244
  space=self.data_model_id.space,
252
245
  external_id=self.data_model_id.external_id,
253
246
  version=self.data_model_id.version,
254
247
  name="Inferred Model",
255
248
  creator="NEAT",
256
- created=datetime.now(),
257
- updated=datetime.now(),
249
+ created=now,
250
+ updated=now,
258
251
  description="Inferred model from knowledge graph",
259
- namespace=DEFAULT_NAMESPACE,
260
252
  )
@@ -1,10 +1,10 @@
1
+ from cognite.neat._client.data_classes.schema import DMSSchema
1
2
  from cognite.neat._rules.models.information._rules import InformationRules
2
3
  from cognite.neat._rules.models.information._rules_input import InformationInputRules
3
4
 
4
5
  from ._base_rules import DataModelType, ExtensionCategory, RoleTypes, SchemaCompleteness, SheetList, SheetRow
5
6
  from .dms._rules import DMSRules
6
7
  from .dms._rules_input import DMSInputRules
7
- from .dms._schema import DMSSchema
8
8
 
9
9
  INPUT_RULES_BY_ROLE: dict[RoleTypes, type[InformationInputRules] | type[DMSInputRules]] = {
10
10
  RoleTypes.information: InformationInputRules,
@@ -2,8 +2,6 @@
2
2
  its sub-models and validators.
3
3
  """
4
4
 
5
- from __future__ import annotations
6
-
7
5
  import math
8
6
  import sys
9
7
  import types
@@ -39,13 +37,16 @@ from rdflib import Namespace, URIRef
39
37
 
40
38
  from cognite.neat._constants import DEFAULT_NAMESPACE
41
39
  from cognite.neat._rules.models._types import (
42
- ClassEntityType,
40
+ ContainerEntityType,
43
41
  DataModelExternalIdType,
44
- InformationPropertyType,
42
+ DmsPropertyType,
45
43
  SpaceType,
46
44
  StrListType,
47
45
  VersionType,
46
+ ViewEntityType,
48
47
  )
48
+ from cognite.neat._rules.models.data_types import DataType
49
+ from cognite.neat._rules.models.entities import EdgeEntity, ReverseConnectionEntity, ViewEntity
49
50
 
50
51
  if sys.version_info >= (3, 11):
51
52
  from enum import StrEnum
@@ -249,7 +250,7 @@ class BaseRules(SchemaModel, ABC):
249
250
  """Returns a list of headers for the model, typically used by ExcelExporter"""
250
251
  headers_by_sheet: dict[str, list[str]] = {}
251
252
  for field_name, field in cls.model_fields.items():
252
- if field_name == "validators_to_skip":
253
+ if field_name in ["validators_to_skip", "post_validate"]:
253
254
  continue
254
255
  sheet_name = (field.alias or field_name) if by_alias else field_name
255
256
  annotation = field.annotation
@@ -379,9 +380,9 @@ class SheetList(list, MutableSequence[T_SheetRow]):
379
380
  def __getitem__(self, index: SupportsIndex) -> T_SheetRow: ...
380
381
 
381
382
  @overload
382
- def __getitem__(self, index: slice) -> SheetList[T_SheetRow]: ...
383
+ def __getitem__(self, index: slice) -> "SheetList[T_SheetRow]": ...
383
384
 
384
- def __getitem__(self, index: SupportsIndex | slice, /) -> T_SheetRow | SheetList[T_SheetRow]:
385
+ def __getitem__(self, index: SupportsIndex | slice, /) -> "T_SheetRow | SheetList[T_SheetRow]":
385
386
  if isinstance(index, slice):
386
387
  return SheetList[T_SheetRow](super().__getitem__(index))
387
388
  return super().__getitem__(index)
@@ -399,10 +400,19 @@ ExtensionCategoryType = Annotated[
399
400
 
400
401
 
401
402
  # Immutable such that this can be used as a key in a dictionary
402
- class PropertyRef(BaseModel, frozen=True):
403
- class_: ClassEntityType = Field(alias="Class")
404
- property_: InformationPropertyType = Field(alias="Property")
403
+ class ContainerProperty(BaseModel, frozen=True):
404
+ container: ContainerEntityType
405
+ property_: DmsPropertyType
406
+
407
+
408
+ class ContainerDestinationProperty(ContainerProperty, frozen=True):
409
+ value_type: DataType | ViewEntity
410
+ connection: Literal["direct"] | ReverseConnectionEntity | EdgeEntity | None = None
411
+
412
+
413
+ class ViewRef(BaseModel, frozen=True):
414
+ view: ViewEntityType
405
415
 
406
416
 
407
- class ClassRef(BaseModel, frozen=True):
408
- class_: ClassEntityType = Field(alias="Class")
417
+ class ViewProperty(ViewRef, frozen=True):
418
+ property_: DmsPropertyType
@@ -1,3 +1,5 @@
1
+ from cognite.neat._client.data_classes.schema import DMSSchema
2
+
1
3
  from ._rules import DMSContainer, DMSEnum, DMSMetadata, DMSNode, DMSProperty, DMSRules, DMSView
2
4
  from ._rules_input import (
3
5
  DMSInputContainer,
@@ -8,7 +10,6 @@ from ._rules_input import (
8
10
  DMSInputRules,
9
11
  DMSInputView,
10
12
  )
11
- from ._schema import DMSSchema, PipelineSchema
12
13
 
13
14
  __all__ = [
14
15
  "DMSRules",
@@ -19,7 +20,6 @@ __all__ = [
19
20
  "DMSContainer",
20
21
  "DMSNode",
21
22
  "DMSEnum",
22
- "PipelineSchema",
23
23
  "DMSInputRules",
24
24
  "DMSInputMetadata",
25
25
  "DMSInputView",
@@ -13,6 +13,13 @@ from cognite.client.data_classes.data_modeling.views import (
13
13
  ViewPropertyApply,
14
14
  )
15
15
 
16
+ from cognite.neat._client.data_classes.data_modeling import (
17
+ ContainerApplyDict,
18
+ NodeApplyDict,
19
+ SpaceApplyDict,
20
+ ViewApplyDict,
21
+ )
22
+ from cognite.neat._client.data_classes.schema import DMSSchema
16
23
  from cognite.neat._issues.errors import NeatTypeError, ResourceNotFoundError
17
24
  from cognite.neat._issues.warnings import NotSupportedWarning, PropertyNotFoundWarning
18
25
  from cognite.neat._issues.warnings.user_modeling import (
@@ -33,10 +40,8 @@ from cognite.neat._rules.models.entities import (
33
40
  UnitEntity,
34
41
  ViewEntity,
35
42
  )
36
- from cognite.neat._utils.cdf.data_classes import ContainerApplyDict, NodeApplyDict, SpaceApplyDict, ViewApplyDict
37
43
 
38
44
  from ._rules import DMSEnum, DMSMetadata, DMSProperty, DMSRules, DMSView
39
- from ._schema import DMSSchema, PipelineSchema
40
45
 
41
46
 
42
47
  class _DMSExporter:
@@ -51,13 +56,7 @@ class _DMSExporter:
51
56
  instance_space (str): The space to use for the instance. Defaults to None,`Rules.metadata.space` will be used
52
57
  """
53
58
 
54
- def __init__(
55
- self,
56
- rules: DMSRules,
57
- include_pipeline: bool = False,
58
- instance_space: str | None = None,
59
- ):
60
- self.include_pipeline = include_pipeline
59
+ def __init__(self, rules: DMSRules, instance_space: str | None = None):
61
60
  self.instance_space = instance_space
62
61
  self.rules = rules
63
62
  self._ref_schema = None
@@ -79,9 +78,11 @@ class _DMSExporter:
79
78
  view_properties_by_id, rules.views
80
79
  )
81
80
 
82
- views, view_node_type_filters = self._create_views_with_node_types(
83
- view_properties_by_id, view_properties_with_ancestors_by_id
84
- )
81
+ views = self._create_views_with_node_types(view_properties_by_id, view_properties_with_ancestors_by_id)
82
+ view_node_type_filters: set[dm.NodeId] = set()
83
+ for dms_view in rules.views:
84
+ if isinstance(dms_view.filter_, NodeTypeFilter):
85
+ view_node_type_filters.update(node.as_id() for node in dms_view.filter_.inner or [])
85
86
  if rules.nodes:
86
87
  node_types = NodeApplyDict(
87
88
  [node.as_node() for node in rules.nodes]
@@ -109,8 +110,6 @@ class _DMSExporter:
109
110
  containers=containers,
110
111
  node_types=node_types,
111
112
  )
112
- if self.include_pipeline:
113
- return PipelineSchema.from_dms(output, self.instance_space)
114
113
 
115
114
  if self._ref_schema:
116
115
  output.reference = self._ref_schema
@@ -141,7 +140,7 @@ class _DMSExporter:
141
140
  self,
142
141
  view_properties_by_id: dict[dm.ViewId, list[DMSProperty]],
143
142
  view_properties_with_ancestors_by_id: dict[dm.ViewId, list[DMSProperty]],
144
- ) -> tuple[ViewApplyDict, set[dm.NodeId]]:
143
+ ) -> ViewApplyDict:
145
144
  input_views = list(self.rules.views)
146
145
 
147
146
  views = ViewApplyDict([dms_view.as_view() for dms_view in input_views])
@@ -155,11 +154,7 @@ class _DMSExporter:
155
154
  if view_property is not None:
156
155
  view.properties[prop.view_property] = view_property
157
156
 
158
- unique_node_types: set[dm.NodeId] = set()
159
- for view in views.values():
160
- unique_node_types.add(dm.NodeId(space=view.space, external_id=view.external_id))
161
-
162
- return views, unique_node_types
157
+ return views
163
158
 
164
159
  @classmethod
165
160
  def _create_edge_type_from_prop(cls, prop: DMSProperty) -> dm.DirectRelationReference:
@@ -8,18 +8,23 @@ from pydantic import Field, field_serializer, field_validator, model_validator
8
8
  from pydantic_core.core_schema import SerializationInfo, ValidationInfo
9
9
  from rdflib import URIRef
10
10
 
11
+ from cognite.neat._client.data_classes.schema import DMSSchema
11
12
  from cognite.neat._constants import COGNITE_SPACES
12
13
  from cognite.neat._issues import MultiValueError
14
+ from cognite.neat._issues.errors import NeatValueError
13
15
  from cognite.neat._issues.warnings import (
14
16
  PrincipleMatchingSpaceAndVersionWarning,
15
17
  )
16
18
  from cognite.neat._rules.models._base_rules import (
17
19
  BaseMetadata,
18
20
  BaseRules,
21
+ ContainerProperty,
19
22
  DataModelAspect,
20
23
  RoleTypes,
21
24
  SheetList,
22
25
  SheetRow,
26
+ ViewProperty,
27
+ ViewRef,
23
28
  )
24
29
  from cognite.neat._rules.models._types import (
25
30
  ClassEntityType,
@@ -44,8 +49,6 @@ from cognite.neat._rules.models.entities import (
44
49
  ViewEntityList,
45
50
  )
46
51
 
47
- from ._schema import DMSSchema
48
-
49
52
  _DEFAULT_VERSION = "1"
50
53
 
51
54
 
@@ -180,6 +183,14 @@ class DMSProperty(SheetRow):
180
183
  return value.dump(space=metadata.space, version=metadata.version, type=default_type)
181
184
  return str(value)
182
185
 
186
+ def as_container_reference(self) -> ContainerProperty:
187
+ if self.container is None or self.container_property is None:
188
+ raise NeatValueError("Accessing container reference without container and container property set")
189
+ return ContainerProperty(container=self.container, property_=self.container_property)
190
+
191
+ def as_view_reference(self) -> ViewProperty:
192
+ return ViewProperty(view=self.view, property_=self.view_property)
193
+
183
194
 
184
195
  class DMSContainer(SheetRow):
185
196
  container: ContainerEntityType = Field(alias="Container")
@@ -272,10 +283,14 @@ class DMSView(SheetRow):
272
283
  version=view_id.version or _DEFAULT_VERSION,
273
284
  name=self.name or None,
274
285
  description=self.description,
286
+ filter=None if self.filter_ is None else self.filter_.as_dms_filter(),
275
287
  implements=implements,
276
288
  properties={},
277
289
  )
278
290
 
291
+ def as_view_reference(self) -> ViewRef:
292
+ return ViewRef(view=self.view)
293
+
279
294
 
280
295
  class DMSNode(SheetRow):
281
296
  node: DMSNodeEntity = Field(alias="Node")
@@ -324,6 +339,9 @@ class DMSRules(BaseRules):
324
339
  containers: SheetList[DMSContainer] | None = Field(None, alias="Containers")
325
340
  enum: SheetList[DMSEnum] | None = Field(None, alias="Enum")
326
341
  nodes: SheetList[DMSNode] | None = Field(None, alias="Nodes")
342
+ # This is a hack to allow the post_validation to be turned off when needed
343
+ # Will likely be moved completely out of the rules in the future
344
+ post_validate: bool = Field(default=True, exclude=True, repr=False)
327
345
 
328
346
  @field_validator("views")
329
347
  def matching_version_and_space(cls, value: SheetList[DMSView], info: ValidationInfo) -> SheetList[DMSView]:
@@ -360,6 +378,8 @@ class DMSRules(BaseRules):
360
378
  def post_validation(self) -> "DMSRules":
361
379
  from ._validation import DMSPostValidation
362
380
 
381
+ if not self.post_validate:
382
+ return self
363
383
  issue_list = DMSPostValidation(self).validate()
364
384
  if issue_list.warnings:
365
385
  issue_list.trigger_warnings()
@@ -370,7 +390,7 @@ class DMSRules(BaseRules):
370
390
  def as_schema(self, include_pipeline: bool = False, instance_space: str | None = None) -> DMSSchema:
371
391
  from ._exporter import _DMSExporter
372
392
 
373
- return _DMSExporter(self, include_pipeline, instance_space).to_schema()
393
+ return _DMSExporter(self, instance_space).to_schema()
374
394
 
375
395
  def _repr_html_(self) -> str:
376
396
  summary = {
@@ -386,3 +406,28 @@ class DMSRules(BaseRules):
386
406
  }
387
407
 
388
408
  return pd.DataFrame([summary]).T.rename(columns={0: ""})._repr_html_() # type: ignore
409
+
410
+ def imported_views_and_containers_ids(
411
+ self, include_model_views_with_no_properties: bool = True
412
+ ) -> tuple[set[dm.ViewId], set[dm.ContainerId]]:
413
+ existing_views = {view.view for view in self.views}
414
+ imported_views: set[dm.ViewId] = set()
415
+ for view in self.views:
416
+ for parent in view.implements or []:
417
+ if parent not in existing_views:
418
+ imported_views.add(parent.as_id())
419
+ existing_containers = {container.container for container in self.containers or []}
420
+ imported_containers: set[dm.ContainerId] = set()
421
+ view_with_properties: set[ViewEntity] = set()
422
+ for prop in self.properties:
423
+ if prop.container and prop.container not in existing_containers:
424
+ imported_containers.add(prop.container.as_id())
425
+ if prop.view not in existing_views:
426
+ imported_views.add(prop.view.as_id())
427
+ view_with_properties.add(prop.view)
428
+
429
+ if include_model_views_with_no_properties:
430
+ extra_views = existing_views - view_with_properties
431
+ imported_views.update({view.as_id() for view in extra_views})
432
+
433
+ return imported_views, imported_containers
@@ -5,6 +5,7 @@ from typing import Any, Literal
5
5
 
6
6
  import pandas as pd
7
7
  from cognite.client import data_modeling as dm
8
+ from cognite.client.data_classes.data_modeling import ContainerId, ViewId
8
9
  from rdflib import Namespace, URIRef
9
10
 
10
11
  from cognite.neat._constants import DEFAULT_NAMESPACE
@@ -125,6 +126,12 @@ class DMSInputProperty(InputComponent[DMSProperty]):
125
126
  )
126
127
  return output
127
128
 
129
+ def referenced_view(self, default_space: str, default_version: str) -> ViewEntity:
130
+ return ViewEntity.load(self.view, strict=True, space=default_space, version=default_version)
131
+
132
+ def referenced_container(self, default_space: str) -> ContainerEntity | None:
133
+ return ContainerEntity.load(self.container, strict=True, space=default_space) if self.container else None
134
+
128
135
 
129
136
  @dataclass
130
137
  class DMSInputContainer(InputComponent[DMSContainer]):
@@ -140,8 +147,7 @@ class DMSInputContainer(InputComponent[DMSContainer]):
140
147
 
141
148
  def dump(self, default_space: str) -> dict[str, Any]: # type: ignore[override]
142
149
  output = super().dump()
143
- container = ContainerEntity.load(self.container, space=default_space)
144
- output["Container"] = container
150
+ output["Container"] = self.as_entity_id(default_space)
145
151
  output["Constraint"] = (
146
152
  [ContainerEntity.load(constraint.strip(), space=default_space) for constraint in self.constraint.split(",")]
147
153
  if self.constraint
@@ -149,6 +155,9 @@ class DMSInputContainer(InputComponent[DMSContainer]):
149
155
  )
150
156
  return output
151
157
 
158
+ def as_entity_id(self, default_space: str) -> ContainerEntity:
159
+ return ContainerEntity.load(self.container, strict=True, space=default_space)
160
+
152
161
  @classmethod
153
162
  def from_container(cls, container: dm.ContainerApply) -> "DMSInputContainer":
154
163
  constraints: list[str] = []
@@ -172,7 +181,7 @@ class DMSInputView(InputComponent[DMSView]):
172
181
  name: str | None = None
173
182
  description: str | None = None
174
183
  implements: str | None = None
175
- filter_: Literal["hasData", "nodeType", "rawFilter"] | None = None
184
+ filter_: Literal["hasData", "nodeType", "rawFilter"] | str | None = None
176
185
  in_model: bool = True
177
186
  logical: str | None = None
178
187
 
@@ -182,17 +191,25 @@ class DMSInputView(InputComponent[DMSView]):
182
191
 
183
192
  def dump(self, default_space: str, default_version: str) -> dict[str, Any]: # type: ignore[override]
184
193
  output = super().dump()
185
- view = ViewEntity.load(self.view, space=default_space, version=default_version)
186
- output["View"] = view
187
- output["Implements"] = (
194
+ output["View"] = self.as_entity_id(default_space, default_version)
195
+ output["Implements"] = self._load_implements(default_space, default_version)
196
+ return output
197
+
198
+ def as_entity_id(self, default_space: str, default_version: str) -> ViewEntity:
199
+ return ViewEntity.load(self.view, strict=True, space=default_space, version=default_version)
200
+
201
+ def _load_implements(self, default_space: str, default_version: str) -> list[ViewEntity] | None:
202
+ return (
188
203
  [
189
- ViewEntity.load(implement, space=default_space, version=default_version)
204
+ ViewEntity.load(implement, strict=True, space=default_space, version=default_version)
190
205
  for implement in self.implements.split(",")
191
206
  ]
192
207
  if self.implements
193
208
  else None
194
209
  )
195
- return output
210
+
211
+ def referenced_views(self, default_space: str, default_version: str) -> list[ViewEntity]:
212
+ return self._load_implements(default_space, default_version) or []
196
213
 
197
214
  @classmethod
198
215
  def from_view(cls, view: dm.ViewApply, in_model: bool) -> "DMSInputView":
@@ -287,3 +304,30 @@ class DMSInputRules(InputRules[DMSRules]):
287
304
  return DEFAULT_NAMESPACE[
288
305
  f"data-model/unverified/dms/{self.metadata.space}/{self.metadata.external_id}/{self.metadata.version}"
289
306
  ]
307
+
308
+ def referenced_views_and_containers(self) -> tuple[set[ViewEntity], set[ContainerEntity]]:
309
+ default_space = self.metadata.space
310
+ default_version = self.metadata.version
311
+
312
+ containers: set[ContainerEntity] = set()
313
+ views = {parent for view in self.views for parent in view.referenced_views(default_space, default_version)}
314
+ for prop in self.properties:
315
+ views.add(prop.referenced_view(default_space, default_version))
316
+ if ref_container := prop.referenced_container(default_space):
317
+ containers.add(ref_container)
318
+
319
+ return views, containers
320
+
321
+ def as_view_entities(self) -> list[ViewEntity]:
322
+ return [view.as_entity_id(self.metadata.space, self.metadata.version) for view in self.views]
323
+
324
+ def as_container_entities(self) -> list[ContainerEntity]:
325
+ return [container.as_entity_id(self.metadata.space) for container in self.containers or []]
326
+
327
+ def imported_views_and_containers(self) -> tuple[set[ViewEntity], set[ContainerEntity]]:
328
+ views, containers = self.referenced_views_and_containers()
329
+ return views - set(self.as_view_entities()), containers - set(self.as_container_entities())
330
+
331
+ def imported_views_and_containers_ids(self) -> tuple[set[ViewId], set[ContainerId]]:
332
+ views, containers = self.imported_views_and_containers()
333
+ return {view.as_id() for view in views}, {container.as_id() for container in containers}
@@ -3,6 +3,7 @@ from typing import Any, ClassVar, cast
3
3
 
4
4
  from cognite.client import data_modeling as dm
5
5
 
6
+ from cognite.neat._client.data_classes.schema import DMSSchema
6
7
  from cognite.neat._constants import COGNITE_MODELS, DMS_CONTAINER_PROPERTY_SIZE_LIMIT
7
8
  from cognite.neat._issues import IssueList, NeatError, NeatIssue, NeatIssueList
8
9
  from cognite.neat._issues.errors import (
@@ -19,6 +20,7 @@ from cognite.neat._issues.warnings.user_modeling import (
19
20
  NotNeatSupportedFilterWarning,
20
21
  ViewPropertyLimitWarning,
21
22
  )
23
+ from cognite.neat._rules.analysis import DMSAnalysis
22
24
  from cognite.neat._rules.models.data_types import DataType
23
25
  from cognite.neat._rules.models.entities import ContainerEntity, RawFilter
24
26
  from cognite.neat._rules.models.entities._single_value import (
@@ -27,7 +29,6 @@ from cognite.neat._rules.models.entities._single_value import (
27
29
  )
28
30
 
29
31
  from ._rules import DMSProperty, DMSRules
30
- from ._schema import DMSSchema
31
32
 
32
33
 
33
34
  class DMSPostValidation:
@@ -45,6 +46,7 @@ class DMSPostValidation:
45
46
  self.containers = rules.containers
46
47
  self.views = rules.views
47
48
  self.issue_list = IssueList()
49
+ self.probe = DMSAnalysis(rules)
48
50
 
49
51
  def validate(self) -> NeatIssueList:
50
52
  self._validate_raw_filter()
@@ -164,7 +166,7 @@ class DMSPostValidation:
164
166
  view_id = prop.view.as_id()
165
167
  if view_id not in defined_views:
166
168
  errors.append(
167
- ResourceNotDefinedError[dm.ViewId](
169
+ ResourceNotDefinedError(
168
170
  identifier=view_id,
169
171
  resource_type="view",
170
172
  location="Views Sheet",
@@ -229,7 +231,12 @@ class DMSPostValidation:
229
231
  if self.metadata.as_data_model_id() in COGNITE_MODELS:
230
232
  return None
231
233
 
232
- properties_by_ids = {f"{prop_.view!s}.{prop_.view_property}": prop_ for prop_ in self.properties}
234
+ properties_by_ids = {
235
+ f"{prop_.view!s}.{prop_.view_property}": prop_
236
+ for properties in self.probe.classes_with_properties(True, True).values()
237
+ for prop_ in properties
238
+ }
239
+
233
240
  reversed_by_ids = {
234
241
  id_: prop_
235
242
  for id_, prop_ in properties_by_ids.items()
@@ -239,7 +246,6 @@ class DMSPostValidation:
239
246
  for id_, prop_ in reversed_by_ids.items():
240
247
  source_id = f"{prop_.value_type!s}." f"{cast(ReverseConnectionEntity, prop_.connection).property_}"
241
248
  if source_id not in properties_by_ids:
242
- print(f"source_id: {source_id}, first issue")
243
249
  self.issue_list.append(
244
250
  ReversedConnectionNotFeasibleError(
245
251
  id_,
@@ -252,7 +258,6 @@ class DMSPostValidation:
252
258
  )
253
259
 
254
260
  elif source_id in properties_by_ids and properties_by_ids[source_id].value_type != prop_.view:
255
- print(f"source_id: {source_id}, second issue")
256
261
  self.issue_list.append(
257
262
  ReversedConnectionNotFeasibleError(
258
263
  id_,