cognite-neat 0.75.8__py3-none-any.whl → 0.76.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 (99) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/app/api/configuration.py +4 -9
  3. cognite/neat/app/api/routers/configuration.py +2 -1
  4. cognite/neat/app/api/routers/crud.py +5 -5
  5. cognite/neat/app/api/routers/data_exploration.py +3 -1
  6. cognite/neat/app/api/routers/rules.py +3 -3
  7. cognite/neat/app/api/routers/workflows.py +3 -3
  8. cognite/neat/app/ui/neat-app/build/asset-manifest.json +3 -3
  9. cognite/neat/app/ui/neat-app/build/index.html +1 -1
  10. cognite/neat/app/ui/neat-app/build/static/js/{main.4345d42f.js → main.ec7f72e2.js} +3 -3
  11. cognite/neat/app/ui/neat-app/build/static/js/{main.4345d42f.js.map → main.ec7f72e2.js.map} +1 -1
  12. cognite/neat/config.py +147 -12
  13. cognite/neat/constants.py +1 -0
  14. cognite/neat/graph/exceptions.py +1 -2
  15. cognite/neat/legacy/graph/exceptions.py +1 -2
  16. cognite/neat/legacy/graph/extractors/_mock_graph_generator.py +1 -2
  17. cognite/neat/legacy/graph/loaders/_asset_loader.py +8 -13
  18. cognite/neat/legacy/graph/loaders/_base.py +2 -4
  19. cognite/neat/legacy/graph/loaders/_exceptions.py +1 -3
  20. cognite/neat/legacy/graph/loaders/core/rdf_to_assets.py +4 -8
  21. cognite/neat/legacy/graph/loaders/core/rdf_to_relationships.py +2 -4
  22. cognite/neat/legacy/graph/loaders/rdf_to_dms.py +2 -4
  23. cognite/neat/legacy/graph/loaders/validator.py +1 -1
  24. cognite/neat/legacy/graph/transformations/transformer.py +1 -2
  25. cognite/neat/legacy/rules/exporters/_rules2dms.py +1 -2
  26. cognite/neat/legacy/rules/exporters/_validation.py +4 -8
  27. cognite/neat/legacy/rules/importers/_base.py +0 -4
  28. cognite/neat/legacy/rules/importers/_dms2rules.py +0 -2
  29. cognite/neat/legacy/rules/models/rdfpath.py +1 -2
  30. cognite/neat/legacy/workflows/examples/Export_DMS/workflow.yaml +89 -0
  31. cognite/neat/legacy/workflows/examples/Export_Rules_to_Ontology/workflow.yaml +152 -0
  32. cognite/neat/legacy/workflows/examples/Extract_DEXPI_Graph_and_Export_Rules/workflow.yaml +139 -0
  33. cognite/neat/legacy/workflows/examples/Extract_RDF_Graph_and_Generate_Assets/workflow.yaml +270 -0
  34. cognite/neat/legacy/workflows/examples/Import_DMS/workflow.yaml +65 -0
  35. cognite/neat/legacy/workflows/examples/Ontology_to_Data_Model/workflow.yaml +116 -0
  36. cognite/neat/legacy/workflows/examples/Validate_Rules/workflow.yaml +67 -0
  37. cognite/neat/legacy/workflows/examples/Validate_Solution_Model/workflow.yaml +64 -0
  38. cognite/neat/legacy/workflows/examples/Visualize_Data_Model_Using_Mock_Graph/workflow.yaml +95 -0
  39. cognite/neat/legacy/workflows/examples/Visualize_Semantic_Data_Model/workflow.yaml +111 -0
  40. cognite/neat/rules/exporters/_models.py +3 -0
  41. cognite/neat/rules/exporters/_rules2dms.py +46 -4
  42. cognite/neat/rules/exporters/_rules2excel.py +2 -11
  43. cognite/neat/rules/exporters/_validation.py +6 -8
  44. cognite/neat/rules/importers/_base.py +8 -4
  45. cognite/neat/rules/importers/_dms2rules.py +321 -129
  46. cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +2 -4
  47. cognite/neat/rules/importers/_dtdl2rules/spec.py +2 -4
  48. cognite/neat/rules/importers/_owl2rules/_owl2rules.py +2 -4
  49. cognite/neat/rules/importers/_spreadsheet2rules.py +18 -16
  50. cognite/neat/rules/importers/_yaml2rules.py +2 -4
  51. cognite/neat/rules/issues/base.py +3 -0
  52. cognite/neat/rules/issues/dms.py +144 -58
  53. cognite/neat/rules/issues/fileread.py +41 -0
  54. cognite/neat/rules/issues/formatters.py +3 -1
  55. cognite/neat/rules/issues/importing.py +155 -0
  56. cognite/neat/rules/issues/spreadsheet.py +12 -9
  57. cognite/neat/rules/models/entities.py +30 -8
  58. cognite/neat/rules/models/rdfpath.py +1 -2
  59. cognite/neat/rules/models/rules/_base.py +5 -6
  60. cognite/neat/rules/models/rules/_dms_architect_rules.py +494 -333
  61. cognite/neat/rules/models/rules/_dms_rules_write.py +43 -52
  62. cognite/neat/rules/models/rules/_dms_schema.py +112 -22
  63. cognite/neat/rules/models/rules/_domain_rules.py +5 -0
  64. cognite/neat/rules/models/rules/_information_rules.py +13 -6
  65. cognite/neat/rules/models/wrapped_entities.py +166 -0
  66. cognite/neat/utils/cdf_loaders/_data_modeling.py +3 -1
  67. cognite/neat/utils/cdf_loaders/_ingestion.py +2 -4
  68. cognite/neat/utils/spreadsheet.py +2 -4
  69. cognite/neat/utils/utils.py +2 -4
  70. cognite/neat/workflows/base.py +5 -5
  71. cognite/neat/workflows/manager.py +32 -22
  72. cognite/neat/workflows/model.py +3 -3
  73. cognite/neat/workflows/steps/lib/__init__.py +0 -7
  74. cognite/neat/workflows/steps/lib/current/__init__.py +6 -0
  75. cognite/neat/workflows/steps/lib/{rules_exporter.py → current/rules_exporter.py} +8 -8
  76. cognite/neat/workflows/steps/lib/{rules_importer.py → current/rules_importer.py} +4 -4
  77. cognite/neat/workflows/steps/lib/io/__init__.py +1 -0
  78. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_contextualization.py +2 -2
  79. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_extractor.py +9 -9
  80. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_loader.py +9 -9
  81. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_store.py +4 -4
  82. cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_transformer.py +2 -2
  83. cognite/neat/workflows/steps/lib/{v1 → legacy}/rules_exporter.py +15 -17
  84. cognite/neat/workflows/steps/lib/{v1 → legacy}/rules_importer.py +7 -7
  85. cognite/neat/workflows/steps/step_model.py +5 -9
  86. cognite/neat/workflows/steps_registry.py +20 -11
  87. {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/METADATA +1 -1
  88. {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/RECORD +98 -86
  89. cognite/neat/app/api/data_classes/configuration.py +0 -121
  90. /cognite/neat/app/ui/neat-app/build/static/js/{main.4345d42f.js.LICENSE.txt → main.ec7f72e2.js.LICENSE.txt} +0 -0
  91. /cognite/neat/workflows/steps/lib/{graph_extractor.py → current/graph_extractor.py} +0 -0
  92. /cognite/neat/workflows/steps/lib/{graph_loader.py → current/graph_loader.py} +0 -0
  93. /cognite/neat/workflows/steps/lib/{graph_store.py → current/graph_store.py} +0 -0
  94. /cognite/neat/workflows/steps/lib/{rules_validator.py → current/rules_validator.py} +0 -0
  95. /cognite/neat/workflows/steps/lib/{io_steps.py → io/io_steps.py} +0 -0
  96. /cognite/neat/workflows/steps/lib/{v1 → legacy}/__init__.py +0 -0
  97. {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/LICENSE +0 -0
  98. {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/WHEEL +0 -0
  99. {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/entry_points.txt +0 -0
@@ -15,7 +15,7 @@ from cognite.neat.rules.models.entities import (
15
15
  ViewPropertyEntity,
16
16
  )
17
17
 
18
- from ._base import ExtensionCategory, SchemaCompleteness
18
+ from ._base import DataModelType, ExtensionCategory, SchemaCompleteness
19
19
  from ._dms_architect_rules import DMSContainer, DMSMetadata, DMSProperty, DMSRules, DMSView
20
20
 
21
21
 
@@ -27,11 +27,11 @@ class DMSMetadataWrite:
27
27
  creator: str
28
28
  version: str
29
29
  extension: Literal["addition", "reshape", "rebuild"] = "addition"
30
+ data_model_type: Literal["solution", "enterprise"] = "solution"
30
31
  name: str | None = None
31
32
  description: str | None = None
32
33
  created: datetime | str | None = None
33
34
  updated: datetime | str | None = None
34
- default_view_version: str | None = None
35
35
 
36
36
  @classmethod
37
37
  def load(cls, data: dict[str, Any] | None) -> "DMSMetadataWrite | None":
@@ -45,11 +45,11 @@ class DMSMetadataWrite:
45
45
  creator=data.get("creator"), # type: ignore[arg-type]
46
46
  version=data.get("version"), # type: ignore[arg-type]
47
47
  extension=data.get("extension", "addition"),
48
+ data_model_type=data.get("data_model_type", "solution"),
48
49
  name=data.get("name"),
49
50
  description=data.get("description"),
50
51
  created=data.get("created"),
51
52
  updated=data.get("updated"),
52
- default_view_version=data.get("default_view_version"),
53
53
  )
54
54
 
55
55
  def dump(self) -> dict[str, Any]:
@@ -58,13 +58,13 @@ class DMSMetadataWrite:
58
58
  extension=ExtensionCategory(self.extension),
59
59
  space=self.space,
60
60
  externalId=self.external_id,
61
+ dataModelType=DataModelType(self.data_model_type),
61
62
  creator=self.creator,
62
63
  version=self.version,
63
64
  name=self.name,
64
65
  description=self.description,
65
66
  created=self.created or datetime.now(),
66
67
  updated=self.updated or datetime.now(),
67
- default_view_version=self.default_view_version or self.version,
68
68
  )
69
69
 
70
70
 
@@ -77,7 +77,7 @@ class DMSPropertyWrite:
77
77
  class_: str | None = None
78
78
  name: str | None = None
79
79
  description: str | None = None
80
- relation: Literal["direct", "reversedirect", "multiedge"] | None = None
80
+ connection: Literal["direct", "edge", "reverse"] | None = None
81
81
  nullable: bool | None = None
82
82
  is_list: bool | None = None
83
83
  default: str | int | dict | None = None
@@ -89,18 +89,15 @@ class DMSPropertyWrite:
89
89
 
90
90
  @classmethod
91
91
  @overload
92
- def load(cls, data: None) -> None:
93
- ...
92
+ def load(cls, data: None) -> None: ...
94
93
 
95
94
  @classmethod
96
95
  @overload
97
- def load(cls, data: dict[str, Any]) -> "DMSPropertyWrite":
98
- ...
96
+ def load(cls, data: dict[str, Any]) -> "DMSPropertyWrite": ...
99
97
 
100
98
  @classmethod
101
99
  @overload
102
- def load(cls, data: list[dict[str, Any]]) -> list["DMSPropertyWrite"]:
103
- ...
100
+ def load(cls, data: list[dict[str, Any]]) -> list["DMSPropertyWrite"]: ...
104
101
 
105
102
  @classmethod
106
103
  def load(
@@ -121,7 +118,7 @@ class DMSPropertyWrite:
121
118
  class_=data.get("class_"),
122
119
  name=data.get("name"),
123
120
  description=data.get("description"),
124
- relation=data.get("relation"),
121
+ connection=data.get("connection"),
125
122
  nullable=data.get("nullable"),
126
123
  is_list=data.get("is_list"),
127
124
  default=data.get("default"),
@@ -146,23 +143,23 @@ class DMSPropertyWrite:
146
143
 
147
144
  return {
148
145
  "View": ViewEntity.load(self.view, space=default_space, version=default_version),
149
- "ViewProperty": self.view_property,
146
+ "View Property": self.view_property,
150
147
  "Value Type": value_type,
151
- "Property": self.property_ or self.view_property,
152
- "Class": ClassEntity.load(self.class_, prefix=default_space, version=default_version)
148
+ "Property (linage)": self.property_ or self.view_property,
149
+ "Class (linage)": ClassEntity.load(self.class_, prefix=default_space, version=default_version)
153
150
  if self.class_
154
151
  else None,
155
152
  "Name": self.name,
156
153
  "Description": self.description,
157
- "Relation": self.relation,
154
+ "Connection": self.connection,
158
155
  "Nullable": self.nullable,
159
- "IsList": self.is_list,
156
+ "Is List": self.is_list,
160
157
  "Default": self.default,
161
158
  "Reference": self.reference,
162
159
  "Container": ContainerEntity.load(self.container, space=default_space, version=default_version)
163
160
  if self.container
164
161
  else None,
165
- "ContainerProperty": self.container_property,
162
+ "Container Property": self.container_property,
166
163
  "Index": self.index,
167
164
  "Constraint": self.constraint,
168
165
  }
@@ -179,18 +176,15 @@ class DMSContainerWrite:
179
176
 
180
177
  @classmethod
181
178
  @overload
182
- def load(cls, data: None) -> None:
183
- ...
179
+ def load(cls, data: None) -> None: ...
184
180
 
185
181
  @classmethod
186
182
  @overload
187
- def load(cls, data: dict[str, Any]) -> "DMSContainerWrite":
188
- ...
183
+ def load(cls, data: dict[str, Any]) -> "DMSContainerWrite": ...
189
184
 
190
185
  @classmethod
191
186
  @overload
192
- def load(cls, data: list[dict[str, Any]]) -> list["DMSContainerWrite"]:
193
- ...
187
+ def load(cls, data: list[dict[str, Any]]) -> list["DMSContainerWrite"]: ...
194
188
 
195
189
  @classmethod
196
190
  def load(
@@ -214,19 +208,21 @@ class DMSContainerWrite:
214
208
 
215
209
  def dump(self, default_space: str) -> dict[str, Any]:
216
210
  container = ContainerEntity.load(self.container, space=default_space)
217
- return dict(
218
- Container=container,
219
- Class=ClassEntity.load(self.class_, prefix=default_space) if self.class_ else container.as_class(),
220
- Name=self.name,
221
- Description=self.description,
222
- Reference=self.reference,
223
- Constraint=[
211
+ return {
212
+ "Container": container,
213
+ "Class (linage)": ClassEntity.load(self.class_, prefix=default_space)
214
+ if self.class_
215
+ else container.as_class(),
216
+ "Name": self.name,
217
+ "Description": self.description,
218
+ "Reference": self.reference,
219
+ "Constraint": [
224
220
  ContainerEntity.load(constraint.strip(), space=default_space)
225
221
  for constraint in self.constraint.split(",")
226
222
  ]
227
223
  if self.constraint
228
224
  else None,
229
- )
225
+ }
230
226
 
231
227
 
232
228
  @dataclass
@@ -242,18 +238,15 @@ class DMSViewWrite:
242
238
 
243
239
  @classmethod
244
240
  @overload
245
- def load(cls, data: None) -> None:
246
- ...
241
+ def load(cls, data: None) -> None: ...
247
242
 
248
243
  @classmethod
249
244
  @overload
250
- def load(cls, data: dict[str, Any]) -> "DMSViewWrite":
251
- ...
245
+ def load(cls, data: dict[str, Any]) -> "DMSViewWrite": ...
252
246
 
253
247
  @classmethod
254
248
  @overload
255
- def load(cls, data: list[dict[str, Any]]) -> list["DMSViewWrite"]:
256
- ...
249
+ def load(cls, data: list[dict[str, Any]]) -> list["DMSViewWrite"]: ...
257
250
 
258
251
  @classmethod
259
252
  def load(cls, data: dict[str, Any] | list[dict[str, Any]] | None) -> "DMSViewWrite | list[DMSViewWrite] | None":
@@ -277,23 +270,23 @@ class DMSViewWrite:
277
270
 
278
271
  def dump(self, default_space: str, default_version: str) -> dict[str, Any]:
279
272
  view = ViewEntity.load(self.view, space=default_space, version=default_version)
280
- return dict(
281
- View=view,
282
- Class=ClassEntity.load(self.class_, prefix=default_space, version=default_version)
273
+ return {
274
+ "View": view,
275
+ "Class (linage)": ClassEntity.load(self.class_, prefix=default_space, version=default_version)
283
276
  if self.class_
284
277
  else view.as_class(),
285
- Name=self.name,
286
- Description=self.description,
287
- Implements=[
278
+ "Name": self.name,
279
+ "Description": self.description,
280
+ "Implements": [
288
281
  ViewEntity.load(implement, space=default_space, version=default_version)
289
282
  for implement in self.implements.split(",")
290
283
  ]
291
284
  if self.implements
292
285
  else None,
293
- Reference=self.reference,
294
- Filter=self.filter_,
295
- InModel=self.in_model,
296
- )
286
+ "Reference": self.reference,
287
+ "Filter": self.filter_,
288
+ "In Model": self.in_model,
289
+ }
297
290
 
298
291
 
299
292
  @dataclass
@@ -306,13 +299,11 @@ class DMSRulesWrite:
306
299
 
307
300
  @classmethod
308
301
  @overload
309
- def load(cls, data: dict[str, Any]) -> "DMSRulesWrite":
310
- ...
302
+ def load(cls, data: dict[str, Any]) -> "DMSRulesWrite": ...
311
303
 
312
304
  @classmethod
313
305
  @overload
314
- def load(cls, data: None) -> None:
315
- ...
306
+ def load(cls, data: None) -> None: ...
316
307
 
317
308
  @classmethod
318
309
  def load(cls, data: dict | None) -> "DMSRulesWrite | None":
@@ -3,7 +3,7 @@ import sys
3
3
  import warnings
4
4
  import zipfile
5
5
  from collections import Counter, defaultdict
6
- from dataclasses import dataclass, field, fields
6
+ from dataclasses import Field, dataclass, field, fields
7
7
  from pathlib import Path
8
8
  from typing import Any, ClassVar, cast
9
9
 
@@ -14,6 +14,7 @@ from cognite.client.data_classes import DatabaseWrite, DatabaseWriteList, Transf
14
14
  from cognite.client.data_classes.data_modeling import ViewApply
15
15
  from cognite.client.data_classes.transformations.common import Edges, EdgeType, Nodes, ViewInfo
16
16
 
17
+ from cognite.neat.rules import issues
17
18
  from cognite.neat.rules.issues.dms import (
18
19
  ContainerPropertyUsedMultipleTimesError,
19
20
  DirectRelationMissingSourceWarning,
@@ -106,23 +107,33 @@ class DMSSchema:
106
107
  The directory is expected to follow the Cognite-Toolkit convention
107
108
  where each file is named as `resource_type.resource_name.yaml`.
108
109
  """
109
- data = cls._read_directory(Path(directory))
110
- return cls.load(data)
110
+ data, context = cls._read_directory(Path(directory))
111
+ return cls.load(data, context)
111
112
 
112
113
  @classmethod
113
- def _read_directory(cls, directory: Path) -> dict[str, list[Any]]:
114
+ def _read_directory(cls, directory: Path) -> tuple[dict[str, list[Any]], dict[str, list[Path]]]:
114
115
  data: dict[str, Any] = {}
116
+ context: dict[str, list[Path]] = {}
115
117
  for yaml_file in directory.rglob("*.yaml"):
116
118
  if "." in yaml_file.stem:
117
119
  resource_type = yaml_file.stem.rsplit(".", 1)[-1]
118
120
  if attr_name := cls._FIELD_NAME_BY_RESOURCE_TYPE.get(resource_type):
119
121
  data.setdefault(attr_name, [])
120
- loaded = yaml.safe_load(yaml_file.read_text())
122
+ context.setdefault(attr_name, [])
123
+ try:
124
+ # Using CSafeLoader over safe_load for ~10x speedup
125
+ loaded = yaml.safe_load(yaml_file.read_text())
126
+ except Exception as e:
127
+ warnings.warn(issues.fileread.InvalidFileFormatWarning(yaml_file, str(e)), stacklevel=2)
128
+ continue
129
+
121
130
  if isinstance(loaded, list):
122
131
  data[attr_name].extend(loaded)
132
+ context[attr_name].extend([yaml_file] * len(loaded))
123
133
  else:
124
134
  data[attr_name].append(loaded)
125
- return data
135
+ context[attr_name].append(yaml_file)
136
+ return data, context
126
137
 
127
138
  def to_directory(
128
139
  self,
@@ -182,12 +193,13 @@ class DMSSchema:
182
193
  The ZIP file is expected to follow the Cognite-Toolkit convention
183
194
  where each file is named as `resource_type.resource_name.yaml`.
184
195
  """
185
- data = cls._read_zip(Path(zip_file))
186
- return cls.load(data)
196
+ data, context = cls._read_zip(Path(zip_file))
197
+ return cls.load(data, context)
187
198
 
188
199
  @classmethod
189
- def _read_zip(cls, zip_file: Path) -> dict[str, list[Any]]:
200
+ def _read_zip(cls, zip_file: Path) -> tuple[dict[str, list[Any]], dict[str, list[Path]]]:
190
201
  data: dict[str, list[Any]] = {}
202
+ context: dict[str, list[Path]] = {}
191
203
  with zipfile.ZipFile(zip_file, "r") as zip_ref:
192
204
  for file_info in zip_ref.infolist():
193
205
  if file_info.filename.endswith(".yaml"):
@@ -199,12 +211,19 @@ class DMSSchema:
199
211
  resource_type = filename.stem.rsplit(".", 1)[-1]
200
212
  if attr_name := cls._FIELD_NAME_BY_RESOURCE_TYPE.get(resource_type):
201
213
  data.setdefault(attr_name, [])
202
- loaded = yaml.safe_load(zip_ref.read(file_info).decode())
214
+ context.setdefault(attr_name, [])
215
+ try:
216
+ loaded = yaml.safe_load(zip_ref.read(file_info).decode())
217
+ except Exception as e:
218
+ warnings.warn(issues.fileread.InvalidFileFormatWarning(filename, str(e)), stacklevel=2)
219
+ continue
203
220
  if isinstance(loaded, list):
204
221
  data[attr_name].extend(loaded)
222
+ context[attr_name].extend([filename] * len(loaded))
205
223
  else:
206
224
  data[attr_name].append(loaded)
207
- return data
225
+ context[attr_name].append(filename)
226
+ return data, context
208
227
 
209
228
  def to_zip(self, zip_file: str | Path, exclude: set[str] | None = None) -> None:
210
229
  """Save the schema to a ZIP file as YAML files. This is compatible with the Cognite-Toolkit convention.
@@ -234,18 +253,63 @@ class DMSSchema:
234
253
  zip_ref.writestr(f"data_models/nodes/{node.external_id}.node.yaml", node.dump_yaml())
235
254
 
236
255
  @classmethod
237
- def load(cls, data: str | dict[str, Any]) -> Self:
256
+ def load(cls, data: str | dict[str, list[Any]], context: dict[str, list[Path]] | None = None) -> Self:
257
+ """Loads a schema from a dictionary or a YAML or JSON formatted string.
258
+
259
+ Args:
260
+ data: The data to load the schema from. This can be a dictionary, a YAML or JSON formatted string.
261
+ context: This provides linage for where the data was loaded from. This is used in Warnings
262
+ if a single item fails to load.
263
+
264
+ Returns:
265
+ DMSSchema: The loaded schema.
266
+ """
267
+ context = context or {}
238
268
  if isinstance(data, str):
239
269
  # YAML is a superset of JSON, so we can use the same parser
240
- data_dict = yaml.safe_load(data)
270
+ try:
271
+ data_dict = yaml.safe_load(data)
272
+ except Exception as e:
273
+ raise issues.fileread.FailedStringLoadError(".yaml", str(e)).as_exception() from None
274
+ if not isinstance(data_dict, dict) and all(isinstance(v, list) for v in data_dict.values()):
275
+ raise issues.fileread.FailedStringLoadError(
276
+ "dict[str, list[Any]]", f"Invalid data structure: {type(data)}"
277
+ ).as_exception() from None
241
278
  else:
242
279
  data_dict = data
243
280
  loaded: dict[str, Any] = {}
244
281
  for attr in fields(cls):
245
282
  if items := data_dict.get(attr.name) or data_dict.get(to_camel(attr.name)):
246
- loaded[attr.name] = attr.type.load(items)
283
+ try:
284
+ loaded[attr.name] = attr.type.load(items)
285
+ except Exception as e:
286
+ loaded[attr.name] = cls._load_individual_resources(items, attr, str(e), context.get(attr.name, []))
247
287
  return cls(**loaded)
248
288
 
289
+ @classmethod
290
+ def _load_individual_resources(cls, items: list, attr: Field, trigger_error: str, resource_context) -> list[Any]:
291
+ resources = attr.type([])
292
+ if not hasattr(attr.type, "_RESOURCE"):
293
+ warnings.warn(
294
+ issues.fileread.FailedLoadWarning(Path("UNKNOWN"), attr.type.__name__, trigger_error), stacklevel=2
295
+ )
296
+ return resources
297
+ # Fallback to load individual resources.
298
+ single_cls = attr.type._RESOURCE
299
+ for no, item in enumerate(items):
300
+ try:
301
+ loaded_instance = single_cls.load(item)
302
+ except Exception as e:
303
+ try:
304
+ filepath = resource_context[no]
305
+ except IndexError:
306
+ filepath = Path("UNKNOWN")
307
+ # We use repr(e) instead of str(e) to include the exception type in the warning message
308
+ warnings.warn(issues.fileread.FailedLoadWarning(filepath, single_cls.__name__, repr(e)), stacklevel=2)
309
+ else:
310
+ resources.append(loaded_instance)
311
+ return resources
312
+
249
313
  def dump(self, camel_case: bool = True, sort: bool = True) -> dict[str, Any]:
250
314
  """Dump the schema to a dictionary that can be serialized to JSON.
251
315
 
@@ -410,6 +474,18 @@ class DMSSchema:
410
474
  )
411
475
  return None
412
476
 
477
+ def referenced_spaces(self) -> set[str]:
478
+ referenced_spaces = {container.space for container in self.containers}
479
+ referenced_spaces |= {view.space for view in self.views}
480
+ referenced_spaces |= {container.space for view in self.views for container in view.referenced_containers()}
481
+ referenced_spaces |= {parent.space for view in self.views for parent in view.implements or []}
482
+ referenced_spaces |= {node.space for node in self.node_types}
483
+ referenced_spaces |= {model.space for model in self.data_models}
484
+ referenced_spaces |= {view.space for model in self.data_models for view in model.views or []}
485
+ referenced_spaces |= {s.space for s in self.spaces}
486
+
487
+ return referenced_spaces
488
+
413
489
 
414
490
  @dataclass
415
491
  class PipelineSchema(DMSSchema):
@@ -429,18 +505,25 @@ class PipelineSchema(DMSSchema):
429
505
  self.databases.extend([DatabaseWrite(name=database) for database in missing])
430
506
 
431
507
  @classmethod
432
- def _read_directory(cls, directory: Path) -> dict[str, list[Any]]:
433
- data = super()._read_directory(directory)
508
+ def _read_directory(cls, directory: Path) -> tuple[dict[str, list[Any]], dict[str, list[Path]]]:
509
+ data, context = super()._read_directory(directory)
434
510
  for yaml_file in directory.rglob("*.yaml"):
435
511
  if yaml_file.parent.name in ("transformations", "raw"):
436
512
  attr_name = cls._FIELD_NAME_BY_RESOURCE_TYPE.get(yaml_file.parent.name, yaml_file.parent.name)
437
513
  data.setdefault(attr_name, [])
438
- loaded = yaml.safe_load(yaml_file.read_text())
514
+ context.setdefault(attr_name, [])
515
+ try:
516
+ loaded = yaml.safe_load(yaml_file.read_text())
517
+ except Exception as e:
518
+ warnings.warn(issues.fileread.InvalidFileFormatWarning(yaml_file, str(e)), stacklevel=2)
519
+ continue
439
520
  if isinstance(loaded, list):
440
521
  data[attr_name].extend(loaded)
522
+ context[attr_name].extend([yaml_file] * len(loaded))
441
523
  else:
442
524
  data[attr_name].append(loaded)
443
- return data
525
+ context[attr_name].append(yaml_file)
526
+ return data, context
444
527
 
445
528
  def to_directory(
446
529
  self,
@@ -483,8 +566,8 @@ class PipelineSchema(DMSSchema):
483
566
  zip_ref.writestr(f"raw/{raw_table.name}.yaml", raw_table.dump_yaml())
484
567
 
485
568
  @classmethod
486
- def _read_zip(cls, zip_file: Path) -> dict[str, list[Any]]:
487
- data = super()._read_zip(zip_file)
569
+ def _read_zip(cls, zip_file: Path) -> tuple[dict[str, list[Any]], dict[str, list[Path]]]:
570
+ data, context = super()._read_zip(zip_file)
488
571
  with zipfile.ZipFile(zip_file, "r") as zip_ref:
489
572
  for file_info in zip_ref.infolist():
490
573
  if file_info.filename.endswith(".yaml"):
@@ -494,12 +577,19 @@ class PipelineSchema(DMSSchema):
494
577
  if (parent := filepath.parent.name) in ("transformations", "raw"):
495
578
  attr_name = cls._FIELD_NAME_BY_RESOURCE_TYPE.get(parent, parent)
496
579
  data.setdefault(attr_name, [])
497
- loaded = yaml.safe_load(zip_ref.read(file_info).decode())
580
+ context.setdefault(attr_name, [])
581
+ try:
582
+ loaded = yaml.safe_load(zip_ref.read(file_info).decode())
583
+ except Exception as e:
584
+ warnings.warn(issues.fileread.InvalidFileFormatWarning(filepath, str(e)), stacklevel=2)
585
+ continue
498
586
  if isinstance(loaded, list):
499
587
  data[attr_name].extend(loaded)
588
+ context[attr_name].extend([filepath] * len(loaded))
500
589
  else:
501
590
  data[attr_name].append(loaded)
502
- return data
591
+ context[attr_name].append(filepath)
592
+ return data, context
503
593
 
504
594
  @classmethod
505
595
  def from_dms(cls, schema: DMSSchema, instance_space: str | None = None) -> "PipelineSchema":
@@ -23,7 +23,10 @@ class DomainMetadata(BaseMetadata):
23
23
 
24
24
 
25
25
  class DomainProperty(SheetEntity):
26
+ class_: ClassEntity = Field(alias="Class")
26
27
  property_: PropertyType = Field(alias="Property")
28
+ name: str | None = Field(alias="Name", default=None)
29
+ description: str | None = Field(alias="Description", default=None)
27
30
  value_type: DataType | ClassEntity = Field(alias="Value Type")
28
31
  min_count: int | None = Field(alias="Min Count", default=None)
29
32
  max_count: int | float | None = Field(alias="Max Count", default=None)
@@ -42,6 +45,8 @@ class DomainProperty(SheetEntity):
42
45
 
43
46
 
44
47
  class DomainClass(SheetEntity):
48
+ class_: ClassEntity = Field(alias="Class")
49
+ name: str | None = Field(alias="Name", default=None)
45
50
  description: str | None = Field(None, alias="Description")
46
51
  parent: ParentEntityList | None = Field(alias="Parent Class")
47
52
 
@@ -45,6 +45,7 @@ from cognite.neat.rules.models.rdfpath import (
45
45
 
46
46
  from ._base import (
47
47
  BaseMetadata,
48
+ DataModelType,
48
49
  ExtensionCategory,
49
50
  ExtensionCategoryType,
50
51
  MatchType,
@@ -75,6 +76,7 @@ else:
75
76
 
76
77
  class InformationMetadata(BaseMetadata):
77
78
  role: ClassVar[RoleTypes] = RoleTypes.information_architect
79
+ data_model_type: DataModelType = Field(DataModelType.solution, alias="dataModelType")
78
80
  schema_: SchemaCompleteness = Field(alias="schema")
79
81
  extension: ExtensionCategoryType | None = ExtensionCategory.addition
80
82
  prefix: PrefixType
@@ -123,6 +125,9 @@ class InformationClass(SheetEntity):
123
125
  match_type: The match type of the resource being described and the source entity.
124
126
  """
125
127
 
128
+ class_: ClassEntity = Field(alias="Class")
129
+ name: str | None = Field(alias="Name", default=None)
130
+ description: str | None = Field(alias="Description", default=None)
126
131
  parent: ParentEntityList | None = Field(alias="Parent Class", default=None)
127
132
  reference: URLEntity | ReferenceEntity | None = Field(alias="Reference", default=None, union_mode="left_to_right")
128
133
  match_type: MatchType | None = Field(alias="Match Type", default=None)
@@ -150,7 +155,10 @@ class InformationProperty(SheetEntity):
150
155
  knowledge graph. Defaults to None (no transformation)
151
156
  """
152
157
 
158
+ class_: ClassEntity = Field(alias="Class")
153
159
  property_: PropertyType = Field(alias="Property")
160
+ name: str | None = Field(alias="Name", default=None)
161
+ description: str | None = Field(alias="Description", default=None)
154
162
  value_type: DataType | ClassEntity | UnknownEntity = Field(alias="Value Type", union_mode="left_to_right")
155
163
  min_count: int | None = Field(alias="Min Count", default=None)
156
164
  max_count: int | float | None = Field(alias="Max Count", default=None)
@@ -430,7 +438,7 @@ class _InformationRulesConverter:
430
438
  for class_ in self.information.classes:
431
439
  properties: list[DMSProperty] = properties_by_class.get(class_.class_.versioned_id, [])
432
440
  if not properties or all(
433
- isinstance(prop.value_type, ViewPropertyEntity) and prop.relation != "direct" for prop in properties
441
+ isinstance(prop.value_type, ViewPropertyEntity) and prop.connection != "direct" for prop in properties
434
442
  ):
435
443
  classes_without_properties.add(class_.class_.versioned_id)
436
444
 
@@ -480,16 +488,15 @@ class _InformationRulesConverter:
480
488
  else:
481
489
  raise ValueError(f"Unsupported value type: {prop.value_type.type_}")
482
490
 
483
- relation: Literal["direct", "multiedge"] | None = None
491
+ relation: Literal["direct", "edge", "reverse"] | None = None
484
492
  if isinstance(value_type, ViewEntity | ViewPropertyEntity):
485
- relation = "multiedge" if prop.is_list else "direct"
493
+ relation = "edge" if prop.is_list else "direct"
486
494
 
487
495
  container: ContainerEntity | None = None
488
496
  container_property: str | None = None
489
497
  is_list: bool | None = prop.is_list
490
498
  nullable: bool | None = not prop.is_mandatory
491
- if relation == "multiedge":
492
- is_list = None
499
+ if relation == "edge":
493
500
  nullable = None
494
501
  elif relation == "direct":
495
502
  nullable = True
@@ -504,7 +511,7 @@ class _InformationRulesConverter:
504
511
  value_type=value_type,
505
512
  nullable=nullable,
506
513
  is_list=is_list,
507
- relation=relation,
514
+ connection=relation,
508
515
  default=prop.default,
509
516
  reference=prop.reference,
510
517
  container=container,