cognite-neat 0.98.0__py3-none-any.whl → 0.99.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/data_modeling_loaders.py +585 -0
- cognite/neat/_client/_api/schema.py +111 -0
- cognite/neat/_client/_api_client.py +17 -0
- cognite/neat/_client/data_classes/__init__.py +0 -0
- cognite/neat/{_utils/cdf/data_classes.py → _client/data_classes/data_modeling.py} +8 -135
- cognite/neat/_client/data_classes/schema.py +495 -0
- cognite/neat/_constants.py +27 -4
- cognite/neat/_graph/_shared.py +14 -15
- cognite/neat/_graph/extractors/_classic_cdf/_assets.py +14 -154
- cognite/neat/_graph/extractors/_classic_cdf/_base.py +154 -7
- cognite/neat/_graph/extractors/_classic_cdf/_classic.py +25 -14
- cognite/neat/_graph/extractors/_classic_cdf/_data_sets.py +17 -92
- cognite/neat/_graph/extractors/_classic_cdf/_events.py +13 -162
- cognite/neat/_graph/extractors/_classic_cdf/_files.py +15 -179
- cognite/neat/_graph/extractors/_classic_cdf/_labels.py +32 -100
- cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +27 -178
- cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +14 -139
- cognite/neat/_graph/extractors/_classic_cdf/_timeseries.py +15 -173
- cognite/neat/_graph/extractors/_rdf_file.py +6 -7
- cognite/neat/_graph/loaders/_rdf2dms.py +2 -2
- cognite/neat/_graph/queries/_base.py +17 -1
- cognite/neat/_graph/transformers/_classic_cdf.py +74 -147
- cognite/neat/_graph/transformers/_prune_graph.py +1 -1
- cognite/neat/_graph/transformers/_rdfpath.py +1 -1
- cognite/neat/_issues/_base.py +26 -17
- cognite/neat/_issues/errors/__init__.py +4 -2
- cognite/neat/_issues/errors/_external.py +7 -0
- cognite/neat/_issues/errors/_properties.py +2 -7
- cognite/neat/_issues/errors/_resources.py +1 -1
- cognite/neat/_issues/warnings/__init__.py +8 -0
- cognite/neat/_issues/warnings/_external.py +16 -0
- cognite/neat/_issues/warnings/_properties.py +16 -0
- cognite/neat/_issues/warnings/_resources.py +26 -2
- cognite/neat/_issues/warnings/user_modeling.py +4 -4
- cognite/neat/_rules/_constants.py +8 -11
- cognite/neat/_rules/analysis/_base.py +8 -4
- cognite/neat/_rules/exporters/_base.py +3 -4
- cognite/neat/_rules/exporters/_rules2dms.py +33 -46
- cognite/neat/_rules/importers/__init__.py +1 -3
- cognite/neat/_rules/importers/_base.py +1 -1
- cognite/neat/_rules/importers/_dms2rules.py +6 -29
- cognite/neat/_rules/importers/_rdf/__init__.py +5 -0
- cognite/neat/_rules/importers/_rdf/_base.py +34 -11
- cognite/neat/_rules/importers/_rdf/_imf2rules.py +91 -0
- cognite/neat/_rules/importers/_rdf/_inference2rules.py +43 -35
- cognite/neat/_rules/importers/_rdf/_owl2rules.py +80 -0
- cognite/neat/_rules/importers/_rdf/_shared.py +138 -441
- cognite/neat/_rules/models/__init__.py +1 -1
- cognite/neat/_rules/models/_base_rules.py +22 -12
- cognite/neat/_rules/models/dms/__init__.py +4 -2
- cognite/neat/_rules/models/dms/_exporter.py +45 -48
- cognite/neat/_rules/models/dms/_rules.py +20 -17
- cognite/neat/_rules/models/dms/_rules_input.py +52 -8
- cognite/neat/_rules/models/dms/_validation.py +391 -119
- cognite/neat/_rules/models/entities/_single_value.py +32 -4
- cognite/neat/_rules/models/information/__init__.py +2 -0
- cognite/neat/_rules/models/information/_rules.py +0 -67
- cognite/neat/_rules/models/information/_validation.py +9 -9
- cognite/neat/_rules/models/mapping/__init__.py +2 -3
- cognite/neat/_rules/models/mapping/_classic2core.py +36 -146
- cognite/neat/_rules/models/mapping/_classic2core.yaml +343 -0
- cognite/neat/_rules/transformers/__init__.py +2 -2
- cognite/neat/_rules/transformers/_converters.py +110 -11
- cognite/neat/_rules/transformers/_mapping.py +105 -30
- cognite/neat/_rules/transformers/_pipelines.py +1 -1
- cognite/neat/_rules/transformers/_verification.py +31 -3
- cognite/neat/_session/_base.py +24 -8
- cognite/neat/_session/_drop.py +35 -0
- cognite/neat/_session/_inspect.py +17 -5
- cognite/neat/_session/_mapping.py +39 -0
- cognite/neat/_session/_prepare.py +219 -23
- cognite/neat/_session/_read.py +49 -12
- cognite/neat/_session/_to.py +8 -5
- cognite/neat/_session/exceptions.py +4 -0
- cognite/neat/_store/_base.py +27 -24
- cognite/neat/_utils/rdf_.py +34 -5
- cognite/neat/_version.py +1 -1
- cognite/neat/_workflows/steps/lib/current/rules_exporter.py +5 -88
- cognite/neat/_workflows/steps/lib/current/rules_importer.py +3 -14
- cognite/neat/_workflows/steps/lib/current/rules_validator.py +6 -7
- {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/METADATA +3 -3
- {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/RECORD +87 -92
- cognite/neat/_rules/importers/_rdf/_imf2rules/__init__.py +0 -3
- cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2classes.py +0 -86
- cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2metadata.py +0 -29
- cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2properties.py +0 -130
- cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2rules.py +0 -154
- cognite/neat/_rules/importers/_rdf/_owl2rules/__init__.py +0 -3
- cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2classes.py +0 -58
- cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2metadata.py +0 -65
- cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2properties.py +0 -59
- cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2rules.py +0 -39
- cognite/neat/_rules/models/dms/_schema.py +0 -1101
- cognite/neat/_rules/models/mapping/_base.py +0 -131
- cognite/neat/_utils/cdf/loaders/__init__.py +0 -25
- cognite/neat/_utils/cdf/loaders/_base.py +0 -54
- cognite/neat/_utils/cdf/loaders/_data_modeling.py +0 -339
- cognite/neat/_utils/cdf/loaders/_ingestion.py +0 -167
- /cognite/neat/{_utils/cdf → _client/_api}/__init__.py +0 -0
- {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/LICENSE +0 -0
- {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/WHEEL +0 -0
- {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,36 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
1
|
+
import warnings
|
|
2
|
+
from collections import Counter, defaultdict
|
|
3
|
+
from typing import ClassVar
|
|
3
4
|
|
|
4
5
|
from cognite.client import data_modeling as dm
|
|
6
|
+
from cognite.client.data_classes.data_modeling import ContainerList, ViewId, ViewList
|
|
7
|
+
from cognite.client.data_classes.data_modeling.views import (
|
|
8
|
+
ReverseDirectRelation,
|
|
9
|
+
ReverseDirectRelationApply,
|
|
10
|
+
ViewProperty,
|
|
11
|
+
ViewPropertyApply,
|
|
12
|
+
)
|
|
5
13
|
|
|
6
|
-
from cognite.neat.
|
|
7
|
-
from cognite.neat.
|
|
14
|
+
from cognite.neat._client import NeatClient
|
|
15
|
+
from cognite.neat._client.data_classes.data_modeling import ViewApplyDict
|
|
16
|
+
from cognite.neat._client.data_classes.schema import DMSSchema
|
|
17
|
+
from cognite.neat._constants import COGNITE_MODELS, DMS_CONTAINER_PROPERTY_SIZE_LIMIT, DMS_VIEW_CONTAINER_SIZE_LIMIT
|
|
18
|
+
from cognite.neat._issues import IssueList, NeatError, NeatIssueList
|
|
8
19
|
from cognite.neat._issues.errors import (
|
|
20
|
+
CDFMissingClientError,
|
|
9
21
|
PropertyDefinitionDuplicatedError,
|
|
10
|
-
|
|
22
|
+
PropertyMappingDuplicatedError,
|
|
23
|
+
PropertyNotFoundError,
|
|
24
|
+
ResourceDuplicatedError,
|
|
25
|
+
ResourceNotFoundError,
|
|
26
|
+
ReversedConnectionNotFeasibleError,
|
|
11
27
|
)
|
|
12
|
-
from cognite.neat._issues.errors._properties import ReversedConnectionNotFeasibleError
|
|
13
28
|
from cognite.neat._issues.warnings import (
|
|
14
29
|
NotSupportedHasDataFilterLimitWarning,
|
|
15
30
|
NotSupportedViewContainerLimitWarning,
|
|
16
31
|
UndefinedViewWarning,
|
|
17
32
|
)
|
|
18
33
|
from cognite.neat._issues.warnings.user_modeling import (
|
|
34
|
+
ContainerPropertyLimitWarning,
|
|
35
|
+
DirectRelationMissingSourceWarning,
|
|
19
36
|
NotNeatSupportedFilterWarning,
|
|
20
|
-
ViewPropertyLimitWarning,
|
|
21
37
|
)
|
|
22
38
|
from cognite.neat._rules.models.data_types import DataType
|
|
23
39
|
from cognite.neat._rules.models.entities import ContainerEntity, RawFilter
|
|
24
40
|
from cognite.neat._rules.models.entities._single_value import (
|
|
25
|
-
ReverseConnectionEntity,
|
|
26
41
|
ViewEntity,
|
|
27
42
|
)
|
|
28
43
|
|
|
29
44
|
from ._rules import DMSProperty, DMSRules
|
|
30
|
-
from ._schema import DMSSchema
|
|
31
45
|
|
|
32
46
|
|
|
33
|
-
class
|
|
47
|
+
class DMSValidation:
|
|
34
48
|
"""This class does all the validation of the DMS rules that have dependencies between
|
|
35
49
|
components."""
|
|
36
50
|
|
|
@@ -38,32 +52,132 @@ class DMSPostValidation:
|
|
|
38
52
|
# For example, changing the filter is allowed, but changing the properties is not.
|
|
39
53
|
changeable_view_attributes: ClassVar[set[str]] = {"filter"}
|
|
40
54
|
|
|
41
|
-
def __init__(self, rules: DMSRules):
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
45
|
-
self.
|
|
46
|
-
self.
|
|
47
|
-
self.
|
|
55
|
+
def __init__(self, rules: DMSRules, client: NeatClient | None = None) -> None:
|
|
56
|
+
self._rules = rules
|
|
57
|
+
self._client = client
|
|
58
|
+
self._metadata = rules.metadata
|
|
59
|
+
self._properties = rules.properties
|
|
60
|
+
self._containers = rules.containers
|
|
61
|
+
self._views = rules.views
|
|
62
|
+
|
|
63
|
+
def imported_views_and_containers_ids(
|
|
64
|
+
self, include_views_with_no_properties: bool = True
|
|
65
|
+
) -> tuple[set[ViewEntity], set[ContainerEntity]]:
|
|
66
|
+
existing_views = {view.view for view in self._views}
|
|
67
|
+
imported_views: set[ViewEntity] = set()
|
|
68
|
+
for view in self._views:
|
|
69
|
+
for parent in view.implements or []:
|
|
70
|
+
if parent not in existing_views:
|
|
71
|
+
imported_views.add(parent)
|
|
72
|
+
existing_containers = {container.container for container in self._containers or []}
|
|
73
|
+
imported_containers: set[ContainerEntity] = set()
|
|
74
|
+
view_with_properties: set[ViewEntity] = set()
|
|
75
|
+
for prop in self._properties:
|
|
76
|
+
if prop.container and prop.container not in existing_containers:
|
|
77
|
+
imported_containers.add(prop.container)
|
|
78
|
+
if prop.view not in existing_views:
|
|
79
|
+
imported_views.add(prop.view)
|
|
80
|
+
view_with_properties.add(prop.view)
|
|
81
|
+
|
|
82
|
+
if include_views_with_no_properties:
|
|
83
|
+
extra_views = existing_views - view_with_properties
|
|
84
|
+
imported_views.update({view for view in extra_views})
|
|
85
|
+
|
|
86
|
+
return imported_views, imported_containers
|
|
48
87
|
|
|
49
88
|
def validate(self) -> NeatIssueList:
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
self.
|
|
89
|
+
imported_views, imported_containers = self.imported_views_and_containers_ids(
|
|
90
|
+
include_views_with_no_properties=False
|
|
91
|
+
)
|
|
92
|
+
if (imported_views or imported_containers) and self._client is None:
|
|
93
|
+
raise CDFMissingClientError(
|
|
94
|
+
f"{self._rules.metadata.as_data_model_id()} has imported views and/or container: "
|
|
95
|
+
f"{imported_views}, {imported_containers}."
|
|
96
|
+
)
|
|
97
|
+
referenced_views = ViewList([])
|
|
98
|
+
referenced_containers = ContainerList([])
|
|
99
|
+
if self._client:
|
|
100
|
+
referenced_views = self._client.loaders.views.retrieve(
|
|
101
|
+
list(imported_views), include_connected=True, include_ancestor=True
|
|
102
|
+
)
|
|
103
|
+
referenced_containers = self._client.loaders.containers.retrieve(
|
|
104
|
+
list(imported_containers), include_connected=True
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Setup data structures for validation
|
|
108
|
+
dms_schema = self._rules.as_schema()
|
|
109
|
+
ref_view_by_id = {view.as_id(): view for view in referenced_views}
|
|
110
|
+
ref_container_by_id = {container.as_id(): container for container in referenced_containers}
|
|
111
|
+
all_containers_by_id: dict[dm.ContainerId, dm.ContainerApply | dm.Container] = {
|
|
112
|
+
**dict(dms_schema.containers.items()),
|
|
113
|
+
**ref_container_by_id,
|
|
114
|
+
}
|
|
115
|
+
all_views_by_id: dict[dm.ViewId, dm.ViewApply | dm.View] = {**dict(dms_schema.views.items()), **ref_view_by_id}
|
|
116
|
+
properties_by_ids = self._as_properties_by_ids(dms_schema, ref_view_by_id)
|
|
117
|
+
view_properties_by_id: dict[dm.ViewId, list[tuple[str, ViewProperty | ViewPropertyApply]]] = defaultdict(list)
|
|
118
|
+
for (view_id, prop_id), prop in properties_by_ids.items():
|
|
119
|
+
view_properties_by_id[view_id].append((prop_id, prop))
|
|
120
|
+
|
|
121
|
+
issue_list = IssueList()
|
|
122
|
+
# Neat DMS classes Validation
|
|
123
|
+
# These are errors that can only happen due to the format of the Neat DMS classes
|
|
124
|
+
issue_list.extend(self._validate_raw_filter())
|
|
125
|
+
issue_list.extend(self._consistent_container_properties())
|
|
126
|
+
issue_list.extend(self._validate_value_type_existence())
|
|
127
|
+
issue_list.extend(
|
|
128
|
+
self._validate_property_referenced_views_and_containers_exists(all_views_by_id, all_containers_by_id)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# SDK classes validation
|
|
132
|
+
issue_list.extend(self._containers_are_proper_size(dms_schema))
|
|
133
|
+
issue_list.extend(self._validate_reverse_connections(properties_by_ids, all_containers_by_id))
|
|
134
|
+
issue_list.extend(self._validate_schema(dms_schema, all_views_by_id, all_containers_by_id))
|
|
135
|
+
issue_list.extend(self._validate_referenced_container_limits(dms_schema.views, view_properties_by_id))
|
|
136
|
+
return issue_list
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _as_properties_by_ids(
|
|
140
|
+
dms_schema: DMSSchema, ref_view_by_id: dict[dm.ViewId, dm.View]
|
|
141
|
+
) -> dict[tuple[ViewId, str], ViewPropertyApply | ViewProperty]:
|
|
142
|
+
# Priority DMS schema properties.
|
|
143
|
+
# No need to do long lookups in ref_views as these already contain all ancestor properties.
|
|
144
|
+
properties_by_id: dict[tuple[ViewId, str], ViewPropertyApply | ViewProperty] = {}
|
|
145
|
+
for view in dms_schema.views.values():
|
|
146
|
+
view_id = view.as_id()
|
|
147
|
+
for prop_id, prop in (view.properties or {}).items():
|
|
148
|
+
properties_by_id[(view_id, prop_id)] = prop
|
|
149
|
+
if view.implements:
|
|
150
|
+
to_check = view.implements.copy()
|
|
151
|
+
while to_check:
|
|
152
|
+
parent_id = to_check.pop()
|
|
153
|
+
if parent_id in dms_schema.views:
|
|
154
|
+
# Priority DMS Schema properties
|
|
155
|
+
parent_view = dms_schema.views[parent_id]
|
|
156
|
+
for prop_id, prop in (parent_view.properties or {}).items():
|
|
157
|
+
if (view_id, prop_id) not in properties_by_id:
|
|
158
|
+
properties_by_id[(view_id, prop_id)] = prop
|
|
159
|
+
to_check.extend(parent_view.implements or [])
|
|
160
|
+
elif parent_id in ref_view_by_id:
|
|
161
|
+
# SDK properties
|
|
162
|
+
parent_read_view = ref_view_by_id[parent_id]
|
|
163
|
+
for prop_id, read_prop in parent_read_view.properties.items():
|
|
164
|
+
if (view_id, prop_id) not in properties_by_id:
|
|
165
|
+
properties_by_id[(view_id, prop_id)] = read_prop
|
|
166
|
+
# Read format of views already includes all ancestor properties
|
|
167
|
+
# so no need to check further
|
|
168
|
+
else:
|
|
169
|
+
# Missing views are caught else where
|
|
170
|
+
continue
|
|
54
171
|
|
|
55
|
-
|
|
56
|
-
dms_schema = self.rules.as_schema()
|
|
57
|
-
self._validate_performance(dms_schema)
|
|
58
|
-
return self.issue_list
|
|
172
|
+
return properties_by_id
|
|
59
173
|
|
|
60
|
-
def _consistent_container_properties(self) ->
|
|
174
|
+
def _consistent_container_properties(self) -> IssueList:
|
|
61
175
|
container_properties_by_id: dict[tuple[ContainerEntity, str], list[tuple[int, DMSProperty]]] = defaultdict(list)
|
|
62
|
-
for prop_no, prop in enumerate(self.
|
|
176
|
+
for prop_no, prop in enumerate(self._properties):
|
|
63
177
|
if prop.container and prop.container_property:
|
|
64
178
|
container_properties_by_id[(prop.container, prop.container_property)].append((prop_no, prop))
|
|
65
179
|
|
|
66
|
-
errors
|
|
180
|
+
errors = IssueList()
|
|
67
181
|
for (container, prop_name), properties in container_properties_by_id.items():
|
|
68
182
|
if len(properties) == 1:
|
|
69
183
|
continue
|
|
@@ -150,132 +264,290 @@ class DMSPostValidation:
|
|
|
150
264
|
)
|
|
151
265
|
)
|
|
152
266
|
|
|
153
|
-
|
|
267
|
+
return errors
|
|
154
268
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
property_count_by_view: dict[dm.ViewId, int] = defaultdict(int)
|
|
162
|
-
errors: list[NeatIssue] = []
|
|
163
|
-
for prop_no, prop in enumerate(self.properties):
|
|
164
|
-
view_id = prop.view.as_id()
|
|
165
|
-
if view_id not in defined_views:
|
|
166
|
-
errors.append(
|
|
167
|
-
ResourceNotDefinedError[dm.ViewId](
|
|
168
|
-
identifier=view_id,
|
|
169
|
-
resource_type="view",
|
|
170
|
-
location="Views Sheet",
|
|
171
|
-
column_name="View",
|
|
172
|
-
row_number=prop_no,
|
|
173
|
-
sheet_name="Properties",
|
|
174
|
-
),
|
|
175
|
-
)
|
|
176
|
-
else:
|
|
177
|
-
property_count_by_view[view_id] += 1
|
|
178
|
-
for view_id, count in property_count_by_view.items():
|
|
269
|
+
@staticmethod
|
|
270
|
+
def _containers_are_proper_size(dms_schema: DMSSchema) -> IssueList:
|
|
271
|
+
errors = IssueList()
|
|
272
|
+
for container_id, container in dms_schema.containers.items():
|
|
273
|
+
count = len(container.properties or {})
|
|
179
274
|
if count > DMS_CONTAINER_PROPERTY_SIZE_LIMIT:
|
|
180
|
-
errors.append(
|
|
275
|
+
errors.append(ContainerPropertyLimitWarning(container_id, count))
|
|
181
276
|
|
|
182
|
-
|
|
277
|
+
return errors
|
|
183
278
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
279
|
+
@staticmethod
|
|
280
|
+
def _validate_referenced_container_limits(
|
|
281
|
+
views: ViewApplyDict, view_properties_by_id: dict[dm.ViewId, list[tuple[str, ViewProperty | ViewPropertyApply]]]
|
|
282
|
+
) -> IssueList:
|
|
283
|
+
issue_list = IssueList()
|
|
284
|
+
for view_id, view in views.items():
|
|
285
|
+
view_properties = view_properties_by_id.get(view_id, [])
|
|
286
|
+
mapped_containers = {
|
|
287
|
+
prop.container
|
|
288
|
+
for _, prop in view_properties
|
|
289
|
+
if isinstance(prop, dm.MappedPropertyApply | dm.MappedProperty)
|
|
290
|
+
}
|
|
187
291
|
|
|
188
|
-
if mapped_containers and len(mapped_containers) >
|
|
189
|
-
|
|
292
|
+
if mapped_containers and len(mapped_containers) > DMS_VIEW_CONTAINER_SIZE_LIMIT:
|
|
293
|
+
issue_list.append(
|
|
190
294
|
NotSupportedViewContainerLimitWarning(
|
|
191
295
|
view_id,
|
|
192
296
|
len(mapped_containers),
|
|
193
297
|
)
|
|
194
298
|
)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
NotSupportedHasDataFilterLimitWarning(
|
|
202
|
-
view_id,
|
|
203
|
-
len(view.filter.dump()["hasData"]),
|
|
204
|
-
)
|
|
299
|
+
|
|
300
|
+
if view.filter and isinstance(view.filter, dm.filters.HasData) and len(view.filter.dump()["hasData"]) > 10:
|
|
301
|
+
issue_list.append(
|
|
302
|
+
NotSupportedHasDataFilterLimitWarning(
|
|
303
|
+
view_id,
|
|
304
|
+
len(view.filter.dump()["hasData"]),
|
|
205
305
|
)
|
|
306
|
+
)
|
|
307
|
+
return issue_list
|
|
206
308
|
|
|
207
|
-
def _validate_raw_filter(self) ->
|
|
208
|
-
|
|
309
|
+
def _validate_raw_filter(self) -> IssueList:
|
|
310
|
+
issue_list = IssueList()
|
|
311
|
+
for view in self._views:
|
|
209
312
|
if view.filter_ and isinstance(view.filter_, RawFilter):
|
|
210
|
-
|
|
313
|
+
issue_list.append(
|
|
211
314
|
NotNeatSupportedFilterWarning(view.view.as_id()),
|
|
212
315
|
)
|
|
316
|
+
return issue_list
|
|
213
317
|
|
|
214
|
-
def _validate_value_type_existence(self) ->
|
|
215
|
-
views = {prop_.view for prop_ in self.
|
|
216
|
-
|
|
217
|
-
for prop_ in self.
|
|
318
|
+
def _validate_value_type_existence(self) -> IssueList:
|
|
319
|
+
views = {prop_.view for prop_ in self._properties}.union({view_.view for view_ in self._views})
|
|
320
|
+
issue_list = IssueList()
|
|
321
|
+
for prop_ in self._properties:
|
|
218
322
|
if isinstance(prop_.value_type, ViewEntity) and prop_.value_type not in views:
|
|
219
|
-
|
|
323
|
+
issue_list.append(
|
|
220
324
|
UndefinedViewWarning(
|
|
221
325
|
str(prop_.view),
|
|
222
326
|
str(prop_.value_type),
|
|
223
327
|
prop_.view_property,
|
|
224
328
|
)
|
|
225
329
|
)
|
|
330
|
+
return issue_list
|
|
226
331
|
|
|
227
|
-
def
|
|
332
|
+
def _validate_property_referenced_views_and_containers_exists(
|
|
333
|
+
self,
|
|
334
|
+
view_by_id: dict[dm.ViewId, dm.ViewApply | dm.View],
|
|
335
|
+
containers_by_id: dict[dm.ContainerId, dm.ContainerApply | dm.Container],
|
|
336
|
+
) -> IssueList:
|
|
337
|
+
issue_list = IssueList()
|
|
338
|
+
for prop in self._properties:
|
|
339
|
+
if prop.container:
|
|
340
|
+
container_id = prop.container.as_id()
|
|
341
|
+
if container_id not in containers_by_id:
|
|
342
|
+
issue_list.append(
|
|
343
|
+
ResourceNotFoundError(
|
|
344
|
+
container_id,
|
|
345
|
+
"container",
|
|
346
|
+
prop.view,
|
|
347
|
+
"view",
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
elif (
|
|
351
|
+
prop.container_property and prop.container_property not in containers_by_id[container_id].properties
|
|
352
|
+
):
|
|
353
|
+
issue_list.append(
|
|
354
|
+
PropertyNotFoundError(
|
|
355
|
+
prop.container,
|
|
356
|
+
"container property",
|
|
357
|
+
prop.container_property,
|
|
358
|
+
dm.PropertyId(prop.view.as_id(), prop.view_property),
|
|
359
|
+
"view property",
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if prop.view.as_id() not in view_by_id:
|
|
364
|
+
issue_list.append(
|
|
365
|
+
ResourceNotFoundError(
|
|
366
|
+
prop.view,
|
|
367
|
+
"view",
|
|
368
|
+
prop.view_property,
|
|
369
|
+
"property",
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return issue_list
|
|
374
|
+
|
|
375
|
+
def _validate_reverse_connections(
|
|
376
|
+
self,
|
|
377
|
+
properties_by_ids: dict[tuple[dm.ViewId, str], ViewPropertyApply | ViewProperty],
|
|
378
|
+
containers_by_id: dict[dm.ContainerId, dm.ContainerApply | dm.Container],
|
|
379
|
+
) -> IssueList:
|
|
380
|
+
issue_list = IssueList()
|
|
228
381
|
# do not check for reverse connections in Cognite models
|
|
229
|
-
if self.
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
properties_by_ids = {f"{prop_.view!s}.{prop_.view_property}": prop_ for prop_ in self.properties}
|
|
233
|
-
reversed_by_ids = {
|
|
234
|
-
id_: prop_
|
|
235
|
-
for id_, prop_ in properties_by_ids.items()
|
|
236
|
-
if prop_.connection and isinstance(prop_.connection, ReverseConnectionEntity)
|
|
237
|
-
}
|
|
382
|
+
if self._metadata.as_data_model_id() in COGNITE_MODELS:
|
|
383
|
+
return issue_list
|
|
238
384
|
|
|
239
|
-
for
|
|
240
|
-
|
|
385
|
+
for (view_id, prop_id), prop_ in properties_by_ids.items():
|
|
386
|
+
if not isinstance(prop_, ReverseDirectRelationApply | ReverseDirectRelation):
|
|
387
|
+
continue
|
|
388
|
+
source_id = prop_.through.source, prop_.through.property
|
|
241
389
|
if source_id not in properties_by_ids:
|
|
242
|
-
|
|
243
|
-
self.issue_list.append(
|
|
390
|
+
issue_list.append(
|
|
244
391
|
ReversedConnectionNotFeasibleError(
|
|
245
|
-
|
|
392
|
+
view_id,
|
|
246
393
|
"reversed connection",
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
str(prop_.value_type),
|
|
250
|
-
cast(ReverseConnectionEntity, prop_.connection).property_,
|
|
394
|
+
prop_id,
|
|
395
|
+
f"The {prop_.through.source} {prop_.through.property} does not exist",
|
|
251
396
|
)
|
|
252
397
|
)
|
|
398
|
+
continue
|
|
399
|
+
if isinstance(source_id[0], dm.ContainerId):
|
|
400
|
+
# Todo: How to handle this case? Should not happen if you created the model with Neat
|
|
401
|
+
continue
|
|
253
402
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
403
|
+
target_property = properties_by_ids[(source_id[0], source_id[1])]
|
|
404
|
+
# Validate that the target is a direct relation pointing to the view_id
|
|
405
|
+
is_direct_relation = False
|
|
406
|
+
if isinstance(target_property, dm.MappedProperty) and isinstance(target_property.type, dm.DirectRelation):
|
|
407
|
+
is_direct_relation = True
|
|
408
|
+
elif isinstance(target_property, dm.MappedPropertyApply):
|
|
409
|
+
container = containers_by_id[target_property.container]
|
|
410
|
+
if target_property.container_property_identifier in container.properties:
|
|
411
|
+
container_property = container.properties[target_property.container_property_identifier]
|
|
412
|
+
if isinstance(container_property.type, dm.DirectRelation):
|
|
413
|
+
is_direct_relation = True
|
|
414
|
+
if not is_direct_relation:
|
|
415
|
+
issue_list.append(
|
|
257
416
|
ReversedConnectionNotFeasibleError(
|
|
258
|
-
|
|
259
|
-
"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
str(prop_.value_type),
|
|
263
|
-
cast(ReverseConnectionEntity, prop_.connection).property_,
|
|
417
|
+
view_id,
|
|
418
|
+
"reversed connection",
|
|
419
|
+
prop_id,
|
|
420
|
+
f"{prop_.through.source} {prop_.through.property} is not a direct relation",
|
|
264
421
|
)
|
|
265
422
|
)
|
|
266
|
-
|
|
267
|
-
else:
|
|
268
423
|
continue
|
|
424
|
+
if not (
|
|
425
|
+
isinstance(target_property, dm.MappedPropertyApply | dm.MappedProperty)
|
|
426
|
+
and target_property.source == view_id
|
|
427
|
+
):
|
|
428
|
+
issue_list.append(
|
|
429
|
+
ReversedConnectionNotFeasibleError(
|
|
430
|
+
view_id,
|
|
431
|
+
"reversed connection",
|
|
432
|
+
prop_id,
|
|
433
|
+
f"{prop_.through.source} {prop_.through.property} is not pointing to {view_id}",
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
return issue_list
|
|
269
437
|
|
|
270
438
|
@staticmethod
|
|
271
|
-
def
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
439
|
+
def _validate_schema(
|
|
440
|
+
schema: DMSSchema,
|
|
441
|
+
view_by_id: dict[dm.ViewId, dm.ViewApply | dm.View],
|
|
442
|
+
containers_by_id: dict[dm.ContainerId, dm.ContainerApply | dm.Container],
|
|
443
|
+
) -> IssueList:
|
|
444
|
+
errors: set[NeatError] = set()
|
|
445
|
+
defined_spaces = schema.spaces.copy()
|
|
446
|
+
|
|
447
|
+
for container_id, container in schema.containers.items():
|
|
448
|
+
if container.space not in defined_spaces:
|
|
449
|
+
errors.add(ResourceNotFoundError(container.space, "space", container_id, "container"))
|
|
450
|
+
for constraint in container.constraints.values():
|
|
451
|
+
if isinstance(constraint, dm.RequiresConstraint) and constraint.require not in containers_by_id:
|
|
452
|
+
errors.add(ResourceNotFoundError(constraint.require, "container", container_id, "container"))
|
|
453
|
+
|
|
454
|
+
for view_id, view in schema.views.items():
|
|
455
|
+
if view.space not in defined_spaces:
|
|
456
|
+
errors.add(ResourceNotFoundError(view.space, "space", view_id, "view"))
|
|
457
|
+
|
|
458
|
+
for parent in view.implements or []:
|
|
459
|
+
if parent not in view_by_id:
|
|
460
|
+
errors.add(PropertyNotFoundError(parent, "view", "implements", view_id, "view"))
|
|
461
|
+
|
|
462
|
+
for prop_name, prop in (view.properties or {}).items():
|
|
463
|
+
if isinstance(prop, dm.MappedPropertyApply):
|
|
464
|
+
ref_container = containers_by_id.get(prop.container)
|
|
465
|
+
if ref_container is None:
|
|
466
|
+
errors.add(ResourceNotFoundError(prop.container, "container", view_id, "view"))
|
|
467
|
+
elif prop.container_property_identifier not in ref_container.properties:
|
|
468
|
+
errors.add(
|
|
469
|
+
PropertyNotFoundError(
|
|
470
|
+
prop.container,
|
|
471
|
+
"container",
|
|
472
|
+
prop.container_property_identifier,
|
|
473
|
+
view_id,
|
|
474
|
+
"view",
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
else:
|
|
478
|
+
container_property = ref_container.properties[prop.container_property_identifier]
|
|
479
|
+
|
|
480
|
+
if isinstance(container_property.type, dm.DirectRelation) and prop.source is None:
|
|
481
|
+
warnings.warn(
|
|
482
|
+
DirectRelationMissingSourceWarning(view_id, prop_name),
|
|
483
|
+
stacklevel=2,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if (
|
|
487
|
+
isinstance(prop, dm.EdgeConnectionApply | ReverseDirectRelationApply)
|
|
488
|
+
and prop.source not in view_by_id
|
|
489
|
+
):
|
|
490
|
+
errors.add(PropertyNotFoundError(prop.source, "view", prop_name, view_id, "view"))
|
|
491
|
+
|
|
492
|
+
if (
|
|
493
|
+
isinstance(prop, dm.EdgeConnectionApply)
|
|
494
|
+
and prop.edge_source is not None
|
|
495
|
+
and prop.edge_source not in view_by_id
|
|
496
|
+
):
|
|
497
|
+
errors.add(PropertyNotFoundError(prop.edge_source, "view", prop_name, view_id, "view"))
|
|
498
|
+
|
|
499
|
+
# This allows for multiple view properties to be mapped to the same container property,
|
|
500
|
+
# as long as they have different external_id, otherwise this will lead to raising
|
|
501
|
+
# error ContainerPropertyUsedMultipleTimesError
|
|
502
|
+
property_count = Counter(
|
|
503
|
+
(prop.container, prop.container_property_identifier, view_property_identifier)
|
|
504
|
+
for view_property_identifier, prop in (view.properties or {}).items()
|
|
505
|
+
if isinstance(prop, dm.MappedPropertyApply)
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
for (
|
|
509
|
+
container_id,
|
|
510
|
+
container_property_identifier,
|
|
511
|
+
_,
|
|
512
|
+
), count in property_count.items():
|
|
513
|
+
if count > 1:
|
|
514
|
+
view_properties = [
|
|
515
|
+
prop_name
|
|
516
|
+
for prop_name, prop in (view.properties or {}).items()
|
|
517
|
+
if isinstance(prop, dm.MappedPropertyApply)
|
|
518
|
+
and (prop.container, prop.container_property_identifier)
|
|
519
|
+
== (container_id, container_property_identifier)
|
|
520
|
+
]
|
|
521
|
+
errors.add(
|
|
522
|
+
PropertyMappingDuplicatedError(
|
|
523
|
+
container_id,
|
|
524
|
+
"container",
|
|
525
|
+
container_property_identifier,
|
|
526
|
+
frozenset({dm.PropertyId(view_id, prop_name) for prop_name in view_properties}),
|
|
527
|
+
"view property",
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
if schema.data_model:
|
|
532
|
+
model = schema.data_model
|
|
533
|
+
if model.space not in defined_spaces:
|
|
534
|
+
errors.add(ResourceNotFoundError(model.space, "space", model.as_id(), "data model"))
|
|
535
|
+
|
|
536
|
+
view_counts: dict[dm.ViewId, int] = defaultdict(int)
|
|
537
|
+
for view_id_or_class in model.views or []:
|
|
538
|
+
view_id = view_id_or_class if isinstance(view_id_or_class, dm.ViewId) else view_id_or_class.as_id()
|
|
539
|
+
if view_id not in view_by_id:
|
|
540
|
+
errors.add(ResourceNotFoundError(view_id, "view", model.as_id(), "data model"))
|
|
541
|
+
view_counts[view_id] += 1
|
|
542
|
+
|
|
543
|
+
for view_id, count in view_counts.items():
|
|
544
|
+
if count > 1:
|
|
545
|
+
errors.add(
|
|
546
|
+
ResourceDuplicatedError(
|
|
547
|
+
view_id,
|
|
548
|
+
"view",
|
|
549
|
+
repr(model.as_id()),
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
return IssueList(list(errors))
|
|
@@ -3,7 +3,7 @@ import sys
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from functools import total_ordering
|
|
5
5
|
from types import UnionType
|
|
6
|
-
from typing import Any, ClassVar, Generic, Literal, TypeVar, Union, cast, get_args, get_origin
|
|
6
|
+
from typing import Any, ClassVar, Generic, Literal, TypeVar, Union, cast, get_args, get_origin, overload
|
|
7
7
|
|
|
8
8
|
from cognite.client.data_classes.data_modeling import DirectRelationReference
|
|
9
9
|
from cognite.client.data_classes.data_modeling.data_types import UnitReference
|
|
@@ -22,6 +22,7 @@ from pydantic import (
|
|
|
22
22
|
model_validator,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
+
from cognite.neat._issues.errors import NeatValueError
|
|
25
26
|
from cognite.neat._utils.text import replace_non_alphanumeric_with_underscore
|
|
26
27
|
|
|
27
28
|
if sys.version_info >= (3, 11):
|
|
@@ -56,14 +57,29 @@ class Entity(BaseModel, extra="ignore"):
|
|
|
56
57
|
suffix: str
|
|
57
58
|
|
|
58
59
|
@classmethod
|
|
59
|
-
|
|
60
|
+
@overload
|
|
61
|
+
def load(cls: "type[T_Entity]", data: Any, strict: Literal[True], **defaults) -> "T_Entity": ...
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
@overload
|
|
65
|
+
def load(
|
|
66
|
+
cls: "type[T_Entity]", data: Any, strict: Literal[False] = False, **defaults
|
|
67
|
+
) -> "T_Entity | UnknownEntity": ...
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def load(cls: "type[T_Entity]", data: Any, strict: bool = False, **defaults) -> "T_Entity | UnknownEntity":
|
|
60
71
|
if isinstance(data, cls):
|
|
61
72
|
return data
|
|
62
73
|
elif isinstance(data, str) and data == str(Unknown):
|
|
74
|
+
if strict:
|
|
75
|
+
raise NeatValueError(f"Failed to load entity {data!s}")
|
|
63
76
|
return UnknownEntity(prefix=Undefined, suffix=Unknown)
|
|
64
77
|
if defaults and isinstance(defaults, dict):
|
|
65
78
|
# This is a trick to pass in default values
|
|
66
|
-
|
|
79
|
+
try:
|
|
80
|
+
return cls.model_validate({_PARSE: data, "defaults": defaults})
|
|
81
|
+
except ValueError:
|
|
82
|
+
raise
|
|
67
83
|
else:
|
|
68
84
|
return cls.model_validate(data)
|
|
69
85
|
|
|
@@ -291,9 +307,21 @@ class DMSEntity(Entity, Generic[T_ID], ABC):
|
|
|
291
307
|
defaults["prefix"] = defaults.pop("space")
|
|
292
308
|
return super().dump(**defaults)
|
|
293
309
|
|
|
310
|
+
@classmethod # type: ignore[override]
|
|
311
|
+
@overload
|
|
312
|
+
def load(cls: "type[T_DMSEntity]", data: Any, strict: Literal[True], **defaults) -> "T_DMSEntity": ...
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
@overload
|
|
316
|
+
def load(
|
|
317
|
+
cls: "type[T_DMSEntity]", data: Any, strict: Literal[False] = False, **defaults
|
|
318
|
+
) -> "T_DMSEntity | DMSUnknownEntity": ...
|
|
319
|
+
|
|
294
320
|
@classmethod
|
|
295
|
-
def load(cls: "type[T_DMSEntity]", data: Any, **defaults) -> "T_DMSEntity | DMSUnknownEntity": # type: ignore[override]
|
|
321
|
+
def load(cls: "type[T_DMSEntity]", data: Any, strict: bool = False, **defaults) -> "T_DMSEntity | DMSUnknownEntity": # type: ignore[override]
|
|
296
322
|
if isinstance(data, str) and data == str(Unknown):
|
|
323
|
+
if strict:
|
|
324
|
+
raise NeatValueError(f"Failed to load entity {data!s}")
|
|
297
325
|
return DMSUnknownEntity.from_id(None)
|
|
298
326
|
return cast(T_DMSEntity, super().load(data, **defaults))
|
|
299
327
|
|