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.
- cognite/neat/_client/__init__.py +4 -0
- cognite/neat/_client/api.py +8 -0
- cognite/neat/_client/client.py +19 -0
- cognite/neat/_client/config.py +40 -0
- cognite/neat/_client/containers_api.py +73 -0
- cognite/neat/_client/data_classes.py +10 -0
- cognite/neat/_client/data_model_api.py +63 -0
- cognite/neat/_client/spaces_api.py +67 -0
- cognite/neat/_client/views_api.py +82 -0
- cognite/neat/_data_model/_analysis.py +127 -0
- cognite/neat/_data_model/_constants.py +59 -0
- cognite/neat/_data_model/_shared.py +46 -0
- cognite/neat/_data_model/deployer/__init__.py +0 -0
- cognite/neat/_data_model/deployer/_differ.py +113 -0
- cognite/neat/_data_model/deployer/_differ_container.py +354 -0
- cognite/neat/_data_model/deployer/_differ_data_model.py +29 -0
- cognite/neat/_data_model/deployer/_differ_space.py +9 -0
- cognite/neat/_data_model/deployer/_differ_view.py +194 -0
- cognite/neat/_data_model/deployer/data_classes.py +176 -0
- cognite/neat/_data_model/exporters/__init__.py +4 -0
- cognite/neat/_data_model/exporters/_base.py +6 -1
- cognite/neat/_data_model/exporters/_table_exporter/__init__.py +0 -0
- cognite/neat/_data_model/exporters/_table_exporter/exporter.py +106 -0
- cognite/neat/_data_model/exporters/_table_exporter/workbook.py +414 -0
- cognite/neat/_data_model/exporters/_table_exporter/writer.py +391 -0
- cognite/neat/_data_model/importers/__init__.py +2 -1
- cognite/neat/_data_model/importers/_api_importer.py +88 -0
- cognite/neat/_data_model/importers/_table_importer/data_classes.py +48 -8
- cognite/neat/_data_model/importers/_table_importer/importer.py +74 -5
- cognite/neat/_data_model/importers/_table_importer/reader.py +63 -7
- cognite/neat/_data_model/models/dms/__init__.py +17 -1
- cognite/neat/_data_model/models/dms/_base.py +12 -8
- cognite/neat/_data_model/models/dms/_constants.py +1 -1
- cognite/neat/_data_model/models/dms/_constraints.py +2 -1
- cognite/neat/_data_model/models/dms/_container.py +5 -5
- cognite/neat/_data_model/models/dms/_data_model.py +3 -3
- cognite/neat/_data_model/models/dms/_data_types.py +8 -1
- cognite/neat/_data_model/models/dms/_http.py +18 -0
- cognite/neat/_data_model/models/dms/_indexes.py +2 -1
- cognite/neat/_data_model/models/dms/_references.py +17 -4
- cognite/neat/_data_model/models/dms/_space.py +11 -7
- cognite/neat/_data_model/models/dms/_view_property.py +7 -4
- cognite/neat/_data_model/models/dms/_views.py +16 -6
- cognite/neat/_data_model/validation/__init__.py +0 -0
- cognite/neat/_data_model/validation/_base.py +16 -0
- cognite/neat/_data_model/validation/dms/__init__.py +9 -0
- cognite/neat/_data_model/validation/dms/_orchestrator.py +68 -0
- cognite/neat/_data_model/validation/dms/_validators.py +139 -0
- cognite/neat/_exceptions.py +15 -3
- cognite/neat/_issues.py +39 -6
- cognite/neat/_session/__init__.py +3 -0
- cognite/neat/_session/_physical.py +88 -0
- cognite/neat/_session/_session.py +34 -25
- cognite/neat/_session/_wrappers.py +61 -0
- cognite/neat/_state_machine/__init__.py +10 -0
- cognite/neat/{_session/_state_machine → _state_machine}/_base.py +11 -1
- cognite/neat/_state_machine/_states.py +53 -0
- cognite/neat/_store/__init__.py +3 -0
- cognite/neat/_store/_provenance.py +55 -0
- cognite/neat/_store/_store.py +124 -0
- cognite/neat/_utils/_reader.py +194 -0
- cognite/neat/_utils/http_client/__init__.py +14 -20
- cognite/neat/_utils/http_client/_client.py +22 -61
- cognite/neat/_utils/http_client/_data_classes.py +167 -268
- cognite/neat/_utils/text.py +6 -0
- cognite/neat/_utils/useful_types.py +23 -2
- cognite/neat/_version.py +1 -1
- cognite/neat/v0/core/_data_model/importers/_rdf/_shared.py +2 -2
- {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/METADATA +1 -1
- {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/RECORD +72 -38
- cognite/neat/_data_model/exporters/_table_exporter.py +0 -35
- cognite/neat/_session/_state_machine/__init__.py +0 -23
- cognite/neat/_session/_state_machine/_states.py +0 -150
- {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/WHEEL +0 -0
- {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,
|
|
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,
|
|
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,
|
|
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
|
cognite/neat/_exceptions.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from
|
|
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
|
|
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
|
|
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
|
-
|
|
17
|
+
...
|
|
9
18
|
|
|
10
19
|
|
|
11
|
-
class ImplementationWarning(
|
|
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(
|
|
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(
|
|
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,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 .
|
|
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.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
@@ -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
|
|
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.
|