cognite-neat 0.126.0__py3-none-any.whl → 0.126.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 (75) hide show
  1. cognite/neat/_client/__init__.py +4 -0
  2. cognite/neat/_client/api.py +8 -0
  3. cognite/neat/_client/client.py +19 -0
  4. cognite/neat/_client/config.py +40 -0
  5. cognite/neat/_client/containers_api.py +73 -0
  6. cognite/neat/_client/data_classes.py +10 -0
  7. cognite/neat/_client/data_model_api.py +63 -0
  8. cognite/neat/_client/spaces_api.py +67 -0
  9. cognite/neat/_client/views_api.py +82 -0
  10. cognite/neat/_data_model/_analysis.py +127 -0
  11. cognite/neat/_data_model/_constants.py +59 -0
  12. cognite/neat/_data_model/_shared.py +46 -0
  13. cognite/neat/_data_model/deployer/__init__.py +0 -0
  14. cognite/neat/_data_model/deployer/_differ.py +113 -0
  15. cognite/neat/_data_model/deployer/_differ_container.py +354 -0
  16. cognite/neat/_data_model/deployer/_differ_data_model.py +29 -0
  17. cognite/neat/_data_model/deployer/_differ_space.py +9 -0
  18. cognite/neat/_data_model/deployer/_differ_view.py +194 -0
  19. cognite/neat/_data_model/deployer/data_classes.py +176 -0
  20. cognite/neat/_data_model/exporters/__init__.py +4 -0
  21. cognite/neat/_data_model/exporters/_base.py +6 -1
  22. cognite/neat/_data_model/exporters/_table_exporter/__init__.py +0 -0
  23. cognite/neat/_data_model/exporters/_table_exporter/exporter.py +106 -0
  24. cognite/neat/_data_model/exporters/_table_exporter/workbook.py +414 -0
  25. cognite/neat/_data_model/exporters/_table_exporter/writer.py +391 -0
  26. cognite/neat/_data_model/importers/__init__.py +2 -1
  27. cognite/neat/_data_model/importers/_api_importer.py +88 -0
  28. cognite/neat/_data_model/importers/_table_importer/data_classes.py +48 -8
  29. cognite/neat/_data_model/importers/_table_importer/importer.py +74 -5
  30. cognite/neat/_data_model/importers/_table_importer/reader.py +63 -7
  31. cognite/neat/_data_model/models/dms/__init__.py +17 -1
  32. cognite/neat/_data_model/models/dms/_base.py +12 -8
  33. cognite/neat/_data_model/models/dms/_constants.py +1 -1
  34. cognite/neat/_data_model/models/dms/_constraints.py +2 -1
  35. cognite/neat/_data_model/models/dms/_container.py +5 -5
  36. cognite/neat/_data_model/models/dms/_data_model.py +3 -3
  37. cognite/neat/_data_model/models/dms/_data_types.py +8 -1
  38. cognite/neat/_data_model/models/dms/_http.py +18 -0
  39. cognite/neat/_data_model/models/dms/_indexes.py +2 -1
  40. cognite/neat/_data_model/models/dms/_references.py +17 -4
  41. cognite/neat/_data_model/models/dms/_space.py +11 -7
  42. cognite/neat/_data_model/models/dms/_view_property.py +7 -4
  43. cognite/neat/_data_model/models/dms/_views.py +16 -6
  44. cognite/neat/_data_model/validation/__init__.py +0 -0
  45. cognite/neat/_data_model/validation/_base.py +16 -0
  46. cognite/neat/_data_model/validation/dms/__init__.py +9 -0
  47. cognite/neat/_data_model/validation/dms/_orchestrator.py +68 -0
  48. cognite/neat/_data_model/validation/dms/_validators.py +139 -0
  49. cognite/neat/_exceptions.py +15 -3
  50. cognite/neat/_issues.py +39 -6
  51. cognite/neat/_session/__init__.py +3 -0
  52. cognite/neat/_session/_physical.py +88 -0
  53. cognite/neat/_session/_session.py +34 -25
  54. cognite/neat/_session/_wrappers.py +61 -0
  55. cognite/neat/_state_machine/__init__.py +10 -0
  56. cognite/neat/{_session/_state_machine → _state_machine}/_base.py +11 -1
  57. cognite/neat/_state_machine/_states.py +53 -0
  58. cognite/neat/_store/__init__.py +3 -0
  59. cognite/neat/_store/_provenance.py +55 -0
  60. cognite/neat/_store/_store.py +124 -0
  61. cognite/neat/_utils/_reader.py +194 -0
  62. cognite/neat/_utils/http_client/__init__.py +14 -20
  63. cognite/neat/_utils/http_client/_client.py +22 -61
  64. cognite/neat/_utils/http_client/_data_classes.py +167 -268
  65. cognite/neat/_utils/text.py +6 -0
  66. cognite/neat/_utils/useful_types.py +23 -2
  67. cognite/neat/_version.py +1 -1
  68. cognite/neat/v0/core/_data_model/importers/_rdf/_shared.py +2 -2
  69. {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/METADATA +1 -1
  70. {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/RECORD +72 -38
  71. cognite/neat/_data_model/exporters/_table_exporter.py +0 -35
  72. cognite/neat/_session/_state_machine/__init__.py +0 -23
  73. cognite/neat/_session/_state_machine/_states.py +0 -150
  74. {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/WHEEL +0 -0
  75. {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/licenses/LICENSE +0 -0
@@ -2,11 +2,11 @@ import re
2
2
  from abc import ABC
3
3
  from typing import Literal, TypeVar
4
4
 
5
- from pydantic import Field, Json, field_validator, model_validator
5
+ from pydantic import Field, JsonValue, field_validator, model_validator
6
6
 
7
7
  from cognite.neat._utils.text import humanize_collection
8
8
 
9
- from ._base import Resource, WriteableResource
9
+ from ._base import APIResource, Resource, WriteableResource
10
10
  from ._constants import (
11
11
  CONTAINER_AND_VIEW_PROPERTIES_IDENTIFIER_PATTERN,
12
12
  DM_EXTERNAL_ID_PATTERN,
@@ -15,8 +15,9 @@ from ._constants import (
15
15
  FORBIDDEN_CONTAINER_AND_VIEW_PROPERTIES_IDENTIFIER,
16
16
  SPACE_FORMAT_PATTERN,
17
17
  )
18
- from ._references import ContainerReference, ViewReference
18
+ from ._references import ContainerReference, NodeReference, ViewReference
19
19
  from ._view_property import (
20
+ EdgeProperty,
20
21
  ViewRequestProperty,
21
22
  ViewResponseProperty,
22
23
  )
@@ -24,7 +25,7 @@ from ._view_property import (
24
25
  KEY_PATTERN = re.compile(CONTAINER_AND_VIEW_PROPERTIES_IDENTIFIER_PATTERN)
25
26
 
26
27
 
27
- class View(Resource, ABC):
28
+ class View(Resource, APIResource[ViewReference], ABC):
28
29
  space: str = Field(
29
30
  description="Id of the space that the view belongs to.",
30
31
  min_length=1,
@@ -52,7 +53,7 @@ class View(Resource, ABC):
52
53
  description="Description of the view.",
53
54
  max_length=1024,
54
55
  )
55
- filter: dict[str, Json] | None = Field(
56
+ filter: dict[str, JsonValue] | None = Field(
56
57
  default=None,
57
58
  description="A filter Domain Specific Language (DSL) used to create advanced filter queries.",
58
59
  )
@@ -62,7 +63,7 @@ class View(Resource, ABC):
62
63
  )
63
64
 
64
65
  def as_reference(self) -> ViewReference:
65
- return ViewReference(space=self.space, externalId=self.external_id, version=self.version)
66
+ return ViewReference(space=self.space, external_id=self.external_id, version=self.version)
66
67
 
67
68
  @model_validator(mode="before")
68
69
  def set_connection_type_on_primary_properties(cls, data: dict) -> dict:
@@ -134,6 +135,15 @@ class ViewResponse(View, WriteableResource[ViewRequest]):
134
135
  """Validate properties Identifier"""
135
136
  return _validate_properties_keys(val)
136
137
 
138
+ @property
139
+ def node_types(self) -> list[NodeReference]:
140
+ """Get all node types referenced by this view."""
141
+ nodes_refs: set[NodeReference] = set()
142
+ for prop in self.properties.values():
143
+ if isinstance(prop, EdgeProperty):
144
+ nodes_refs.add(prop.type)
145
+ return list(nodes_refs)
146
+
137
147
  def as_request(self) -> ViewRequest:
138
148
  dumped = self.model_dump(by_alias=True, exclude={"properties"})
139
149
  dumped["properties"] = {
File without changes
@@ -0,0 +1,16 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import ClassVar
3
+
4
+ from cognite.neat._issues import ConsistencyError, Recommendation
5
+
6
+
7
+ class DataModelValidator(ABC):
8
+ """Assessors for fundamental data model principles."""
9
+
10
+ code: ClassVar[str]
11
+
12
+ @abstractmethod
13
+ def run(self) -> list[ConsistencyError] | list[Recommendation]:
14
+ """Execute the success handler on the data model."""
15
+ # do something with data model
16
+ pass
@@ -0,0 +1,9 @@
1
+ from ._orchestrator import DmsDataModelValidation
2
+ from ._validators import UndefinedConnectionEndNodeTypes, VersionSpaceInconsistency, ViewsWithoutProperties
3
+
4
+ __all__ = [
5
+ "DmsDataModelValidation",
6
+ "UndefinedConnectionEndNodeTypes",
7
+ "VersionSpaceInconsistency",
8
+ "ViewsWithoutProperties",
9
+ ]
@@ -0,0 +1,68 @@
1
+ from cognite.neat._client import NeatClient
2
+ from cognite.neat._data_model._analysis import DataModelAnalysis
3
+ from cognite.neat._data_model._shared import OnSuccessIssuesChecker
4
+ from cognite.neat._data_model.models.dms._references import ViewReference
5
+ from cognite.neat._data_model.models.dms._schema import RequestSchema
6
+ from cognite.neat._data_model.models.dms._views import ViewRequest
7
+ from cognite.neat._data_model.validation._base import DataModelValidator
8
+
9
+ from ._validators import UndefinedConnectionEndNodeTypes, VersionSpaceInconsistency, ViewsWithoutProperties
10
+
11
+
12
+ class DmsDataModelValidation(OnSuccessIssuesChecker):
13
+ """Placeholder for DMS Quality Assessment functionality."""
14
+
15
+ def __init__(
16
+ self, client: NeatClient | None = None, codes: list[str] | None = None, modus_operandi: str | None = None
17
+ ) -> None:
18
+ super().__init__(client)
19
+ self._codes = codes or ["all"]
20
+ self._modus_operandi = modus_operandi # will be used later to trigger how validators will behave
21
+
22
+ def run(self, data_model: RequestSchema) -> None:
23
+ """Run quality assessment on the DMS data model."""
24
+
25
+ # Helper wrangled data model components
26
+ analysis = DataModelAnalysis(data_model)
27
+ local_views_by_reference = analysis.view_by_reference(include_inherited_properties=True)
28
+ local_connection_end_node_types = analysis.connection_end_node_types
29
+ cdf_views_by_reference = self._cdf_view_by_reference(
30
+ list(analysis.referenced_views(include_connection_end_node_types=True)),
31
+ include_inherited_properties=True,
32
+ )
33
+
34
+ validators: list[DataModelValidator] = [
35
+ ViewsWithoutProperties(
36
+ local_views_by_reference=local_views_by_reference,
37
+ cdf_views_by_reference=cdf_views_by_reference,
38
+ ),
39
+ UndefinedConnectionEndNodeTypes(
40
+ local_connection_end_node_types=local_connection_end_node_types,
41
+ local_views_by_reference=local_views_by_reference,
42
+ cdf_views_by_reference=cdf_views_by_reference,
43
+ ),
44
+ VersionSpaceInconsistency(
45
+ data_model_reference=data_model.data_model.as_reference(),
46
+ view_references=list(local_views_by_reference.keys()),
47
+ ),
48
+ ]
49
+
50
+ for validator in validators:
51
+ if "all" in self._codes or validator.code in self._codes:
52
+ self._issues.extend(validator.run())
53
+
54
+ self._has_run = True
55
+
56
+ def _cdf_view_by_reference(
57
+ self, views: list[ViewReference], include_inherited_properties: bool = True
58
+ ) -> dict[ViewReference, ViewRequest]:
59
+ """Fetch view definition from CDF."""
60
+
61
+ if not self._client:
62
+ return {}
63
+ return {
64
+ response.as_reference(): response.as_request()
65
+ for response in self._client.views.retrieve(
66
+ views, include_inherited_properties=include_inherited_properties
67
+ )
68
+ }
@@ -0,0 +1,139 @@
1
+ from cognite.neat._data_model._constants import COGNITE_SPACES
2
+ from cognite.neat._data_model.models.dms._references import DataModelReference, ViewReference
3
+ from cognite.neat._data_model.models.dms._views import ViewRequest
4
+ from cognite.neat._data_model.validation._base import DataModelValidator
5
+ from cognite.neat._issues import ConsistencyError, Recommendation
6
+
7
+
8
+ class ViewsWithoutProperties(DataModelValidator):
9
+ """This validator checks for views without properties, i.e. views that do not have any
10
+ property attached to them , either directly or through implements."""
11
+
12
+ code = "NEAT-DMS-001"
13
+
14
+ def __init__(
15
+ self,
16
+ local_views_by_reference: dict[ViewReference, ViewRequest],
17
+ cdf_views_by_reference: dict[ViewReference, ViewRequest],
18
+ ) -> None:
19
+ self.local_views_by_reference = local_views_by_reference
20
+ self.cdf_views_by_reference = cdf_views_by_reference
21
+
22
+ def run(self) -> list[ConsistencyError]:
23
+ views_without_properties = []
24
+
25
+ for ref, view in self.local_views_by_reference.items():
26
+ if not view.properties:
27
+ # Existing CDF view has properties
28
+ if (
29
+ self.cdf_views_by_reference
30
+ and (remote := self.cdf_views_by_reference.get(ref))
31
+ and remote.properties
32
+ ):
33
+ continue
34
+
35
+ # Implemented views have properties
36
+ if view.implements and any(
37
+ self.cdf_views_by_reference
38
+ and (remote_implement := self.cdf_views_by_reference.get(implement))
39
+ and remote_implement.properties
40
+ for implement in view.implements or []
41
+ ):
42
+ continue
43
+
44
+ views_without_properties.append(ref)
45
+
46
+ return [
47
+ ConsistencyError(
48
+ message=(
49
+ f"View {ref!s} does "
50
+ "not have any properties defined, either directly or through implements."
51
+ " This will prohibit your from deploying the data model to CDF."
52
+ ),
53
+ fix="Define properties for the view",
54
+ code=self.code,
55
+ )
56
+ for ref in views_without_properties
57
+ ]
58
+
59
+
60
+ class UndefinedConnectionEndNodeTypes(DataModelValidator):
61
+ """This validator checks for connections where the end node types are not defined"""
62
+
63
+ code = "NEAT-DMS-002"
64
+
65
+ def __init__(
66
+ self,
67
+ local_connection_end_node_types: dict[tuple[ViewReference, str], ViewReference],
68
+ local_views_by_reference: dict[ViewReference, ViewRequest],
69
+ cdf_views_by_reference: dict[ViewReference, ViewRequest],
70
+ ) -> None:
71
+ self.local_connection_end_node_types = local_connection_end_node_types
72
+ self.local_views_by_reference = local_views_by_reference
73
+ self.cdf_views_by_reference = cdf_views_by_reference
74
+
75
+ def run(self) -> list[ConsistencyError]:
76
+ undefined_value_types = []
77
+
78
+ for (view, property_), value_type in self.local_connection_end_node_types.items():
79
+ if value_type not in self.local_views_by_reference and value_type not in self.cdf_views_by_reference:
80
+ undefined_value_types.append((view, property_, value_type))
81
+
82
+ return [
83
+ ConsistencyError(
84
+ message=(
85
+ f"View {view!s} property {property_!s} has value type {value_type!s} "
86
+ "which is not defined as a view in the data model neither exists in CDF."
87
+ " This will prohibit you from deploying the data model to CDF."
88
+ ),
89
+ fix="Define necessary view",
90
+ code=self.code,
91
+ )
92
+ for (view, property_, value_type) in undefined_value_types
93
+ ]
94
+
95
+
96
+ class VersionSpaceInconsistency(DataModelValidator):
97
+ """This validator checks for inconsistencies in versioning and space among views and data model"""
98
+
99
+ code = "NEAT-DMS-003"
100
+
101
+ def __init__(
102
+ self,
103
+ data_model_reference: DataModelReference,
104
+ view_references: list[ViewReference],
105
+ ) -> None:
106
+ self.data_model_reference = data_model_reference
107
+ self.view_references = view_references
108
+
109
+ def run(self) -> list[Recommendation]:
110
+ recommendations: list[Recommendation] = []
111
+
112
+ for view_ref in self.view_references:
113
+ issue_description = ""
114
+
115
+ if view_ref.space not in COGNITE_SPACES:
116
+ # notify about inconsisten space
117
+ if view_ref.space != self.data_model_reference.space:
118
+ issue_description = f"space (view: {view_ref.space}, data model: {self.data_model_reference.space})"
119
+
120
+ # or version if spaces are same
121
+ elif view_ref.version != self.data_model_reference.version:
122
+ issue_description = (
123
+ f"version (view: {view_ref.version}, data model: {self.data_model_reference.version})"
124
+ )
125
+
126
+ if issue_description:
127
+ recommendations.append(
128
+ Recommendation(
129
+ message=(
130
+ f"View {view_ref!s} has inconsistent {issue_description} "
131
+ "with the data model."
132
+ " This may lead to more demanding development and maintenance efforts."
133
+ ),
134
+ fix="Update view version and/or space to match data model",
135
+ code=self.code,
136
+ )
137
+ )
138
+
139
+ return recommendations
@@ -1,4 +1,8 @@
1
- from ._issues import ModelSyntaxError
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from cognite.neat._issues import ModelSyntaxError
5
+ from cognite.neat._utils.http_client import HTTPMessage
2
6
 
3
7
 
4
8
  class NeatException(Exception):
@@ -7,11 +11,19 @@ class NeatException(Exception):
7
11
  pass
8
12
 
9
13
 
10
- class DataModelImportError(NeatException):
14
+ class DataModelImportException(NeatException):
11
15
  """Raised when there is an error importing a model."""
12
16
 
13
- def __init__(self, errors: list[ModelSyntaxError]) -> None:
17
+ def __init__(self, errors: "list[ModelSyntaxError]") -> None:
18
+ super().__init__(errors)
14
19
  self.errors = errors
15
20
 
16
21
  def __str__(self) -> str:
17
22
  return f"Model import failed with {len(self.errors)} errors: " + "; ".join(map(str, self.errors))
23
+
24
+
25
+ class CDFAPIException(NeatException):
26
+ """Raised when there is an error in an API call."""
27
+
28
+ def __init__(self, messages: "list[HTTPMessage]") -> None:
29
+ self.messages = messages
cognite/neat/_issues.py CHANGED
@@ -1,14 +1,23 @@
1
+ from collections import UserList, defaultdict
2
+
1
3
  from pydantic import BaseModel
2
4
 
3
5
 
4
- class ModelSyntaxError(BaseModel):
6
+ class Issue(BaseModel):
7
+ """Base class for all issues"""
8
+
9
+ message: str
10
+ code: str | None = None
11
+
12
+
13
+ class ModelSyntaxError(Issue):
5
14
  """If any syntax error is found. Stop validation
6
15
  and ask user to fix the syntax error first."""
7
16
 
8
- message: str
17
+ ...
9
18
 
10
19
 
11
- class ImplementationWarning(BaseModel):
20
+ class ImplementationWarning(Issue):
12
21
  """This is only for conceptual data model. It means that conversion to DMS
13
22
  will fail unless user implements the missing part."""
14
23
 
@@ -16,18 +25,42 @@ class ImplementationWarning(BaseModel):
16
25
  fix: str
17
26
 
18
27
 
19
- class ConsistencyError(BaseModel):
28
+ class ConsistencyError(Issue):
20
29
  """If any consistency error is found, the deployment of the data model will fail. For example,
21
30
  if a reverse direct relations points to a non-existing direct relation. This is only relevant for
22
31
  DMS model.
23
32
  """
24
33
 
25
34
  message: str
26
- fix: str
35
+ fix: str | None = None
27
36
 
28
37
 
29
- class Recommendation(BaseModel):
38
+ class Recommendation(Issue):
30
39
  """Best practice recommendation."""
31
40
 
32
41
  message: str
33
42
  fix: str | None = None
43
+
44
+
45
+ class IssueList(UserList[Issue]):
46
+ """A list of issues that can be sorted by type and message."""
47
+
48
+ def by_type(self) -> dict[type[Issue], list[Issue]]:
49
+ """Returns a dictionary of issues sorted by their type."""
50
+ result: dict[type[Issue], list[Issue]] = defaultdict(list)
51
+ for issue in self.data:
52
+ issue_type = type(issue)
53
+ if issue_type not in result:
54
+ result[issue_type] = []
55
+ result[issue_type].append(issue)
56
+ return result
57
+
58
+ def by_code(self) -> dict[str, list[Issue]]:
59
+ """Returns a dictionary of issues sorted by their code."""
60
+ result: dict[str, list[Issue]] = defaultdict(list)
61
+ for issue in self.data:
62
+ if issue.code is not None:
63
+ result[issue.code].append(issue)
64
+ else:
65
+ result["UNDEFINED"].append(issue)
66
+ return dict(result)
@@ -0,0 +1,3 @@
1
+ from ._session import NeatSession
2
+
3
+ __all__ = ["NeatSession"]
@@ -0,0 +1,88 @@
1
+ from typing import Any
2
+
3
+ from cognite.neat._client import NeatClient
4
+ from cognite.neat._data_model.exporters import DMSExcelExporter, DMSYamlExporter
5
+ from cognite.neat._data_model.importers import DMSAPIImporter, DMSTableImporter
6
+ from cognite.neat._data_model.models.dms import DataModelReference
7
+ from cognite.neat._data_model.validation.dms import DmsDataModelValidation
8
+ from cognite.neat._store._store import NeatStore
9
+ from cognite.neat._utils._reader import NeatReader
10
+
11
+ from ._wrappers import session_wrapper
12
+
13
+
14
+ class PhysicalDataModel:
15
+ """Read from a data source into NeatSession graph store."""
16
+
17
+ def __init__(self, store: NeatStore, client: NeatClient) -> None:
18
+ self._store = store
19
+ self._client = client
20
+ self.read = ReadPhysicalDataModel(self._store, self._client)
21
+ self.write = WritePhysicalDataModel(self._store)
22
+
23
+
24
+ @session_wrapper
25
+ class ReadPhysicalDataModel:
26
+ """Read physical data model from various sources into NeatSession graph store."""
27
+
28
+ def __init__(self, store: NeatStore, client: NeatClient) -> None:
29
+ self._store = store
30
+ self._client = client
31
+
32
+ def yaml(self, io: Any) -> None:
33
+ """Read physical data model from YAML file"""
34
+
35
+ path = NeatReader.create(io).materialize_path()
36
+ reader = DMSTableImporter.from_yaml(path)
37
+ on_success = DmsDataModelValidation(self._client)
38
+
39
+ return self._store.read_physical(reader, on_success)
40
+
41
+ def excel(self, io: Any) -> None:
42
+ """Read physical data model from Excel file"""
43
+
44
+ path = NeatReader.create(io).materialize_path()
45
+ reader = DMSTableImporter.from_excel(path)
46
+ on_success = DmsDataModelValidation(self._client)
47
+
48
+ return self._store.read_physical(reader, on_success)
49
+
50
+ def cdf(self, space: str, external_id: str, version: str) -> None:
51
+ """Read physical data model from CDF
52
+
53
+ Args:
54
+ space (str): The schema space of the data model.
55
+ external_id (str): The external id of the data model.
56
+ version (str): The version of the data model.
57
+
58
+ """
59
+ reader = DMSAPIImporter.from_cdf(
60
+ DataModelReference(space=space, external_id=external_id, version=version), self._client
61
+ )
62
+ on_success = DmsDataModelValidation(self._client)
63
+
64
+ return self._store.read_physical(reader, on_success)
65
+
66
+
67
+ @session_wrapper
68
+ class WritePhysicalDataModel:
69
+ """Write physical data model to various sources from NeatSession graph store."""
70
+
71
+ def __init__(self, store: NeatStore) -> None:
72
+ self._store = store
73
+
74
+ def yaml(self, io: Any) -> None:
75
+ """Write physical data model to YAML file"""
76
+
77
+ file_path = NeatReader.create(io).materialize_path()
78
+ writer = DMSYamlExporter()
79
+
80
+ return self._store.write_physical(writer, file_path=file_path)
81
+
82
+ def excel(self, io: Any) -> None:
83
+ """Write physical data model to Excel file"""
84
+
85
+ file_path = NeatReader.create(io).materialize_path()
86
+ writer = DMSExcelExporter()
87
+
88
+ return self._store.write_physical(writer, file_path=file_path)
@@ -1,4 +1,9 @@
1
- from ._state_machine import EmptyState, ForbiddenState, State
1
+ from cognite.client import ClientConfig, CogniteClient
2
+
3
+ from cognite.neat._client import NeatClient
4
+ from cognite.neat._store import NeatStore
5
+
6
+ from ._physical import PhysicalDataModel
2
7
 
3
8
 
4
9
  class NeatSession:
@@ -7,27 +12,31 @@ class NeatSession:
7
12
  the state machine for data model and instance operations.
8
13
  """
9
14
 
10
- def __init__(self) -> None:
11
- self.state: State = EmptyState()
12
-
13
- def _execute_event(self, event: str) -> bool:
14
- """Place holder function for executing events and transitioning states.
15
- It will be modified to include actual logic as we progress with v1 of neat.
16
-
17
- """
18
- print(f"\n--- Executing event: '{event}' from {self.state} ---")
19
-
20
- old_state = self.state
21
- new_state = self.state.on_event(event)
22
-
23
- # Handle ForbiddenState
24
- if isinstance(new_state, ForbiddenState):
25
- print(f"❌ Event '{event}' is FORBIDDEN from {old_state}")
26
- # Return to previous state (as per your table logic)
27
- self.state = new_state.on_event("undo")
28
- print(f"↩️ Returned to: {self.state}")
29
- return False
30
- else:
31
- self.state = new_state
32
- print(f"✅ Transition successful: {old_state} → {self.state}")
33
- return True
15
+ def __init__(self, cognite_client: CogniteClient | ClientConfig) -> None:
16
+ self._store = NeatStore()
17
+ self._client = NeatClient(cognite_client)
18
+ self.physical_data_model = PhysicalDataModel(self._store, self._client)
19
+ self.issues = Issues(self._store)
20
+
21
+
22
+ class Issues:
23
+ """Class to handle issues in the NeatSession."""
24
+
25
+ def __init__(self, store: NeatStore) -> None:
26
+ self._store = store
27
+
28
+ def __call__(self) -> None:
29
+ if change := self._store.provenance.last_change:
30
+ if change.errors:
31
+ print("Critical Issues")
32
+ for type_, issues in change.errors.by_type().items():
33
+ print(f"{type_.__name__}:")
34
+ for issue in issues:
35
+ print(f"- {issue.message}")
36
+
37
+ if change.issues:
38
+ print("Non-Critical Issues")
39
+ for type_, issues in change.issues.by_type().items():
40
+ print(f"{type_.__name__}:")
41
+ for issue in issues:
42
+ print(f"- {issue.message}")
@@ -0,0 +1,61 @@
1
+ from collections.abc import Callable
2
+ from functools import wraps
3
+ from typing import Any, Protocol, TypeVar
4
+
5
+ from cognite.neat._store._store import NeatStore
6
+ from cognite.neat._utils.text import split_on_capitals
7
+
8
+
9
+ class HasStore(Protocol):
10
+ _store: NeatStore
11
+
12
+
13
+ T_Class = TypeVar("T_Class", bound=object)
14
+
15
+
16
+ def session_wrapper(cls: type[T_Class]) -> type[T_Class]:
17
+ # 1. Define the method decorator inside
18
+ def _handle_method_call(func: Callable[..., Any]) -> Callable[..., Any]:
19
+ """Decorator to handle exceptions and print provenance length"""
20
+
21
+ @wraps(func)
22
+ def wrapper(self: HasStore, *args: Any, **kwargs: Any) -> Any:
23
+ try:
24
+ res = func(self, *args, **kwargs)
25
+ change = self._store.provenance[-1]
26
+
27
+ issues_count = len(change.issues) if change.issues else 0
28
+ errors_count = len(change.errors) if change.errors else 0
29
+ total_issues = issues_count + errors_count
30
+
31
+ newline = "\n" # python 3.10 compatibility
32
+ print(
33
+ f"{' '.join(split_on_capitals(cls.__name__))} - {func.__name__} "
34
+ f"{'✅' if change.successful else '❌'}"
35
+ f" | Issues: {total_issues}"
36
+ f" (of which {errors_count} critical)"
37
+ f"{newline + 'For more details run neat.issues()' if change.issues or change.errors else ''}"
38
+ )
39
+
40
+ return res
41
+
42
+ # if an error occurs, we catch it and print it out instead of
43
+ # getting a full traceback
44
+ except Exception as e:
45
+ print(f"{' '.join(split_on_capitals(cls.__name__))} - {func.__name__} ❌")
46
+ print(f"Error: {e}")
47
+
48
+ return wrapper
49
+
50
+ # Iterate through all attributes of the class
51
+ for attr_name in dir(cls):
52
+ # Skip private/protected methods (starting with _)
53
+ if not attr_name.startswith("_"):
54
+ attr = getattr(cls, attr_name)
55
+ # Only wrap callable methods
56
+ if callable(attr):
57
+ # Replace the original method with wrapped version
58
+ setattr(cls, attr_name, _handle_method_call(attr))
59
+
60
+ # Return the modified class
61
+ return cls
@@ -0,0 +1,10 @@
1
+ from ._base import State
2
+ from ._states import EmptyState, ForbiddenState, PhysicalState, Undo
3
+
4
+ __all__ = [
5
+ "EmptyState",
6
+ "ForbiddenState",
7
+ "PhysicalState",
8
+ "State",
9
+ "Undo",
10
+ ]
@@ -1,4 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
+ from typing import Any
2
3
 
3
4
 
4
5
  class State(ABC):
@@ -8,12 +9,21 @@ class State(ABC):
8
9
  self._store = None
9
10
 
10
11
  @abstractmethod
11
- def on_event(self, event: str) -> "State":
12
+ def transition(self, event: Any) -> "State":
12
13
  """
13
14
  Handle events that are delegated to this State.
14
15
  """
15
16
  raise NotImplementedError("on_event() must be implemented by the subclass.")
16
17
 
18
+ def can_transition(self, event: Any) -> bool:
19
+ """
20
+ Check if the state can transition on the given event.
21
+ """
22
+ # avoiding circular import
23
+ from cognite.neat._state_machine._states import ForbiddenState
24
+
25
+ return not isinstance(self.transition(event), ForbiddenState)
26
+
17
27
  def __repr__(self) -> str:
18
28
  """
19
29
  Leverages the __str__ method to describe the State.