cognite-neat 0.108.0__py3-none-any.whl → 0.109.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cognite-neat might be problematic. Click here for more details.
- cognite/neat/_constants.py +1 -1
- cognite/neat/_graph/extractors/_classic_cdf/_classic.py +8 -4
- cognite/neat/_graph/queries/_base.py +4 -0
- cognite/neat/_graph/transformers/__init__.py +3 -3
- cognite/neat/_graph/transformers/_base.py +4 -4
- cognite/neat/_graph/transformers/_classic_cdf.py +13 -13
- cognite/neat/_graph/transformers/_prune_graph.py +3 -3
- cognite/neat/_graph/transformers/_rdfpath.py +3 -4
- cognite/neat/_graph/transformers/_value_type.py +23 -16
- cognite/neat/_issues/errors/__init__.py +2 -0
- cognite/neat/_issues/errors/_external.py +8 -0
- cognite/neat/_issues/warnings/_resources.py +1 -1
- cognite/neat/_rules/exporters/_rules2yaml.py +1 -1
- cognite/neat/_rules/importers/_rdf/_inference2rules.py +179 -118
- cognite/neat/_rules/models/_base_rules.py +9 -8
- cognite/neat/_rules/models/dms/_exporter.py +5 -4
- cognite/neat/_rules/transformers/__init__.py +4 -3
- cognite/neat/_rules/transformers/_base.py +6 -1
- cognite/neat/_rules/transformers/_converters.py +436 -361
- cognite/neat/_rules/transformers/_mapping.py +4 -4
- cognite/neat/_session/_base.py +71 -69
- cognite/neat/_session/_create.py +133 -0
- cognite/neat/_session/_drop.py +55 -1
- cognite/neat/_session/_fix.py +28 -0
- cognite/neat/_session/_inspect.py +19 -5
- cognite/neat/_session/_mapping.py +8 -8
- cognite/neat/_session/_prepare.py +3 -247
- cognite/neat/_session/_read.py +78 -4
- cognite/neat/_session/_set.py +34 -12
- cognite/neat/_session/_show.py +14 -41
- cognite/neat/_session/_state.py +48 -51
- cognite/neat/_session/_to.py +7 -3
- cognite/neat/_session/exceptions.py +7 -1
- cognite/neat/_store/_graph_store.py +14 -13
- cognite/neat/_store/_provenance.py +36 -20
- cognite/neat/_store/_rules_store.py +172 -293
- cognite/neat/_store/exceptions.py +40 -4
- cognite/neat/_utils/auth.py +4 -2
- cognite/neat/_version.py +1 -1
- {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.0.dist-info}/RECORD +44 -42
- {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import dataclasses
|
|
2
1
|
import re
|
|
3
2
|
import warnings
|
|
4
3
|
from abc import ABC
|
|
@@ -9,12 +8,14 @@ from typing import ClassVar, Literal, TypeVar, cast, overload
|
|
|
9
8
|
|
|
10
9
|
from cognite.client.data_classes import data_modeling as dms
|
|
11
10
|
from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier, ViewId
|
|
11
|
+
from cognite.client.utils.useful_types import SequenceNotStr
|
|
12
12
|
from rdflib import Namespace
|
|
13
13
|
|
|
14
14
|
from cognite.neat._client import NeatClient
|
|
15
15
|
from cognite.neat._client.data_classes.data_modeling import ContainerApplyDict, ViewApplyDict
|
|
16
16
|
from cognite.neat._constants import (
|
|
17
17
|
COGNITE_MODELS,
|
|
18
|
+
COGNITE_SPACES,
|
|
18
19
|
DMS_CONTAINER_PROPERTY_SIZE_LIMIT,
|
|
19
20
|
DMS_RESERVED_PROPERTIES,
|
|
20
21
|
get_default_prefixes_and_namespaces,
|
|
@@ -27,8 +28,6 @@ from cognite.neat._issues.warnings._models import (
|
|
|
27
28
|
)
|
|
28
29
|
from cognite.neat._rules._shared import (
|
|
29
30
|
ReadInputRules,
|
|
30
|
-
ReadRules,
|
|
31
|
-
T_InputRules,
|
|
32
31
|
VerifiedRules,
|
|
33
32
|
)
|
|
34
33
|
from cognite.neat._rules.analysis import DMSAnalysis
|
|
@@ -50,38 +49,29 @@ from cognite.neat._rules.models.entities import (
|
|
|
50
49
|
ContainerEntity,
|
|
51
50
|
DMSUnknownEntity,
|
|
52
51
|
EdgeEntity,
|
|
53
|
-
Entity,
|
|
54
52
|
HasDataFilter,
|
|
55
53
|
MultiValueTypeInfo,
|
|
56
54
|
ReverseConnectionEntity,
|
|
57
|
-
T_Entity,
|
|
58
55
|
UnknownEntity,
|
|
59
56
|
ViewEntity,
|
|
60
57
|
)
|
|
61
58
|
from cognite.neat._rules.models.information import InformationClass, InformationMetadata, InformationProperty
|
|
62
|
-
from cognite.neat._rules.models.information._rules_input import (
|
|
63
|
-
InformationInputClass,
|
|
64
|
-
InformationInputProperty,
|
|
65
|
-
InformationInputRules,
|
|
66
|
-
)
|
|
67
59
|
from cognite.neat._utils.text import to_camel
|
|
68
60
|
|
|
69
|
-
from ._base import
|
|
61
|
+
from ._base import T_VerifiedIn, T_VerifiedOut, VerifiedRulesTransformer
|
|
70
62
|
from ._verification import VerifyDMSRules
|
|
71
63
|
|
|
72
|
-
T_VerifiedInRules = TypeVar("T_VerifiedInRules", bound=VerifiedRules)
|
|
73
|
-
T_VerifiedOutRules = TypeVar("T_VerifiedOutRules", bound=VerifiedRules)
|
|
74
64
|
T_InputInRules = TypeVar("T_InputInRules", bound=ReadInputRules)
|
|
75
65
|
T_InputOutRules = TypeVar("T_InputOutRules", bound=ReadInputRules)
|
|
76
66
|
|
|
77
67
|
|
|
78
|
-
class ConversionTransformer(
|
|
68
|
+
class ConversionTransformer(VerifiedRulesTransformer[T_VerifiedIn, T_VerifiedOut], ABC):
|
|
79
69
|
"""Base class for all conversion transformers."""
|
|
80
70
|
|
|
81
71
|
...
|
|
82
72
|
|
|
83
73
|
|
|
84
|
-
class ToCompliantEntities(
|
|
74
|
+
class ToCompliantEntities(VerifiedRulesTransformer[InformationRules, InformationRules]): # type: ignore[misc]
|
|
85
75
|
"""Converts input rules to rules with compliant entity IDs that match regex patters used
|
|
86
76
|
by DMS schema components."""
|
|
87
77
|
|
|
@@ -89,13 +79,11 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
|
|
|
89
79
|
def description(self) -> str:
|
|
90
80
|
return "Ensures externalIDs are compliant with CDF"
|
|
91
81
|
|
|
92
|
-
def transform(self, rules:
|
|
93
|
-
|
|
94
|
-
return rules
|
|
95
|
-
copy: InformationInputRules = dataclasses.replace(rules.rules)
|
|
82
|
+
def transform(self, rules: InformationRules) -> InformationRules:
|
|
83
|
+
copy = rules.model_copy(deep=True)
|
|
96
84
|
copy.classes = self._fix_classes(copy.classes)
|
|
97
85
|
copy.properties = self._fix_properties(copy.properties)
|
|
98
|
-
return
|
|
86
|
+
return copy
|
|
99
87
|
|
|
100
88
|
@classmethod
|
|
101
89
|
def _fix_entity(cls, entity: str) -> str:
|
|
@@ -112,16 +100,8 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
|
|
|
112
100
|
return re.sub(r"[^a-zA-Z0-9]+", "_", entity)
|
|
113
101
|
|
|
114
102
|
@classmethod
|
|
115
|
-
def _fix_class(cls, class_:
|
|
116
|
-
if isinstance(class_, str
|
|
117
|
-
if len(class_.split(":")) == 2:
|
|
118
|
-
prefix, suffix = class_.split(":")
|
|
119
|
-
class_ = f"{cls._fix_entity(prefix)}:{cls._fix_entity(suffix)}"
|
|
120
|
-
|
|
121
|
-
else:
|
|
122
|
-
class_ = cls._fix_entity(class_)
|
|
123
|
-
|
|
124
|
-
elif isinstance(class_, ClassEntity) and type(class_.prefix) is str:
|
|
103
|
+
def _fix_class(cls, class_: ClassEntity) -> ClassEntity:
|
|
104
|
+
if isinstance(class_, ClassEntity) and type(class_.prefix) is str:
|
|
125
105
|
class_ = ClassEntity(
|
|
126
106
|
prefix=cls._fix_entity(class_.prefix),
|
|
127
107
|
suffix=cls._fix_entity(class_.suffix),
|
|
@@ -131,28 +111,17 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
|
|
|
131
111
|
|
|
132
112
|
@classmethod
|
|
133
113
|
def _fix_value_type(
|
|
134
|
-
cls, value_type:
|
|
135
|
-
) ->
|
|
136
|
-
fixed_value_type:
|
|
137
|
-
|
|
138
|
-
if isinstance(value_type, str):
|
|
139
|
-
# this is a multi value type but as string
|
|
140
|
-
if " | " in value_type:
|
|
141
|
-
value_types = value_type.split(" | ")
|
|
142
|
-
fixed_value_type = " | ".join([cast(str, cls._fix_value_type(v)) for v in value_types])
|
|
143
|
-
# this is value type specified with prefix:suffix string
|
|
144
|
-
elif ":" in value_type:
|
|
145
|
-
fixed_value_type = cls._fix_class(value_type)
|
|
146
|
-
|
|
147
|
-
# this is value type specified as suffix only
|
|
148
|
-
else:
|
|
149
|
-
fixed_value_type = cls._fix_entity(value_type)
|
|
114
|
+
cls, value_type: DataType | ClassEntity | MultiValueTypeInfo
|
|
115
|
+
) -> DataType | ClassEntity | MultiValueTypeInfo:
|
|
116
|
+
fixed_value_type: DataType | ClassEntity | MultiValueTypeInfo
|
|
150
117
|
|
|
151
|
-
# value type specified as
|
|
152
|
-
|
|
118
|
+
# value type specified as MultiValueTypeInfo
|
|
119
|
+
if isinstance(value_type, MultiValueTypeInfo):
|
|
153
120
|
fixed_value_type = MultiValueTypeInfo(
|
|
154
121
|
types=[cast(DataType | ClassEntity, cls._fix_value_type(type_)) for type_ in value_type.types],
|
|
155
122
|
)
|
|
123
|
+
|
|
124
|
+
# value type specified as ClassEntity instance
|
|
156
125
|
elif isinstance(value_type, ClassEntity):
|
|
157
126
|
fixed_value_type = cls._fix_class(value_type)
|
|
158
127
|
|
|
@@ -163,16 +132,16 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
|
|
|
163
132
|
return fixed_value_type
|
|
164
133
|
|
|
165
134
|
@classmethod
|
|
166
|
-
def _fix_classes(cls, definitions:
|
|
167
|
-
fixed_definitions = []
|
|
135
|
+
def _fix_classes(cls, definitions: SheetList[InformationClass]) -> SheetList[InformationClass]:
|
|
136
|
+
fixed_definitions = SheetList[InformationClass]()
|
|
168
137
|
for definition in definitions:
|
|
169
138
|
definition.class_ = cls._fix_class(definition.class_)
|
|
170
139
|
fixed_definitions.append(definition)
|
|
171
140
|
return fixed_definitions
|
|
172
141
|
|
|
173
142
|
@classmethod
|
|
174
|
-
def _fix_properties(cls, definitions:
|
|
175
|
-
fixed_definitions = []
|
|
143
|
+
def _fix_properties(cls, definitions: SheetList[InformationProperty]) -> SheetList[InformationProperty]:
|
|
144
|
+
fixed_definitions = SheetList[InformationProperty]()
|
|
176
145
|
for definition in definitions:
|
|
177
146
|
definition.class_ = cls._fix_class(definition.class_)
|
|
178
147
|
definition.property_ = cls._fix_entity(definition.property_)
|
|
@@ -181,80 +150,107 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
|
|
|
181
150
|
return fixed_definitions
|
|
182
151
|
|
|
183
152
|
|
|
184
|
-
class PrefixEntities(
|
|
185
|
-
"""Prefixes all entities with a given prefix."""
|
|
153
|
+
class PrefixEntities(ConversionTransformer): # type: ignore[type-var]
|
|
154
|
+
"""Prefixes all entities with a given prefix if they are in the same space as data model."""
|
|
186
155
|
|
|
187
156
|
def __init__(self, prefix: str) -> None:
|
|
188
157
|
self._prefix = prefix
|
|
189
158
|
|
|
190
159
|
@property
|
|
191
160
|
def description(self) -> str:
|
|
192
|
-
return f"Prefixes all
|
|
161
|
+
return f"Prefixes all entities with {self._prefix!r} prefix if they are in the same space as data model."
|
|
193
162
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
163
|
+
@overload
|
|
164
|
+
def transform(self, rules: DMSRules) -> DMSRules: ...
|
|
165
|
+
|
|
166
|
+
@overload
|
|
167
|
+
def transform(self, rules: InformationRules) -> InformationRules: ...
|
|
168
|
+
|
|
169
|
+
def transform(self, rules: InformationRules | DMSRules) -> InformationRules | DMSRules:
|
|
170
|
+
copy: InformationRules | DMSRules = rules.model_copy(deep=True)
|
|
171
|
+
|
|
172
|
+
# Case: Prefix Information Rules
|
|
173
|
+
if isinstance(copy, InformationRules):
|
|
174
|
+
# prefix classes
|
|
201
175
|
for cls in copy.classes:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
176
|
+
if cls.class_.prefix == copy.metadata.prefix:
|
|
177
|
+
cls.class_ = self._with_prefix(cls.class_)
|
|
178
|
+
|
|
179
|
+
if cls.implements:
|
|
180
|
+
# prefix parents
|
|
181
|
+
for i, parent_class in enumerate(cls.implements):
|
|
182
|
+
if parent_class.prefix == copy.metadata.prefix:
|
|
183
|
+
cls.implements[i] = self._with_prefix(parent_class)
|
|
184
|
+
|
|
205
185
|
for prop in copy.properties:
|
|
206
|
-
prop.class_
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
186
|
+
if prop.class_.prefix == copy.metadata.prefix:
|
|
187
|
+
prop.class_ = self._with_prefix(prop.class_)
|
|
188
|
+
|
|
189
|
+
# value type property is not multi and it is ClassEntity
|
|
190
|
+
|
|
191
|
+
if isinstance(prop.value_type, ClassEntity) and prop.value_type.prefix == copy.metadata.prefix:
|
|
192
|
+
prop.value_type = self._with_prefix(cast(ClassEntity, prop.value_type))
|
|
193
|
+
elif isinstance(prop.value_type, MultiValueTypeInfo):
|
|
194
|
+
for i, value_type in enumerate(prop.value_type.types):
|
|
195
|
+
if isinstance(value_type, ClassEntity) and value_type.prefix == copy.metadata.prefix:
|
|
196
|
+
prop.value_type.types[i] = self._with_prefix(cast(ClassEntity, value_type))
|
|
197
|
+
return copy
|
|
198
|
+
|
|
199
|
+
# Case: Prefix DMS Rules
|
|
200
|
+
elif isinstance(copy, DMSRules):
|
|
212
201
|
for view in copy.views:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
202
|
+
if view.view.space == copy.metadata.space:
|
|
203
|
+
view.view = self._with_prefix(view.view)
|
|
204
|
+
|
|
205
|
+
if view.implements:
|
|
206
|
+
for i, parent_view in enumerate(view.implements):
|
|
207
|
+
if parent_view.space == copy.metadata.space:
|
|
208
|
+
view.implements[i] = self._with_prefix(parent_view)
|
|
209
|
+
|
|
216
210
|
for dms_prop in copy.properties:
|
|
217
|
-
dms_prop.view
|
|
218
|
-
|
|
219
|
-
|
|
211
|
+
if dms_prop.view.space == copy.metadata.space:
|
|
212
|
+
dms_prop.view = self._with_prefix(dms_prop.view)
|
|
213
|
+
|
|
214
|
+
if isinstance(dms_prop.value_type, ViewEntity) and dms_prop.value_type.space == copy.metadata.space:
|
|
215
|
+
dms_prop.value_type = self._with_prefix(dms_prop.value_type)
|
|
216
|
+
|
|
217
|
+
if isinstance(dms_prop.container, ContainerEntity) and dms_prop.container.space == copy.metadata.space:
|
|
218
|
+
dms_prop.container = self._with_prefix(dms_prop.container)
|
|
219
|
+
|
|
220
220
|
if copy.containers:
|
|
221
221
|
for container in copy.containers:
|
|
222
|
-
container.container
|
|
223
|
-
|
|
222
|
+
if container.container.space == copy.metadata.space:
|
|
223
|
+
container.container = self._with_prefix(container.container)
|
|
224
|
+
return copy
|
|
225
|
+
|
|
224
226
|
raise NeatValueError(f"Unsupported rules type: {type(copy)}")
|
|
225
227
|
|
|
226
228
|
@overload
|
|
227
|
-
def _with_prefix(self,
|
|
229
|
+
def _with_prefix(self, entity: ClassEntity) -> ClassEntity: ...
|
|
228
230
|
|
|
229
231
|
@overload
|
|
230
|
-
def _with_prefix(self,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
elif isinstance(entity, ContainerEntity):
|
|
243
|
-
output = ContainerEntity(space=entity.space, externalId=f"{self._prefix}{entity.external_id}")
|
|
244
|
-
elif isinstance(entity, UnknownEntity | Entity):
|
|
245
|
-
return f"{self._prefix}{raw}"
|
|
232
|
+
def _with_prefix(self, entity: ViewEntity) -> ViewEntity: ...
|
|
233
|
+
|
|
234
|
+
@overload
|
|
235
|
+
def _with_prefix(self, entity: ContainerEntity) -> ContainerEntity: ...
|
|
236
|
+
|
|
237
|
+
def _with_prefix(
|
|
238
|
+
self, entity: ViewEntity | ContainerEntity | ClassEntity
|
|
239
|
+
) -> ViewEntity | ContainerEntity | ClassEntity:
|
|
240
|
+
if isinstance(entity, ViewEntity | ContainerEntity | ClassEntity):
|
|
241
|
+
entity.suffix = f"{self._prefix}{entity.suffix}"
|
|
242
|
+
|
|
246
243
|
else:
|
|
247
244
|
raise NeatValueError(f"Unsupported entity type: {type(entity)}")
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return str(output)
|
|
245
|
+
|
|
246
|
+
return entity
|
|
251
247
|
|
|
252
248
|
|
|
253
249
|
class InformationToDMS(ConversionTransformer[InformationRules, DMSRules]):
|
|
254
250
|
"""Converts InformationRules to DMSRules."""
|
|
255
251
|
|
|
256
252
|
def __init__(
|
|
257
|
-
self, ignore_undefined_value_types: bool = False, reserved_properties: Literal["error", "
|
|
253
|
+
self, ignore_undefined_value_types: bool = False, reserved_properties: Literal["error", "warning"] = "error"
|
|
258
254
|
):
|
|
259
255
|
self.ignore_undefined_value_types = ignore_undefined_value_types
|
|
260
256
|
self.reserved_properties = reserved_properties
|
|
@@ -294,7 +290,7 @@ class ConvertToRules(ConversionTransformer[VerifiedRules, VerifiedRules]):
|
|
|
294
290
|
_T_Entity = TypeVar("_T_Entity", bound=ClassEntity | ViewEntity)
|
|
295
291
|
|
|
296
292
|
|
|
297
|
-
class SetIDDMSModel(
|
|
293
|
+
class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
298
294
|
def __init__(self, new_id: DataModelId | tuple[str, str, str]):
|
|
299
295
|
self.new_id = DataModelId.load(new_id)
|
|
300
296
|
|
|
@@ -314,77 +310,17 @@ class SetIDDMSModel(RulesTransformer[DMSRules, DMSRules]):
|
|
|
314
310
|
return DMSRules.model_validate(DMSInputRules.load(dump).dump())
|
|
315
311
|
|
|
316
312
|
|
|
317
|
-
class ToExtensionModel(
|
|
313
|
+
class ToExtensionModel(VerifiedRulesTransformer[DMSRules, DMSRules], ABC):
|
|
318
314
|
type_: ClassVar[str]
|
|
319
315
|
|
|
320
|
-
def __init__(self, new_model_id: DataModelIdentifier
|
|
316
|
+
def __init__(self, new_model_id: DataModelIdentifier) -> None:
|
|
321
317
|
self.new_model_id = DataModelId.load(new_model_id)
|
|
322
318
|
if not self.new_model_id.version:
|
|
323
319
|
raise NeatValueError("Version is required for the new model.")
|
|
324
|
-
self.org_name = org_name
|
|
325
|
-
self.dummy_property = dummy_property
|
|
326
320
|
|
|
327
321
|
@property
|
|
328
322
|
def description(self) -> str:
|
|
329
|
-
return f"
|
|
330
|
-
|
|
331
|
-
def _create_new_views(
|
|
332
|
-
self, rules: DMSRules
|
|
333
|
-
) -> tuple[SheetList[DMSView], SheetList[DMSContainer], SheetList[DMSProperty]]:
|
|
334
|
-
"""Creates new views for the new model.
|
|
335
|
-
|
|
336
|
-
If the dummy property is provided, it will also create a new container for each view
|
|
337
|
-
with a single property that is the dummy property.
|
|
338
|
-
"""
|
|
339
|
-
new_views = SheetList[DMSView]()
|
|
340
|
-
new_containers = SheetList[DMSContainer]()
|
|
341
|
-
new_properties = SheetList[DMSProperty]()
|
|
342
|
-
|
|
343
|
-
for definition in rules.views:
|
|
344
|
-
view_entity = self._remove_cognite_affix(definition.view)
|
|
345
|
-
view_entity.version = cast(str, self.new_model_id.version)
|
|
346
|
-
view_entity.prefix = self.new_model_id.space
|
|
347
|
-
|
|
348
|
-
new_views.append(
|
|
349
|
-
DMSView(
|
|
350
|
-
view=view_entity,
|
|
351
|
-
implements=[definition.view],
|
|
352
|
-
in_model=True,
|
|
353
|
-
name=definition.name,
|
|
354
|
-
)
|
|
355
|
-
)
|
|
356
|
-
if self.dummy_property is None:
|
|
357
|
-
continue
|
|
358
|
-
|
|
359
|
-
container_entity = ContainerEntity(space=view_entity.prefix, externalId=view_entity.external_id)
|
|
360
|
-
|
|
361
|
-
container = DMSContainer(container=container_entity)
|
|
362
|
-
|
|
363
|
-
prefix = to_camel(view_entity.suffix)
|
|
364
|
-
property_ = DMSProperty(
|
|
365
|
-
view=view_entity,
|
|
366
|
-
view_property=f"{prefix}{self.dummy_property}",
|
|
367
|
-
value_type=String(),
|
|
368
|
-
nullable=True,
|
|
369
|
-
immutable=False,
|
|
370
|
-
is_list=False,
|
|
371
|
-
container=container_entity,
|
|
372
|
-
container_property=f"{prefix}{self.dummy_property}",
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
new_properties.append(property_)
|
|
376
|
-
new_containers.append(container)
|
|
377
|
-
|
|
378
|
-
return new_views, new_containers, new_properties
|
|
379
|
-
|
|
380
|
-
def _remove_cognite_affix(self, entity: _T_Entity) -> _T_Entity:
|
|
381
|
-
"""This method removes `Cognite` affix from the entity."""
|
|
382
|
-
new_suffix = entity.suffix.replace("Cognite", self.org_name or "")
|
|
383
|
-
if isinstance(entity, ViewEntity):
|
|
384
|
-
return ViewEntity(space=entity.space, externalId=new_suffix, version=entity.version) # type: ignore[return-value]
|
|
385
|
-
elif isinstance(entity, ClassEntity):
|
|
386
|
-
return ClassEntity(prefix=entity.prefix, suffix=new_suffix, version=entity.version) # type: ignore[return-value]
|
|
387
|
-
raise ValueError(f"Unsupported entity type: {type(entity)}")
|
|
323
|
+
return f"Create new data model {self.new_model_id} of type {self.type_.replace('_', ' ')} data model."
|
|
388
324
|
|
|
389
325
|
|
|
390
326
|
class ToEnterpriseModel(ToExtensionModel):
|
|
@@ -397,7 +333,9 @@ class ToEnterpriseModel(ToExtensionModel):
|
|
|
397
333
|
dummy_property: str = "GUID",
|
|
398
334
|
move_connections: bool = False,
|
|
399
335
|
):
|
|
400
|
-
super().__init__(new_model_id
|
|
336
|
+
super().__init__(new_model_id)
|
|
337
|
+
self.dummy_property = dummy_property
|
|
338
|
+
self.org_name = org_name
|
|
401
339
|
self.move_connections = move_connections
|
|
402
340
|
|
|
403
341
|
def transform(self, rules: DMSRules) -> DMSRules:
|
|
@@ -411,13 +349,8 @@ class ToEnterpriseModel(ToExtensionModel):
|
|
|
411
349
|
return self._to_enterprise(rules)
|
|
412
350
|
|
|
413
351
|
def _to_enterprise(self, reference_model: DMSRules) -> DMSRules:
|
|
414
|
-
|
|
352
|
+
enterprise_model = reference_model.model_copy(deep=True)
|
|
415
353
|
|
|
416
|
-
# This will create reference model components in the enterprise model space
|
|
417
|
-
enterprise_model = DMSRules.model_validate(DMSInputRules.load(dump).dump())
|
|
418
|
-
|
|
419
|
-
# Post validation metadata update:
|
|
420
|
-
enterprise_model.metadata.name = self.type_
|
|
421
354
|
enterprise_model.metadata.name = f"{self.org_name} {self.type_} data model"
|
|
422
355
|
enterprise_model.metadata.space = self.new_model_id.space
|
|
423
356
|
enterprise_model.metadata.external_id = self.new_model_id.external_id
|
|
@@ -432,43 +365,91 @@ class ToEnterpriseModel(ToExtensionModel):
|
|
|
432
365
|
|
|
433
366
|
if self.move_connections:
|
|
434
367
|
# Move connections from reference model to new enterprise model
|
|
435
|
-
enterprise_properties.extend(self.
|
|
368
|
+
enterprise_properties.extend(self._create_connection_properties(enterprise_model, enterprise_views))
|
|
436
369
|
|
|
437
370
|
# ... however, we do not want to keep the reference containers and properties
|
|
371
|
+
# these we are getting for free through the implements.
|
|
438
372
|
enterprise_model.containers = enterprise_containers
|
|
439
373
|
enterprise_model.properties = enterprise_properties
|
|
440
374
|
|
|
441
375
|
return enterprise_model
|
|
442
376
|
|
|
443
377
|
@staticmethod
|
|
444
|
-
def
|
|
445
|
-
|
|
378
|
+
def _create_connection_properties(rules: DMSRules, new_views: SheetList[DMSView]) -> SheetList[DMSProperty]:
|
|
379
|
+
"""Creates a new connection property for each connection property in the reference model.
|
|
380
|
+
|
|
381
|
+
This is for example when you create an enterprise model from CogniteCore, you ensure that your
|
|
382
|
+
new Asset, Equipment, TimeSeries, Activity, and File views all point to each other.
|
|
383
|
+
"""
|
|
384
|
+
# Note all new news have an implements attribute that points to the original view
|
|
385
|
+
previous_by_new_view = {view.implements[0]: view.view for view in new_views if view.implements}
|
|
386
|
+
connection_properties = SheetList[DMSProperty]()
|
|
387
|
+
for prop in rules.properties:
|
|
388
|
+
if (
|
|
389
|
+
isinstance(prop.value_type, ViewEntity)
|
|
390
|
+
and prop.view in previous_by_new_view
|
|
391
|
+
and prop.value_type in previous_by_new_view
|
|
392
|
+
):
|
|
393
|
+
new_property = prop.model_copy(deep=True)
|
|
394
|
+
new_property.view = previous_by_new_view[prop.view]
|
|
395
|
+
new_property.value_type = previous_by_new_view[prop.value_type]
|
|
396
|
+
connection_properties.append(new_property)
|
|
397
|
+
|
|
398
|
+
return connection_properties
|
|
399
|
+
|
|
400
|
+
def _create_new_views(
|
|
401
|
+
self, rules: DMSRules
|
|
402
|
+
) -> tuple[SheetList[DMSView], SheetList[DMSContainer], SheetList[DMSProperty]]:
|
|
403
|
+
"""Creates new views for the new model.
|
|
404
|
+
|
|
405
|
+
If the dummy property is provided, it will also create a new container for each view
|
|
406
|
+
with a single property that is the dummy property.
|
|
407
|
+
"""
|
|
408
|
+
new_views = SheetList[DMSView]()
|
|
409
|
+
new_containers = SheetList[DMSContainer]()
|
|
446
410
|
new_properties = SheetList[DMSProperty]()
|
|
447
411
|
|
|
448
|
-
for
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
412
|
+
for definition in rules.views:
|
|
413
|
+
view_entity = self._remove_cognite_affix(definition.view)
|
|
414
|
+
view_entity.version = cast(str, self.new_model_id.version)
|
|
415
|
+
view_entity.prefix = self.new_model_id.space
|
|
416
|
+
new_views.append(
|
|
417
|
+
DMSView(
|
|
418
|
+
view=view_entity,
|
|
419
|
+
implements=[definition.view],
|
|
420
|
+
in_model=True,
|
|
421
|
+
name=definition.name,
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if self.dummy_property is None:
|
|
426
|
+
continue
|
|
427
|
+
|
|
428
|
+
container_entity = ContainerEntity(space=view_entity.prefix, externalId=view_entity.external_id)
|
|
429
|
+
|
|
430
|
+
container = DMSContainer(container=container_entity)
|
|
431
|
+
|
|
432
|
+
property_id = f"{to_camel(view_entity.suffix)}{self.dummy_property}"
|
|
433
|
+
property_ = DMSProperty(
|
|
434
|
+
view=view_entity,
|
|
435
|
+
view_property=property_id,
|
|
436
|
+
value_type=String(),
|
|
437
|
+
nullable=True,
|
|
438
|
+
immutable=False,
|
|
439
|
+
is_list=False,
|
|
440
|
+
container=container_entity,
|
|
441
|
+
container_property=property_id,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
new_properties.append(property_)
|
|
445
|
+
new_containers.append(container)
|
|
446
|
+
|
|
447
|
+
return new_views, new_containers, new_properties
|
|
448
|
+
|
|
449
|
+
def _remove_cognite_affix(self, entity: ViewEntity) -> ViewEntity:
|
|
450
|
+
"""This method removes `Cognite` affix from the entity."""
|
|
451
|
+
new_suffix = entity.suffix.replace("Cognite", self.org_name)
|
|
452
|
+
return ViewEntity(space=entity.space, externalId=new_suffix, version=entity.version)
|
|
472
453
|
|
|
473
454
|
|
|
474
455
|
class ToSolutionModel(ToExtensionModel):
|
|
@@ -478,18 +459,10 @@ class ToSolutionModel(ToExtensionModel):
|
|
|
478
459
|
|
|
479
460
|
Args:
|
|
480
461
|
new_model_id: DataData model identifier for the new model.
|
|
481
|
-
org_name: If the existing model is a Cognite Data Model, this will replace the "Cognite" affix.
|
|
482
|
-
mode: The mode of the solution model. Either "read" or "write". A "write" model will create a new
|
|
483
|
-
container for each view with a dummy property. Read mode will only inherit the view filter from the
|
|
484
|
-
original model.
|
|
485
462
|
dummy_property: Only applicable if mode='write'. The identifier of the dummy property in the newly created
|
|
486
463
|
container.
|
|
487
464
|
exclude_views_in_other_spaces: Whether to exclude views that are not in the same space as the existing model,
|
|
488
465
|
when creating the solution model.
|
|
489
|
-
filter_type: If mode="read", this is the type of filter to apply to the new views. The filter is used to
|
|
490
|
-
ensure that the new views will return the same instance as the original views. The view filter is the
|
|
491
|
-
simplest filter, but it has limitation in the fusion UI. The container filter is in essence a more
|
|
492
|
-
verbose version of the view filter, and it has better support in the fusion UI. The default is "container".
|
|
493
466
|
|
|
494
467
|
"""
|
|
495
468
|
|
|
@@ -498,152 +471,71 @@ class ToSolutionModel(ToExtensionModel):
|
|
|
498
471
|
def __init__(
|
|
499
472
|
self,
|
|
500
473
|
new_model_id: DataModelIdentifier,
|
|
501
|
-
|
|
502
|
-
mode: Literal["read", "write"] = "read",
|
|
474
|
+
properties: Literal["repeat", "connection"] = "connection",
|
|
503
475
|
dummy_property: str | None = "GUID",
|
|
504
|
-
|
|
476
|
+
direct_property: str = "enterprise",
|
|
477
|
+
view_prefix: str = "Enterprise",
|
|
505
478
|
filter_type: Literal["container", "view"] = "container",
|
|
479
|
+
exclude_views_in_other_spaces: bool = False,
|
|
480
|
+
skip_cognite_views: bool = True,
|
|
506
481
|
):
|
|
507
|
-
super().__init__(new_model_id
|
|
508
|
-
self.
|
|
509
|
-
self.
|
|
482
|
+
super().__init__(new_model_id)
|
|
483
|
+
self.properties = properties
|
|
484
|
+
self.dummy_property = dummy_property
|
|
485
|
+
self.direct_property = direct_property
|
|
486
|
+
self.view_prefix = view_prefix
|
|
510
487
|
self.filter_type = filter_type
|
|
488
|
+
self.exclude_views_in_other_spaces = exclude_views_in_other_spaces
|
|
489
|
+
self.skip_cognite_views = skip_cognite_views
|
|
511
490
|
|
|
512
491
|
def transform(self, rules: DMSRules) -> DMSRules:
|
|
513
492
|
reference_model = rules
|
|
514
493
|
reference_model_id = reference_model.metadata.as_data_model_id()
|
|
515
|
-
|
|
516
|
-
# if model is solution then we need to get correct space for views and containers
|
|
517
|
-
if self.mode not in ["read", "write"]:
|
|
518
|
-
raise NeatValueError(f"Unsupported mode: {self.mode}")
|
|
519
|
-
|
|
520
494
|
if reference_model_id in COGNITE_MODELS:
|
|
521
495
|
warnings.warn(
|
|
522
496
|
SolutionModelBuildOnTopOfCDMWarning(reference_model_id=reference_model_id),
|
|
523
497
|
stacklevel=2,
|
|
524
498
|
)
|
|
525
|
-
|
|
526
499
|
return self._to_solution(reference_model)
|
|
527
500
|
|
|
528
|
-
@staticmethod
|
|
529
|
-
def _has_views_in_multiple_space(rules: DMSRules) -> bool:
|
|
530
|
-
return any(view.view.space != rules.metadata.space for view in rules.views)
|
|
531
|
-
|
|
532
501
|
def _to_solution(self, reference_rules: DMSRules) -> DMSRules:
|
|
533
502
|
"""For creation of solution data model / rules specifically for mapping over existing containers."""
|
|
503
|
+
reference_rules = self._expand_properties(reference_rules.model_copy(deep=True))
|
|
534
504
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
# This is not desirable for the containers, so we manually fix that here.
|
|
548
|
-
# It is easier to change the space for all entities and then revert the containers, than
|
|
549
|
-
# to change the space for all entities except the containers.
|
|
550
|
-
for prop in solution_model.properties:
|
|
551
|
-
if prop.container and prop.container.space == self.new_model_id.space:
|
|
552
|
-
# If the container is in the new model space, we want to map it to the reference model space
|
|
553
|
-
# This is reverting the .dump() -> .load() above.
|
|
554
|
-
prop.container = ContainerEntity(
|
|
555
|
-
space=reference_rules.metadata.space,
|
|
556
|
-
externalId=prop.container.suffix,
|
|
505
|
+
new_views, new_properties, read_view_by_new_view = self._create_views(reference_rules)
|
|
506
|
+
new_containers, new_container_properties = self._create_containers_update_view_filter(
|
|
507
|
+
new_views, reference_rules, read_view_by_new_view
|
|
508
|
+
)
|
|
509
|
+
new_properties.extend(new_container_properties)
|
|
510
|
+
|
|
511
|
+
if self.properties == "connection":
|
|
512
|
+
# Ensure the Enterprise view and the new solution view are next to each other
|
|
513
|
+
new_properties.sort(
|
|
514
|
+
key=lambda prop: (
|
|
515
|
+
prop.view.external_id.removeprefix(self.view_prefix),
|
|
516
|
+
bool(prop.view.external_id.startswith(self.view_prefix)),
|
|
557
517
|
)
|
|
558
|
-
|
|
559
|
-
for view in solution_model.views:
|
|
560
|
-
view.implements = None
|
|
561
|
-
|
|
562
|
-
if self.exclude_views_in_other_spaces and self._has_views_in_multiple_space(reference_rules):
|
|
563
|
-
solution_model.views = SheetList[DMSView](
|
|
564
|
-
[view for view in solution_model.views if view.view.space == solution_model.metadata.space]
|
|
565
518
|
)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
# corresponding solution model space containers to hold them
|
|
587
|
-
solution_model.containers = new_containers
|
|
588
|
-
solution_model.properties.extend(new_properties)
|
|
589
|
-
elif self.mode == "read":
|
|
590
|
-
# Inherit view filter from original model to ensure the same instances are returned
|
|
591
|
-
# when querying the new view.
|
|
592
|
-
ref_views_by_external_id = {
|
|
593
|
-
view.view.external_id: view
|
|
594
|
-
for view in reference_rules.views
|
|
595
|
-
if view.view.space == reference_rules.metadata.space
|
|
596
|
-
}
|
|
597
|
-
ref_containers_by_ref_view = defaultdict(set)
|
|
598
|
-
for prop in reference_rules.properties:
|
|
599
|
-
if prop.container:
|
|
600
|
-
ref_containers_by_ref_view[prop.view].add(prop.container)
|
|
601
|
-
for view in solution_model.views:
|
|
602
|
-
if ref_view := ref_views_by_external_id.get(view.view.external_id):
|
|
603
|
-
if self.filter_type == "view":
|
|
604
|
-
view.filter_ = HasDataFilter(inner=[ref_view.view])
|
|
605
|
-
elif self.filter_type == "container" and (
|
|
606
|
-
ref_containers := ref_containers_by_ref_view.get(ref_view.view)
|
|
607
|
-
):
|
|
608
|
-
# Sorting to ensure deterministic order
|
|
609
|
-
view.filter_ = HasDataFilter(inner=sorted(ref_containers))
|
|
610
|
-
|
|
611
|
-
return solution_model
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
class ToDataProductModel(ToSolutionModel):
|
|
615
|
-
type_: ClassVar[str] = "data_product"
|
|
616
|
-
|
|
617
|
-
def __init__(
|
|
618
|
-
self,
|
|
619
|
-
new_model_id: DataModelIdentifier,
|
|
620
|
-
org_name: str = "My",
|
|
621
|
-
include: Literal["same-space", "all"] = "same-space",
|
|
622
|
-
):
|
|
623
|
-
super().__init__(new_model_id, org_name, mode="read", dummy_property=None, exclude_views_in_other_spaces=False)
|
|
624
|
-
self.include = include
|
|
625
|
-
|
|
626
|
-
def transform(self, rules: DMSRules) -> DMSRules:
|
|
627
|
-
# Copy to ensure immutability
|
|
628
|
-
expanded = self._expand_properties(rules.model_copy(deep=True))
|
|
629
|
-
if self.include == "same-space":
|
|
630
|
-
expanded.views = SheetList[DMSView](
|
|
631
|
-
[view for view in expanded.views if view.view.space == expanded.metadata.space]
|
|
632
|
-
)
|
|
633
|
-
used_view_entities = {view.view for view in expanded.views}
|
|
634
|
-
expanded.properties = SheetList[DMSProperty](
|
|
635
|
-
[
|
|
636
|
-
prop
|
|
637
|
-
for prop in expanded.properties
|
|
638
|
-
if prop.view.space == expanded.metadata.space
|
|
639
|
-
and (
|
|
640
|
-
(isinstance(prop.value_type, ViewEntity) and prop.value_type in used_view_entities)
|
|
641
|
-
or not isinstance(prop.value_type, ViewEntity)
|
|
642
|
-
)
|
|
643
|
-
]
|
|
644
|
-
)
|
|
645
|
-
|
|
646
|
-
return self._to_solution(expanded)
|
|
519
|
+
else:
|
|
520
|
+
new_properties.sort(key=lambda prop: (prop.view.external_id, prop.view_property))
|
|
521
|
+
|
|
522
|
+
metadata = reference_rules.metadata.model_copy(
|
|
523
|
+
deep=True,
|
|
524
|
+
update={
|
|
525
|
+
"space": self.new_model_id.space,
|
|
526
|
+
"external_id": self.new_model_id.external_id,
|
|
527
|
+
"version": self.new_model_id.version,
|
|
528
|
+
"name": f"{self.type_} data model",
|
|
529
|
+
},
|
|
530
|
+
)
|
|
531
|
+
return DMSRules(
|
|
532
|
+
metadata=metadata,
|
|
533
|
+
properties=new_properties,
|
|
534
|
+
views=new_views,
|
|
535
|
+
containers=new_containers or None,
|
|
536
|
+
enum=reference_rules.enum,
|
|
537
|
+
nodes=reference_rules.nodes,
|
|
538
|
+
)
|
|
647
539
|
|
|
648
540
|
@staticmethod
|
|
649
541
|
def _expand_properties(rules: DMSRules) -> DMSRules:
|
|
@@ -667,8 +559,169 @@ class ToDataProductModel(ToSolutionModel):
|
|
|
667
559
|
property_ids.add(prop.view_property)
|
|
668
560
|
return rules
|
|
669
561
|
|
|
562
|
+
def _create_views(
|
|
563
|
+
self, reference: DMSRules
|
|
564
|
+
) -> tuple[SheetList[DMSView], SheetList[DMSProperty], dict[ViewEntity, ViewEntity]]:
|
|
565
|
+
renaming: dict[ViewEntity, ViewEntity] = {}
|
|
566
|
+
new_views = SheetList[DMSView]()
|
|
567
|
+
read_view_by_new_view: dict[ViewEntity, ViewEntity] = {}
|
|
568
|
+
skipped_views: set[ViewEntity] = set()
|
|
569
|
+
for ref_view in reference.views:
|
|
570
|
+
if (self.skip_cognite_views and ref_view.view.space in COGNITE_SPACES) or (
|
|
571
|
+
self.exclude_views_in_other_spaces and ref_view.view.space != reference.metadata.space
|
|
572
|
+
):
|
|
573
|
+
skipped_views.add(ref_view.view)
|
|
574
|
+
continue
|
|
575
|
+
new_entity = ViewEntity(
|
|
576
|
+
# MyPy we validate that version is string in the constructor
|
|
577
|
+
space=self.new_model_id.space,
|
|
578
|
+
externalId=ref_view.view.external_id,
|
|
579
|
+
version=self.new_model_id.version, # type: ignore[arg-type]
|
|
580
|
+
)
|
|
581
|
+
if self.properties == "connection":
|
|
582
|
+
# Set suffix to existing view and introduce a new view.
|
|
583
|
+
# This will be used to point to the one view in the Enterprise model,
|
|
584
|
+
# while the new view will to be written to.
|
|
585
|
+
new_entity.suffix = f"{self.view_prefix}{ref_view.view.suffix}"
|
|
586
|
+
new_view = DMSView(
|
|
587
|
+
view=ViewEntity(
|
|
588
|
+
# MyPy we validate that version is string in the constructor
|
|
589
|
+
space=self.new_model_id.space,
|
|
590
|
+
externalId=ref_view.view.external_id,
|
|
591
|
+
version=self.new_model_id.version, # type: ignore[arg-type]
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
new_views.append(new_view)
|
|
595
|
+
read_view_by_new_view[new_view.view] = new_entity
|
|
596
|
+
elif self.properties == "repeat":
|
|
597
|
+
# This is a slight misuse of the read_view_by_new_view. For the repeat mode, we only
|
|
598
|
+
# care about the keys in this dictionary. But instead of creating a separate set, we
|
|
599
|
+
# do this.
|
|
600
|
+
read_view_by_new_view[new_entity] = new_entity
|
|
601
|
+
|
|
602
|
+
renaming[ref_view.view] = new_entity
|
|
603
|
+
new_views.append(ref_view.model_copy(deep=True, update={"implements": None, "view": new_entity}))
|
|
604
|
+
|
|
605
|
+
new_properties = SheetList[DMSProperty]()
|
|
606
|
+
for prop in reference.properties:
|
|
607
|
+
if prop.view in skipped_views:
|
|
608
|
+
continue
|
|
609
|
+
new_property = prop.model_copy(deep=True)
|
|
610
|
+
if new_property.value_type in renaming and isinstance(new_property.value_type, ViewEntity):
|
|
611
|
+
new_property.value_type = renaming[new_property.value_type]
|
|
612
|
+
if new_property.view in renaming:
|
|
613
|
+
new_property.view = renaming[new_property.view]
|
|
614
|
+
new_properties.append(new_property)
|
|
615
|
+
return new_views, new_properties, read_view_by_new_view
|
|
616
|
+
|
|
617
|
+
def _create_containers_update_view_filter(
|
|
618
|
+
self, new_views: SheetList[DMSView], reference: DMSRules, read_view_by_new_view: dict[ViewEntity, ViewEntity]
|
|
619
|
+
) -> tuple[SheetList[DMSContainer], SheetList[DMSProperty]]:
|
|
620
|
+
new_containers = SheetList[DMSContainer]()
|
|
621
|
+
container_properties: SheetList[DMSProperty] = SheetList[DMSProperty]()
|
|
622
|
+
ref_containers_by_ref_view: dict[ViewEntity, set[ContainerEntity]] = defaultdict(set)
|
|
623
|
+
ref_views_by_external_id = {
|
|
624
|
+
view.view.external_id: view for view in reference.views if view.view.space == reference.metadata.space
|
|
625
|
+
}
|
|
626
|
+
if self.filter_type == "container":
|
|
627
|
+
for prop in reference.properties:
|
|
628
|
+
if prop.container:
|
|
629
|
+
ref_containers_by_ref_view[prop.view].add(prop.container)
|
|
630
|
+
read_views = set(read_view_by_new_view.values())
|
|
631
|
+
for view in new_views:
|
|
632
|
+
if view.view in read_view_by_new_view:
|
|
633
|
+
read_view = read_view_by_new_view[view.view]
|
|
634
|
+
container_entity = ContainerEntity(space=self.new_model_id.space, externalId=view.view.external_id)
|
|
635
|
+
prefix = to_camel(view.view.suffix)
|
|
636
|
+
if self.properties == "repeat" and self.dummy_property:
|
|
637
|
+
property_ = DMSProperty(
|
|
638
|
+
view=view.view,
|
|
639
|
+
view_property=f"{prefix}{self.dummy_property}",
|
|
640
|
+
value_type=String(),
|
|
641
|
+
nullable=True,
|
|
642
|
+
immutable=False,
|
|
643
|
+
is_list=False,
|
|
644
|
+
container=container_entity,
|
|
645
|
+
container_property=f"{prefix}{self.dummy_property}",
|
|
646
|
+
)
|
|
647
|
+
new_containers.append(DMSContainer(container=container_entity))
|
|
648
|
+
container_properties.append(property_)
|
|
649
|
+
elif self.properties == "repeat" and self.dummy_property is None:
|
|
650
|
+
# For this case we set the filter. This is used by the DataProductModel.
|
|
651
|
+
# Inherit view filter from original model to ensure the same instances are returned
|
|
652
|
+
# when querying the new view.
|
|
653
|
+
if ref_view := ref_views_by_external_id.get(view.view.external_id):
|
|
654
|
+
self._set_view_filter(view, ref_containers_by_ref_view, ref_view)
|
|
655
|
+
elif self.properties == "connection" and self.direct_property:
|
|
656
|
+
property_ = DMSProperty(
|
|
657
|
+
view=view.view,
|
|
658
|
+
view_property=self.direct_property,
|
|
659
|
+
value_type=read_view,
|
|
660
|
+
nullable=True,
|
|
661
|
+
immutable=False,
|
|
662
|
+
is_list=False,
|
|
663
|
+
container=container_entity,
|
|
664
|
+
container_property=self.direct_property,
|
|
665
|
+
)
|
|
666
|
+
new_containers.append(DMSContainer(container=container_entity))
|
|
667
|
+
container_properties.append(property_)
|
|
668
|
+
else:
|
|
669
|
+
raise NeatValueError(f"Unsupported properties mode: {self.properties}")
|
|
670
|
+
|
|
671
|
+
if self.properties == "connection" and view.view in read_views:
|
|
672
|
+
# Need to ensure that the 'Enterprise' view always returns the same instances
|
|
673
|
+
# as the original view, no matter which properties are removed by the user.
|
|
674
|
+
if ref_view := ref_views_by_external_id.get(view.view.external_id.removeprefix(self.view_prefix)):
|
|
675
|
+
self._set_view_filter(view, ref_containers_by_ref_view, ref_view)
|
|
676
|
+
return new_containers, container_properties
|
|
677
|
+
|
|
678
|
+
def _set_view_filter(
|
|
679
|
+
self, view: DMSView, ref_containers_by_ref_view: dict[ViewEntity, set[ContainerEntity]], ref_view: DMSView
|
|
680
|
+
) -> None:
|
|
681
|
+
if self.filter_type == "view":
|
|
682
|
+
view.filter_ = HasDataFilter(inner=[ref_view.view])
|
|
683
|
+
elif self.filter_type == "container" and (ref_containers := ref_containers_by_ref_view.get(ref_view.view)):
|
|
684
|
+
# Sorting to ensure deterministic order
|
|
685
|
+
view.filter_ = HasDataFilter(inner=sorted(ref_containers))
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class ToDataProductModel(ToSolutionModel):
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
new_model_id: DataData model identifier for the new model.
|
|
693
|
+
include: The views to include in the data product data model. Can be either "same-space" or "all".
|
|
694
|
+
filter_type: This is the type of filter to apply to the new views. The filter is used to
|
|
695
|
+
ensure that the new views will return the same instance as the original views. The view filter is the
|
|
696
|
+
simplest filter, but it has limitation in the fusion UI. The container filter is in essence a more
|
|
697
|
+
verbose version of the view filter, and it has better support in the fusion UI. The default is "container".
|
|
698
|
+
"""
|
|
699
|
+
|
|
700
|
+
type_: ClassVar[str] = "data_product"
|
|
670
701
|
|
|
671
|
-
|
|
702
|
+
def __init__(
|
|
703
|
+
self,
|
|
704
|
+
new_model_id: DataModelIdentifier,
|
|
705
|
+
include: Literal["same-space", "all"] = "same-space",
|
|
706
|
+
filter_type: Literal["container", "view"] = "container",
|
|
707
|
+
skip_cognite_views: bool = True,
|
|
708
|
+
):
|
|
709
|
+
super().__init__(
|
|
710
|
+
new_model_id,
|
|
711
|
+
properties="repeat",
|
|
712
|
+
dummy_property=None,
|
|
713
|
+
filter_type=filter_type,
|
|
714
|
+
exclude_views_in_other_spaces=include == "same-space",
|
|
715
|
+
skip_cognite_views=skip_cognite_views,
|
|
716
|
+
)
|
|
717
|
+
self.include = include
|
|
718
|
+
|
|
719
|
+
def transform(self, rules: DMSRules) -> DMSRules:
|
|
720
|
+
# Overwrite this to avoid the warning.
|
|
721
|
+
return self._to_solution(rules)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class DropModelViews(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
672
725
|
_ASSET_VIEW = ViewId("cdf_cdm", "CogniteAsset", "v1")
|
|
673
726
|
_VIEW_BY_COLLECTION: Mapping[Literal["3D", "Annotation", "BaseViews"], frozenset[ViewId]] = {
|
|
674
727
|
"3D": frozenset(
|
|
@@ -707,51 +760,73 @@ class ReduceCogniteModel(RulesTransformer[DMSRules, DMSRules]):
|
|
|
707
760
|
),
|
|
708
761
|
}
|
|
709
762
|
|
|
710
|
-
def __init__(
|
|
711
|
-
self
|
|
712
|
-
|
|
713
|
-
|
|
763
|
+
def __init__(
|
|
764
|
+
self,
|
|
765
|
+
view_external_id: str | SequenceNotStr[str] | None = None,
|
|
766
|
+
group: Literal["3D", "Annotation", "BaseViews"]
|
|
767
|
+
| Collection[Literal["3D", "Annotation", "BaseViews"]]
|
|
768
|
+
| None = None,
|
|
769
|
+
):
|
|
770
|
+
self.drop_external_ids = (
|
|
771
|
+
{view_external_id} if isinstance(view_external_id, str) else set(view_external_id or [])
|
|
772
|
+
)
|
|
773
|
+
self.drop_collection = (
|
|
774
|
+
[group]
|
|
775
|
+
if isinstance(group, str)
|
|
776
|
+
else cast(
|
|
777
|
+
list[Literal["3D", "Annotation", "BaseViews"]],
|
|
778
|
+
[collection for collection in group or [] if collection in self._VIEW_BY_COLLECTION],
|
|
779
|
+
)
|
|
714
780
|
)
|
|
715
|
-
self.drop_external_ids = {external_id for external_id in drop if external_id not in self._VIEW_BY_COLLECTION}
|
|
716
781
|
|
|
717
782
|
def transform(self, rules: DMSRules) -> DMSRules:
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
783
|
+
exclude_views: set[ViewEntity] = {
|
|
784
|
+
view.view for view in rules.views if view.view.suffix in self.drop_external_ids
|
|
785
|
+
}
|
|
786
|
+
if rules.metadata.as_data_model_id() in COGNITE_MODELS:
|
|
787
|
+
exclude_views |= {
|
|
788
|
+
ViewEntity.from_id(view_id, "v1")
|
|
789
|
+
for collection in self.drop_collection
|
|
790
|
+
for view_id in self._VIEW_BY_COLLECTION[collection]
|
|
791
|
+
}
|
|
792
|
+
new_model = rules.model_copy(deep=True)
|
|
725
793
|
|
|
726
794
|
properties_by_view = DMSAnalysis(new_model).classes_with_properties(consider_inheritance=True)
|
|
727
795
|
|
|
728
|
-
new_model.views = SheetList[DMSView](
|
|
729
|
-
[view for view in new_model.views if view.view.as_id() not in exclude_views]
|
|
730
|
-
)
|
|
796
|
+
new_model.views = SheetList[DMSView]([view for view in new_model.views if view.view not in exclude_views])
|
|
731
797
|
new_properties = SheetList[DMSProperty]()
|
|
732
|
-
|
|
798
|
+
mapped_containers: set[ContainerEntity] = set()
|
|
733
799
|
for view in new_model.views:
|
|
734
800
|
for prop in properties_by_view[view.view]:
|
|
735
|
-
if self._is_asset_3D_property(prop):
|
|
801
|
+
if "3D" in self.drop_collection and self._is_asset_3D_property(prop):
|
|
736
802
|
# We filter out the 3D property of asset
|
|
737
803
|
continue
|
|
804
|
+
if isinstance(prop.value_type, ViewEntity) and prop.value_type in exclude_views:
|
|
805
|
+
continue
|
|
738
806
|
new_properties.append(prop)
|
|
807
|
+
if prop.container:
|
|
808
|
+
mapped_containers.add(prop.container)
|
|
739
809
|
|
|
740
810
|
new_model.properties = new_properties
|
|
811
|
+
new_model.containers = (
|
|
812
|
+
SheetList[DMSContainer](
|
|
813
|
+
[container for container in new_model.containers or [] if container.container in mapped_containers]
|
|
814
|
+
)
|
|
815
|
+
or None
|
|
816
|
+
)
|
|
741
817
|
|
|
742
818
|
return new_model
|
|
743
819
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
return prop.view.as_id() == self._ASSET_VIEW and prop.view_property == "object3D"
|
|
820
|
+
@classmethod
|
|
821
|
+
def _is_asset_3D_property(cls, prop: DMSProperty) -> bool:
|
|
822
|
+
return prop.view.as_id() == cls._ASSET_VIEW and prop.view_property == "object3D"
|
|
748
823
|
|
|
749
824
|
@property
|
|
750
825
|
def description(self) -> str:
|
|
751
826
|
return f"Removed {len(self.drop_external_ids) + len(self.drop_collection)} views from data model"
|
|
752
827
|
|
|
753
828
|
|
|
754
|
-
class IncludeReferenced(
|
|
829
|
+
class IncludeReferenced(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
755
830
|
def __init__(self, client: NeatClient, include_properties: bool = False) -> None:
|
|
756
831
|
self._client = client
|
|
757
832
|
self.include_properties = include_properties
|
|
@@ -802,7 +877,7 @@ class IncludeReferenced(RulesTransformer[DMSRules, DMSRules]):
|
|
|
802
877
|
return "Included referenced views and containers in the data model."
|
|
803
878
|
|
|
804
879
|
|
|
805
|
-
class AddClassImplements(
|
|
880
|
+
class AddClassImplements(VerifiedRulesTransformer[InformationRules, InformationRules]):
|
|
806
881
|
def __init__(self, implements: str, suffix: str):
|
|
807
882
|
self.implements = implements
|
|
808
883
|
self.suffix = suffix
|
|
@@ -820,7 +895,7 @@ class AddClassImplements(RulesTransformer[InformationRules, InformationRules]):
|
|
|
820
895
|
return f"Added implements property to classes with suffix {self.suffix}"
|
|
821
896
|
|
|
822
897
|
|
|
823
|
-
class ClassicPrepareCore(
|
|
898
|
+
class ClassicPrepareCore(VerifiedRulesTransformer[InformationRules, InformationRules]):
|
|
824
899
|
"""Update the classic data model with the following:
|
|
825
900
|
|
|
826
901
|
This is a special purpose transformer that is only intended to be used with when reading
|
|
@@ -902,7 +977,7 @@ class ClassicPrepareCore(RulesTransformer[InformationRules, InformationRules]):
|
|
|
902
977
|
return output
|
|
903
978
|
|
|
904
979
|
|
|
905
|
-
class ChangeViewPrefix(
|
|
980
|
+
class ChangeViewPrefix(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
906
981
|
def __init__(self, old: str, new: str) -> None:
|
|
907
982
|
self.old = old
|
|
908
983
|
self.new = new
|
|
@@ -927,7 +1002,7 @@ class ChangeViewPrefix(RulesTransformer[DMSRules, DMSRules]):
|
|
|
927
1002
|
return output
|
|
928
1003
|
|
|
929
1004
|
|
|
930
|
-
class MergeDMSRules(
|
|
1005
|
+
class MergeDMSRules(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
931
1006
|
def __init__(self, extra: DMSRules) -> None:
|
|
932
1007
|
self.extra = extra
|
|
933
1008
|
|
|
@@ -969,7 +1044,7 @@ class MergeDMSRules(RulesTransformer[DMSRules, DMSRules]):
|
|
|
969
1044
|
return f"Merged with {self.extra.metadata.as_data_model_id()}"
|
|
970
1045
|
|
|
971
1046
|
|
|
972
|
-
class MergeInformationRules(
|
|
1047
|
+
class MergeInformationRules(VerifiedRulesTransformer[InformationRules, InformationRules]):
|
|
973
1048
|
def __init__(self, extra: InformationRules) -> None:
|
|
974
1049
|
self.extra = extra
|
|
975
1050
|
|
|
@@ -997,7 +1072,7 @@ class _InformationRulesConverter:
|
|
|
997
1072
|
self.property_count_by_container: dict[ContainerEntity, int] = defaultdict(int)
|
|
998
1073
|
|
|
999
1074
|
def as_dms_rules(
|
|
1000
|
-
self, ignore_undefined_value_types: bool = False, reserved_properties: Literal["error", "
|
|
1075
|
+
self, ignore_undefined_value_types: bool = False, reserved_properties: Literal["error", "warning"] = "error"
|
|
1001
1076
|
) -> "DMSRules":
|
|
1002
1077
|
from cognite.neat._rules.models.dms._rules import (
|
|
1003
1078
|
DMSContainer,
|