cognite-neat 0.88.0__py3-none-any.whl → 0.88.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (52) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/app/api/routers/configuration.py +1 -1
  3. cognite/neat/app/ui/neat-app/build/asset-manifest.json +7 -7
  4. cognite/neat/app/ui/neat-app/build/index.html +1 -1
  5. cognite/neat/app/ui/neat-app/build/static/css/{main.38a62222.css → main.72e3d92e.css} +2 -2
  6. cognite/neat/app/ui/neat-app/build/static/css/main.72e3d92e.css.map +1 -0
  7. cognite/neat/app/ui/neat-app/build/static/js/main.5a52cf09.js +3 -0
  8. cognite/neat/app/ui/neat-app/build/static/js/{main.ec7f72e2.js.LICENSE.txt → main.5a52cf09.js.LICENSE.txt} +0 -9
  9. cognite/neat/app/ui/neat-app/build/static/js/main.5a52cf09.js.map +1 -0
  10. cognite/neat/config.py +44 -27
  11. cognite/neat/exceptions.py +6 -0
  12. cognite/neat/graph/extractors/_classic_cdf/_assets.py +21 -73
  13. cognite/neat/graph/extractors/_classic_cdf/_base.py +102 -0
  14. cognite/neat/graph/extractors/_classic_cdf/_events.py +46 -42
  15. cognite/neat/graph/extractors/_classic_cdf/_files.py +41 -45
  16. cognite/neat/graph/extractors/_classic_cdf/_labels.py +75 -52
  17. cognite/neat/graph/extractors/_classic_cdf/_relationships.py +49 -27
  18. cognite/neat/graph/extractors/_classic_cdf/_sequences.py +47 -50
  19. cognite/neat/graph/extractors/_classic_cdf/_timeseries.py +47 -49
  20. cognite/neat/graph/queries/_base.py +22 -29
  21. cognite/neat/graph/queries/_shared.py +1 -1
  22. cognite/neat/graph/stores/_base.py +19 -11
  23. cognite/neat/graph/transformers/_rdfpath.py +3 -2
  24. cognite/neat/issues.py +8 -0
  25. cognite/neat/rules/exporters/_rules2ontology.py +28 -20
  26. cognite/neat/rules/exporters/_validation.py +15 -21
  27. cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +3 -7
  28. cognite/neat/rules/importers/_spreadsheet2rules.py +30 -27
  29. cognite/neat/rules/issues/dms.py +20 -0
  30. cognite/neat/rules/issues/importing.py +15 -0
  31. cognite/neat/rules/issues/ontology.py +298 -0
  32. cognite/neat/rules/issues/spreadsheet.py +48 -0
  33. cognite/neat/rules/issues/tables.py +72 -0
  34. cognite/neat/rules/models/_rdfpath.py +4 -4
  35. cognite/neat/rules/models/_types/_field.py +9 -19
  36. cognite/neat/rules/models/information/_rules.py +5 -4
  37. cognite/neat/utils/rdf_.py +17 -9
  38. cognite/neat/utils/regex_patterns.py +52 -0
  39. {cognite_neat-0.88.0.dist-info → cognite_neat-0.88.1.dist-info}/METADATA +2 -6
  40. {cognite_neat-0.88.0.dist-info → cognite_neat-0.88.1.dist-info}/RECORD +43 -45
  41. cognite/neat/app/ui/neat-app/build/static/css/main.38a62222.css.map +0 -1
  42. cognite/neat/app/ui/neat-app/build/static/js/main.ec7f72e2.js +0 -3
  43. cognite/neat/app/ui/neat-app/build/static/js/main.ec7f72e2.js.map +0 -1
  44. cognite/neat/graph/stores/_oxrdflib.py +0 -247
  45. cognite/neat/rules/exceptions.py +0 -2972
  46. cognite/neat/rules/models/_types/_base.py +0 -16
  47. cognite/neat/workflows/examples/Export_Rules_to_Ontology/workflow.yaml +0 -152
  48. cognite/neat/workflows/examples/Extract_DEXPI_Graph_and_Export_Rules/workflow.yaml +0 -139
  49. cognite/neat/workflows/examples/Ontology_to_Data_Model/workflow.yaml +0 -116
  50. {cognite_neat-0.88.0.dist-info → cognite_neat-0.88.1.dist-info}/LICENSE +0 -0
  51. {cognite_neat-0.88.0.dist-info → cognite_neat-0.88.1.dist-info}/WHEEL +0 -0
  52. {cognite_neat-0.88.0.dist-info → cognite_neat-0.88.1.dist-info}/entry_points.txt +0 -0
@@ -160,7 +160,7 @@ def triples2dictionary(triples: Iterable[tuple[URIRef, URIRef, str | URIRef]]) -
160
160
  value: str
161
161
  uri: URIRef
162
162
 
163
- id_, property_, value = remove_namespace_from_uri(*triple) # type: ignore[misc]
163
+ id_, property_, value = remove_namespace_from_uri(triple) # type: ignore[misc]
164
164
  uri = triple[0]
165
165
 
166
166
  if uri not in dictionary:
@@ -108,7 +108,7 @@ class NeatGraphStore:
108
108
 
109
109
  @classmethod
110
110
  def from_memory_store(cls, rules: InformationRules | None = None) -> "Self":
111
- return cls(Graph(), rules)
111
+ return cls(Graph(identifier=DEFAULT_NAMESPACE), rules)
112
112
 
113
113
  @classmethod
114
114
  def from_sparql_store(
@@ -126,17 +126,17 @@ class NeatGraphStore:
126
126
  postAsEncoded=False,
127
127
  autocommit=False,
128
128
  )
129
- graph = Graph(store=store)
129
+ graph = Graph(store=store, identifier=DEFAULT_NAMESPACE)
130
130
  return cls(graph, rules)
131
131
 
132
132
  @classmethod
133
133
  def from_oxi_store(cls, storage_dir: Path | None = None, rules: InformationRules | None = None) -> "Self":
134
134
  """Creates a NeatGraphStore from an Oxigraph store."""
135
135
  local_import("pyoxigraph", "oxi")
136
+ local_import("oxrdflib", "oxi")
137
+ import oxrdflib
136
138
  import pyoxigraph
137
139
 
138
- from cognite.neat.graph.stores._oxrdflib import OxigraphStore
139
-
140
140
  # Adding support for both oxigraph in-memory and file-based storage
141
141
  for i in range(4):
142
142
  try:
@@ -149,8 +149,10 @@ class NeatGraphStore:
149
149
  else:
150
150
  raise Exception("Error initializing Oxigraph store")
151
151
 
152
- graph = Graph(store=OxigraphStore(store=oxi_store))
153
- graph.default_union = True
152
+ graph = Graph(
153
+ store=oxrdflib.OxigraphStore(store=oxi_store),
154
+ identifier=DEFAULT_NAMESPACE,
155
+ )
154
156
 
155
157
  return cls(graph, rules)
156
158
 
@@ -208,7 +210,8 @@ class NeatGraphStore:
208
210
  property_renaming_config = InformationAnalysis(self.rules).define_property_renaming_config(class_entity)
209
211
 
210
212
  for instance_id in instance_ids:
211
- yield self.queries.describe(instance_id, property_renaming_config)
213
+ if res := self.queries.describe(instance_id, property_renaming_config):
214
+ yield res
212
215
 
213
216
  def _parse_file(
214
217
  self,
@@ -221,7 +224,7 @@ class NeatGraphStore:
221
224
  Args:
222
225
  filepath : File path to file containing graph data, by default None
223
226
  mime_type : MIME type of graph data, by default "application/rdf+xml"
224
- add_base_iri : Add base IRI to graph, by default True
227
+ base_uri : Add base IRI to graph, by default True
225
228
  """
226
229
 
227
230
  # Oxigraph store, do not want to type hint this as it is an optional dependency
@@ -229,10 +232,15 @@ class NeatGraphStore:
229
232
 
230
233
  def parse_to_oxi_store():
231
234
  local_import("pyoxigraph", "oxi")
232
- from cognite.neat.graph.stores._oxrdflib import OxigraphStore
235
+ import pyoxigraph
233
236
 
234
- cast(OxigraphStore, self.graph.store)._inner.bulk_load(str(filepath), mime_type, base_iri=base_uri) # type: ignore[attr-defined]
235
- cast(OxigraphStore, self.graph.store)._inner.optimize() # type: ignore[attr-defined]
237
+ cast(pyoxigraph.Store, self.graph.store._store).bulk_load(
238
+ str(filepath),
239
+ mime_type,
240
+ base_iri=base_uri,
241
+ to_graph=pyoxigraph.NamedNode(self.graph.identifier),
242
+ )
243
+ cast(pyoxigraph.Store, self.graph.store._store).optimize()
236
244
 
237
245
  parse_to_oxi_store()
238
246
 
@@ -1,4 +1,4 @@
1
- from rdflib import RDF, Graph
1
+ from rdflib import Graph
2
2
 
3
3
  from cognite.neat.rules.analysis import InformationAnalysis
4
4
  from cognite.neat.rules.models._rdfpath import RDFPath, SingleProperty
@@ -17,6 +17,7 @@ class AddSelfReferenceProperty(BaseTransformer):
17
17
  description: str = "Adds property that contains id of reference to all references of given class in Rules"
18
18
  _use_only_once: bool = True
19
19
  _need_changes = frozenset({})
20
+ _ref_template: str = """SELECT ?s WHERE {{?s a <{type_}>}}"""
20
21
 
21
22
  def __init__(
22
23
  self,
@@ -32,7 +33,7 @@ class AddSelfReferenceProperty(BaseTransformer):
32
33
 
33
34
  namespace = self.rules.prefixes[prefix] if prefix in self.rules.prefixes else self.rules.metadata.namespace
34
35
 
35
- for reference in graph.subjects(RDF.type, namespace[suffix]):
36
+ for (reference,) in graph.query(self._ref_template.format(type_=namespace[suffix])): # type: ignore [misc]
36
37
  graph.add(
37
38
  (
38
39
  reference,
cognite/neat/issues.py CHANGED
@@ -9,6 +9,7 @@ from typing import Any, ClassVar, TypeVar
9
9
  from warnings import WarningMessage
10
10
 
11
11
  import pandas as pd
12
+ from pydantic_core import PydanticCustomError
12
13
 
13
14
  if sys.version_info < (3, 11):
14
15
  from exceptiongroup import ExceptionGroup
@@ -56,6 +57,13 @@ class NeatError(NeatIssue, ABC):
56
57
  def as_exception(self) -> ValueError:
57
58
  return ValueError(self.message())
58
59
 
60
+ def as_pydantic_exception(self) -> PydanticCustomError:
61
+ return PydanticCustomError(
62
+ self.__class__.__name__,
63
+ self.message(),
64
+ dict(description=self.description, fix=self.fix),
65
+ )
66
+
59
67
 
60
68
  @dataclass(frozen=True)
61
69
  class NeatWarning(NeatIssue, ABC, UserWarning):
@@ -9,8 +9,19 @@ from rdflib import DCTERMS, OWL, RDF, RDFS, XSD, BNode, Graph, Literal, Namespac
9
9
  from rdflib.collection import Collection as GraphCollection
10
10
 
11
11
  from cognite.neat.constants import DEFAULT_NAMESPACE as NEAT_NAMESPACE
12
- from cognite.neat.rules import exceptions
13
12
  from cognite.neat.rules.analysis import InformationAnalysis
13
+ from cognite.neat.rules.issues.ontology import (
14
+ MetadataSheetNamespaceNotDefinedError,
15
+ MissingDataModelPrefixOrNamespaceWarning,
16
+ OntologyMultiDefinitionPropertyWarning,
17
+ OntologyMultiDomainPropertyWarning,
18
+ OntologyMultiLabeledPropertyWarning,
19
+ OntologyMultiRangePropertyWarning,
20
+ OntologyMultiTypePropertyWarning,
21
+ PrefixMissingError,
22
+ PropertiesDefinedMultipleTimesError,
23
+ PropertyDefinitionsNotForSamePropertyError,
24
+ )
14
25
  from cognite.neat.rules.models import DMSRules
15
26
  from cognite.neat.rules.models.data_types import DataType
16
27
  from cognite.neat.rules.models.entities import ClassEntity, EntityTypes
@@ -102,13 +113,15 @@ class Ontology(OntologyModel):
102
113
 
103
114
  properties_redefined, redefinition_warnings = are_properties_redefined(rules, return_report=True)
104
115
  if properties_redefined:
105
- raise exceptions.PropertiesDefinedMultipleTimes(report=generate_exception_report(redefinition_warnings))
116
+ raise PropertiesDefinedMultipleTimesError(
117
+ report=generate_exception_report(redefinition_warnings)
118
+ ).as_exception()
106
119
 
107
120
  if rules.prefixes is None:
108
- raise exceptions.PrefixMissing()
121
+ raise PrefixMissingError().as_exception()
109
122
 
110
123
  if rules.metadata.namespace is None:
111
- raise exceptions.MissingDataModelPrefixOrNamespace()
124
+ raise MissingDataModelPrefixOrNamespaceWarning()
112
125
 
113
126
  class_dict = InformationAnalysis(rules).as_class_dict()
114
127
  return cls(
@@ -172,7 +185,7 @@ class Ontology(OntologyModel):
172
185
  owl.bind(prefix, namespace)
173
186
 
174
187
  if self.metadata.namespace is None:
175
- raise exceptions.MetadataSheetNamespaceNotDefined()
188
+ raise MetadataSheetNamespaceNotDefinedError().as_exception()
176
189
 
177
190
  owl.add((URIRef(self.metadata.namespace), RDF.type, OWL.Ontology))
178
191
  for property_ in self.properties:
@@ -221,7 +234,7 @@ class OWLMetadata(InformationMetadata):
221
234
  def triples(self) -> list[tuple]:
222
235
  # Mandatory triples originating from Metadata mandatory fields
223
236
  if self.namespace is None:
224
- raise exceptions.MetadataSheetNamespaceNotDefined()
237
+ raise MetadataSheetNamespaceNotDefinedError().as_exception()
225
238
  triples: list[tuple] = [
226
239
  (URIRef(self.namespace), DCTERMS.hasVersion, Literal(self.version)),
227
240
  (URIRef(self.namespace), OWL.versionInfo, Literal(self.version)),
@@ -319,7 +332,7 @@ class OWLProperty(OntologyModel):
319
332
  """Here list of properties is a list of properties with the same id, but different definitions."""
320
333
 
321
334
  if not cls.same_property_id(definitions):
322
- raise exceptions.PropertyDefinitionsNotForSameProperty()
335
+ raise PropertyDefinitionsNotForSamePropertyError().as_exception()
323
336
 
324
337
  owl_property = cls.model_construct(
325
338
  id_=namespace[definitions[0].property_],
@@ -350,10 +363,9 @@ class OWLProperty(OntologyModel):
350
363
  def is_multi_type(cls, v, info: ValidationInfo):
351
364
  if len(v) > 1:
352
365
  warnings.warn(
353
- exceptions.OntologyMultiTypeProperty(
366
+ OntologyMultiTypePropertyWarning(
354
367
  remove_namespace_from_uri(info.data["id_"]), [remove_namespace_from_uri(t) for t in v]
355
- ).message,
356
- category=exceptions.OntologyMultiTypeProperty,
368
+ ),
357
369
  stacklevel=2,
358
370
  )
359
371
  return v
@@ -362,10 +374,9 @@ class OWLProperty(OntologyModel):
362
374
  def is_multi_range(cls, v, info: ValidationInfo):
363
375
  if len(v) > 1:
364
376
  warnings.warn(
365
- exceptions.OntologyMultiRangeProperty(
377
+ OntologyMultiRangePropertyWarning(
366
378
  remove_namespace_from_uri(info.data["id_"]), [remove_namespace_from_uri(t) for t in v]
367
- ).message,
368
- category=exceptions.OntologyMultiRangeProperty,
379
+ ),
369
380
  stacklevel=2,
370
381
  )
371
382
  return v
@@ -374,10 +385,9 @@ class OWLProperty(OntologyModel):
374
385
  def is_multi_domain(cls, v, info: ValidationInfo):
375
386
  if len(v) > 1:
376
387
  warnings.warn(
377
- exceptions.OntologyMultiDomainProperty(
388
+ OntologyMultiDomainPropertyWarning(
378
389
  remove_namespace_from_uri(info.data["id_"]), [remove_namespace_from_uri(t) for t in v]
379
- ).message,
380
- category=exceptions.OntologyMultiDomainProperty,
390
+ ),
381
391
  stacklevel=2,
382
392
  )
383
393
  return v
@@ -386,8 +396,7 @@ class OWLProperty(OntologyModel):
386
396
  def has_multi_name(cls, v, info: ValidationInfo):
387
397
  if len(v) > 1:
388
398
  warnings.warn(
389
- exceptions.OntologyMultiLabeledProperty(remove_namespace_from_uri(info.data["id_"]), v).message,
390
- category=exceptions.OntologyMultiLabeledProperty,
399
+ OntologyMultiLabeledPropertyWarning(remove_namespace_from_uri(info.data["id_"]), v),
391
400
  stacklevel=2,
392
401
  )
393
402
  return v
@@ -396,8 +405,7 @@ class OWLProperty(OntologyModel):
396
405
  def has_multi_comment(cls, v, info: ValidationInfo):
397
406
  if len(v) > 1:
398
407
  warnings.warn(
399
- exceptions.OntologyMultiDefinitionProperty(remove_namespace_from_uri(info.data["id_"])).message,
400
- category=exceptions.OntologyMultiDefinitionProperty,
408
+ OntologyMultiDefinitionPropertyWarning(remove_namespace_from_uri(info.data["id_"])),
401
409
  stacklevel=2,
402
410
  )
403
411
  return v
@@ -1,11 +1,11 @@
1
- import re
2
1
  import warnings
3
2
  from typing import Literal, overload
4
3
 
5
4
  from cognite.neat.exceptions import wrangle_warnings
6
- from cognite.neat.rules import exceptions
5
+ from cognite.neat.rules.issues.dms import EntityIDNotDMSCompliantWarning
6
+ from cognite.neat.rules.issues.importing import PropertyRedefinedWarning
7
7
  from cognite.neat.rules.models import InformationRules
8
- from cognite.neat.rules.models._types._base import DMS_PROPERTY_ID_COMPLIANCE_REGEX, VIEW_ID_COMPLIANCE_REGEX
8
+ from cognite.neat.utils.regex_patterns import DMS_PROPERTY_ID_COMPLIANCE_REGEX, PATTERNS, VIEW_ID_COMPLIANCE_REGEX
9
9
 
10
10
 
11
11
  @overload
@@ -26,35 +26,30 @@ def are_entity_names_dms_compliant(
26
26
  flag: bool = True
27
27
  with warnings.catch_warnings(record=True) as validation_warnings:
28
28
  for class_ in rules.classes:
29
- if not re.match(VIEW_ID_COMPLIANCE_REGEX, str(class_.class_.suffix)):
29
+ if not PATTERNS.view_id_compliance.match(class_.class_.suffix):
30
30
  warnings.warn(
31
- exceptions.EntityIDNotDMSCompliant(
32
- "Class", class_.class_.versioned_id, f"[Classes/Class/{class_.class_.versioned_id}]"
33
- ).message,
34
- category=exceptions.EntityIDNotDMSCompliant,
31
+ EntityIDNotDMSCompliantWarning(class_.class_.versioned_id, "Class", VIEW_ID_COMPLIANCE_REGEX),
35
32
  stacklevel=2,
36
33
  )
37
34
  flag = False
38
35
 
39
- for row, property_ in enumerate(rules.properties):
36
+ for _, property_ in enumerate(rules.properties):
40
37
  # check class id which would resolve as view/container id
41
- if not re.match(VIEW_ID_COMPLIANCE_REGEX, str(property_.class_.suffix)):
38
+ if not PATTERNS.view_id_compliance.match(property_.class_.suffix):
42
39
  warnings.warn(
43
- exceptions.EntityIDNotDMSCompliant(
44
- "Class", property_.class_.versioned_id, f"[Properties/Class/{row}]"
45
- ).message,
46
- category=exceptions.EntityIDNotDMSCompliant,
40
+ EntityIDNotDMSCompliantWarning(
41
+ property_.class_.versioned_id,
42
+ "Class",
43
+ VIEW_ID_COMPLIANCE_REGEX,
44
+ ),
47
45
  stacklevel=2,
48
46
  )
49
47
  flag = False
50
48
 
51
49
  # check property id which would resolve as view/container id
52
- if not re.match(DMS_PROPERTY_ID_COMPLIANCE_REGEX, property_.property_):
50
+ if not PATTERNS.dms_property_id_compliance.match(property_.property_):
53
51
  warnings.warn(
54
- exceptions.EntityIDNotDMSCompliant(
55
- "Property", property_.property_, f"[Properties/Property/{row}]"
56
- ).message,
57
- category=exceptions.EntityIDNotDMSCompliant,
52
+ EntityIDNotDMSCompliantWarning(property_.property_, "Property", DMS_PROPERTY_ID_COMPLIANCE_REGEX),
58
53
  stacklevel=2,
59
54
  )
60
55
  flag = False
@@ -83,8 +78,7 @@ def are_properties_redefined(rules: InformationRules, return_report: bool = Fals
83
78
  elif property_.class_ in analyzed_properties[property_.property_]:
84
79
  flag = True
85
80
  warnings.warn(
86
- exceptions.PropertyRedefined(property_.property_, property_.class_.versioned_id).message,
87
- category=exceptions.EntityIDNotDMSCompliant,
81
+ PropertyRedefinedWarning(property_.property_, property_.class_.versioned_id),
88
82
  stacklevel=2,
89
83
  )
90
84
 
@@ -1,16 +1,12 @@
1
1
  import datetime
2
- import re
3
2
 
4
3
  from rdflib import Graph, Namespace
5
4
 
6
5
  from cognite.neat.constants import DEFAULT_NAMESPACE
7
6
  from cognite.neat.rules.models import RoleTypes, SchemaCompleteness
8
- from cognite.neat.rules.models._types._base import (
9
- PREFIX_COMPLIANCE_REGEX,
10
- VERSION_COMPLIANCE_REGEX,
11
- )
12
7
  from cognite.neat.utils.collection_ import remove_none_elements_from_set
13
8
  from cognite.neat.utils.rdf_ import convert_rdflib_content
9
+ from cognite.neat.utils.regex_patterns import PATTERNS
14
10
 
15
11
 
16
12
  def parse_owl_metadata(graph: Graph) -> dict:
@@ -144,7 +140,7 @@ def fix_description(metadata: dict, default: str = "This model has been inferred
144
140
 
145
141
  def fix_prefix(metadata: dict, default: str = "neat") -> dict:
146
142
  if prefix := metadata.get("prefix", None):
147
- if not isinstance(prefix, str) or not re.match(PREFIX_COMPLIANCE_REGEX, prefix):
143
+ if not isinstance(prefix, str) or not PATTERNS.prefix_compliance.match(prefix):
148
144
  metadata["prefix"] = default
149
145
  else:
150
146
  metadata["prefix"] = default
@@ -189,7 +185,7 @@ def fix_date(
189
185
 
190
186
  def fix_version(metadata: dict, default: str = "1.0.0") -> dict:
191
187
  if version := metadata.get("version", None):
192
- if not re.match(VERSION_COMPLIANCE_REGEX, version):
188
+ if not PATTERNS.version_compliance.match(version):
193
189
  metadata["version"] = default
194
190
  else:
195
191
  metadata["version"] = default
@@ -131,25 +131,24 @@ class SpreadsheetReader:
131
131
  names = MANDATORY_SHEETS_BY_ROLE[role]
132
132
  return {f"{self._sheet_prefix}{sheet_name}" for sheet_name in names if sheet_name != "Metadata"}
133
133
 
134
- def read(self, filepath: Path) -> None | ReadResult:
135
- with pd.ExcelFile(filepath) as excel_file:
136
- self._seen_files.add(filepath)
137
- self._seen_sheets.update(map(str, excel_file.sheet_names))
138
- metadata: MetadataRaw | None
139
- if self.metadata is not None:
140
- metadata = self.metadata
141
- else:
142
- metadata = self._read_metadata(excel_file, filepath)
143
- if metadata is None:
144
- # The reading of metadata failed, so we can't continue
145
- return None
146
-
147
- sheets, read_info_by_sheet = self._read_sheets(excel_file, metadata.role)
148
- if sheets is None or self.issue_list.has_errors:
134
+ def read(self, excel_file: pd.ExcelFile, filepath: Path) -> None | ReadResult:
135
+ self._seen_files.add(filepath)
136
+ self._seen_sheets.update(map(str, excel_file.sheet_names))
137
+ metadata: MetadataRaw | None
138
+ if self.metadata is not None:
139
+ metadata = self.metadata
140
+ else:
141
+ metadata = self._read_metadata(excel_file, filepath)
142
+ if metadata is None:
143
+ # The reading of metadata failed, so we can't continue
149
144
  return None
150
- sheets["Metadata"] = dict(metadata)
151
145
 
152
- return ReadResult(sheets, read_info_by_sheet, metadata)
146
+ sheets, read_info_by_sheet = self._read_sheets(excel_file, metadata.role)
147
+ if sheets is None or self.issue_list.has_errors:
148
+ return None
149
+ sheets["Metadata"] = dict(metadata)
150
+
151
+ return ReadResult(sheets, read_info_by_sheet, metadata)
153
152
 
154
153
  def _read_metadata(self, excel_file: ExcelFile, filepath: Path) -> MetadataRaw | None:
155
154
  if self.metadata_sheet_name not in excel_file.sheet_names:
@@ -232,17 +231,21 @@ class ExcelImporter(BaseImporter):
232
231
  issue_list.append(issues.spreadsheet_file.SpreadsheetNotFoundError(self.filepath))
233
232
  return self._return_or_raise(issue_list, errors)
234
233
 
235
- user_reader = SpreadsheetReader(issue_list)
236
- user_read = user_reader.read(self.filepath)
237
- if user_read is None or issue_list.has_errors:
238
- return self._return_or_raise(issue_list, errors)
234
+ with pd.ExcelFile(self.filepath) as excel_file:
235
+ user_reader = SpreadsheetReader(issue_list)
239
236
 
240
- last_read: ReadResult | None = None
241
- if any(sheet_name.startswith("Last") for sheet_name in user_reader.seen_sheets):
242
- last_read = SpreadsheetReader(issue_list, required=False, sheet_prefix="Last").read(self.filepath)
243
- reference_read: ReadResult | None = None
244
- if any(sheet_name.startswith("Ref") for sheet_name in user_reader.seen_sheets):
245
- reference_read = SpreadsheetReader(issue_list, sheet_prefix="Ref").read(self.filepath)
237
+ user_read = user_reader.read(excel_file, self.filepath)
238
+ if user_read is None or issue_list.has_errors:
239
+ return self._return_or_raise(issue_list, errors)
240
+
241
+ last_read: ReadResult | None = None
242
+ if any(sheet_name.startswith("Last") for sheet_name in user_reader.seen_sheets):
243
+ last_read = SpreadsheetReader(issue_list, required=False, sheet_prefix="Last").read(
244
+ excel_file, self.filepath
245
+ )
246
+ reference_read: ReadResult | None = None
247
+ if any(sheet_name.startswith("Ref") for sheet_name in user_reader.seen_sheets):
248
+ reference_read = SpreadsheetReader(issue_list, sheet_prefix="Ref").read(excel_file, self.filepath)
246
249
 
247
250
  if issue_list.has_errors:
248
251
  return self._return_or_raise(issue_list, errors)
@@ -432,6 +432,26 @@ class ChangingViewError(DMSSchemaError):
432
432
  return output
433
433
 
434
434
 
435
+ @dataclass(frozen=True)
436
+ class EntityIDNotDMSCompliantWarning(DMSSchemaWarning):
437
+ description = "The entity ID, {entity_id} of type {entity_type}, is not DMS compliant. Violating regex {regex}"
438
+ fix = "Change the entity ID to be DMS compliant"
439
+ error_name: ClassVar[str] = "EntityIDNotDMSCompliantWarning"
440
+ entity_id: str
441
+ entity_type: str
442
+ regex: str
443
+
444
+ def message(self) -> str:
445
+ return self.description.format(entity_id=self.entity_id, entity_type=self.entity_type, regex=self.regex)
446
+
447
+ def dump(self) -> dict[str, Any]:
448
+ output = super().dump()
449
+ output["entity_id"] = self.entity_id
450
+ output["dms_type"] = self.entity_type
451
+ output["regex"] = self.regex
452
+ return output
453
+
454
+
435
455
  @dataclass(frozen=True)
436
456
  class EmptyContainerWarning(DMSSchemaWarning):
437
457
  description = "The container is empty"
@@ -126,6 +126,21 @@ class UnknownPropertyWarning(ValidationWarning):
126
126
  return f"{prefix} This will be ignored in the imports."
127
127
 
128
128
 
129
+ @dataclass(frozen=True)
130
+ class PropertyRedefinedWarning(ValidationWarning):
131
+ description = "Property, {property}, redefined in {class_}. This will be ignored in the imports."
132
+ fix = "Check if the property is defined only once."
133
+
134
+ property_id: str
135
+ class_id: str
136
+
137
+ def dump(self) -> dict[str, str]:
138
+ return {"property_id": self.property_id, "class_id": self.class_id}
139
+
140
+ def message(self) -> str:
141
+ return self.description.format(property=self.property_id, class_=self.class_id)
142
+
143
+
129
144
  @dataclass(frozen=True)
130
145
  class UnknownValueTypeWarning(ModelImportWarning):
131
146
  description = "Unknown value type. This limits validation done by NEAT. "