cognite-neat 0.96.5__py3-none-any.whl → 0.97.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 (70) hide show
  1. cognite/neat/_constants.py +4 -1
  2. cognite/neat/_graph/extractors/__init__.py +3 -0
  3. cognite/neat/_graph/extractors/_base.py +1 -1
  4. cognite/neat/_graph/extractors/_classic_cdf/_assets.py +1 -1
  5. cognite/neat/_graph/extractors/_classic_cdf/_base.py +1 -1
  6. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +1 -1
  7. cognite/neat/_graph/extractors/_classic_cdf/_data_sets.py +1 -1
  8. cognite/neat/_graph/extractors/_classic_cdf/_events.py +1 -1
  9. cognite/neat/_graph/extractors/_classic_cdf/_files.py +1 -1
  10. cognite/neat/_graph/extractors/_classic_cdf/_labels.py +1 -1
  11. cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +1 -1
  12. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +1 -1
  13. cognite/neat/_graph/extractors/_classic_cdf/_timeseries.py +1 -1
  14. cognite/neat/_graph/extractors/_dexpi.py +1 -1
  15. cognite/neat/_graph/extractors/_dms.py +1 -1
  16. cognite/neat/_graph/extractors/_iodd.py +1 -1
  17. cognite/neat/_graph/extractors/_mock_graph_generator.py +1 -1
  18. cognite/neat/_graph/extractors/_rdf_file.py +1 -1
  19. cognite/neat/_graph/loaders/_rdf2dms.py +1 -1
  20. cognite/neat/_graph/queries/_base.py +1 -1
  21. cognite/neat/_graph/transformers/__init__.py +3 -1
  22. cognite/neat/_graph/transformers/_rdfpath.py +60 -1
  23. cognite/neat/_issues/errors/__init__.py +2 -0
  24. cognite/neat/_issues/errors/_properties.py +12 -0
  25. cognite/neat/_issues/warnings/__init__.py +2 -0
  26. cognite/neat/_issues/warnings/_models.py +11 -0
  27. cognite/neat/_rules/importers/__init__.py +11 -0
  28. cognite/neat/_rules/importers/_base.py +7 -0
  29. cognite/neat/_rules/importers/_dms2rules.py +12 -3
  30. cognite/neat/_rules/importers/_rdf/_inference2rules.py +17 -2
  31. cognite/neat/_rules/importers/_spreadsheet2rules.py +5 -1
  32. cognite/neat/_rules/models/asset/_rules.py +6 -2
  33. cognite/neat/_rules/models/asset/_rules_input.py +6 -1
  34. cognite/neat/_rules/models/data_types.py +6 -0
  35. cognite/neat/_rules/models/dms/_exporter.py +16 -3
  36. cognite/neat/_rules/models/dms/_rules.py +37 -12
  37. cognite/neat/_rules/models/dms/_rules_input.py +8 -0
  38. cognite/neat/_rules/models/dms/_validation.py +64 -2
  39. cognite/neat/_rules/models/domain.py +10 -0
  40. cognite/neat/_rules/models/entities/_loaders.py +3 -5
  41. cognite/neat/_rules/models/information/_rules.py +6 -2
  42. cognite/neat/_rules/models/information/_rules_input.py +6 -1
  43. cognite/neat/_rules/transformers/_base.py +7 -0
  44. cognite/neat/_rules/transformers/_converters.py +56 -4
  45. cognite/neat/_session/_base.py +94 -23
  46. cognite/neat/_session/_inspect.py +12 -4
  47. cognite/neat/_session/_prepare.py +144 -21
  48. cognite/neat/_session/_read.py +137 -30
  49. cognite/neat/_session/_set.py +22 -3
  50. cognite/neat/_session/_show.py +171 -45
  51. cognite/neat/_session/_state.py +79 -30
  52. cognite/neat/_session/_to.py +16 -17
  53. cognite/neat/_session/engine/__init__.py +4 -0
  54. cognite/neat/_session/engine/_import.py +7 -0
  55. cognite/neat/_session/engine/_interface.py +24 -0
  56. cognite/neat/_session/engine/_load.py +129 -0
  57. cognite/neat/_session/exceptions.py +13 -3
  58. cognite/neat/_shared.py +6 -1
  59. cognite/neat/_store/_base.py +3 -24
  60. cognite/neat/_store/_provenance.py +185 -42
  61. cognite/neat/_utils/rdf_.py +34 -1
  62. cognite/neat/_utils/reader/__init__.py +3 -0
  63. cognite/neat/_utils/reader/_base.py +162 -0
  64. cognite/neat/_version.py +2 -1
  65. {cognite_neat-0.96.5.dist-info → cognite_neat-0.97.0.dist-info}/METADATA +5 -3
  66. {cognite_neat-0.96.5.dist-info → cognite_neat-0.97.0.dist-info}/RECORD +69 -64
  67. cognite/neat/_graph/models.py +0 -7
  68. {cognite_neat-0.96.5.dist-info → cognite_neat-0.97.0.dist-info}/LICENSE +0 -0
  69. {cognite_neat-0.96.5.dist-info → cognite_neat-0.97.0.dist-info}/WHEEL +0 -0
  70. {cognite_neat-0.96.5.dist-info → cognite_neat-0.97.0.dist-info}/entry_points.txt +0 -0
@@ -402,17 +402,30 @@ class _DMSExporter:
402
402
 
403
403
  return container_properties_by_id, view_properties_by_id
404
404
 
405
- @staticmethod
406
405
  def _gather_properties_with_ancestors(
406
+ self,
407
407
  view_properties_by_id: dict[dm.ViewId, list[DMSProperty]],
408
408
  views: Sequence[DMSView],
409
409
  ) -> dict[dm.ViewId, list[DMSProperty]]:
410
+ all_view_properties_by_id = view_properties_by_id.copy()
411
+ if self.rules.reference:
412
+ # We need to include t
413
+ ref_view_properties_by_id = self._gather_properties(self.rules.reference.properties)[1]
414
+ for view_id, properties in ref_view_properties_by_id.items():
415
+ if view_id not in all_view_properties_by_id:
416
+ all_view_properties_by_id[view_id] = properties
417
+ else:
418
+ existing_properties = {prop._identifier() for prop in all_view_properties_by_id[view_id]}
419
+ for prop in properties:
420
+ if prop._identifier() not in existing_properties:
421
+ all_view_properties_by_id[view_id].append(prop)
422
+
410
423
  view_properties_with_parents_by_id: dict[dm.ViewId, list[DMSProperty]] = defaultdict(list)
411
424
  view_by_view_id = {view.view.as_id(): view for view in views}
412
425
  for view in views:
413
426
  view_id = view.view.as_id()
414
427
  seen: set[Hashable] = set()
415
- if view_properties := view_properties_by_id.get(view_id):
428
+ if view_properties := all_view_properties_by_id.get(view_id):
416
429
  view_properties_with_parents_by_id[view_id].extend(view_properties)
417
430
  seen.update(prop._identifier() for prop in view_properties)
418
431
  if not view.implements:
@@ -428,7 +441,7 @@ class _DMSExporter:
428
441
  parents.append(grandparent)
429
442
  seen_parents.add(grandparent)
430
443
 
431
- if not (parent_view_properties := view_properties_by_id.get(parent_view_id)):
444
+ if not (parent_view_properties := all_view_properties_by_id.get(parent_view_id)):
432
445
  continue
433
446
  for prop in parent_view_properties:
434
447
  new_prop = prop.model_copy(update={"view": view.view})
@@ -1,15 +1,16 @@
1
1
  import math
2
- import sys
3
2
  import warnings
4
3
  from collections.abc import Hashable
5
4
  from datetime import datetime
6
- from typing import TYPE_CHECKING, Any, ClassVar, Literal
5
+ from typing import Any, ClassVar, Literal
7
6
 
8
7
  import pandas as pd
9
8
  from cognite.client import data_modeling as dm
10
9
  from pydantic import Field, field_serializer, field_validator, model_validator
11
10
  from pydantic_core.core_schema import SerializationInfo, ValidationInfo
11
+ from rdflib import URIRef
12
12
 
13
+ from cognite.neat._constants import COGNITE_SPACES, DEFAULT_NAMESPACE
13
14
  from cognite.neat._issues import MultiValueError
14
15
  from cognite.neat._issues.warnings import (
15
16
  PrincipleMatchingSpaceAndVersionWarning,
@@ -56,14 +57,6 @@ from cognite.neat._rules.models.entities import (
56
57
 
57
58
  from ._schema import DMSSchema
58
59
 
59
- if TYPE_CHECKING:
60
- pass
61
-
62
- if sys.version_info >= (3, 11):
63
- pass
64
- else:
65
- pass
66
-
67
60
  _DEFAULT_VERSION = "1"
68
61
 
69
62
 
@@ -197,6 +190,24 @@ class DMSProperty(SheetRow):
197
190
  raise ValueError(f"Reverse connection must have a value type that points to a view, got {value}")
198
191
  return value
199
192
 
193
+ @field_validator("container", "container_property", mode="after")
194
+ def container_set_correctly(cls, value: Any, info: ValidationInfo) -> Any:
195
+ if (connection := info.data.get("connection")) is None:
196
+ return value
197
+ if connection == "direct" and value is None:
198
+ raise ValueError(
199
+ "You must provide a container and container property for where to store direct connections"
200
+ )
201
+ elif isinstance(connection, EdgeEntity) and value is not None:
202
+ raise ValueError(
203
+ "Edge connections are not stored in a container, please remove the container and container property"
204
+ )
205
+ elif isinstance(connection, ReverseConnectionEntity) and value is not None:
206
+ raise ValueError(
207
+ "Reverse connection are not stored in a container, please remove the container and container property"
208
+ )
209
+ return value
210
+
200
211
  @field_serializer("reference", when_used="always")
201
212
  def set_reference(self, value: Any, info: SerializationInfo) -> str | None:
202
213
  if isinstance(info.context, dict) and info.context.get("as_reference") is True:
@@ -421,7 +432,11 @@ class DMSRules(BaseRules):
421
432
  if not (metadata := info.data.get("metadata")):
422
433
  return value
423
434
  model_version = metadata.version
424
- if different_version := [view.view.as_id() for view in value if view.view.version != model_version]:
435
+ if different_version := [
436
+ view.view.as_id()
437
+ for view in value
438
+ if view.view.version != model_version and view.view.space not in COGNITE_SPACES
439
+ ]:
425
440
  for view_id in different_version:
426
441
  warnings.warn(
427
442
  PrincipleMatchingSpaceAndVersionWarning(
@@ -429,7 +444,11 @@ class DMSRules(BaseRules):
429
444
  ),
430
445
  stacklevel=2,
431
446
  )
432
- if different_space := [view.view.as_id() for view in value if view.view.space != metadata.space]:
447
+ if different_space := [
448
+ view.view.as_id()
449
+ for view in value
450
+ if view.view.space != metadata.space and view.view.space not in COGNITE_SPACES
451
+ ]:
433
452
  for view_id in different_space:
434
453
  warnings.warn(
435
454
  PrincipleMatchingSpaceAndVersionWarning(
@@ -469,3 +488,9 @@ class DMSRules(BaseRules):
469
488
  }
470
489
 
471
490
  return pd.DataFrame([summary]).T.rename(columns={0: ""})._repr_html_() # type: ignore
491
+
492
+ @property
493
+ def id_(self) -> URIRef:
494
+ return DEFAULT_NAMESPACE[
495
+ f"data-model/verified/dms/{self.metadata.space}/{self.metadata.external_id}/{self.metadata.version}"
496
+ ]
@@ -5,7 +5,9 @@ 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 rdflib import URIRef
8
9
 
10
+ from cognite.neat._constants import DEFAULT_NAMESPACE
9
11
  from cognite.neat._rules.models._base_input import InputComponent, InputRules
10
12
  from cognite.neat._rules.models.data_types import DataType
11
13
  from cognite.neat._rules.models.entities import (
@@ -307,3 +309,9 @@ class DMSInputRules(InputRules[DMSRules]):
307
309
  }
308
310
 
309
311
  return pd.DataFrame([summary]).T.rename(columns={0: ""})._repr_html_() # type: ignore
312
+
313
+ @property
314
+ def id_(self) -> URIRef:
315
+ return DEFAULT_NAMESPACE[
316
+ f"data-model/unverified/dms/{self.metadata.space}/{self.metadata.external_id}/{self.metadata.version}"
317
+ ]
@@ -1,18 +1,20 @@
1
1
  from collections import defaultdict
2
- from typing import Any, ClassVar
2
+ from typing import Any, ClassVar, cast
3
3
 
4
4
  from cognite.client import data_modeling as dm
5
5
 
6
- from cognite.neat._constants import DMS_CONTAINER_PROPERTY_SIZE_LIMIT
6
+ from cognite.neat._constants import COGNITE_MODELS, DMS_CONTAINER_PROPERTY_SIZE_LIMIT
7
7
  from cognite.neat._issues import IssueList, NeatError, NeatIssue, NeatIssueList
8
8
  from cognite.neat._issues.errors import (
9
9
  PropertyDefinitionDuplicatedError,
10
10
  ResourceChangedError,
11
11
  ResourceNotDefinedError,
12
12
  )
13
+ from cognite.neat._issues.errors._properties import ReversedConnectionNotFeasibleError
13
14
  from cognite.neat._issues.warnings import (
14
15
  NotSupportedHasDataFilterLimitWarning,
15
16
  NotSupportedViewContainerLimitWarning,
17
+ UndefinedViewWarning,
16
18
  )
17
19
  from cognite.neat._issues.warnings.user_modeling import (
18
20
  NotNeatSupportedFilterWarning,
@@ -21,6 +23,10 @@ from cognite.neat._issues.warnings.user_modeling import (
21
23
  from cognite.neat._rules.models._base_rules import DataModelType, ExtensionCategory, SchemaCompleteness
22
24
  from cognite.neat._rules.models.data_types import DataType
23
25
  from cognite.neat._rules.models.entities import ContainerEntity, RawFilter
26
+ from cognite.neat._rules.models.entities._single_value import (
27
+ ReverseConnectionEntity,
28
+ ViewEntity,
29
+ )
24
30
 
25
31
  from ._rules import DMSProperty, DMSRules
26
32
  from ._schema import DMSSchema
@@ -45,6 +51,8 @@ class DMSPostValidation:
45
51
  def validate(self) -> NeatIssueList:
46
52
  self._validate_raw_filter()
47
53
  self._consistent_container_properties()
54
+ self._validate_value_type_existence()
55
+ self._validate_reverse_connections()
48
56
 
49
57
  self._referenced_views_and_containers_are_existing_and_proper_size()
50
58
  if self.metadata.schema_ is SchemaCompleteness.extended:
@@ -318,6 +326,60 @@ class DMSPostValidation:
318
326
  NotNeatSupportedFilterWarning(view.view.as_id()),
319
327
  )
320
328
 
329
+ def _validate_value_type_existence(self) -> None:
330
+ views = {prop_.view for prop_ in self.properties}.union({view_.view for view_ in self.views})
331
+
332
+ for prop_ in self.properties:
333
+ if isinstance(prop_.value_type, ViewEntity) and prop_.value_type not in views:
334
+ self.issue_list.append(
335
+ UndefinedViewWarning(
336
+ str(prop_.view),
337
+ str(prop_.value_type),
338
+ prop_.property_,
339
+ )
340
+ )
341
+
342
+ def _validate_reverse_connections(self) -> None:
343
+ # do not check for reverse connections in Cognite models
344
+ if self.metadata.as_data_model_id() in COGNITE_MODELS:
345
+ return None
346
+
347
+ properties_by_ids = {f"{prop_.view!s}.{prop_.property_}": prop_ for prop_ in self.properties}
348
+ reversed_by_ids = {
349
+ id_: prop_
350
+ for id_, prop_ in properties_by_ids.items()
351
+ if prop_.connection and isinstance(prop_.connection, ReverseConnectionEntity)
352
+ }
353
+
354
+ for id_, prop_ in reversed_by_ids.items():
355
+ source_id = f"{prop_.value_type!s}." f"{cast(ReverseConnectionEntity, prop_.connection).property_}"
356
+ if source_id not in properties_by_ids:
357
+ self.issue_list.append(
358
+ ReversedConnectionNotFeasibleError(
359
+ id_,
360
+ "reversed connection",
361
+ prop_.property_,
362
+ str(prop_.view),
363
+ str(prop_.value_type),
364
+ cast(ReverseConnectionEntity, prop_.connection).property_,
365
+ )
366
+ )
367
+
368
+ elif source_id in properties_by_ids and properties_by_ids[source_id].value_type != prop_.view:
369
+ self.issue_list.append(
370
+ ReversedConnectionNotFeasibleError(
371
+ id_,
372
+ "view property",
373
+ prop_.property_,
374
+ str(prop_.view),
375
+ str(prop_.value_type),
376
+ cast(ReverseConnectionEntity, prop_.connection).property_,
377
+ )
378
+ )
379
+
380
+ else:
381
+ continue
382
+
321
383
  @staticmethod
322
384
  def _changed_attributes_and_properties(
323
385
  new_dumped: dict[str, Any], existing_dumped: dict[str, Any]
@@ -4,7 +4,9 @@ from dataclasses import dataclass, field
4
4
  from typing import ClassVar
5
5
 
6
6
  from pydantic import Field, field_serializer, field_validator
7
+ from rdflib import URIRef
7
8
 
9
+ from cognite.neat._constants import DEFAULT_NAMESPACE
8
10
  from cognite.neat._rules.models.data_types import DataType
9
11
  from cognite.neat._rules.models.entities import ClassEntity, ClassEntityList
10
12
 
@@ -76,6 +78,10 @@ class DomainRules(BaseRules):
76
78
  last: "DomainRules | None" = Field(None, alias="Last")
77
79
  reference: "DomainRules | None" = Field(None, alias="Reference")
78
80
 
81
+ @property
82
+ def id_(self) -> URIRef:
83
+ return DEFAULT_NAMESPACE["data-model/verified/domain"]
84
+
79
85
 
80
86
  @dataclass
81
87
  class DomainInputMetadata(InputComponent[DomainMetadata]):
@@ -124,3 +130,7 @@ class DomainInputRules(InputRules[DomainRules]):
124
130
  @classmethod
125
131
  def _get_verified_cls(cls) -> type[DomainRules]:
126
132
  return DomainRules
133
+
134
+ @property
135
+ def id_(self) -> URIRef:
136
+ return DEFAULT_NAMESPACE["data-model/unverified/domain"]
@@ -63,11 +63,9 @@ def load_connection(
63
63
  default_space: str,
64
64
  default_version: str,
65
65
  ) -> Literal["direct"] | ReverseConnectionEntity | EdgeEntity | None:
66
- if (
67
- isinstance(raw, EdgeEntity | ReverseConnectionEntity)
68
- or raw is None
69
- or (isinstance(raw, str) and raw == "direct")
70
- ):
66
+ if isinstance(raw, str) and raw.lower() == "direct":
67
+ return "direct" # type: ignore[return-value]
68
+ elif isinstance(raw, EdgeEntity | ReverseConnectionEntity) or raw is None:
71
69
  return raw # type: ignore[return-value]
72
70
  elif isinstance(raw, str) and raw.startswith("edge"):
73
71
  return EdgeEntity.load(raw, space=default_space, version=default_version) # type: ignore[return-value]
@@ -7,9 +7,9 @@ from typing import TYPE_CHECKING, Any, ClassVar
7
7
  import pandas as pd
8
8
  from pydantic import Field, field_serializer, field_validator, model_validator
9
9
  from pydantic_core.core_schema import SerializationInfo
10
- from rdflib import Namespace
10
+ from rdflib import Namespace, URIRef
11
11
 
12
- from cognite.neat._constants import get_default_prefixes
12
+ from cognite.neat._constants import DEFAULT_NAMESPACE, get_default_prefixes
13
13
  from cognite.neat._issues.errors import NeatValueError, PropertyDefinitionError
14
14
  from cognite.neat._rules._constants import EntityTypes
15
15
  from cognite.neat._rules.models._base_rules import (
@@ -394,3 +394,7 @@ class InformationRules(BaseRules):
394
394
  }
395
395
 
396
396
  return pd.DataFrame([summary]).T.rename(columns={0: ""})._repr_html_() # type: ignore
397
+
398
+ @property
399
+ def id_(self) -> URIRef:
400
+ return DEFAULT_NAMESPACE[f"data-model/verified/info/{self.metadata.prefix}/{self.metadata.version}"]
@@ -3,8 +3,9 @@ from datetime import datetime
3
3
  from typing import Any, Literal
4
4
 
5
5
  import pandas as pd
6
- from rdflib import Namespace
6
+ from rdflib import Namespace, URIRef
7
7
 
8
+ from cognite.neat._constants import DEFAULT_NAMESPACE
8
9
  from cognite.neat._rules.models._base_input import InputComponent, InputRules
9
10
  from cognite.neat._rules.models.data_types import DataType
10
11
  from cognite.neat._rules.models.entities import (
@@ -158,3 +159,7 @@ class InformationInputRules(InputRules[InformationRules]):
158
159
  }
159
160
 
160
161
  return pd.DataFrame([summary]).T.rename(columns={0: ""})._repr_html_() # type: ignore
162
+
163
+ @property
164
+ def id_(self) -> URIRef:
165
+ return DEFAULT_NAMESPACE[f"data-model/unverified/info/{self.metadata.prefix}/{self.metadata.version}"]
@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
2
2
  from collections.abc import MutableSequence
3
3
  from typing import Generic, TypeVar
4
4
 
5
+ from cognite.neat._constants import DEFAULT_NAMESPACE
5
6
  from cognite.neat._issues import IssueList, NeatError
6
7
  from cognite.neat._issues.errors import NeatTypeError, NeatValueError
7
8
  from cognite.neat._rules._shared import (
@@ -12,6 +13,7 @@ from cognite.neat._rules._shared import (
12
13
  Rules,
13
14
  VerifiedRules,
14
15
  )
16
+ from cognite.neat._store._provenance import Agent as ProvenanceAgent
15
17
 
16
18
  T_RulesIn = TypeVar("T_RulesIn", bound=Rules)
17
19
  T_RulesOut = TypeVar("T_RulesOut", bound=Rules)
@@ -54,6 +56,11 @@ class RulesTransformer(ABC, Generic[T_RulesIn, T_RulesOut]):
54
56
  else:
55
57
  raise NeatTypeError(f"Unsupported type: {type(rules)}")
56
58
 
59
+ @property
60
+ def agent(self) -> ProvenanceAgent:
61
+ """Provenance agent for the importer."""
62
+ return ProvenanceAgent(id_=DEFAULT_NAMESPACE[f"agent/{type(self).__name__}"])
63
+
57
64
 
58
65
  class RulesPipeline(list, MutableSequence[RulesTransformer], Generic[T_RulesIn, T_RulesOut]):
59
66
  def transform(self, rules: T_RulesIn | OutRules[T_RulesIn]) -> OutRules[T_RulesOut]:
@@ -10,7 +10,11 @@ from cognite.client.data_classes import data_modeling as dms
10
10
  from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier, ViewId
11
11
  from rdflib import Namespace
12
12
 
13
- from cognite.neat._constants import COGNITE_MODELS, DMS_CONTAINER_PROPERTY_SIZE_LIMIT
13
+ from cognite.neat._constants import (
14
+ COGNITE_MODELS,
15
+ DMS_CONTAINER_PROPERTY_SIZE_LIMIT,
16
+ )
17
+ from cognite.neat._issues._base import IssueList
14
18
  from cognite.neat._issues.errors import NeatValueError
15
19
  from cognite.neat._issues.warnings._models import (
16
20
  EnterpriseModelNotBuildOnTopOfCDMWarning,
@@ -18,7 +22,13 @@ from cognite.neat._issues.warnings._models import (
18
22
  )
19
23
  from cognite.neat._issues.warnings.user_modeling import ParentInDifferentSpaceWarning
20
24
  from cognite.neat._rules._constants import EntityTypes
21
- from cognite.neat._rules._shared import InputRules, JustRules, OutRules, VerifiedRules
25
+ from cognite.neat._rules._shared import (
26
+ InputRules,
27
+ JustRules,
28
+ OutRules,
29
+ ReadRules,
30
+ VerifiedRules,
31
+ )
22
32
  from cognite.neat._rules.analysis import DMSAnalysis
23
33
  from cognite.neat._rules.models import (
24
34
  AssetRules,
@@ -83,13 +93,16 @@ class ToCompliantEntities(RulesTransformer[InformationInputRules, InformationInp
83
93
 
84
94
  def transform(
85
95
  self, rules: InformationInputRules | OutRules[InformationInputRules]
86
- ) -> OutRules[InformationInputRules]:
87
- return JustRules(self._transform(self._to_rules(rules)))
96
+ ) -> ReadRules[InformationInputRules]:
97
+ return ReadRules(self._transform(self._to_rules(rules)), IssueList(), {})
88
98
 
89
99
  def _transform(self, rules: InformationInputRules) -> InformationInputRules:
90
100
  rules.metadata.prefix = self._fix_entity(rules.metadata.prefix)
91
101
  rules.classes = self._fix_classes(rules.classes)
92
102
  rules.properties = self._fix_properties(rules.properties)
103
+
104
+ rules.metadata.version += "_dms_compliant"
105
+
93
106
  return rules
94
107
 
95
108
  @classmethod
@@ -258,6 +271,7 @@ class ToExtension(RulesTransformer[DMSRules, DMSRules]):
258
271
  type_: Literal["enterprise", "solution"] = "enterprise",
259
272
  mode: Literal["read", "write"] = "read",
260
273
  dummy_property: str = "GUID",
274
+ move_connections: bool = False,
261
275
  ):
262
276
  self.new_model_id = DataModelId.load(new_model_id)
263
277
  if not self.new_model_id.version:
@@ -267,6 +281,7 @@ class ToExtension(RulesTransformer[DMSRules, DMSRules]):
267
281
  self.mode = mode
268
282
  self.type_ = type_
269
283
  self.dummy_property = dummy_property
284
+ self.move_connections = move_connections
270
285
 
271
286
  def transform(self, rules: DMSRules | OutRules[DMSRules]) -> JustRules[DMSRules]:
272
287
  # Copy to ensure immutability
@@ -393,10 +408,18 @@ class ToExtension(RulesTransformer[DMSRules, DMSRules]):
393
408
  # extending reference views with new ones
394
409
  enterprise_model.views.extend(enterprise_views)
395
410
 
411
+ # Move connections from reference model to enterprise model
412
+ if self.move_connections:
413
+ enterprise_connections = self._move_connections(enterprise_model)
414
+ else:
415
+ enterprise_connections = SheetList[DMSProperty]()
416
+
396
417
  # while overwriting containers and properties with new ones
397
418
  enterprise_model.containers = enterprise_containers
398
419
  enterprise_model.properties = enterprise_properties
399
420
 
421
+ enterprise_properties.extend(enterprise_connections)
422
+
400
423
  return JustRules(enterprise_model)
401
424
 
402
425
  def _remove_cognite_affix(self, entity: _T_Entity) -> _T_Entity:
@@ -455,6 +478,35 @@ class ToExtension(RulesTransformer[DMSRules, DMSRules]):
455
478
 
456
479
  return new_views, new_containers, new_properties
457
480
 
481
+ def _move_connections(self, rules: DMSRules) -> SheetList[DMSProperty]:
482
+ implements: dict[ViewEntity, list[ViewEntity]] = defaultdict(list)
483
+ new_properties = SheetList[DMSProperty]()
484
+
485
+ for view in rules.views:
486
+ if view.view.space == rules.metadata.space and view.implements:
487
+ for implemented_view in view.implements:
488
+ implements.setdefault(implemented_view, []).append(view.view)
489
+
490
+ # currently only supporting single implementation of reference view in enterprise view
491
+ # connections that do not have properties
492
+ if all(len(v) == 1 for v in implements.values()):
493
+ for prop_ in rules.properties:
494
+ if (
495
+ prop_.view.space != rules.metadata.space
496
+ and prop_.connection
497
+ and isinstance(prop_.value_type, ViewEntity)
498
+ and implements.get(prop_.view)
499
+ and implements.get(prop_.value_type)
500
+ ):
501
+ if isinstance(prop_.connection, EdgeEntity) and prop_.connection.properties:
502
+ continue
503
+ new_property = prop_.model_copy(deep=True)
504
+ new_property.view = implements[prop_.view][0]
505
+ new_property.value_type = implements[prop_.value_type][0]
506
+ new_properties.append(new_property)
507
+
508
+ return new_properties
509
+
458
510
 
459
511
  class ReduceCogniteModel(RulesTransformer[DMSRules, DMSRules]):
460
512
  _ASSET_VIEW = ViewId("cdf_cdm", "CogniteAsset", "v1")
@@ -1,3 +1,4 @@
1
+ from datetime import datetime, timezone
1
2
  from typing import Literal, cast
2
3
 
3
4
  from cognite.client import CogniteClient
@@ -14,6 +15,10 @@ from cognite.neat._rules.models.entities._single_value import UnknownEntity
14
15
  from cognite.neat._rules.models.information._rules import InformationRules
15
16
  from cognite.neat._rules.models.information._rules_input import InformationInputRules
16
17
  from cognite.neat._rules.transformers import ConvertToRules, VerifyAnyRules
18
+ from cognite.neat._store._provenance import (
19
+ INSTANCES_ENTITY,
20
+ Change,
21
+ )
17
22
 
18
23
  from ._inspect import InspectAPI
19
24
  from ._prepare import PrepareAPI
@@ -22,7 +27,8 @@ from ._set import SetAPI
22
27
  from ._show import ShowAPI
23
28
  from ._state import SessionState
24
29
  from ._to import ToAPI
25
- from .exceptions import intercept_session_exceptions
30
+ from .engine import load_neat_engine
31
+ from .exceptions import NeatSessionError, intercept_session_exceptions
26
32
 
27
33
 
28
34
  @intercept_session_exceptions
@@ -32,6 +38,7 @@ class NeatSession:
32
38
  client: CogniteClient | None = None,
33
39
  storage: Literal["memory", "oxigraph"] = "memory",
34
40
  verbose: bool = True,
41
+ load_engine: Literal["newest", "cache", "skip"] = "cache",
35
42
  ) -> None:
36
43
  self._client = client
37
44
  self._verbose = verbose
@@ -42,26 +49,69 @@ class NeatSession:
42
49
  self.show = ShowAPI(self._state)
43
50
  self.set = SetAPI(self._state, verbose)
44
51
  self.inspect = InspectAPI(self._state)
52
+ if load_engine != "skip" and (engine_version := load_neat_engine(client, load_engine)):
53
+ print(f"Neat Engine {engine_version} loaded.")
45
54
 
46
55
  @property
47
56
  def version(self) -> str:
48
57
  return _version.__version__
49
58
 
50
59
  def verify(self) -> IssueList:
51
- output = VerifyAnyRules("continue").try_transform(self._state.input_rule)
60
+ source_id, last_unverified_rule = self._state.data_model.last_unverified_rule
61
+ transformer = VerifyAnyRules("continue")
62
+ start = datetime.now(timezone.utc)
63
+ output = transformer.try_transform(last_unverified_rule)
64
+ end = datetime.now(timezone.utc)
65
+
52
66
  if output.rules:
53
- self._state.verified_rules.append(output.rules)
67
+ change = Change.from_rules_activity(
68
+ output.rules,
69
+ transformer.agent,
70
+ start,
71
+ end,
72
+ f"Verified data model {source_id} as {output.rules.id_}",
73
+ self._state.data_model.provenance.source_entity(source_id)
74
+ or self._state.data_model.provenance.target_entity(source_id),
75
+ )
76
+
77
+ self._state.data_model.write(output.rules, change)
78
+
54
79
  if isinstance(output.rules, InformationRules):
55
- self._state.store.add_rules(output.rules)
80
+ self._state.instances.store.add_rules(output.rules)
81
+
56
82
  output.issues.action = "verify"
57
- self._state.issue_lists.append(output.issues)
83
+ self._state.data_model.issue_lists.append(output.issues)
58
84
  if output.issues:
59
85
  print("You can inspect the issues with the .inspect.issues(...) method.")
60
86
  return output.issues
61
87
 
62
- def convert(self, target: Literal["dms"]) -> None:
63
- converted = ConvertToRules(DMSRules).transform(self._state.last_verified_rule)
64
- self._state.verified_rules.append(converted.rules)
88
+ def convert(self, target: Literal["dms", "information"]) -> None:
89
+ start = datetime.now(timezone.utc)
90
+ if target == "dms":
91
+ source_id, info_rules = self._state.data_model.last_verified_information_rules
92
+ converter = ConvertToRules(DMSRules)
93
+ converted_rules = converter.transform(info_rules).rules
94
+ elif target == "information":
95
+ source_id, dms_rules = self._state.data_model.last_verified_dms_rules
96
+ converter = ConvertToRules(InformationRules)
97
+ converted_rules = converter.transform(dms_rules).rules
98
+ else:
99
+ raise NeatSessionError(f"Target {target} not supported.")
100
+ end = datetime.now(timezone.utc)
101
+
102
+ # Provenance
103
+ change = Change.from_rules_activity(
104
+ converted_rules,
105
+ converter.agent,
106
+ start,
107
+ end,
108
+ f"Converted data model {source_id} to {converted_rules.id_}",
109
+ self._state.data_model.provenance.source_entity(source_id)
110
+ or self._state.data_model.provenance.target_entity(source_id),
111
+ )
112
+
113
+ self._state.data_model.write(converted_rules, change)
114
+
65
115
  if self._verbose:
66
116
  print(f"Rules converted to {target}")
67
117
 
@@ -73,6 +123,7 @@ class NeatSession:
73
123
  "v1",
74
124
  ),
75
125
  non_existing_node_type: UnknownEntity | AnyURI = DEFAULT_NON_EXISTING_NODE_TYPE,
126
+ max_number_of_instance: int = 100,
76
127
  ) -> IssueList:
77
128
  """Data model inference from instances.
78
129
 
@@ -83,35 +134,55 @@ class NeatSession:
83
134
 
84
135
  model_id = dm.DataModelId.load(model_id)
85
136
 
86
- input_rules: ReadRules = importers.InferenceImporter.from_graph_store(
87
- store=self._state.store,
137
+ start = datetime.now(timezone.utc)
138
+ importer = importers.InferenceImporter.from_graph_store(
139
+ store=self._state.instances.store,
88
140
  non_existing_node_type=non_existing_node_type,
89
- ).to_rules()
141
+ max_number_of_instance=max_number_of_instance,
142
+ )
143
+ inferred_rules: ReadRules = importer.to_rules()
144
+ end = datetime.now(timezone.utc)
90
145
 
91
146
  if model_id.space:
92
- cast(InformationInputRules, input_rules.rules).metadata.prefix = model_id.space
147
+ cast(InformationInputRules, inferred_rules.rules).metadata.prefix = model_id.space
93
148
  if model_id.external_id:
94
- cast(InformationInputRules, input_rules.rules).metadata.name = model_id.external_id
149
+ cast(InformationInputRules, inferred_rules.rules).metadata.name = model_id.external_id
95
150
 
96
151
  if model_id.version:
97
- cast(InformationInputRules, input_rules.rules).metadata.version = model_id.version
152
+ cast(InformationInputRules, inferred_rules.rules).metadata.version = model_id.version
153
+
154
+ # Provenance
155
+ change = Change.from_rules_activity(
156
+ inferred_rules,
157
+ importer.agent,
158
+ start,
159
+ end,
160
+ "Inferred data model",
161
+ INSTANCES_ENTITY,
162
+ )
98
163
 
99
- self.read.rdf._store_rules(self._state.store, input_rules, "Data Model Inference")
100
- return input_rules.issues
164
+ self._state.data_model.write(inferred_rules, change)
165
+ return inferred_rules.issues
101
166
 
102
167
  def _repr_html_(self) -> str:
103
168
  state = self._state
104
- if not state.has_store and not state.input_rules:
169
+ if (
170
+ not state.instances.has_store
171
+ and not state.data_model.has_unverified_rules
172
+ and not state.data_model.has_verified_rules
173
+ ):
105
174
  return "<strong>Empty session</strong>. Get started by reading something with the <em>.read</em> attribute."
106
175
 
107
176
  output = []
108
- if state.input_rules and not state.verified_rules:
109
- output.append(f"<H2>Unverified Data Model</H2><br />{state.input_rule.rules._repr_html_()}") # type: ignore
110
177
 
111
- if state.verified_rules:
112
- output.append(f"<H2>Verified Data Model</H2><br />{state.last_verified_rule._repr_html_()}") # type: ignore
178
+ if state.data_model.has_unverified_rules and not state.data_model.has_verified_rules:
179
+ rules: ReadRules = state.data_model.last_unverified_rule[1]
180
+ output.append(f"<H2>Unverified Data Model</H2><br />{rules.rules._repr_html_()}") # type: ignore
181
+
182
+ if state.data_model.has_verified_rules:
183
+ output.append(f"<H2>Verified Data Model</H2><br />{state.data_model.last_verified_rule[1]._repr_html_()}") # type: ignore
113
184
 
114
- if state.has_store:
115
- output.append(f"<H2>Instances</H2> {state.store._repr_html_()}")
185
+ if state.instances.has_store:
186
+ output.append(f"<H2>Instances</H2> {state.instances.store._repr_html_()}")
116
187
 
117
188
  return "<br />".join(output)