cognite-neat 0.75.9__py3-none-any.whl → 0.76.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,6 @@
1
+ from collections import Counter
2
+ from collections.abc import Sequence
3
+ from datetime import datetime
1
4
  from pathlib import Path
2
5
  from typing import Literal, cast, overload
3
6
 
@@ -5,21 +8,29 @@ from cognite.client import CogniteClient
5
8
  from cognite.client import data_modeling as dm
6
9
  from cognite.client.data_classes.data_modeling import DataModelIdentifier
7
10
  from cognite.client.data_classes.data_modeling.containers import BTreeIndex, InvertedIndex
11
+ from cognite.client.data_classes.data_modeling.views import (
12
+ MultiEdgeConnectionApply,
13
+ MultiReverseDirectRelationApply,
14
+ SingleEdgeConnectionApply,
15
+ SingleReverseDirectRelationApply,
16
+ ViewPropertyApply,
17
+ )
8
18
  from cognite.client.utils import ms_to_datetime
9
19
 
10
20
  from cognite.neat.rules import issues
11
- from cognite.neat.rules.importers._base import BaseImporter, Rules
12
- from cognite.neat.rules.issues import IssueList
21
+ from cognite.neat.rules.importers._base import BaseImporter, Rules, _handle_issues
22
+ from cognite.neat.rules.issues import IssueList, ValidationIssue
13
23
  from cognite.neat.rules.models.data_types import DataType
14
24
  from cognite.neat.rules.models.entities import (
15
25
  ClassEntity,
16
26
  ContainerEntity,
27
+ DataModelEntity,
17
28
  DMSUnknownEntity,
18
29
  ViewEntity,
19
30
  ViewPropertyEntity,
20
31
  )
21
32
  from cognite.neat.rules.models.rules import DMSRules, DMSSchema, RoleTypes
22
- from cognite.neat.rules.models.rules._base import ExtensionCategory, SchemaCompleteness
33
+ from cognite.neat.rules.models.rules._base import DataModelType, ExtensionCategory, SchemaCompleteness
23
34
  from cognite.neat.rules.models.rules._dms_architect_rules import (
24
35
  DMSContainer,
25
36
  DMSMetadata,
@@ -30,9 +41,16 @@ from cognite.neat.rules.models.rules._dms_architect_rules import (
30
41
 
31
42
 
32
43
  class DMSImporter(BaseImporter):
33
- def __init__(self, schema: DMSSchema, metadata: DMSMetadata | None = None):
44
+ def __init__(
45
+ self,
46
+ schema: DMSSchema,
47
+ read_issues: Sequence[ValidationIssue] | None = None,
48
+ metadata: DMSMetadata | None = None,
49
+ ):
34
50
  self.schema = schema
35
51
  self.metadata = metadata
52
+ self.issue_list = IssueList(read_issues)
53
+ self._container_by_id = {container.as_id(): container for container in schema.containers}
36
54
 
37
55
  @classmethod
38
56
  def from_data_model_id(cls, client: CogniteClient, data_model_id: DataModelIdentifier) -> "DMSImporter":
@@ -47,38 +65,59 @@ class DMSImporter(BaseImporter):
47
65
  """
48
66
  data_models = client.data_modeling.data_models.retrieve(data_model_id, inline_views=True)
49
67
  if len(data_models) == 0:
50
- raise ValueError(f"Data model {data_model_id} not found")
68
+ return cls(DMSSchema(), [issues.importing.NoDataModelError(f"Data model {data_model_id} not found")])
51
69
  data_model = data_models.latest_version()
52
- schema = DMSSchema.from_data_model(client, data_model)
53
- description, creator = DMSMetadata._get_description_and_creator(data_model.description)
70
+
71
+ try:
72
+ schema = DMSSchema.from_data_model(client, data_model)
73
+ except Exception as e:
74
+ return cls(DMSSchema(), [issues.importing.APIError(str(e))])
54
75
 
55
76
  created = ms_to_datetime(data_model.created_time)
56
77
  updated = ms_to_datetime(data_model.last_updated_time)
57
78
 
58
- metadata = DMSMetadata(
79
+ metadata = cls._create_metadata_from_model(data_model, created, updated)
80
+
81
+ return cls(schema, [], metadata)
82
+
83
+ @classmethod
84
+ def _create_metadata_from_model(
85
+ cls,
86
+ model: dm.DataModel[dm.View] | dm.DataModelApply,
87
+ created: datetime | None = None,
88
+ updated: datetime | None = None,
89
+ ) -> DMSMetadata:
90
+ description, creator = DMSMetadata._get_description_and_creator(model.description)
91
+ now = datetime.now().replace(microsecond=0)
92
+ return DMSMetadata(
59
93
  schema_=SchemaCompleteness.complete,
60
94
  extension=ExtensionCategory.addition,
61
- space=data_model.space,
62
- external_id=data_model.external_id,
63
- name=data_model.name or data_model.external_id,
64
- version=data_model.version or "0.1.0",
65
- updated=updated,
66
- created=created,
95
+ space=model.space,
96
+ external_id=model.external_id,
97
+ name=model.name or model.external_id,
98
+ version=model.version or "0.1.0",
99
+ updated=updated or now,
100
+ created=created or now,
67
101
  creator=creator,
68
102
  description=description,
69
- default_view_version=data_model.version or "0.1.0",
70
103
  )
71
- return cls(schema, metadata)
72
104
 
73
105
  @classmethod
74
106
  def from_directory(cls, directory: str | Path) -> "DMSImporter":
75
- return cls(DMSSchema.from_directory(directory))
107
+ issue_list = IssueList()
108
+ with _handle_issues(issue_list) as _:
109
+ schema = DMSSchema.from_directory(directory)
110
+ # If there were errors during the import, the to_rules
111
+ return cls(schema, issue_list)
76
112
 
77
113
  @classmethod
78
114
  def from_zip_file(cls, zip_file: str | Path) -> "DMSImporter":
79
115
  if Path(zip_file).suffix != ".zip":
80
- raise ValueError("File extension is not .zip")
81
- return cls(DMSSchema.from_zip(zip_file))
116
+ return cls(DMSSchema(), [issues.fileread.InvalidFileFormatError(Path(zip_file), [".zip"])])
117
+ issue_list = IssueList()
118
+ with _handle_issues(issue_list) as _:
119
+ schema = DMSSchema.from_zip(zip_file)
120
+ return cls(schema, issue_list)
82
121
 
83
122
  @overload
84
123
  def to_rules(self, errors: Literal["raise"], role: RoleTypes | None = None) -> Rules: ...
@@ -91,126 +130,281 @@ class DMSImporter(BaseImporter):
91
130
  def to_rules(
92
131
  self, errors: Literal["raise", "continue"] = "continue", role: RoleTypes | None = None
93
132
  ) -> tuple[Rules | None, IssueList] | Rules:
94
- if role is RoleTypes.domain_expert:
95
- raise ValueError(f"Role {role} is not supported for DMSImporter")
96
- issue_list = IssueList()
97
- data_model = self.schema.data_models[0]
133
+ if self.issue_list.has_errors:
134
+ # In case there were errors during the import, the to_rules method will return None
135
+ return self._return_or_raise(self.issue_list, errors)
98
136
 
99
- container_by_id = {container.as_id(): container for container in self.schema.containers}
137
+ if len(self.schema.data_models) == 0:
138
+ self.issue_list.append(issues.importing.NoDataModelError("No data model found."))
139
+ return self._return_or_raise(self.issue_list, errors)
140
+
141
+ if len(self.schema.data_models) > 2:
142
+ # Creating a DataModelEntity to convert the data model id to a string.
143
+ self.issue_list.append(
144
+ issues.importing.MultipleDataModelsWarning(
145
+ [str(DataModelEntity.from_id(model.as_id())) for model in self.schema.data_models]
146
+ )
147
+ )
148
+
149
+ data_model = self.schema.data_models[0]
100
150
 
101
151
  properties = SheetList[DMSProperty]()
152
+ ref_properties = SheetList[DMSProperty]()
102
153
  for view in self.schema.views:
103
- class_entity = ClassEntity(prefix=view.space, suffix=view.external_id, version=view.version)
154
+ view_id = view.as_id()
155
+ view_entity = ViewEntity.from_id(view_id)
156
+ class_entity = view_entity.as_class()
104
157
  for prop_id, prop in (view.properties or {}).items():
105
- if isinstance(prop, dm.MappedPropertyApply):
106
- if prop.container not in container_by_id:
107
- raise ValueError(f"Container {prop.container} not found")
108
- container = container_by_id[prop.container]
109
- if prop.container_property_identifier not in container.properties:
110
- raise ValueError(
111
- f"Property {prop.container_property_identifier} not found "
112
- f"in container {container.external_id}"
113
- )
114
- container_prop = container.properties[prop.container_property_identifier]
115
-
116
- index: list[str] = []
117
- for index_name, index_obj in (container.indexes or {}).items():
118
- if isinstance(index_obj, BTreeIndex | InvertedIndex) and prop_id in index_obj.properties:
119
- index.append(index_name)
120
- unique_constraints: list[str] = []
121
- for constraint_name, constraint_obj in (container.constraints or {}).items():
122
- if isinstance(constraint_obj, dm.RequiresConstraint):
123
- # This is handled in the .from_container method of DMSContainer
124
- continue
125
- elif (
126
- isinstance(constraint_obj, dm.UniquenessConstraint) and prop_id in constraint_obj.properties
127
- ):
128
- unique_constraints.append(constraint_name)
129
- elif isinstance(constraint_obj, dm.UniquenessConstraint):
130
- # This does not apply to this property
131
- continue
132
- else:
133
- raise NotImplementedError(f"Constraint type {type(constraint_obj)} not implemented")
134
-
135
- if isinstance(container_prop.type, dm.DirectRelation):
136
- direct_value_type: str | ViewEntity | DataType | DMSUnknownEntity
137
- if prop.source is None:
138
- issue_list.append(
139
- issues.importing.UnknownValueTypeWarning(class_entity.versioned_id, prop_id)
140
- )
141
- direct_value_type = DMSUnknownEntity()
142
- else:
143
- direct_value_type = ViewEntity.from_id(prop.source)
144
-
145
- dms_property = DMSProperty(
146
- class_=class_entity,
147
- property_=prop_id,
148
- description=prop.description,
149
- name=prop.name,
150
- value_type=direct_value_type,
151
- relation="direct",
152
- nullable=container_prop.nullable,
153
- default=container_prop.default_value,
154
- is_list=False,
155
- container=ContainerEntity.from_id(container.as_id()),
156
- container_property=prop.container_property_identifier,
157
- view=ViewEntity.from_id(view.as_id()),
158
- view_property=prop_id,
159
- index=index or None,
160
- constraint=unique_constraints or None,
161
- )
158
+ dms_property = self._create_dms_property(prop_id, prop, view_entity, class_entity)
159
+ if dms_property is not None:
160
+ if view_id in self.schema.frozen_ids:
161
+ ref_properties.append(dms_property)
162
162
  else:
163
- dms_property = DMSProperty(
164
- class_=ClassEntity(prefix=view.space, suffix=view.external_id, version=view.version),
165
- property_=prop_id,
166
- description=prop.description,
167
- name=prop.name,
168
- value_type=cast(ViewPropertyEntity | DataType, container_prop.type._type),
169
- nullable=container_prop.nullable,
170
- is_list=container_prop.type.is_list,
171
- default=container_prop.default_value,
172
- container=ContainerEntity.from_id(container.as_id()),
173
- container_property=prop.container_property_identifier,
174
- view=ViewEntity.from_id(view.as_id()),
175
- view_property=prop_id,
176
- index=index or None,
177
- constraint=unique_constraints or None,
178
- )
179
- elif isinstance(prop, dm.MultiEdgeConnectionApply):
180
- view_entity = ViewEntity.from_id(prop.source)
181
- dms_property = DMSProperty(
182
- class_=ClassEntity(prefix=view.space, suffix=view.external_id, version=view.version),
183
- property_=prop_id,
184
- relation="multiedge",
185
- description=prop.description,
186
- name=prop.name,
187
- value_type=view_entity,
188
- view=ViewEntity.from_id(view.as_id()),
189
- view_property=prop_id,
190
- )
191
- else:
192
- raise NotImplementedError(f"Property type {type(prop)} not implemented")
193
-
194
- properties.append(dms_property)
163
+ properties.append(dms_property)
195
164
 
196
165
  data_model_view_ids: set[dm.ViewId] = {
197
166
  view.as_id() if isinstance(view, dm.View | dm.ViewApply) else view for view in data_model.views or []
198
167
  }
199
168
 
200
- dms_rules = DMSRules(
201
- metadata=self.metadata or DMSMetadata.from_data_model(data_model),
169
+ metadata = self.metadata or DMSMetadata.from_data_model(data_model)
170
+ metadata.data_model_type = self._infer_data_model_type(metadata.space)
171
+ if ref_properties:
172
+ metadata.schema_ = SchemaCompleteness.extended
173
+
174
+ with _handle_issues(
175
+ self.issue_list,
176
+ ) as future:
177
+ user_rules = DMSRules(
178
+ metadata=metadata,
179
+ properties=properties,
180
+ containers=SheetList[DMSContainer](
181
+ data=[
182
+ DMSContainer.from_container(container)
183
+ for container in self.schema.containers
184
+ if container.as_id() not in self.schema.frozen_ids
185
+ ]
186
+ ),
187
+ views=SheetList[DMSView](
188
+ data=[
189
+ DMSView.from_view(view, in_model=view.as_id() in data_model_view_ids)
190
+ for view in self.schema.views
191
+ if view.as_id() not in self.schema.frozen_ids
192
+ ]
193
+ ),
194
+ reference=self._create_reference_rules(ref_properties),
195
+ )
196
+
197
+ if future.result == "failure" or self.issue_list.has_errors:
198
+ return self._return_or_raise(self.issue_list, errors)
199
+
200
+ return self._to_output(user_rules, self.issue_list, errors, role)
201
+
202
+ def _create_reference_rules(self, properties: SheetList[DMSProperty]) -> DMSRules | None:
203
+ if not properties:
204
+ return None
205
+
206
+ if len(self.schema.data_models) == 2:
207
+ data_model = self.schema.data_models[1]
208
+ data_model_view_ids: set[dm.ViewId] = {
209
+ view.as_id() if isinstance(view, dm.View | dm.ViewApply) else view for view in data_model.views or []
210
+ }
211
+ metadata = self._create_metadata_from_model(data_model)
212
+ else:
213
+ data_model_view_ids = set()
214
+ now = datetime.now().replace(microsecond=0)
215
+ space = Counter(prop.view.space for prop in properties).most_common(1)[0][0]
216
+ metadata = DMSMetadata(
217
+ schema_=SchemaCompleteness.complete,
218
+ extension=ExtensionCategory.addition,
219
+ space=space,
220
+ external_id="Unknown",
221
+ version="0.1.0",
222
+ creator=["Unknown"],
223
+ created=now,
224
+ updated=now,
225
+ )
226
+
227
+ metadata.data_model_type = DataModelType.enterprise
228
+ return DMSRules(
229
+ metadata=metadata,
202
230
  properties=properties,
231
+ views=SheetList[DMSView](
232
+ data=[
233
+ DMSView.from_view(view, in_model=not data_model_view_ids or (view.as_id() in data_model_view_ids))
234
+ for view in self.schema.views
235
+ if view.as_id() in self.schema.frozen_ids
236
+ ]
237
+ ),
203
238
  containers=SheetList[DMSContainer](
204
- data=[DMSContainer.from_container(container) for container in self.schema.containers]
239
+ data=[
240
+ DMSContainer.from_container(container)
241
+ for container in self.schema.containers
242
+ if container.as_id() in self.schema.frozen_ids
243
+ ]
205
244
  ),
206
- views=SheetList[DMSView](data=[DMSView.from_view(view, data_model_view_ids) for view in self.schema.views]),
245
+ reference=None,
246
+ )
247
+
248
+ def _infer_data_model_type(self, space: str) -> DataModelType:
249
+ if self.schema.referenced_spaces() - {space}:
250
+ # If the data model has containers, views, node types in another space
251
+ # we assume it is a solution model.
252
+ return DataModelType.solution
253
+ else:
254
+ # All containers, views, node types are in the same space as the data model
255
+ return DataModelType.enterprise
256
+
257
+ def _create_dms_property(
258
+ self, prop_id: str, prop: ViewPropertyApply, view_entity: ViewEntity, class_entity: ClassEntity
259
+ ) -> DMSProperty | None:
260
+ if isinstance(prop, dm.MappedPropertyApply) and prop.container not in self._container_by_id:
261
+ self.issue_list.append(
262
+ issues.importing.MissingContainerWarning(
263
+ view_id=str(view_entity),
264
+ property_=prop_id,
265
+ container_id=str(ContainerEntity.from_id(prop.container)),
266
+ )
267
+ )
268
+ return None
269
+ if (
270
+ isinstance(prop, dm.MappedPropertyApply)
271
+ and prop.container_property_identifier not in self._container_by_id[prop.container].properties
272
+ ):
273
+ self.issue_list.append(
274
+ issues.importing.MissingContainerPropertyWarning(
275
+ view_id=str(view_entity),
276
+ property_=prop_id,
277
+ container_id=str(ContainerEntity.from_id(prop.container)),
278
+ )
279
+ )
280
+ return None
281
+ if not isinstance(
282
+ prop,
283
+ dm.MappedPropertyApply
284
+ | SingleEdgeConnectionApply
285
+ | MultiEdgeConnectionApply
286
+ | SingleReverseDirectRelationApply
287
+ | MultiReverseDirectRelationApply,
288
+ ):
289
+ self.issue_list.append(
290
+ issues.importing.UnknownPropertyTypeWarning(view_entity.versioned_id, prop_id, type(prop).__name__)
291
+ )
292
+ return None
293
+
294
+ value_type = self._get_value_type(prop, view_entity, prop_id)
295
+ if value_type is None:
296
+ return None
297
+
298
+ return DMSProperty(
299
+ class_=class_entity,
300
+ property_=prop_id,
301
+ description=prop.description,
302
+ name=prop.name,
303
+ connection=self._get_relation_type(prop),
304
+ value_type=value_type,
305
+ is_list=self._get_is_list(prop),
306
+ nullable=self._get_nullable(prop),
307
+ default=self._get_default(prop),
308
+ container=ContainerEntity.from_id(prop.container) if isinstance(prop, dm.MappedPropertyApply) else None,
309
+ container_property=prop.container_property_identifier if isinstance(prop, dm.MappedPropertyApply) else None,
310
+ view=view_entity,
311
+ view_property=prop_id,
312
+ index=self._get_index(prop, prop_id),
313
+ constraint=self._get_constraint(prop, prop_id),
207
314
  )
208
- output_rules: Rules
209
- if role is RoleTypes.information_architect:
210
- output_rules = dms_rules.as_information_architect_rules()
315
+
316
+ def _container_prop_unsafe(self, prop: dm.MappedPropertyApply) -> dm.ContainerProperty:
317
+ """This method assumes you have already checked that the container with property exists."""
318
+ return self._container_by_id[prop.container].properties[prop.container_property_identifier]
319
+
320
+ def _get_relation_type(self, prop: ViewPropertyApply) -> Literal["edge", "reverse", "direct"] | None:
321
+ if isinstance(prop, SingleEdgeConnectionApply | MultiEdgeConnectionApply) and prop.direction == "outwards":
322
+ return "edge"
323
+ elif isinstance(prop, SingleEdgeConnectionApply | MultiEdgeConnectionApply) and prop.direction == "inwards":
324
+ return "reverse"
325
+ elif isinstance(prop, SingleReverseDirectRelationApply | MultiReverseDirectRelationApply):
326
+ return "reverse"
327
+ elif isinstance(prop, dm.MappedPropertyApply) and isinstance(
328
+ self._container_prop_unsafe(prop).type, dm.DirectRelation
329
+ ):
330
+ return "direct"
211
331
  else:
212
- output_rules = dms_rules
213
- if errors == "raise":
214
- return output_rules
332
+ return None
333
+
334
+ def _get_value_type(
335
+ self, prop: ViewPropertyApply, view_entity: ViewEntity, prop_id
336
+ ) -> DataType | ViewEntity | ViewPropertyEntity | DMSUnknownEntity | None:
337
+ if isinstance(prop, SingleEdgeConnectionApply | MultiEdgeConnectionApply) and prop.direction == "outwards":
338
+ return ViewEntity.from_id(prop.source)
339
+ elif isinstance(prop, SingleReverseDirectRelationApply | MultiReverseDirectRelationApply):
340
+ return ViewPropertyEntity.from_id(prop.through)
341
+ elif isinstance(prop, SingleEdgeConnectionApply | MultiEdgeConnectionApply) and prop.direction == "inwards":
342
+ return ViewEntity.from_id(prop.source)
343
+ elif isinstance(prop, dm.MappedPropertyApply):
344
+ container_prop = self._container_prop_unsafe(cast(dm.MappedPropertyApply, prop))
345
+ if isinstance(container_prop.type, dm.DirectRelation):
346
+ if prop.source is None:
347
+ # The warning is issued when the DMS Rules are created.
348
+ return DMSUnknownEntity()
349
+ else:
350
+ return ViewEntity.from_id(prop.source)
351
+ else:
352
+ return DataType.load(container_prop.type._type)
353
+ else:
354
+ self.issue_list.append(issues.importing.FailedToInferValueTypeWarning(str(view_entity), prop_id))
355
+ return None
356
+
357
+ def _get_nullable(self, prop: ViewPropertyApply) -> bool | None:
358
+ if isinstance(prop, dm.MappedPropertyApply):
359
+ return self._container_prop_unsafe(prop).nullable
360
+ else:
361
+ return None
362
+
363
+ def _get_is_list(self, prop: ViewPropertyApply) -> bool | None:
364
+ if isinstance(prop, dm.MappedPropertyApply):
365
+ return self._container_prop_unsafe(prop).type.is_list
366
+ elif isinstance(prop, MultiEdgeConnectionApply | MultiReverseDirectRelationApply):
367
+ return True
368
+ elif isinstance(prop, SingleEdgeConnectionApply | SingleReverseDirectRelationApply):
369
+ return False
215
370
  else:
216
- return output_rules, issue_list
371
+ return None
372
+
373
+ def _get_default(self, prop: ViewPropertyApply) -> str | None:
374
+ if isinstance(prop, dm.MappedPropertyApply):
375
+ default = self._container_prop_unsafe(prop).default_value
376
+ if default is not None:
377
+ return str(default)
378
+ return None
379
+
380
+ def _get_index(self, prop: ViewPropertyApply, prop_id) -> list[str] | None:
381
+ if not isinstance(prop, dm.MappedPropertyApply):
382
+ return None
383
+ container = self._container_by_id[prop.container]
384
+ index: list[str] = []
385
+ for index_name, index_obj in (container.indexes or {}).items():
386
+ if isinstance(index_obj, BTreeIndex | InvertedIndex) and prop_id in index_obj.properties:
387
+ index.append(index_name)
388
+ return index or None
389
+
390
+ def _get_constraint(self, prop: ViewPropertyApply, prop_id: str) -> list[str] | None:
391
+ if not isinstance(prop, dm.MappedPropertyApply):
392
+ return None
393
+ container = self._container_by_id[prop.container]
394
+ unique_constraints: list[str] = []
395
+ for constraint_name, constraint_obj in (container.constraints or {}).items():
396
+ if isinstance(constraint_obj, dm.RequiresConstraint):
397
+ # This is handled in the .from_container method of DMSContainer
398
+ continue
399
+ elif isinstance(constraint_obj, dm.UniquenessConstraint) and prop_id in constraint_obj.properties:
400
+ unique_constraints.append(constraint_name)
401
+ elif isinstance(constraint_obj, dm.UniquenessConstraint):
402
+ # This does not apply to this property
403
+ continue
404
+ else:
405
+ self.issue_list.append(
406
+ issues.importing.UnknownContainerConstraintWarning(
407
+ str(ContainerEntity.from_id(prop.container)), prop_id, type(constraint_obj).__name__
408
+ )
409
+ )
410
+ return unique_constraints or None
@@ -22,7 +22,15 @@ from cognite.neat.utils.spreadsheet import SpreadsheetRead, read_individual_shee
22
22
  from ._base import BaseImporter, Rules, _handle_issues
23
23
 
24
24
  SOURCE_SHEET__TARGET_FIELD__HEADERS = [
25
- ("Properties", "Properties", "Class"),
25
+ (
26
+ "Properties",
27
+ "Properties",
28
+ {
29
+ RoleTypes.domain_expert: "Property",
30
+ RoleTypes.information_architect: "Property",
31
+ RoleTypes.dms_architect: "View Property",
32
+ },
33
+ ),
26
34
  ("Classes", "Classes", "Class"),
27
35
  ("Containers", "Containers", "Container"),
28
36
  ("Views", "Views", "View"),
@@ -131,11 +139,15 @@ class SpreadsheetReader:
131
139
  )
132
140
  return None, read_info_by_sheet
133
141
 
134
- for source_sheet_name, target_sheet_name, headers in SOURCE_SHEET__TARGET_FIELD__HEADERS:
142
+ for source_sheet_name, target_sheet_name, headers_input in SOURCE_SHEET__TARGET_FIELD__HEADERS:
135
143
  source_sheet_name = self.to_reference_sheet(source_sheet_name) if self._is_reference else source_sheet_name
136
144
 
137
145
  if source_sheet_name not in excel_file.sheet_names:
138
146
  continue
147
+ if isinstance(headers_input, dict):
148
+ headers = headers_input[metadata.role]
149
+ else:
150
+ headers = headers_input
139
151
 
140
152
  try:
141
153
  sheets[target_sheet_name], read_info_by_sheet[source_sheet_name] = read_individual_sheet(
@@ -229,12 +241,6 @@ class ExcelImporter(BaseImporter):
229
241
  role=role,
230
242
  )
231
243
 
232
- @classmethod
233
- def _return_or_raise(cls, issue_list: IssueList, errors: Literal["raise", "continue"]) -> tuple[None, IssueList]:
234
- if errors == "raise":
235
- raise issue_list.as_errors()
236
- return None, issue_list
237
-
238
244
 
239
245
  class GoogleSheetImporter(BaseImporter):
240
246
  def __init__(self, sheet_id: str, skiprows: int = 1):
@@ -78,6 +78,9 @@ class NeatValidationError(ValidationIssue, ABC):
78
78
  all_errors.append(DefaultPydanticError.from_pydantic_error(error))
79
79
  return all_errors
80
80
 
81
+ def as_exception(self) -> Exception:
82
+ return ValueError(self.message())
83
+
81
84
 
82
85
  @dataclass(frozen=True)
83
86
  class DefaultPydanticError(NeatValidationError):