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.
- cognite/neat/_version.py +1 -1
- cognite/neat/app/api/data_classes/rest.py +0 -19
- cognite/neat/app/api/explorer.py +6 -4
- cognite/neat/app/api/routers/configuration.py +1 -1
- cognite/neat/app/api/routers/crud.py +11 -21
- cognite/neat/app/api/routers/workflows.py +24 -94
- cognite/neat/app/ui/neat-app/build/asset-manifest.json +7 -7
- cognite/neat/app/ui/neat-app/build/index.html +1 -1
- cognite/neat/app/ui/neat-app/build/static/css/{main.38a62222.css → main.72e3d92e.css} +2 -2
- cognite/neat/app/ui/neat-app/build/static/css/main.72e3d92e.css.map +1 -0
- cognite/neat/app/ui/neat-app/build/static/js/main.5a52cf09.js +3 -0
- cognite/neat/app/ui/neat-app/build/static/js/{main.ec7f72e2.js.LICENSE.txt → main.5a52cf09.js.LICENSE.txt} +0 -9
- cognite/neat/app/ui/neat-app/build/static/js/main.5a52cf09.js.map +1 -0
- cognite/neat/config.py +44 -27
- cognite/neat/exceptions.py +6 -0
- cognite/neat/graph/extractors/_classic_cdf/_assets.py +21 -73
- cognite/neat/graph/extractors/_classic_cdf/_base.py +102 -0
- cognite/neat/graph/extractors/_classic_cdf/_events.py +46 -42
- cognite/neat/graph/extractors/_classic_cdf/_files.py +41 -45
- cognite/neat/graph/extractors/_classic_cdf/_labels.py +75 -52
- cognite/neat/graph/extractors/_classic_cdf/_relationships.py +49 -27
- cognite/neat/graph/extractors/_classic_cdf/_sequences.py +47 -50
- cognite/neat/graph/extractors/_classic_cdf/_timeseries.py +47 -49
- cognite/neat/graph/queries/_base.py +22 -29
- cognite/neat/graph/queries/_shared.py +1 -1
- cognite/neat/graph/stores/_base.py +24 -11
- cognite/neat/graph/transformers/_rdfpath.py +3 -2
- cognite/neat/issues.py +8 -0
- cognite/neat/rules/exporters/_rules2ontology.py +28 -20
- cognite/neat/rules/exporters/_validation.py +15 -21
- cognite/neat/rules/importers/_inference2rules.py +31 -35
- cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +3 -7
- cognite/neat/rules/importers/_spreadsheet2rules.py +30 -27
- cognite/neat/rules/issues/dms.py +20 -0
- cognite/neat/rules/issues/importing.py +15 -0
- cognite/neat/rules/issues/ontology.py +298 -0
- cognite/neat/rules/issues/spreadsheet.py +48 -0
- cognite/neat/rules/issues/tables.py +72 -0
- cognite/neat/rules/models/_rdfpath.py +4 -4
- cognite/neat/rules/models/_types/_field.py +9 -19
- cognite/neat/rules/models/information/_rules.py +5 -4
- cognite/neat/utils/rdf_.py +17 -9
- cognite/neat/utils/regex_patterns.py +52 -0
- cognite/neat/workflows/steps/data_contracts.py +17 -43
- cognite/neat/workflows/steps/lib/current/graph_extractor.py +28 -24
- cognite/neat/workflows/steps/lib/current/graph_loader.py +4 -21
- cognite/neat/workflows/steps/lib/current/graph_store.py +18 -134
- cognite/neat/workflows/steps_registry.py +5 -7
- {cognite_neat-0.87.6.dist-info → cognite_neat-0.88.1.dist-info}/METADATA +2 -6
- cognite_neat-0.88.1.dist-info/RECORD +209 -0
- cognite/neat/app/api/routers/core.py +0 -91
- cognite/neat/app/api/routers/data_exploration.py +0 -336
- cognite/neat/app/api/routers/rules.py +0 -203
- cognite/neat/app/ui/neat-app/build/static/css/main.38a62222.css.map +0 -1
- cognite/neat/app/ui/neat-app/build/static/js/main.ec7f72e2.js +0 -3
- cognite/neat/app/ui/neat-app/build/static/js/main.ec7f72e2.js.map +0 -1
- cognite/neat/graph/stores/_oxrdflib.py +0 -247
- cognite/neat/legacy/__init__.py +0 -0
- cognite/neat/legacy/graph/__init__.py +0 -3
- cognite/neat/legacy/graph/examples/Knowledge-Graph-Nordic44-dirty.xml +0 -20182
- cognite/neat/legacy/graph/examples/Knowledge-Graph-Nordic44.xml +0 -20163
- cognite/neat/legacy/graph/examples/__init__.py +0 -10
- cognite/neat/legacy/graph/examples/skos-capturing-sheet-wind-topics.xlsx +0 -0
- cognite/neat/legacy/graph/exceptions.py +0 -90
- cognite/neat/legacy/graph/extractors/__init__.py +0 -6
- cognite/neat/legacy/graph/extractors/_base.py +0 -14
- cognite/neat/legacy/graph/extractors/_dexpi.py +0 -44
- cognite/neat/legacy/graph/extractors/_graph_capturing_sheet.py +0 -403
- cognite/neat/legacy/graph/extractors/_mock_graph_generator.py +0 -361
- cognite/neat/legacy/graph/loaders/__init__.py +0 -23
- cognite/neat/legacy/graph/loaders/_asset_loader.py +0 -511
- cognite/neat/legacy/graph/loaders/_base.py +0 -67
- cognite/neat/legacy/graph/loaders/_exceptions.py +0 -85
- cognite/neat/legacy/graph/loaders/core/__init__.py +0 -0
- cognite/neat/legacy/graph/loaders/core/labels.py +0 -58
- cognite/neat/legacy/graph/loaders/core/models.py +0 -136
- cognite/neat/legacy/graph/loaders/core/rdf_to_assets.py +0 -1046
- cognite/neat/legacy/graph/loaders/core/rdf_to_relationships.py +0 -559
- cognite/neat/legacy/graph/loaders/rdf_to_dms.py +0 -309
- cognite/neat/legacy/graph/loaders/validator.py +0 -87
- cognite/neat/legacy/graph/models.py +0 -6
- cognite/neat/legacy/graph/stores/__init__.py +0 -13
- cognite/neat/legacy/graph/stores/_base.py +0 -400
- cognite/neat/legacy/graph/stores/_graphdb_store.py +0 -52
- cognite/neat/legacy/graph/stores/_memory_store.py +0 -43
- cognite/neat/legacy/graph/stores/_oxigraph_store.py +0 -151
- cognite/neat/legacy/graph/stores/_oxrdflib.py +0 -247
- cognite/neat/legacy/graph/stores/_rdf_to_graph.py +0 -42
- cognite/neat/legacy/graph/transformations/__init__.py +0 -0
- cognite/neat/legacy/graph/transformations/entity_matcher.py +0 -101
- cognite/neat/legacy/graph/transformations/query_generator/__init__.py +0 -3
- cognite/neat/legacy/graph/transformations/query_generator/sparql.py +0 -575
- cognite/neat/legacy/graph/transformations/transformer.py +0 -322
- cognite/neat/legacy/rules/__init__.py +0 -0
- cognite/neat/legacy/rules/analysis.py +0 -231
- cognite/neat/legacy/rules/examples/Rules-Nordic44-to-graphql.xlsx +0 -0
- cognite/neat/legacy/rules/examples/Rules-Nordic44.xlsx +0 -0
- cognite/neat/legacy/rules/examples/__init__.py +0 -18
- cognite/neat/legacy/rules/examples/power-grid-containers.yaml +0 -124
- cognite/neat/legacy/rules/examples/power-grid-example.xlsx +0 -0
- cognite/neat/legacy/rules/examples/power-grid-model.yaml +0 -224
- cognite/neat/legacy/rules/examples/rules-template.xlsx +0 -0
- cognite/neat/legacy/rules/examples/sheet2cdf-transformation-rules.xlsx +0 -0
- cognite/neat/legacy/rules/examples/skos-rules.xlsx +0 -0
- cognite/neat/legacy/rules/examples/source-to-solution-mapping-rules.xlsx +0 -0
- cognite/neat/legacy/rules/examples/wind-energy.owl +0 -1511
- cognite/neat/legacy/rules/exceptions.py +0 -2972
- cognite/neat/legacy/rules/exporters/__init__.py +0 -20
- cognite/neat/legacy/rules/exporters/_base.py +0 -45
- cognite/neat/legacy/rules/exporters/_core/__init__.py +0 -5
- cognite/neat/legacy/rules/exporters/_core/rules2labels.py +0 -24
- cognite/neat/legacy/rules/exporters/_rules2dms.py +0 -885
- cognite/neat/legacy/rules/exporters/_rules2excel.py +0 -213
- cognite/neat/legacy/rules/exporters/_rules2graphql.py +0 -183
- cognite/neat/legacy/rules/exporters/_rules2ontology.py +0 -524
- cognite/neat/legacy/rules/exporters/_rules2pydantic_models.py +0 -748
- cognite/neat/legacy/rules/exporters/_rules2rules.py +0 -105
- cognite/neat/legacy/rules/exporters/_rules2triples.py +0 -38
- cognite/neat/legacy/rules/exporters/_validation.py +0 -146
- cognite/neat/legacy/rules/importers/__init__.py +0 -22
- cognite/neat/legacy/rules/importers/_base.py +0 -66
- cognite/neat/legacy/rules/importers/_dict2rules.py +0 -158
- cognite/neat/legacy/rules/importers/_dms2rules.py +0 -194
- cognite/neat/legacy/rules/importers/_graph2rules.py +0 -308
- cognite/neat/legacy/rules/importers/_json2rules.py +0 -39
- cognite/neat/legacy/rules/importers/_owl2rules/__init__.py +0 -3
- cognite/neat/legacy/rules/importers/_owl2rules/_owl2classes.py +0 -239
- cognite/neat/legacy/rules/importers/_owl2rules/_owl2metadata.py +0 -260
- cognite/neat/legacy/rules/importers/_owl2rules/_owl2properties.py +0 -217
- cognite/neat/legacy/rules/importers/_owl2rules/_owl2rules.py +0 -290
- cognite/neat/legacy/rules/importers/_spreadsheet2rules.py +0 -45
- cognite/neat/legacy/rules/importers/_xsd2rules.py +0 -20
- cognite/neat/legacy/rules/importers/_yaml2rules.py +0 -39
- cognite/neat/legacy/rules/models/__init__.py +0 -5
- cognite/neat/legacy/rules/models/_base.py +0 -151
- cognite/neat/legacy/rules/models/raw_rules.py +0 -316
- cognite/neat/legacy/rules/models/rdfpath.py +0 -237
- cognite/neat/legacy/rules/models/rules.py +0 -1289
- cognite/neat/legacy/rules/models/tables.py +0 -9
- cognite/neat/legacy/rules/models/value_types.py +0 -118
- cognite/neat/legacy/workflows/examples/Export_DMS/workflow.yaml +0 -89
- cognite/neat/legacy/workflows/examples/Export_Rules_to_Ontology/workflow.yaml +0 -152
- cognite/neat/legacy/workflows/examples/Extract_DEXPI_Graph_and_Export_Rules/workflow.yaml +0 -139
- cognite/neat/legacy/workflows/examples/Extract_RDF_Graph_and_Generate_Assets/workflow.yaml +0 -270
- cognite/neat/legacy/workflows/examples/Import_DMS/workflow.yaml +0 -65
- cognite/neat/legacy/workflows/examples/Ontology_to_Data_Model/workflow.yaml +0 -116
- cognite/neat/legacy/workflows/examples/Validate_Rules/workflow.yaml +0 -67
- cognite/neat/legacy/workflows/examples/Validate_Solution_Model/workflow.yaml +0 -64
- cognite/neat/legacy/workflows/examples/Visualize_Data_Model_Using_Mock_Graph/workflow.yaml +0 -95
- cognite/neat/legacy/workflows/examples/Visualize_Semantic_Data_Model/workflow.yaml +0 -111
- cognite/neat/rules/exceptions.py +0 -2972
- cognite/neat/rules/models/_types/_base.py +0 -16
- cognite/neat/workflows/examples/Export_Rules_to_Ontology/workflow.yaml +0 -152
- cognite/neat/workflows/examples/Extract_DEXPI_Graph_and_Export_Rules/workflow.yaml +0 -139
- cognite/neat/workflows/examples/Extract_RDF_Graph_and_Generate_Assets/workflow.yaml +0 -270
- cognite/neat/workflows/examples/Ontology_to_Data_Model/workflow.yaml +0 -116
- cognite/neat/workflows/migration/__init__.py +0 -0
- cognite/neat/workflows/migration/steps.py +0 -91
- cognite/neat/workflows/migration/wf_manifests.py +0 -33
- cognite/neat/workflows/steps/lib/legacy/__init__.py +0 -7
- cognite/neat/workflows/steps/lib/legacy/graph_contextualization.py +0 -82
- cognite/neat/workflows/steps/lib/legacy/graph_extractor.py +0 -746
- cognite/neat/workflows/steps/lib/legacy/graph_loader.py +0 -606
- cognite/neat/workflows/steps/lib/legacy/graph_store.py +0 -307
- cognite/neat/workflows/steps/lib/legacy/graph_transformer.py +0 -58
- cognite/neat/workflows/steps/lib/legacy/rules_exporter.py +0 -511
- cognite/neat/workflows/steps/lib/legacy/rules_importer.py +0 -612
- cognite_neat-0.87.6.dist-info/RECORD +0 -319
- {cognite_neat-0.87.6.dist-info → cognite_neat-0.88.1.dist-info}/LICENSE +0 -0
- {cognite_neat-0.87.6.dist-info → cognite_neat-0.88.1.dist-info}/WHEEL +0 -0
- {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(
|
|
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(
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
+
import pyoxigraph
|
|
228
236
|
|
|
229
|
-
cast(
|
|
230
|
-
|
|
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
|
|
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.
|
|
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
|
|
116
|
+
raise PropertiesDefinedMultipleTimesError(
|
|
117
|
+
report=generate_exception_report(redefinition_warnings)
|
|
118
|
+
).as_exception()
|
|
106
119
|
|
|
107
120
|
if rules.prefixes is None:
|
|
108
|
-
raise
|
|
121
|
+
raise PrefixMissingError().as_exception()
|
|
109
122
|
|
|
110
123
|
if rules.metadata.namespace is None:
|
|
111
|
-
raise
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
366
|
+
OntologyMultiTypePropertyWarning(
|
|
354
367
|
remove_namespace_from_uri(info.data["id_"]), [remove_namespace_from_uri(t) for t in v]
|
|
355
|
-
)
|
|
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
|
-
|
|
377
|
+
OntologyMultiRangePropertyWarning(
|
|
366
378
|
remove_namespace_from_uri(info.data["id_"]), [remove_namespace_from_uri(t) for t in v]
|
|
367
|
-
)
|
|
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
|
-
|
|
388
|
+
OntologyMultiDomainPropertyWarning(
|
|
378
389
|
remove_namespace_from_uri(info.data["id_"]), [remove_namespace_from_uri(t) for t in v]
|
|
379
|
-
)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
29
|
+
if not PATTERNS.view_id_compliance.match(class_.class_.suffix):
|
|
30
30
|
warnings.warn(
|
|
31
|
-
|
|
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
|
|
36
|
+
for _, property_ in enumerate(rules.properties):
|
|
40
37
|
# check class id which would resolve as view/container id
|
|
41
|
-
if not
|
|
38
|
+
if not PATTERNS.view_id_compliance.match(property_.class_.suffix):
|
|
42
39
|
warnings.warn(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
+
if not PATTERNS.dms_property_id_compliance.match(property_.property_):
|
|
53
51
|
warnings.warn(
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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)
|
cognite/neat/rules/issues/dms.py
CHANGED
|
@@ -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. "
|