cognite-neat 0.88.3__py3-none-any.whl → 0.89.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cognite-neat might be problematic. Click here for more details.

Files changed (75) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/constants.py +3 -0
  3. cognite/neat/graph/extractors/_mock_graph_generator.py +2 -1
  4. cognite/neat/issues/_base.py +2 -1
  5. cognite/neat/issues/errors/__init__.py +2 -1
  6. cognite/neat/issues/errors/_general.py +7 -0
  7. cognite/neat/issues/warnings/_models.py +1 -1
  8. cognite/neat/issues/warnings/user_modeling.py +1 -1
  9. cognite/neat/rules/_shared.py +49 -6
  10. cognite/neat/rules/analysis/_base.py +1 -1
  11. cognite/neat/rules/exporters/_base.py +7 -18
  12. cognite/neat/rules/exporters/_rules2dms.py +8 -18
  13. cognite/neat/rules/exporters/_rules2excel.py +5 -12
  14. cognite/neat/rules/exporters/_rules2ontology.py +9 -19
  15. cognite/neat/rules/exporters/_rules2yaml.py +3 -6
  16. cognite/neat/rules/importers/_base.py +7 -52
  17. cognite/neat/rules/importers/_dms2rules.py +171 -115
  18. cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +26 -18
  19. cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +14 -30
  20. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2classes.py +7 -3
  21. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2metadata.py +3 -3
  22. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2properties.py +18 -11
  23. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2rules.py +9 -18
  24. cognite/neat/rules/importers/_rdf/_inference2rules.py +10 -33
  25. cognite/neat/rules/importers/_rdf/_owl2rules/_owl2rules.py +9 -20
  26. cognite/neat/rules/importers/_rdf/_shared.py +1 -1
  27. cognite/neat/rules/importers/_spreadsheet2rules.py +22 -86
  28. cognite/neat/rules/importers/_yaml2rules.py +14 -41
  29. cognite/neat/rules/models/__init__.py +21 -5
  30. cognite/neat/rules/models/_base_input.py +162 -0
  31. cognite/neat/rules/models/{_base.py → _base_rules.py} +1 -12
  32. cognite/neat/rules/models/asset/__init__.py +5 -2
  33. cognite/neat/rules/models/asset/_rules.py +2 -20
  34. cognite/neat/rules/models/asset/_rules_input.py +40 -115
  35. cognite/neat/rules/models/asset/_validation.py +1 -1
  36. cognite/neat/rules/models/data_types.py +150 -44
  37. cognite/neat/rules/models/dms/__init__.py +19 -7
  38. cognite/neat/rules/models/dms/_exporter.py +72 -26
  39. cognite/neat/rules/models/dms/_rules.py +42 -155
  40. cognite/neat/rules/models/dms/_rules_input.py +186 -254
  41. cognite/neat/rules/models/dms/_serializer.py +44 -3
  42. cognite/neat/rules/models/dms/_validation.py +3 -4
  43. cognite/neat/rules/models/domain.py +52 -1
  44. cognite/neat/rules/models/entities/__init__.py +63 -0
  45. cognite/neat/rules/models/entities/_constants.py +73 -0
  46. cognite/neat/rules/models/entities/_loaders.py +76 -0
  47. cognite/neat/rules/models/entities/_multi_value.py +67 -0
  48. cognite/neat/rules/models/{entities.py → entities/_single_value.py} +74 -232
  49. cognite/neat/rules/models/entities/_types.py +86 -0
  50. cognite/neat/rules/models/{wrapped_entities.py → entities/_wrapped.py} +1 -1
  51. cognite/neat/rules/models/information/__init__.py +10 -2
  52. cognite/neat/rules/models/information/_rules.py +3 -14
  53. cognite/neat/rules/models/information/_rules_input.py +57 -204
  54. cognite/neat/rules/models/information/_validation.py +1 -1
  55. cognite/neat/rules/transformers/__init__.py +21 -0
  56. cognite/neat/rules/transformers/_base.py +69 -3
  57. cognite/neat/rules/{models/information/_converter.py → transformers/_converters.py} +216 -20
  58. cognite/neat/rules/transformers/_map_onto.py +97 -0
  59. cognite/neat/rules/transformers/_pipelines.py +61 -0
  60. cognite/neat/rules/transformers/_verification.py +136 -0
  61. cognite/neat/store/_provenance.py +10 -1
  62. cognite/neat/utils/cdf/data_classes.py +20 -0
  63. cognite/neat/utils/regex_patterns.py +6 -0
  64. cognite/neat/workflows/steps/lib/current/rules_exporter.py +106 -37
  65. cognite/neat/workflows/steps/lib/current/rules_importer.py +24 -22
  66. {cognite_neat-0.88.3.dist-info → cognite_neat-0.89.0.dist-info}/METADATA +1 -1
  67. {cognite_neat-0.88.3.dist-info → cognite_neat-0.89.0.dist-info}/RECORD +71 -66
  68. cognite/neat/rules/models/_constants.py +0 -2
  69. cognite/neat/rules/models/_types/__init__.py +0 -19
  70. cognite/neat/rules/models/asset/_converter.py +0 -4
  71. cognite/neat/rules/models/dms/_converter.py +0 -143
  72. /cognite/neat/rules/models/{_types/_field.py → _types.py} +0 -0
  73. {cognite_neat-0.88.3.dist-info → cognite_neat-0.89.0.dist-info}/LICENSE +0 -0
  74. {cognite_neat-0.88.3.dist-info → cognite_neat-0.89.0.dist-info}/WHEEL +0 -0
  75. {cognite_neat-0.88.3.dist-info → cognite_neat-0.89.0.dist-info}/entry_points.txt +0 -0
@@ -1,30 +1,62 @@
1
+ import re
1
2
  import sys
3
+ import typing
2
4
  from datetime import date, datetime
3
5
  from typing import Any, ClassVar
4
6
 
5
7
  from cognite.client.data_classes import data_modeling as dms
6
- from pydantic import BaseModel, model_serializer, model_validator
8
+ from cognite.client.data_classes.data_modeling.data_types import Enum as DMSEnum
9
+ from pydantic import BaseModel, Field, model_serializer, model_validator
7
10
  from pydantic.functional_validators import ModelWrapValidatorHandler
8
11
 
12
+ from cognite.neat.rules.models.entities._single_value import ClassEntity, UnitEntity
13
+ from cognite.neat.utils.regex_patterns import SPLIT_ON_COMMA_PATTERN, SPLIT_ON_EQUAL_PATTERN
14
+
9
15
  if sys.version_info >= (3, 11):
10
16
  from typing import Self
11
17
  else:
12
18
  from typing_extensions import Self
13
19
 
20
+ # This patterns matches a string that is a data type, with optional content in parentheses.
21
+ # For example, it matches "float(unit=power:megaw)" as name="float" and content="unit=power:megaw"
22
+ _DATATYPE_PATTERN = re.compile(r"^(?P<name>[^(:]*)(\((?P<content>.+)\))?$")
14
23
 
15
- class DataType(BaseModel):
16
- # These are necessary for Pydantic to work
17
- # pydantic gets confused as we have no fields.
18
- __pydantic_extra__ = None
19
- __pydantic_fields_set__ = set()
20
- __pydantic_private__ = {}
21
24
 
22
- name: ClassVar[str]
25
+ class DataType(BaseModel):
23
26
  python: ClassVar[type]
24
27
  dms: ClassVar[type[dms.PropertyType]]
25
28
  graphql: ClassVar[str]
26
29
  xsd: ClassVar[str]
27
30
  sql: ClassVar[str]
31
+ _dms_loaded: bool = False
32
+ # Repeat all here, just to make mypy happy
33
+ name: typing.Literal[
34
+ "boolean",
35
+ "token",
36
+ "float",
37
+ "double",
38
+ "integer",
39
+ "nonPositiveInteger",
40
+ "nonNegativeInteger",
41
+ "long",
42
+ "negativeInteger",
43
+ "string",
44
+ "langString",
45
+ "anyURI",
46
+ "normalizedString",
47
+ "dateTime",
48
+ "timestamp",
49
+ "dateTimeStamp",
50
+ "date",
51
+ "plainLiteral",
52
+ "Literal",
53
+ "timeseries",
54
+ "file",
55
+ "sequence",
56
+ "json",
57
+ "enum",
58
+ "",
59
+ ] = ""
28
60
 
29
61
  @classmethod
30
62
  def load(cls, data: Any) -> Self:
@@ -35,15 +67,31 @@ class DataType(BaseModel):
35
67
 
36
68
  @model_validator(mode="wrap")
37
69
  def _load(cls, value: Any, handler: ModelWrapValidatorHandler["DataType"]) -> Any:
38
- if isinstance(value, cls | dict):
39
- return value
40
- elif isinstance(value, str):
41
- value_standardized = value.casefold()
42
- if cls_ := _DATA_TYPE_BY_DMS_TYPE.get(value_standardized):
43
- return cls_()
44
- elif cls_ := _DATA_TYPE_BY_NAME.get(value_standardized):
45
- return cls_()
46
- raise ValueError(f"Unknown literal type: {value}") from None
70
+ if cls is not DataType or isinstance(value, DataType):
71
+ # This is a subclass, let the subclass handle it
72
+ return handler(value)
73
+ elif isinstance(value, str) and (match := _DATATYPE_PATTERN.match(value)):
74
+ name = match.group("name").casefold()
75
+ cls_: type[DataType]
76
+ if name in _DATA_TYPE_BY_DMS_TYPE:
77
+ cls_ = _DATA_TYPE_BY_DMS_TYPE[name]
78
+ dms_loaded = True
79
+ elif name in _DATA_TYPE_BY_NAME:
80
+ cls_ = _DATA_TYPE_BY_NAME[name]
81
+ dms_loaded = False
82
+ else:
83
+ raise ValueError(f"Unknown data type: {value}") from None
84
+ extra_args: dict[str, Any] = {}
85
+ if content := match.group("content"):
86
+ extra_args = dict(
87
+ SPLIT_ON_EQUAL_PATTERN.split(pair.strip()) for pair in SPLIT_ON_COMMA_PATTERN.split(content)
88
+ )
89
+ # Todo? Raise warning if extra_args contains keys that are not in the model fields
90
+ instance = cls_(**extra_args)
91
+ created = handler(instance)
92
+ # Private attributes are not validated or set. We need to set it manually
93
+ created._dms_loaded = dms_loaded
94
+ return created
47
95
  raise ValueError(f"Cannot load {cls.__name__} from {value}")
48
96
 
49
97
  @model_serializer(when_used="unless-none", return_type=str)
@@ -51,227 +99,285 @@ class DataType(BaseModel):
51
99
  return str(self)
52
100
 
53
101
  def __str__(self) -> str:
54
- return self.name
102
+ if self._dms_loaded:
103
+ base = self.dms._type
104
+ else:
105
+ base = self.model_fields["name"].default
106
+ return self._suffix_extra_args(base)
107
+
108
+ def _suffix_extra_args(self, base: str) -> str:
109
+ extra_fields: dict[str, str] = {}
110
+ for field_id, field_ in self.model_fields.items():
111
+ if field_id == "name":
112
+ continue
113
+ value = getattr(self, field_id)
114
+ if value is None:
115
+ continue
116
+ extra_fields[field_.alias or field_id] = str(value)
117
+ if extra_fields:
118
+ content = ",".join(f"{key}={value}" for key, value in sorted(extra_fields.items(), key=lambda x: x[0]))
119
+ return f"{base}({content})"
120
+ return base
55
121
 
56
122
  def __eq__(self, other: Any) -> bool:
57
- return isinstance(other, type(self))
123
+ return isinstance(other, type(self)) and str(self) == str(other)
58
124
 
59
125
  def __hash__(self) -> int:
60
126
  return hash(str(self))
61
127
 
62
128
  @classmethod
63
129
  def is_data_type(cls, value: str) -> bool:
64
- return value.casefold() in _DATA_TYPE_BY_NAME or value.casefold() in _DATA_TYPE_BY_DMS_TYPE
130
+ if match := _DATATYPE_PATTERN.match(value):
131
+ name = match.group("name").casefold()
132
+ return name in _DATA_TYPE_BY_NAME or name in _DATA_TYPE_BY_DMS_TYPE
133
+ return False
65
134
 
66
135
 
67
136
  class Boolean(DataType):
68
- name = "boolean"
69
137
  python = bool
70
138
  dms = dms.Boolean
71
139
  graphql = "Boolean"
72
140
  xsd = "boolean"
73
141
  sql = "BOOLEAN"
142
+ name: typing.Literal["boolean"] = "boolean"
74
143
 
75
144
 
76
145
  class Float(DataType):
77
- name = "float"
78
146
  python = float
79
147
  dms = dms.Float32
80
148
  graphql = "Float"
81
149
  xsd = "float"
82
150
  sql = "FLOAT"
83
151
 
152
+ name: typing.Literal["float"] = "float"
153
+ unit: UnitEntity | None = None
154
+
84
155
 
85
156
  class Double(DataType):
86
- name = "double"
87
157
  python = float
88
158
  dms = dms.Float64
89
159
  graphql = "Float"
90
160
  xsd = "double"
91
161
  sql = "FLOAT"
92
162
 
163
+ name: typing.Literal["double"] = "double"
164
+ unit: UnitEntity | None = None
165
+
93
166
 
94
167
  class Integer(DataType):
95
- name = "integer"
96
168
  python = int
97
169
  dms = dms.Int32
98
170
  graphql = "Int"
99
171
  xsd = "integer"
100
172
  sql = "INTEGER"
101
173
 
174
+ name: typing.Literal["integer"] = "integer"
175
+
102
176
 
103
177
  class NonPositiveInteger(DataType):
104
- name = "nonPositiveInteger"
105
178
  python = int
106
179
  dms = dms.Int32
107
180
  graphql = "Int"
108
181
  xsd = "nonPositiveInteger"
109
182
  sql = "INTEGER"
110
183
 
184
+ name: typing.Literal["nonPositiveInteger"] = "nonPositiveInteger"
185
+
111
186
 
112
187
  class NonNegativeInteger(DataType):
113
- name = "nonNegativeInteger"
114
188
  python = int
115
189
  dms = dms.Int32
116
190
  graphql = "Int"
117
191
  xsd = "nonNegativeInteger"
118
192
  sql = "INTEGER"
119
193
 
194
+ name: typing.Literal["nonNegativeInteger"] = "nonNegativeInteger"
195
+
120
196
 
121
197
  class NegativeInteger(DataType):
122
- name = "negativeInteger"
123
198
  python = int
124
199
  dms = dms.Int32
125
200
  graphql = "Int"
126
201
  xsd = "negativeInteger"
127
202
  sql = "INTEGER"
128
203
 
204
+ name: typing.Literal["negativeInteger"] = "negativeInteger"
205
+
129
206
 
130
207
  class Long(DataType):
131
- name = "long"
132
208
  python = int
133
209
  dms = dms.Int64
134
210
  graphql = "Int"
135
211
  xsd = "long"
136
212
  sql = "BIGINT"
137
213
 
214
+ name: typing.Literal["long"] = "long"
215
+
138
216
 
139
217
  class String(DataType):
140
- name = "string"
141
218
  python = str
142
219
  dms = dms.Text
143
220
  graphql = "String"
144
221
  xsd = "string"
145
222
  sql = "STRING"
146
223
 
224
+ name: typing.Literal["string"] = "string"
225
+
147
226
 
148
227
  class LangString(DataType):
149
- name = "langString"
150
228
  python = str
151
229
  dms = dms.Text
152
230
  graphql = "String"
153
231
  xsd = "string"
154
232
  sql = "STRING"
155
233
 
234
+ name: typing.Literal["langString"] = "langString"
235
+
156
236
 
157
237
  class AnyURI(DataType):
158
- name = "anyURI"
159
238
  python = str
160
239
  dms = dms.Text
161
240
  graphql = "String"
162
241
  xsd = "anyURI"
163
242
  sql = "STRING"
164
243
 
244
+ name: typing.Literal["anyURI"] = "anyURI"
245
+
165
246
 
166
247
  class NormalizedString(DataType):
167
- name = "normalizedString"
168
248
  python = str
169
249
  dms = dms.Text
170
250
  graphql = "String"
171
251
  xsd = "normalizedString"
172
252
  sql = "STRING"
173
253
 
254
+ name: typing.Literal["normalizedString"] = "normalizedString"
255
+
174
256
 
175
257
  class Token(DataType):
176
- name = "token"
177
258
  python = str
178
259
  dms = dms.Text
179
260
  graphql = "String"
180
261
  xsd = "string"
181
262
  sql = "STRING"
182
263
 
264
+ name: typing.Literal["token"] = "token"
265
+
183
266
 
184
267
  class DateTime(DataType):
185
- name = "dateTime"
186
268
  python = datetime
187
269
  dms = dms.Timestamp
188
270
  graphql = "Timestamp"
189
271
  xsd = "dateTimeStamp"
190
272
  sql = "TIMESTAMP"
191
273
 
274
+ name: typing.Literal["dateTime"] = "dateTime"
275
+
192
276
 
193
277
  class Timestamp(DataType):
194
- name = "timestamp"
195
278
  python = datetime
196
279
  dms = dms.Timestamp
197
280
  graphql = "Timestamp"
198
281
  xsd = "dateTimeStamp"
199
282
  sql = "TIMESTAMP"
200
283
 
284
+ name: typing.Literal["timestamp"] = "timestamp"
285
+
201
286
 
202
287
  class DateTimeStamp(DataType):
203
- name = "dateTimeStamp"
204
288
  python = datetime
205
289
  dms = dms.Timestamp
206
290
  graphql = "Timestamp"
207
291
  xsd = "dateTimeStamp"
208
292
  sql = "TIMESTAMP"
209
293
 
294
+ name: typing.Literal["dateTimeStamp"] = "dateTimeStamp"
295
+
210
296
 
211
297
  class Date(DataType):
212
- name = "date"
213
298
  python = date
214
299
  dms = dms.Date
215
300
  graphql = "String"
216
301
  xsd = "date"
217
302
  sql = "DATE"
218
303
 
304
+ name: typing.Literal["date"] = "date"
305
+
219
306
 
220
307
  class PlainLiteral(DataType):
221
- name = "PlainLiteral"
222
308
  python = str
223
309
  dms = dms.Text
224
310
  graphql = "String"
225
311
  xsd = "plainLiteral"
226
312
  sql = "STRING"
227
313
 
314
+ name: typing.Literal["plainLiteral"] = "plainLiteral"
315
+
228
316
 
229
317
  class Literal(DataType):
230
- name = "Literal"
231
318
  python = str
232
319
  dms = dms.Text
233
320
  graphql = "String"
234
321
  xsd = "string"
235
322
  sql = "STRING"
236
323
 
324
+ name: typing.Literal["Literal"] = "Literal"
325
+
237
326
 
238
327
  class Timeseries(DataType):
239
- name = "timeseries"
240
328
  python = str
241
329
  dms = dms.TimeSeriesReference
242
330
  graphql = "TimeSeries"
243
331
  xsd = "string"
244
332
  sql = "STRING"
245
333
 
334
+ name: typing.Literal["timeseries"] = "timeseries"
335
+
246
336
 
247
337
  class File(DataType):
248
- name = "file"
249
338
  python = str
250
339
  dms = dms.FileReference
251
340
  graphql = "File"
252
341
  xsd = "string"
253
342
  sql = "STRING"
254
343
 
344
+ name: typing.Literal["file"] = "file"
345
+
255
346
 
256
347
  class Sequence(DataType):
257
- name = "sequence"
258
348
  python = str
259
349
  dms = dms.SequenceReference
260
350
  graphql = "Sequence"
261
351
  xsd = "string"
262
352
  sql = "STRING"
263
353
 
354
+ name: typing.Literal["sequence"] = "sequence"
355
+
264
356
 
265
357
  class Json(DataType):
266
- name = "json"
267
358
  python = dict
268
359
  dms = dms.Json
269
360
  graphql = "Json"
270
361
  xsd = "json"
271
362
  sql = "STRING"
272
363
 
364
+ name: typing.Literal["json"] = "json"
365
+
366
+
367
+ class Enum(DataType):
368
+ python = str
369
+ dms = DMSEnum
370
+ graphql = "Enum"
371
+ xsd = "string"
372
+ sql = "STRING"
373
+
374
+ name: typing.Literal["enum"] = "enum"
375
+
376
+ collection: ClassEntity
377
+ unknown_value: str | None = Field(None, alias="unknownValue")
378
+
273
379
 
274
- _DATA_TYPE_BY_NAME = {cls.name.casefold(): cls for cls in DataType.__subclasses__()}
380
+ _DATA_TYPE_BY_NAME = {cls.model_fields["name"].default.casefold(): cls for cls in DataType.__subclasses__()}
275
381
  _seen = set()
276
382
  _DATA_TYPE_BY_DMS_TYPE = {
277
383
  cls.dms._type.casefold(): cls
@@ -1,5 +1,13 @@
1
- from ._rules import DMSContainer, DMSMetadata, DMSProperty, DMSRules, DMSView
2
- from ._rules_input import DMSContainerInput, DMSMetadataInput, DMSPropertyInput, DMSRulesInput, DMSViewInput
1
+ from ._rules import DMSContainer, DMSEnum, DMSMetadata, DMSNode, DMSProperty, DMSRules, DMSView
2
+ from ._rules_input import (
3
+ DMSInputContainer,
4
+ DMSInputEnum,
5
+ DMSInputMetadata,
6
+ DMSInputNode,
7
+ DMSInputProperty,
8
+ DMSInputRules,
9
+ DMSInputView,
10
+ )
3
11
  from ._schema import DMSSchema, PipelineSchema
4
12
 
5
13
  __all__ = [
@@ -9,10 +17,14 @@ __all__ = [
9
17
  "DMSView",
10
18
  "DMSProperty",
11
19
  "DMSContainer",
20
+ "DMSNode",
21
+ "DMSEnum",
12
22
  "PipelineSchema",
13
- "DMSRulesInput",
14
- "DMSMetadataInput",
15
- "DMSViewInput",
16
- "DMSPropertyInput",
17
- "DMSContainerInput",
23
+ "DMSInputRules",
24
+ "DMSInputMetadata",
25
+ "DMSInputView",
26
+ "DMSInputProperty",
27
+ "DMSInputContainer",
28
+ "DMSInputNode",
29
+ "DMSInputEnum",
18
30
  ]
@@ -1,10 +1,11 @@
1
1
  import warnings
2
2
  from collections import defaultdict
3
- from collections.abc import Sequence
3
+ from collections.abc import Collection, Sequence
4
4
  from typing import Any, cast
5
5
 
6
6
  from cognite.client.data_classes import data_modeling as dm
7
7
  from cognite.client.data_classes.data_modeling.containers import BTreeIndex
8
+ from cognite.client.data_classes.data_modeling.data_types import EnumValue as DMSEnumValue
8
9
  from cognite.client.data_classes.data_modeling.data_types import ListablePropertyType
9
10
  from cognite.client.data_classes.data_modeling.views import (
10
11
  SingleEdgeConnectionApply,
@@ -12,6 +13,7 @@ from cognite.client.data_classes.data_modeling.views import (
12
13
  ViewPropertyApply,
13
14
  )
14
15
 
16
+ from cognite.neat.issues.errors import NeatTypeError, ResourceNotFoundError
15
17
  from cognite.neat.issues.warnings import NotSupportedWarning, PropertyNotFoundWarning
16
18
  from cognite.neat.issues.warnings.user_modeling import (
17
19
  EmptyContainerWarning,
@@ -19,20 +21,25 @@ from cognite.neat.issues.warnings.user_modeling import (
19
21
  HasDataFilterOnViewWithReferencesWarning,
20
22
  NodeTypeFilterOnParentViewWarning,
21
23
  )
22
- from cognite.neat.rules.models._base import DataModelType, ExtensionCategory, SchemaCompleteness
23
- from cognite.neat.rules.models.data_types import DataType
24
+ from cognite.neat.rules.models._base_rules import DataModelType, ExtensionCategory, SchemaCompleteness
25
+ from cognite.neat.rules.models.data_types import DataType, Double, Enum, Float
24
26
  from cognite.neat.rules.models.entities import (
27
+ ClassEntity,
25
28
  ContainerEntity,
29
+ DMSFilter,
26
30
  DMSNodeEntity,
27
31
  DMSUnknownEntity,
32
+ EdgeEntity,
33
+ HasDataFilter,
34
+ NodeTypeFilter,
28
35
  ReferenceEntity,
36
+ ReverseConnectionEntity,
37
+ UnitEntity,
29
38
  ViewEntity,
30
- ViewPropertyEntity,
31
39
  )
32
- from cognite.neat.rules.models.wrapped_entities import DMSFilter, HasDataFilter, NodeTypeFilter
33
40
  from cognite.neat.utils.cdf.data_classes import ContainerApplyDict, NodeApplyDict, SpaceApplyDict, ViewApplyDict
34
41
 
35
- from ._rules import DMSMetadata, DMSProperty, DMSRules, DMSView
42
+ from ._rules import DMSEnum, DMSMetadata, DMSProperty, DMSRules, DMSView
36
43
  from ._schema import DMSSchema, PipelineSchema
37
44
 
38
45
 
@@ -111,9 +118,16 @@ class _DMSExporter:
111
118
  ]
112
119
  self._update_with_properties(selected_properties, container_properties_by_id, None)
113
120
 
114
- containers = self._create_containers(container_properties_by_id)
121
+ containers = self._create_containers(container_properties_by_id, rules.enum) # type: ignore[arg-type]
115
122
 
116
- views, node_types = self._create_views_with_node_types(view_properties_by_id)
123
+ views, view_node_type_filters = self._create_views_with_node_types(view_properties_by_id)
124
+ if rules.nodes:
125
+ node_types = NodeApplyDict(
126
+ [node.as_node() for node in rules.nodes]
127
+ + [dm.NodeApply(node.space, node.external_id) for node in view_node_type_filters]
128
+ )
129
+ else:
130
+ node_types = NodeApplyDict([dm.NodeApply(node.space, node.external_id) for node in view_node_type_filters])
117
131
 
118
132
  last_schema: DMSSchema | None = None
119
133
  if self.rules.last:
@@ -184,7 +198,7 @@ class _DMSExporter:
184
198
  def _create_views_with_node_types(
185
199
  self,
186
200
  view_properties_by_id: dict[dm.ViewId, list[DMSProperty]],
187
- ) -> tuple[ViewApplyDict, NodeApplyDict]:
201
+ ) -> tuple[ViewApplyDict, set[dm.NodeId]]:
188
202
  input_views = list(self.rules.views)
189
203
  if self.rules.last:
190
204
  existing = {view.view.as_id() for view in input_views}
@@ -245,17 +259,19 @@ class _DMSExporter:
245
259
  # as they are expected for the solution model.
246
260
  unique_node_types.add(dm.NodeId(space=view.space, external_id=view.external_id))
247
261
 
248
- return views, NodeApplyDict(
249
- [dm.NodeApply(space=node.space, external_id=node.external_id) for node in unique_node_types]
250
- )
262
+ return views, unique_node_types
251
263
 
252
264
  @classmethod
253
265
  def _create_edge_type_from_prop(cls, prop: DMSProperty) -> dm.DirectRelationReference:
254
- if isinstance(prop.reference, ReferenceEntity):
266
+ if isinstance(prop.connection, EdgeEntity) and prop.connection.edge_type is not None:
267
+ return prop.connection.edge_type.as_reference()
268
+ elif isinstance(prop.reference, ReferenceEntity):
255
269
  ref_view_prop = prop.reference.as_view_property_id()
256
270
  return cls._create_edge_type_from_view_id(cast(dm.ViewId, ref_view_prop.source), ref_view_prop.property)
257
- else:
271
+ elif isinstance(prop.value_type, ViewEntity):
258
272
  return cls._create_edge_type_from_view_id(prop.view.as_id(), prop.view_property)
273
+ else:
274
+ raise NeatTypeError(f"Invalid valueType {prop.value_type!r}")
259
275
 
260
276
  @staticmethod
261
277
  def _create_edge_type_from_view_id(view_id: dm.ViewId, property_: str) -> dm.DirectRelationReference:
@@ -268,7 +284,12 @@ class _DMSExporter:
268
284
  def _create_containers(
269
285
  self,
270
286
  container_properties_by_id: dict[dm.ContainerId, list[DMSProperty]],
287
+ enum: Collection[DMSEnum] | None,
271
288
  ) -> ContainerApplyDict:
289
+ enum_values_by_collection: dict[ClassEntity, list[DMSEnum]] = defaultdict(list)
290
+ for enum_value in enum or []:
291
+ enum_values_by_collection[enum_value.collection].append(enum_value)
292
+
272
293
  containers = list(self.rules.containers or [])
273
294
  if self.rules.last:
274
295
  existing = {container.container.as_id() for container in containers}
@@ -297,11 +318,27 @@ class _DMSExporter:
297
318
  type_cls = prop.value_type.dms
298
319
  else:
299
320
  type_cls = dm.DirectRelation
300
- type_: dm.PropertyType
321
+
322
+ args: dict[str, Any] = {}
301
323
  if issubclass(type_cls, ListablePropertyType):
302
- type_ = type_cls(is_list=prop.is_list or False)
303
- else:
304
- type_ = type_cls()
324
+ args["is_list"] = prop.is_list or False
325
+ if isinstance(prop.value_type, Double | Float) and isinstance(prop.value_type.unit, UnitEntity):
326
+ args["unit"] = prop.value_type.unit.as_reference()
327
+ if isinstance(prop.value_type, Enum):
328
+ if prop.value_type.collection not in enum_values_by_collection:
329
+ raise ResourceNotFoundError(
330
+ prop.value_type.collection, "enum collection", prop.container, "container"
331
+ )
332
+ args["unknown_value"] = prop.value_type.unknown_value
333
+ args["values"] = {
334
+ value.value: DMSEnumValue(
335
+ name=value.name,
336
+ description=value.description,
337
+ )
338
+ for value in enum_values_by_collection[prop.value_type.collection]
339
+ }
340
+
341
+ type_ = type_cls(**args)
305
342
  container.properties[prop.container_property] = dm.ContainerProperty(
306
343
  type=type_,
307
344
  # If not set, nullable is True and immutable is False
@@ -463,7 +500,7 @@ class _DMSExporter:
463
500
  description=prop.description,
464
501
  **extra_args,
465
502
  )
466
- elif prop.connection == "edge":
503
+ elif isinstance(prop.connection, EdgeEntity):
467
504
  if isinstance(prop.value_type, ViewEntity):
468
505
  source_view_id = prop.value_type.as_id()
469
506
  else:
@@ -472,6 +509,9 @@ class _DMSExporter:
472
509
  "If this error occurs it is a bug in NEAT, please report"
473
510
  f"Debug Info, Invalid valueType edge: {prop.model_dump_json()}"
474
511
  )
512
+ edge_source: dm.ViewId | None = None
513
+ if prop.connection.properties is not None:
514
+ edge_source = prop.connection.properties.as_id()
475
515
  edge_cls: type[dm.EdgeConnectionApply] = dm.MultiEdgeConnectionApply
476
516
  # If is_list is not set, we default to a MultiEdgeConnection
477
517
  if prop.is_list is False:
@@ -483,13 +523,11 @@ class _DMSExporter:
483
523
  direction="outwards",
484
524
  name=prop.name,
485
525
  description=prop.description,
526
+ edge_source=edge_source,
486
527
  )
487
- elif prop.connection == "reverse":
488
- reverse_prop_id: str | None = None
489
- if isinstance(prop.value_type, ViewPropertyEntity):
490
- source_view_id = prop.value_type.as_view_id()
491
- reverse_prop_id = prop.value_type.property_
492
- elif isinstance(prop.value_type, ViewEntity):
528
+ elif isinstance(prop.connection, ReverseConnectionEntity):
529
+ reverse_prop_id = prop.connection.property_
530
+ if isinstance(prop.value_type, ViewEntity):
493
531
  source_view_id = prop.value_type.as_id()
494
532
  else:
495
533
  # Should have been validated.
@@ -498,6 +536,7 @@ class _DMSExporter:
498
536
  f"Debug Info, Invalid valueType reverse connection: {prop.model_dump_json()}"
499
537
  )
500
538
  reverse_prop: DMSProperty | None = None
539
+ edge_source = None
501
540
  if reverse_prop_id is not None:
502
541
  reverse_prop = next(
503
542
  (
@@ -507,6 +546,12 @@ class _DMSExporter:
507
546
  ),
508
547
  None,
509
548
  )
549
+ if (
550
+ reverse_prop
551
+ and isinstance(reverse_prop.connection, EdgeEntity)
552
+ and reverse_prop.connection.properties is not None
553
+ ):
554
+ edge_source = reverse_prop.connection.properties.as_id()
510
555
 
511
556
  if reverse_prop is None:
512
557
  warnings.warn(
@@ -520,7 +565,7 @@ class _DMSExporter:
520
565
  stacklevel=2,
521
566
  )
522
567
 
523
- if reverse_prop is None or reverse_prop.connection == "edge":
568
+ if reverse_prop is None or isinstance(reverse_prop.connection, EdgeEntity):
524
569
  inwards_edge_cls = (
525
570
  dm.MultiEdgeConnectionApply if prop.is_list in [True, None] else SingleEdgeConnectionApply
526
571
  )
@@ -530,6 +575,7 @@ class _DMSExporter:
530
575
  name=prop.name,
531
576
  description=prop.description,
532
577
  direction="inwards",
578
+ edge_source=edge_source,
533
579
  )
534
580
  elif reverse_prop_id and reverse_prop and reverse_prop.connection == "direct":
535
581
  reverse_direct_cls = (