cognite-neat 0.77.8__py3-none-any.whl → 0.77.10__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/rules/importers/__init__.py +2 -0
- cognite/neat/rules/importers/_inference2rules.py +221 -0
- cognite/neat/rules/importers/_owl2rules/_owl2rules.py +2 -3
- {cognite_neat-0.77.8.dist-info → cognite_neat-0.77.10.dist-info}/METADATA +2 -2
- {cognite_neat-0.77.8.dist-info → cognite_neat-0.77.10.dist-info}/RECORD +9 -8
- {cognite_neat-0.77.8.dist-info → cognite_neat-0.77.10.dist-info}/LICENSE +0 -0
- {cognite_neat-0.77.8.dist-info → cognite_neat-0.77.10.dist-info}/WHEEL +0 -0
- {cognite_neat-0.77.8.dist-info → cognite_neat-0.77.10.dist-info}/entry_points.txt +0 -0
cognite/neat/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.77.
|
|
1
|
+
__version__ = "0.77.10"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from ._base import BaseImporter
|
|
2
2
|
from ._dms2rules import DMSImporter
|
|
3
3
|
from ._dtdl2rules import DTDLImporter
|
|
4
|
+
from ._inference2rules import InferenceImporter
|
|
4
5
|
from ._owl2rules import OWLImporter
|
|
5
6
|
from ._spreadsheet2rules import ExcelImporter, GoogleSheetImporter
|
|
6
7
|
from ._yaml2rules import YAMLImporter
|
|
@@ -13,4 +14,5 @@ __all__ = [
|
|
|
13
14
|
"GoogleSheetImporter",
|
|
14
15
|
"DTDLImporter",
|
|
15
16
|
"YAMLImporter",
|
|
17
|
+
"InferenceImporter",
|
|
16
18
|
]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Literal, cast, overload
|
|
4
|
+
|
|
5
|
+
from rdflib import Graph, Namespace, URIRef
|
|
6
|
+
from rdflib import Literal as RdfLiteral
|
|
7
|
+
|
|
8
|
+
import cognite.neat.rules.issues as issues
|
|
9
|
+
from cognite.neat.constants import PREFIXES
|
|
10
|
+
from cognite.neat.rules.importers._base import BaseImporter, Rules, _handle_issues
|
|
11
|
+
from cognite.neat.rules.issues import IssueList
|
|
12
|
+
from cognite.neat.rules.models import InformationRules, RoleTypes
|
|
13
|
+
from cognite.neat.rules.models._base import MatchType
|
|
14
|
+
from cognite.neat.rules.models.information import (
|
|
15
|
+
InformationMetadata,
|
|
16
|
+
InformationRulesInput,
|
|
17
|
+
)
|
|
18
|
+
from cognite.neat.utils.utils import get_namespace, remove_namespace
|
|
19
|
+
|
|
20
|
+
ORDERED_CLASSES_QUERY = """SELECT ?class (count(?s) as ?instances )
|
|
21
|
+
WHERE { ?s a ?class . }
|
|
22
|
+
group by ?class order by DESC(?instances)"""
|
|
23
|
+
|
|
24
|
+
INSTANCES_OF_CLASS_QUERY = """SELECT ?s WHERE { ?s a <class> . }"""
|
|
25
|
+
|
|
26
|
+
INSTANCE_PROPERTIES_DEFINITION = """SELECT ?property (count(?property) as ?occurrence) ?dataType ?objectType
|
|
27
|
+
WHERE {<instance_id> ?property ?value .
|
|
28
|
+
BIND(datatype(?value) AS ?dataType)
|
|
29
|
+
OPTIONAL {?value rdf:type ?objectType .}}
|
|
30
|
+
GROUP BY ?property ?dataType ?objectType"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InferenceImporter(BaseImporter):
|
|
34
|
+
"""Rules inference through analysis of knowledge graph provided in various formats.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
issue_list: Issue list to store issues
|
|
38
|
+
graph: Knowledge graph
|
|
39
|
+
max_number_of_instance: Maximum number of instances to be used in inference
|
|
40
|
+
make_compliant: If True, NEAT will attempt to make the imported rules compliant with CDF
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self, issue_list: IssueList, graph: Graph, max_number_of_instance: int = -1, make_compliant: bool = False
|
|
45
|
+
):
|
|
46
|
+
self.issue_list = issue_list
|
|
47
|
+
self.graph = graph
|
|
48
|
+
self.max_number_of_instance = max_number_of_instance
|
|
49
|
+
self.make_compliant = make_compliant
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_rdf_file(cls, filepath: Path, make_compliant: bool = False, max_number_of_instance: int = -1):
|
|
53
|
+
issue_list = IssueList(title=f"'{filepath.name}'")
|
|
54
|
+
|
|
55
|
+
graph = Graph()
|
|
56
|
+
try:
|
|
57
|
+
graph.parse(filepath)
|
|
58
|
+
except Exception:
|
|
59
|
+
issue_list.append(issues.fileread.FileReadError(filepath))
|
|
60
|
+
|
|
61
|
+
return cls(issue_list, graph, make_compliant=make_compliant, max_number_of_instance=max_number_of_instance)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_json_file(cls, filepath: Path, make_compliant: bool = False, max_number_of_instance: int = -1):
|
|
65
|
+
raise NotImplementedError("JSON file format is not supported yet.")
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_yaml_file(cls, filepath: Path, make_compliant: bool = False, max_number_of_instance: int = -1):
|
|
69
|
+
raise NotImplementedError("YAML file format is not supported yet.")
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_xml_file(cls, filepath: Path, make_compliant: bool = False, max_number_of_instance: int = -1):
|
|
73
|
+
raise NotImplementedError("JSON file format is not supported yet.")
|
|
74
|
+
|
|
75
|
+
@overload
|
|
76
|
+
def to_rules(self, errors: Literal["raise"], role: RoleTypes | None = None) -> Rules: ...
|
|
77
|
+
|
|
78
|
+
@overload
|
|
79
|
+
def to_rules(
|
|
80
|
+
self,
|
|
81
|
+
errors: Literal["continue"] = "continue",
|
|
82
|
+
role: RoleTypes | None = None,
|
|
83
|
+
) -> tuple[Rules | None, IssueList]: ...
|
|
84
|
+
|
|
85
|
+
def to_rules(
|
|
86
|
+
self, errors: Literal["raise", "continue"] = "continue", role: RoleTypes | None = None
|
|
87
|
+
) -> tuple[Rules | None, IssueList] | Rules:
|
|
88
|
+
"""
|
|
89
|
+
Creates `Rules` object from the data for target role.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
if self.issue_list.has_errors:
|
|
93
|
+
# In case there were errors during the import, the to_rules method will return None
|
|
94
|
+
return self._return_or_raise(self.issue_list, errors)
|
|
95
|
+
|
|
96
|
+
rules_dict = self._to_rules_components()
|
|
97
|
+
|
|
98
|
+
# adding additional prefix
|
|
99
|
+
rules_dict["prefixes"][rules_dict["metadata"]["prefix"]] = rules_dict["metadata"]["namespace"]
|
|
100
|
+
|
|
101
|
+
with _handle_issues(self.issue_list) as future:
|
|
102
|
+
rules: InformationRules
|
|
103
|
+
rules = InformationRulesInput.load(rules_dict).as_rules()
|
|
104
|
+
|
|
105
|
+
if future.result == "failure" or self.issue_list.has_errors:
|
|
106
|
+
return self._return_or_raise(self.issue_list, errors)
|
|
107
|
+
|
|
108
|
+
if self.make_compliant and rules:
|
|
109
|
+
rules = self._make_dms_compliant_rules(rules)
|
|
110
|
+
|
|
111
|
+
return self._to_output(
|
|
112
|
+
rules,
|
|
113
|
+
self.issue_list,
|
|
114
|
+
errors=errors,
|
|
115
|
+
role=role,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _to_rules_components(
|
|
119
|
+
self,
|
|
120
|
+
) -> dict:
|
|
121
|
+
"""Convert RDF graph to dictionary defining data model and prefixes of the graph
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
graph: RDF graph to be converted to TransformationRules object
|
|
125
|
+
max_number_of_instance: Max number of instances to be considered for each class
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Tuple of data model and prefixes of the graph
|
|
129
|
+
"""
|
|
130
|
+
classes: dict[str, dict] = {}
|
|
131
|
+
properties: dict[str, dict] = {}
|
|
132
|
+
prefixes: dict[str, Namespace] = PREFIXES
|
|
133
|
+
|
|
134
|
+
# Infers all the classes in the graph
|
|
135
|
+
for class_uri, no_instances in self.graph.query(ORDERED_CLASSES_QUERY): # type: ignore[misc]
|
|
136
|
+
self._add_uri_namespace_to_prefixes(cast(URIRef, class_uri), prefixes)
|
|
137
|
+
|
|
138
|
+
if (class_id := remove_namespace(class_uri)) in classes:
|
|
139
|
+
# handles cases when class id is already present in classes
|
|
140
|
+
class_id = f"{class_id}_{len(classes)+1}"
|
|
141
|
+
|
|
142
|
+
classes[class_id] = {
|
|
143
|
+
"class_": class_id,
|
|
144
|
+
"reference": class_uri,
|
|
145
|
+
"match_type": MatchType.exact,
|
|
146
|
+
"comment": f"Inferred from knowledge graph, where this class has {no_instances} instances",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Infers all the properties of the class
|
|
150
|
+
for class_id, class_definition in classes.items():
|
|
151
|
+
for (instance,) in self.graph.query( # type: ignore[misc]
|
|
152
|
+
INSTANCES_OF_CLASS_QUERY.replace("class", class_definition["reference"])
|
|
153
|
+
if self.max_number_of_instance < 0
|
|
154
|
+
else INSTANCES_OF_CLASS_QUERY.replace("class", class_definition["reference"])
|
|
155
|
+
+ f" LIMIT {self.max_number_of_instance}"
|
|
156
|
+
):
|
|
157
|
+
for property_uri, occurrence, data_type_uri, object_type_uri in self.graph.query( # type: ignore[misc]
|
|
158
|
+
INSTANCE_PROPERTIES_DEFINITION.replace("instance_id", instance)
|
|
159
|
+
): # type: ignore[misc]
|
|
160
|
+
property_id = remove_namespace(property_uri)
|
|
161
|
+
self._add_uri_namespace_to_prefixes(cast(URIRef, property_uri), prefixes)
|
|
162
|
+
value_type_uri = data_type_uri if data_type_uri else object_type_uri
|
|
163
|
+
|
|
164
|
+
# this is to skip rdf:type property
|
|
165
|
+
if not value_type_uri:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
self._add_uri_namespace_to_prefixes(cast(URIRef, value_type_uri), prefixes)
|
|
169
|
+
value_type_id = remove_namespace(value_type_uri)
|
|
170
|
+
id_ = f"{class_id}:{property_id}:{value_type_id}"
|
|
171
|
+
|
|
172
|
+
definition = {
|
|
173
|
+
"class_": class_id,
|
|
174
|
+
"property_": property_id,
|
|
175
|
+
"max_occurrence": cast(RdfLiteral, occurrence).value,
|
|
176
|
+
"value_type": value_type_id,
|
|
177
|
+
"reference": property_uri,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# USE CASE 1: If property is not present in properties
|
|
181
|
+
if id_ not in properties:
|
|
182
|
+
properties[id_] = definition
|
|
183
|
+
|
|
184
|
+
# USE CASE 2: If property is present in properties but with different max count
|
|
185
|
+
elif id_ in properties and not (properties[id_]["max_count"] == definition["max_count"]):
|
|
186
|
+
properties[id_]["max_count"] = max(properties[id_]["max_count"], definition["max_count"])
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
"metadata": self._default_metadata().model_dump(),
|
|
190
|
+
"classes": list(classes.values()),
|
|
191
|
+
"properties": list(properties.values()),
|
|
192
|
+
"prefixes": prefixes,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def _add_uri_namespace_to_prefixes(cls, URI: URIRef, prefixes: dict[str, Namespace]):
|
|
197
|
+
"""Add URI to prefixes dict if not already present
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
URI: URI from which namespace is being extracted
|
|
201
|
+
prefixes: Dict of prefixes and namespaces
|
|
202
|
+
"""
|
|
203
|
+
if Namespace(get_namespace(URI)) not in prefixes.values():
|
|
204
|
+
prefixes[f"prefix-{len(prefixes)+1}"] = Namespace(get_namespace(URI))
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def _default_metadata(cls):
|
|
208
|
+
return InformationMetadata(
|
|
209
|
+
name="Inferred Model",
|
|
210
|
+
creator="NEAT",
|
|
211
|
+
version="inferred",
|
|
212
|
+
created=datetime.now(),
|
|
213
|
+
updated=datetime.now(),
|
|
214
|
+
description="Inferred model from knowledge graph",
|
|
215
|
+
prefix="inferred",
|
|
216
|
+
namespace="http://purl.org/cognite/neat/inferred/",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def _make_dms_compliant_rules(cls, rules: InformationRules) -> InformationRules:
|
|
221
|
+
raise NotImplementedError("Compliance with DMS is not supported yet.")
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""This module performs importing of various formats to one of serializations for which
|
|
2
2
|
there are loaders to TransformationRules pydantic class."""
|
|
3
3
|
|
|
4
|
-
# TODO: if this module grows too big, split it into several files and place under ./converter directory
|
|
5
|
-
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
from typing import Literal, overload
|
|
8
6
|
|
|
@@ -22,7 +20,8 @@ class OWLImporter(BaseImporter):
|
|
|
22
20
|
"""Convert OWL ontology to tables/ transformation rules / Excel file.
|
|
23
21
|
|
|
24
22
|
Args:
|
|
25
|
-
|
|
23
|
+
filepath: Path to OWL ontology
|
|
24
|
+
make_compliant: If True, NEAT will attempt to make the imported rules compliant with CDF
|
|
26
25
|
|
|
27
26
|
!!! Note
|
|
28
27
|
OWL Ontologies typically lacks some information that is required for making a complete
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cognite-neat
|
|
3
|
-
Version: 0.77.
|
|
3
|
+
Version: 0.77.10
|
|
4
4
|
Summary: Knowledge graph transformation
|
|
5
5
|
Home-page: https://cognite-neat.readthedocs-hosted.com/
|
|
6
6
|
License: Apache-2.0
|
|
@@ -50,7 +50,7 @@ Requires-Dist: schedule (>=1,<2)
|
|
|
50
50
|
Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
|
|
51
51
|
Requires-Dist: typing_extensions (>=4.8,<5.0) ; python_version < "3.11"
|
|
52
52
|
Requires-Dist: urllib3 (>=2,<3)
|
|
53
|
-
Requires-Dist: uvicorn[standard] (>=0
|
|
53
|
+
Requires-Dist: uvicorn[standard] (>=0,<1)
|
|
54
54
|
Project-URL: Documentation, https://cognite-neat.readthedocs-hosted.com/
|
|
55
55
|
Project-URL: Repository, https://github.com/cognitedata/neat
|
|
56
56
|
Description-Content-Type: text/markdown
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
cognite/neat/__init__.py,sha256=v-rRiDOgZ3sQSMQKq0vgUQZvpeOkoHFXissAx6Ktg84,61
|
|
2
|
-
cognite/neat/_version.py,sha256
|
|
2
|
+
cognite/neat/_version.py,sha256=-swOEBp1OmcqYEuvtO9gV117lIY0GkQSzACgRA9L1AE,24
|
|
3
3
|
cognite/neat/app/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
cognite/neat/app/api/asgi/metrics.py,sha256=nxFy7L5cChTI0a-zkCiJ59Aq8yLuIJp5c9Dg0wRXtV0,152
|
|
5
5
|
cognite/neat/app/api/configuration.py,sha256=2U5M6M252swvQPQyooA1EBzFUZNtcTmuSaywfJDgckM,4232
|
|
@@ -170,7 +170,7 @@ cognite/neat/rules/exporters/_rules2excel.py,sha256=HvUdXYHxfLMijYWdTnfqCsw3Izf8
|
|
|
170
170
|
cognite/neat/rules/exporters/_rules2ontology.py,sha256=NWS3cn2927LQqW_PdQ-92OLIlmIKGNk7xh5yOMAyj94,20120
|
|
171
171
|
cognite/neat/rules/exporters/_rules2yaml.py,sha256=GA8eUYRxUfIU6IMvlyGO5JidkOD5eUKSbH3qAiFiaCg,3026
|
|
172
172
|
cognite/neat/rules/exporters/_validation.py,sha256=OlKIyf4nhSDehJwFHDQ8Zdf6HpNfW7dSe2s67eywHu4,4078
|
|
173
|
-
cognite/neat/rules/importers/__init__.py,sha256=
|
|
173
|
+
cognite/neat/rules/importers/__init__.py,sha256=gR6_TAEa3iO5NCLKRztHg-FMiLdBnx47Z3iSzbwLfcE,481
|
|
174
174
|
cognite/neat/rules/importers/_base.py,sha256=GUiJrYwJ25thI71iS9hCeP_iSZ0Vv8ou3z6MfD07FAk,4274
|
|
175
175
|
cognite/neat/rules/importers/_dms2rules.py,sha256=5yJGYkM7lAMu-QfO0_r59WE4RGtMu2smMqLm16ohgLQ,18994
|
|
176
176
|
cognite/neat/rules/importers/_dtdl2rules/__init__.py,sha256=CNR-sUihs2mnR1bPMKs3j3L4ds3vFTsrl6YycExZTfU,68
|
|
@@ -178,11 +178,12 @@ cognite/neat/rules/importers/_dtdl2rules/_unit_lookup.py,sha256=wW4saKva61Q_i17g
|
|
|
178
178
|
cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py,sha256=ysmWUxZ0npwrTB0uiH5jA0v37sfCwowGaYk17IyxPUU,12663
|
|
179
179
|
cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py,sha256=QDyGt5YBaxzF4v_oCFSgKRSpwVdVruDU3-VW0DEiHbY,6718
|
|
180
180
|
cognite/neat/rules/importers/_dtdl2rules/spec.py,sha256=tim_MfN1J0F3Oeqk3BMgIA82d_MZvhRuRMsLK3B4PYc,11897
|
|
181
|
+
cognite/neat/rules/importers/_inference2rules.py,sha256=VYs4SCYQk3OMJnCkpJ-fTsK73OfK8cOyYXBAdYI3_WU,9389
|
|
181
182
|
cognite/neat/rules/importers/_owl2rules/__init__.py,sha256=tdGcrgtozdQyST-pTlxIa4cLBNTLvtk1nNYR4vOdFSw,63
|
|
182
183
|
cognite/neat/rules/importers/_owl2rules/_owl2classes.py,sha256=LInFeBq-NbBIuMEAwgWch2a4DbBUt4_OMqPkwehW-sw,7591
|
|
183
184
|
cognite/neat/rules/importers/_owl2rules/_owl2metadata.py,sha256=NdPN0dBB0NYkAcfC0yrYdIrGfdPbl5gfeGnSV3EtUPM,7786
|
|
184
185
|
cognite/neat/rules/importers/_owl2rules/_owl2properties.py,sha256=BLptGmH-Aa5gZu0hDIxSZTrn9GmB2FicWgRYoETLSnQ,7437
|
|
185
|
-
cognite/neat/rules/importers/_owl2rules/_owl2rules.py,sha256=
|
|
186
|
+
cognite/neat/rules/importers/_owl2rules/_owl2rules.py,sha256=vyXtAhHCg-zCv_sfq28bx5BK96GXoCB4KA5LeO_ZuvU,6917
|
|
186
187
|
cognite/neat/rules/importers/_spreadsheet2rules.py,sha256=nKSJyZGoTho0bqQ_5_1XB9Z1C-MwovRgkVrC-AhOuzs,12438
|
|
187
188
|
cognite/neat/rules/importers/_yaml2rules.py,sha256=F0uksSz1A3po5OlRM2152_w5j8D9oYTLB9NFTkSMlWI,4275
|
|
188
189
|
cognite/neat/rules/issues/__init__.py,sha256=Ms6jgCxCezc5IgTOwCFtXQPtoVFfOvdcXj84_rs917I,563
|
|
@@ -275,8 +276,8 @@ cognite/neat/workflows/steps_registry.py,sha256=fkTX14ZA7_gkUYfWIlx7A1XbCidvqR23
|
|
|
275
276
|
cognite/neat/workflows/tasks.py,sha256=dqlJwKAb0jlkl7abbY8RRz3m7MT4SK8-7cntMWkOYjw,788
|
|
276
277
|
cognite/neat/workflows/triggers.py,sha256=_BLNplzoz0iic367u1mhHMHiUrCwP-SLK6_CZzfODX0,7071
|
|
277
278
|
cognite/neat/workflows/utils.py,sha256=gKdy3RLG7ctRhbCRwaDIWpL9Mi98zm56-d4jfHDqP1E,453
|
|
278
|
-
cognite_neat-0.77.
|
|
279
|
-
cognite_neat-0.77.
|
|
280
|
-
cognite_neat-0.77.
|
|
281
|
-
cognite_neat-0.77.
|
|
282
|
-
cognite_neat-0.77.
|
|
279
|
+
cognite_neat-0.77.10.dist-info/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
|
|
280
|
+
cognite_neat-0.77.10.dist-info/METADATA,sha256=ioQ2hIZ3OVqhuHAAeHK1WePxy_c5Hu_n231b-m8E2R4,9307
|
|
281
|
+
cognite_neat-0.77.10.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
282
|
+
cognite_neat-0.77.10.dist-info/entry_points.txt,sha256=61FPqiWb25vbqB0KI7znG8nsg_ibLHBvTjYnkPvNFso,50
|
|
283
|
+
cognite_neat-0.77.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|