cognite-neat 0.87.6__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 (171) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/app/api/data_classes/rest.py +0 -19
  3. cognite/neat/app/api/explorer.py +6 -4
  4. cognite/neat/app/api/routers/configuration.py +1 -1
  5. cognite/neat/app/api/routers/crud.py +11 -21
  6. cognite/neat/app/api/routers/workflows.py +24 -94
  7. cognite/neat/app/ui/neat-app/build/asset-manifest.json +7 -7
  8. cognite/neat/app/ui/neat-app/build/index.html +1 -1
  9. cognite/neat/app/ui/neat-app/build/static/css/{main.38a62222.css → main.72e3d92e.css} +2 -2
  10. cognite/neat/app/ui/neat-app/build/static/css/main.72e3d92e.css.map +1 -0
  11. cognite/neat/app/ui/neat-app/build/static/js/main.5a52cf09.js +3 -0
  12. cognite/neat/app/ui/neat-app/build/static/js/{main.ec7f72e2.js.LICENSE.txt → main.5a52cf09.js.LICENSE.txt} +0 -9
  13. cognite/neat/app/ui/neat-app/build/static/js/main.5a52cf09.js.map +1 -0
  14. cognite/neat/config.py +44 -27
  15. cognite/neat/exceptions.py +6 -0
  16. cognite/neat/graph/extractors/_classic_cdf/_assets.py +21 -73
  17. cognite/neat/graph/extractors/_classic_cdf/_base.py +102 -0
  18. cognite/neat/graph/extractors/_classic_cdf/_events.py +46 -42
  19. cognite/neat/graph/extractors/_classic_cdf/_files.py +41 -45
  20. cognite/neat/graph/extractors/_classic_cdf/_labels.py +75 -52
  21. cognite/neat/graph/extractors/_classic_cdf/_relationships.py +49 -27
  22. cognite/neat/graph/extractors/_classic_cdf/_sequences.py +47 -50
  23. cognite/neat/graph/extractors/_classic_cdf/_timeseries.py +47 -49
  24. cognite/neat/graph/queries/_base.py +22 -29
  25. cognite/neat/graph/queries/_shared.py +1 -1
  26. cognite/neat/graph/stores/_base.py +24 -11
  27. cognite/neat/graph/transformers/_rdfpath.py +3 -2
  28. cognite/neat/issues.py +8 -0
  29. cognite/neat/rules/exporters/_rules2ontology.py +28 -20
  30. cognite/neat/rules/exporters/_validation.py +15 -21
  31. cognite/neat/rules/importers/_inference2rules.py +31 -35
  32. cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +3 -7
  33. cognite/neat/rules/importers/_spreadsheet2rules.py +30 -27
  34. cognite/neat/rules/issues/dms.py +20 -0
  35. cognite/neat/rules/issues/importing.py +15 -0
  36. cognite/neat/rules/issues/ontology.py +298 -0
  37. cognite/neat/rules/issues/spreadsheet.py +48 -0
  38. cognite/neat/rules/issues/tables.py +72 -0
  39. cognite/neat/rules/models/_rdfpath.py +4 -4
  40. cognite/neat/rules/models/_types/_field.py +9 -19
  41. cognite/neat/rules/models/information/_rules.py +5 -4
  42. cognite/neat/utils/rdf_.py +17 -9
  43. cognite/neat/utils/regex_patterns.py +52 -0
  44. cognite/neat/workflows/steps/data_contracts.py +17 -43
  45. cognite/neat/workflows/steps/lib/current/graph_extractor.py +28 -24
  46. cognite/neat/workflows/steps/lib/current/graph_loader.py +4 -21
  47. cognite/neat/workflows/steps/lib/current/graph_store.py +18 -134
  48. cognite/neat/workflows/steps_registry.py +5 -7
  49. {cognite_neat-0.87.6.dist-info → cognite_neat-0.88.1.dist-info}/METADATA +2 -6
  50. cognite_neat-0.88.1.dist-info/RECORD +209 -0
  51. cognite/neat/app/api/routers/core.py +0 -91
  52. cognite/neat/app/api/routers/data_exploration.py +0 -336
  53. cognite/neat/app/api/routers/rules.py +0 -203
  54. cognite/neat/app/ui/neat-app/build/static/css/main.38a62222.css.map +0 -1
  55. cognite/neat/app/ui/neat-app/build/static/js/main.ec7f72e2.js +0 -3
  56. cognite/neat/app/ui/neat-app/build/static/js/main.ec7f72e2.js.map +0 -1
  57. cognite/neat/graph/stores/_oxrdflib.py +0 -247
  58. cognite/neat/legacy/__init__.py +0 -0
  59. cognite/neat/legacy/graph/__init__.py +0 -3
  60. cognite/neat/legacy/graph/examples/Knowledge-Graph-Nordic44-dirty.xml +0 -20182
  61. cognite/neat/legacy/graph/examples/Knowledge-Graph-Nordic44.xml +0 -20163
  62. cognite/neat/legacy/graph/examples/__init__.py +0 -10
  63. cognite/neat/legacy/graph/examples/skos-capturing-sheet-wind-topics.xlsx +0 -0
  64. cognite/neat/legacy/graph/exceptions.py +0 -90
  65. cognite/neat/legacy/graph/extractors/__init__.py +0 -6
  66. cognite/neat/legacy/graph/extractors/_base.py +0 -14
  67. cognite/neat/legacy/graph/extractors/_dexpi.py +0 -44
  68. cognite/neat/legacy/graph/extractors/_graph_capturing_sheet.py +0 -403
  69. cognite/neat/legacy/graph/extractors/_mock_graph_generator.py +0 -361
  70. cognite/neat/legacy/graph/loaders/__init__.py +0 -23
  71. cognite/neat/legacy/graph/loaders/_asset_loader.py +0 -511
  72. cognite/neat/legacy/graph/loaders/_base.py +0 -67
  73. cognite/neat/legacy/graph/loaders/_exceptions.py +0 -85
  74. cognite/neat/legacy/graph/loaders/core/__init__.py +0 -0
  75. cognite/neat/legacy/graph/loaders/core/labels.py +0 -58
  76. cognite/neat/legacy/graph/loaders/core/models.py +0 -136
  77. cognite/neat/legacy/graph/loaders/core/rdf_to_assets.py +0 -1046
  78. cognite/neat/legacy/graph/loaders/core/rdf_to_relationships.py +0 -559
  79. cognite/neat/legacy/graph/loaders/rdf_to_dms.py +0 -309
  80. cognite/neat/legacy/graph/loaders/validator.py +0 -87
  81. cognite/neat/legacy/graph/models.py +0 -6
  82. cognite/neat/legacy/graph/stores/__init__.py +0 -13
  83. cognite/neat/legacy/graph/stores/_base.py +0 -400
  84. cognite/neat/legacy/graph/stores/_graphdb_store.py +0 -52
  85. cognite/neat/legacy/graph/stores/_memory_store.py +0 -43
  86. cognite/neat/legacy/graph/stores/_oxigraph_store.py +0 -151
  87. cognite/neat/legacy/graph/stores/_oxrdflib.py +0 -247
  88. cognite/neat/legacy/graph/stores/_rdf_to_graph.py +0 -42
  89. cognite/neat/legacy/graph/transformations/__init__.py +0 -0
  90. cognite/neat/legacy/graph/transformations/entity_matcher.py +0 -101
  91. cognite/neat/legacy/graph/transformations/query_generator/__init__.py +0 -3
  92. cognite/neat/legacy/graph/transformations/query_generator/sparql.py +0 -575
  93. cognite/neat/legacy/graph/transformations/transformer.py +0 -322
  94. cognite/neat/legacy/rules/__init__.py +0 -0
  95. cognite/neat/legacy/rules/analysis.py +0 -231
  96. cognite/neat/legacy/rules/examples/Rules-Nordic44-to-graphql.xlsx +0 -0
  97. cognite/neat/legacy/rules/examples/Rules-Nordic44.xlsx +0 -0
  98. cognite/neat/legacy/rules/examples/__init__.py +0 -18
  99. cognite/neat/legacy/rules/examples/power-grid-containers.yaml +0 -124
  100. cognite/neat/legacy/rules/examples/power-grid-example.xlsx +0 -0
  101. cognite/neat/legacy/rules/examples/power-grid-model.yaml +0 -224
  102. cognite/neat/legacy/rules/examples/rules-template.xlsx +0 -0
  103. cognite/neat/legacy/rules/examples/sheet2cdf-transformation-rules.xlsx +0 -0
  104. cognite/neat/legacy/rules/examples/skos-rules.xlsx +0 -0
  105. cognite/neat/legacy/rules/examples/source-to-solution-mapping-rules.xlsx +0 -0
  106. cognite/neat/legacy/rules/examples/wind-energy.owl +0 -1511
  107. cognite/neat/legacy/rules/exceptions.py +0 -2972
  108. cognite/neat/legacy/rules/exporters/__init__.py +0 -20
  109. cognite/neat/legacy/rules/exporters/_base.py +0 -45
  110. cognite/neat/legacy/rules/exporters/_core/__init__.py +0 -5
  111. cognite/neat/legacy/rules/exporters/_core/rules2labels.py +0 -24
  112. cognite/neat/legacy/rules/exporters/_rules2dms.py +0 -885
  113. cognite/neat/legacy/rules/exporters/_rules2excel.py +0 -213
  114. cognite/neat/legacy/rules/exporters/_rules2graphql.py +0 -183
  115. cognite/neat/legacy/rules/exporters/_rules2ontology.py +0 -524
  116. cognite/neat/legacy/rules/exporters/_rules2pydantic_models.py +0 -748
  117. cognite/neat/legacy/rules/exporters/_rules2rules.py +0 -105
  118. cognite/neat/legacy/rules/exporters/_rules2triples.py +0 -38
  119. cognite/neat/legacy/rules/exporters/_validation.py +0 -146
  120. cognite/neat/legacy/rules/importers/__init__.py +0 -22
  121. cognite/neat/legacy/rules/importers/_base.py +0 -66
  122. cognite/neat/legacy/rules/importers/_dict2rules.py +0 -158
  123. cognite/neat/legacy/rules/importers/_dms2rules.py +0 -194
  124. cognite/neat/legacy/rules/importers/_graph2rules.py +0 -308
  125. cognite/neat/legacy/rules/importers/_json2rules.py +0 -39
  126. cognite/neat/legacy/rules/importers/_owl2rules/__init__.py +0 -3
  127. cognite/neat/legacy/rules/importers/_owl2rules/_owl2classes.py +0 -239
  128. cognite/neat/legacy/rules/importers/_owl2rules/_owl2metadata.py +0 -260
  129. cognite/neat/legacy/rules/importers/_owl2rules/_owl2properties.py +0 -217
  130. cognite/neat/legacy/rules/importers/_owl2rules/_owl2rules.py +0 -290
  131. cognite/neat/legacy/rules/importers/_spreadsheet2rules.py +0 -45
  132. cognite/neat/legacy/rules/importers/_xsd2rules.py +0 -20
  133. cognite/neat/legacy/rules/importers/_yaml2rules.py +0 -39
  134. cognite/neat/legacy/rules/models/__init__.py +0 -5
  135. cognite/neat/legacy/rules/models/_base.py +0 -151
  136. cognite/neat/legacy/rules/models/raw_rules.py +0 -316
  137. cognite/neat/legacy/rules/models/rdfpath.py +0 -237
  138. cognite/neat/legacy/rules/models/rules.py +0 -1289
  139. cognite/neat/legacy/rules/models/tables.py +0 -9
  140. cognite/neat/legacy/rules/models/value_types.py +0 -118
  141. cognite/neat/legacy/workflows/examples/Export_DMS/workflow.yaml +0 -89
  142. cognite/neat/legacy/workflows/examples/Export_Rules_to_Ontology/workflow.yaml +0 -152
  143. cognite/neat/legacy/workflows/examples/Extract_DEXPI_Graph_and_Export_Rules/workflow.yaml +0 -139
  144. cognite/neat/legacy/workflows/examples/Extract_RDF_Graph_and_Generate_Assets/workflow.yaml +0 -270
  145. cognite/neat/legacy/workflows/examples/Import_DMS/workflow.yaml +0 -65
  146. cognite/neat/legacy/workflows/examples/Ontology_to_Data_Model/workflow.yaml +0 -116
  147. cognite/neat/legacy/workflows/examples/Validate_Rules/workflow.yaml +0 -67
  148. cognite/neat/legacy/workflows/examples/Validate_Solution_Model/workflow.yaml +0 -64
  149. cognite/neat/legacy/workflows/examples/Visualize_Data_Model_Using_Mock_Graph/workflow.yaml +0 -95
  150. cognite/neat/legacy/workflows/examples/Visualize_Semantic_Data_Model/workflow.yaml +0 -111
  151. cognite/neat/rules/exceptions.py +0 -2972
  152. cognite/neat/rules/models/_types/_base.py +0 -16
  153. cognite/neat/workflows/examples/Export_Rules_to_Ontology/workflow.yaml +0 -152
  154. cognite/neat/workflows/examples/Extract_DEXPI_Graph_and_Export_Rules/workflow.yaml +0 -139
  155. cognite/neat/workflows/examples/Extract_RDF_Graph_and_Generate_Assets/workflow.yaml +0 -270
  156. cognite/neat/workflows/examples/Ontology_to_Data_Model/workflow.yaml +0 -116
  157. cognite/neat/workflows/migration/__init__.py +0 -0
  158. cognite/neat/workflows/migration/steps.py +0 -91
  159. cognite/neat/workflows/migration/wf_manifests.py +0 -33
  160. cognite/neat/workflows/steps/lib/legacy/__init__.py +0 -7
  161. cognite/neat/workflows/steps/lib/legacy/graph_contextualization.py +0 -82
  162. cognite/neat/workflows/steps/lib/legacy/graph_extractor.py +0 -746
  163. cognite/neat/workflows/steps/lib/legacy/graph_loader.py +0 -606
  164. cognite/neat/workflows/steps/lib/legacy/graph_store.py +0 -307
  165. cognite/neat/workflows/steps/lib/legacy/graph_transformer.py +0 -58
  166. cognite/neat/workflows/steps/lib/legacy/rules_exporter.py +0 -511
  167. cognite/neat/workflows/steps/lib/legacy/rules_importer.py +0 -612
  168. cognite_neat-0.87.6.dist-info/RECORD +0 -319
  169. {cognite_neat-0.87.6.dist-info → cognite_neat-0.88.1.dist-info}/LICENSE +0 -0
  170. {cognite_neat-0.87.6.dist-info → cognite_neat-0.88.1.dist-info}/WHEEL +0 -0
  171. {cognite_neat-0.87.6.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:
@@ -66,6 +66,11 @@ class NeatGraphStore:
66
66
 
67
67
  self.queries = Queries(self.graph, self.rules)
68
68
 
69
+ @property
70
+ def type_(self) -> str:
71
+ "Return type of the graph store"
72
+ return type(self.graph.store).__name__
73
+
69
74
  def add_rules(self, rules: InformationRules) -> None:
70
75
  """This method is used to add rules to the graph store and it is the only correct
71
76
  way to add rules to the graph store, after the graph store has been initialized.
@@ -103,7 +108,7 @@ class NeatGraphStore:
103
108
 
104
109
  @classmethod
105
110
  def from_memory_store(cls, rules: InformationRules | None = None) -> "Self":
106
- return cls(Graph(), rules)
111
+ return cls(Graph(identifier=DEFAULT_NAMESPACE), rules)
107
112
 
108
113
  @classmethod
109
114
  def from_sparql_store(
@@ -121,17 +126,17 @@ class NeatGraphStore:
121
126
  postAsEncoded=False,
122
127
  autocommit=False,
123
128
  )
124
- graph = Graph(store=store)
129
+ graph = Graph(store=store, identifier=DEFAULT_NAMESPACE)
125
130
  return cls(graph, rules)
126
131
 
127
132
  @classmethod
128
133
  def from_oxi_store(cls, storage_dir: Path | None = None, rules: InformationRules | None = None) -> "Self":
129
134
  """Creates a NeatGraphStore from an Oxigraph store."""
130
135
  local_import("pyoxigraph", "oxi")
136
+ local_import("oxrdflib", "oxi")
137
+ import oxrdflib
131
138
  import pyoxigraph
132
139
 
133
- from cognite.neat.graph.stores._oxrdflib import OxigraphStore
134
-
135
140
  # Adding support for both oxigraph in-memory and file-based storage
136
141
  for i in range(4):
137
142
  try:
@@ -144,8 +149,10 @@ class NeatGraphStore:
144
149
  else:
145
150
  raise Exception("Error initializing Oxigraph store")
146
151
 
147
- graph = Graph(store=OxigraphStore(store=oxi_store))
148
- graph.default_union = True
152
+ graph = Graph(
153
+ store=oxrdflib.OxigraphStore(store=oxi_store),
154
+ identifier=DEFAULT_NAMESPACE,
155
+ )
149
156
 
150
157
  return cls(graph, rules)
151
158
 
@@ -203,7 +210,8 @@ class NeatGraphStore:
203
210
  property_renaming_config = InformationAnalysis(self.rules).define_property_renaming_config(class_entity)
204
211
 
205
212
  for instance_id in instance_ids:
206
- yield self.queries.describe(instance_id, property_renaming_config)
213
+ if res := self.queries.describe(instance_id, property_renaming_config):
214
+ yield res
207
215
 
208
216
  def _parse_file(
209
217
  self,
@@ -216,7 +224,7 @@ class NeatGraphStore:
216
224
  Args:
217
225
  filepath : File path to file containing graph data, by default None
218
226
  mime_type : MIME type of graph data, by default "application/rdf+xml"
219
- add_base_iri : Add base IRI to graph, by default True
227
+ base_uri : Add base IRI to graph, by default True
220
228
  """
221
229
 
222
230
  # Oxigraph store, do not want to type hint this as it is an optional dependency
@@ -224,10 +232,15 @@ class NeatGraphStore:
224
232
 
225
233
  def parse_to_oxi_store():
226
234
  local_import("pyoxigraph", "oxi")
227
- from cognite.neat.graph.stores._oxrdflib import OxigraphStore
235
+ import pyoxigraph
228
236
 
229
- cast(OxigraphStore, self.graph.store)._inner.bulk_load(str(filepath), mime_type, base_iri=base_uri) # type: ignore[attr-defined]
230
- 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()
231
244
 
232
245
  parse_to_oxi_store()
233
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,4 +1,4 @@
1
- import re
1
+ from collections import Counter, defaultdict
2
2
  from datetime import datetime
3
3
  from pathlib import Path
4
4
  from typing import Literal, cast, overload
@@ -201,6 +201,7 @@ class InferenceImporter(BaseImporter):
201
201
  classes: dict[str, dict] = {}
202
202
  properties: dict[str, dict] = {}
203
203
  prefixes: dict[str, Namespace] = get_default_prefixes()
204
+ count_by_value_type_by_property: dict[str, dict[str, int]] = defaultdict(Counter)
204
205
 
205
206
  query = INSTANCE_PROPERTIES_JSON_DEFINITION if self.check_for_json_string else INSTANCE_PROPERTIES_DEFINITION
206
207
  # Adds default namespace to prefixes
@@ -255,12 +256,10 @@ class InferenceImporter(BaseImporter):
255
256
  f"{uri_to_short_form(class_definition['reference'], prefixes)}"
256
257
  f"({uri_to_short_form(cast(URIRef, property_uri), prefixes)})"
257
258
  ),
258
- "comment": (
259
- f"Class <{class_id}> has property <{property_id}> with "
260
- f"value type <{value_type_id}> which occurs <1> times in the graph"
261
- ),
262
259
  }
263
260
 
261
+ count_by_value_type_by_property[id_][value_type_id] += 1
262
+
264
263
  # USE CASE 1: If property is not present in properties
265
264
  if id_ not in properties:
266
265
  properties[id_] = definition
@@ -268,27 +267,41 @@ class InferenceImporter(BaseImporter):
268
267
  # USE CASE 2: first time redefinition, value type change to multi
269
268
  elif id_ in properties and definition["value_type"] not in properties[id_]["value_type"]:
270
269
  properties[id_]["value_type"] = properties[id_]["value_type"] + " | " + definition["value_type"]
271
- properties[id_]["comment"] = (
272
- properties[id_]["comment"] + ", with" + definition["comment"].split("with")[1]
273
- )
274
270
 
275
271
  # USE CASE 3: existing but max count is different
276
272
  elif (
277
273
  id_ in properties
278
274
  and definition["value_type"] in properties[id_]["value_type"]
279
- and not (properties[id_]["max_count"] == definition["max_count"])
275
+ and properties[id_]["max_count"] != definition["max_count"]
280
276
  ):
281
277
  properties[id_]["max_count"] = max(properties[id_]["max_count"], definition["max_count"])
282
278
 
283
- properties[id_]["comment"] = self._update_value_type_occurrence_in_comment(
284
- definition["value_type"], properties[id_]["comment"]
285
- )
286
-
287
- # USE CASE 4: Just update the comment with occurrence
288
- else:
289
- properties[id_]["comment"] = self._update_value_type_occurrence_in_comment(
290
- definition["value_type"], properties[id_]["comment"]
291
- )
279
+ # Add comments
280
+ for id_, property_ in properties.items():
281
+ if id_ not in count_by_value_type_by_property:
282
+ continue
283
+
284
+ count_by_value_type = count_by_value_type_by_property[id_]
285
+ count_list = sorted(count_by_value_type.items(), key=lambda item: item[1], reverse=True)
286
+ # Make the comment more readable by adapting to the number of value types
287
+ base_string = "<{value_type}> which occurs <{count}> times"
288
+ if len(count_list) == 1:
289
+ type_, count = count_list[0]
290
+ counts_str = f"with value type {base_string.format(value_type=type_, count=count)} in the graph"
291
+ elif len(count_list) == 1:
292
+ first = base_string.format(value_type=count_list[0][0], count=count_list[0][1])
293
+ second = base_string.format(value_type=count_list[1][0], count=count_list[1][1])
294
+ counts_str = f"with value types {first} and {second} in the graph"
295
+ else:
296
+ first_part = ", ".join(
297
+ base_string.format(value_type=type_, count=count) for type_, count in count_list[:-1]
298
+ )
299
+ last = base_string.format(value_type=count_list[-1][0], count=count_list[-1][1])
300
+ counts_str = f"with value types {first_part} and {last} in the graph"
301
+
302
+ class_id = property_["class_"]
303
+ property_id = property_["property_"]
304
+ property_["comment"] = f"Class <{class_id}> has property <{property_id}> {counts_str}"
292
305
 
293
306
  return {
294
307
  "metadata": self._default_metadata().model_dump(),
@@ -319,20 +332,3 @@ class InferenceImporter(BaseImporter):
319
332
  prefix=self.prefix,
320
333
  namespace=DEFAULT_NAMESPACE,
321
334
  )
322
-
323
- @classmethod
324
- def _update_value_type_occurrence_in_comment(cls, value_type: str, comment: str) -> str:
325
- occurrence = cls._read_value_type_occurrence_from_comment(value_type, comment)
326
- return comment.replace(
327
- f"with value type <{value_type}> which occurs <{occurrence}> times in the graph",
328
- f"with value type <{value_type}> which occurs <{occurrence+1}> times in the graph",
329
- )
330
-
331
- @classmethod
332
- def _read_value_type_occurrence_from_comment(cls, value_type: str, comment: str) -> int:
333
- if result := re.search(
334
- rf"with value type <{value_type}> which occurs <(\d+)> times in the graph",
335
- comment,
336
- ):
337
- return int(result.group(1))
338
- return 0
@@ -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. "