cognite-neat 0.127.27__py3-none-any.whl → 0.127.29__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.
- cognite/neat/_config.py +185 -0
- cognite/neat/_data_model/validation/dms/_ai_readiness.py +6 -0
- cognite/neat/_data_model/validation/dms/_base.py +1 -0
- cognite/neat/_data_model/validation/dms/_connections.py +11 -0
- cognite/neat/_data_model/validation/dms/_consistency.py +1 -0
- cognite/neat/_data_model/validation/dms/_containers.py +3 -0
- cognite/neat/_data_model/validation/dms/_limits.py +6 -0
- cognite/neat/_data_model/validation/dms/_orchestrator.py +4 -3
- cognite/neat/_data_model/validation/dms/_views.py +2 -0
- cognite/neat/_session/_physical.py +34 -13
- cognite/neat/_session/_session.py +27 -8
- cognite/neat/_version.py +1 -1
- cognite/neat/v0/core/_data_model/models/physical/_exporter.py +14 -10
- {cognite_neat-0.127.27.dist-info → cognite_neat-0.127.29.dist-info}/METADATA +1 -1
- {cognite_neat-0.127.27.dist-info → cognite_neat-0.127.29.dist-info}/RECORD +17 -16
- {cognite_neat-0.127.27.dist-info → cognite_neat-0.127.29.dist-info}/WHEEL +0 -0
- {cognite_neat-0.127.27.dist-info → cognite_neat-0.127.29.dist-info}/licenses/LICENSE +0 -0
cognite/neat/_config.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from cognite.neat._issues import ConsistencyError, ModelSyntaxError
|
|
7
|
+
from cognite.neat._utils.useful_types import ModusOperandi
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 11):
|
|
10
|
+
import tomllib as tomli # Python 3.11+
|
|
11
|
+
else:
|
|
12
|
+
import tomli # type: ignore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ValidationConfig(BaseModel, populate_by_name=True):
|
|
16
|
+
"""Validation configuration."""
|
|
17
|
+
|
|
18
|
+
exclude: list[str] = Field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
def can_run_validator(self, code: str, issue_type: type) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Check if a specific validator should run.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
code: Validation code (e.g., "NEAT-DMS-CONTAINER-001")
|
|
26
|
+
issue_type: Issue type (e.g., ModelSyntaxError, ConsistencyError, Recommendation)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
True if validator should run, False otherwise
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
is_excluded = self._is_excluded(code, self.exclude)
|
|
33
|
+
|
|
34
|
+
if issue_type in [ModelSyntaxError, ConsistencyError] and is_excluded:
|
|
35
|
+
print(f"Validator {code} was excluded however it is a critical validator and will still run.")
|
|
36
|
+
return True
|
|
37
|
+
else:
|
|
38
|
+
return not is_excluded
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def _is_excluded(cls, code: str, patterns: list[str]) -> bool:
|
|
42
|
+
"""Check if code matches any pattern (supports wildcards)."""
|
|
43
|
+
for pattern in patterns:
|
|
44
|
+
if "*" in pattern:
|
|
45
|
+
# Split both pattern and code by hyphens
|
|
46
|
+
pattern_parts = pattern.split("-")
|
|
47
|
+
code_parts = code.split("-")
|
|
48
|
+
|
|
49
|
+
# Pattern must have same or fewer parts than code
|
|
50
|
+
if len(pattern_parts) > len(code_parts):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# Check if all pattern parts match (allowing wildcards)
|
|
54
|
+
match = True
|
|
55
|
+
for p_part, c_part in zip(pattern_parts, code_parts, strict=False):
|
|
56
|
+
if p_part != "*" and p_part != c_part:
|
|
57
|
+
match = False
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
if match:
|
|
61
|
+
return True
|
|
62
|
+
elif code == pattern:
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
"""Human-readable configuration summary."""
|
|
69
|
+
if not self.exclude:
|
|
70
|
+
return "All validators enabled"
|
|
71
|
+
return f"Excluded Rules: {', '.join(self.exclude)}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ModelingConfig(BaseModel, populate_by_name=True):
|
|
75
|
+
"""Modeling configuration."""
|
|
76
|
+
|
|
77
|
+
mode: ModusOperandi = "additive"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class NeatConfig(BaseModel, populate_by_name=True):
|
|
81
|
+
"""Configuration for a custom profile."""
|
|
82
|
+
|
|
83
|
+
profile: str
|
|
84
|
+
validation: ValidationConfig
|
|
85
|
+
modeling: ModelingConfig
|
|
86
|
+
|
|
87
|
+
def __str__(self) -> str:
|
|
88
|
+
"""Human-readable configuration summary."""
|
|
89
|
+
lines = [
|
|
90
|
+
f"Profile: {self.profile}",
|
|
91
|
+
f"Modeling Mode: {self.modeling.mode}",
|
|
92
|
+
f"Validation: {self.validation}",
|
|
93
|
+
]
|
|
94
|
+
return "\n".join(lines)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def internal_profiles() -> dict[str, NeatConfig]:
|
|
98
|
+
"""Get internal NeatConfig profile by name."""
|
|
99
|
+
return {
|
|
100
|
+
"legacy-additive": NeatConfig(
|
|
101
|
+
profile="legacy-additive",
|
|
102
|
+
modeling=ModelingConfig(mode="additive"),
|
|
103
|
+
validation=ValidationConfig(
|
|
104
|
+
exclude=[
|
|
105
|
+
"NEAT-DMS-AI-READINESS-*",
|
|
106
|
+
"NEAT-DMS-CONNECTIONS-002",
|
|
107
|
+
"NEAT-DMS-CONNECTIONS-REVERSE-007",
|
|
108
|
+
"NEAT-DMS-CONNECTIONS-REVERSE-008",
|
|
109
|
+
"NEAT-DMS-CONSISTENCY-001",
|
|
110
|
+
]
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
"legacy-rebuild": NeatConfig(
|
|
114
|
+
profile="legacy-rebuild",
|
|
115
|
+
modeling=ModelingConfig(mode="rebuild"),
|
|
116
|
+
validation=ValidationConfig(
|
|
117
|
+
exclude=[
|
|
118
|
+
"NEAT-DMS-AI-READINESS-*",
|
|
119
|
+
"NEAT-DMS-CONNECTIONS-002",
|
|
120
|
+
"NEAT-DMS-CONNECTIONS-REVERSE-007",
|
|
121
|
+
"NEAT-DMS-CONNECTIONS-REVERSE-008",
|
|
122
|
+
"NEAT-DMS-CONSISTENCY-001",
|
|
123
|
+
]
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
"deep-additive": NeatConfig(
|
|
127
|
+
profile="deep-additive",
|
|
128
|
+
modeling=ModelingConfig(mode="additive"),
|
|
129
|
+
validation=ValidationConfig(exclude=[]),
|
|
130
|
+
),
|
|
131
|
+
"deep-rebuild": NeatConfig(
|
|
132
|
+
profile="deep-rebuild",
|
|
133
|
+
modeling=ModelingConfig(mode="rebuild"),
|
|
134
|
+
validation=ValidationConfig(exclude=[]),
|
|
135
|
+
),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_neat_config(config_file_name: str, profile: str) -> NeatConfig:
|
|
140
|
+
"""Get NeatConfig from file or internal profiles.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
config_file_name: Path to configuration file.
|
|
144
|
+
profile: Profile name to use.
|
|
145
|
+
Returns:
|
|
146
|
+
NeatConfig instance.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
if not config_file_name.endswith(".toml"):
|
|
150
|
+
raise ValueError("config_file_name must end with '.toml'")
|
|
151
|
+
|
|
152
|
+
file_path = Path.cwd() / config_file_name
|
|
153
|
+
|
|
154
|
+
if file_path.exists():
|
|
155
|
+
with file_path.open("rb") as f:
|
|
156
|
+
toml = tomli.load(f)
|
|
157
|
+
|
|
158
|
+
if "tool" in toml and "neat" in toml["tool"]:
|
|
159
|
+
data = toml["tool"]["neat"]
|
|
160
|
+
elif "neat" in toml:
|
|
161
|
+
data = toml["neat"]
|
|
162
|
+
else:
|
|
163
|
+
raise ValueError("No [tool.neat] or [neat] section found in the configuration file.")
|
|
164
|
+
|
|
165
|
+
toml_profile = data.get("profile")
|
|
166
|
+
toml_profiles = data.get("profiles")
|
|
167
|
+
hardcoded_profiles = internal_profiles()
|
|
168
|
+
|
|
169
|
+
if toml_profile and toml_profile in hardcoded_profiles:
|
|
170
|
+
raise ValueError(f"Internal profile '{toml_profile}' cannot be used in external configuration file.")
|
|
171
|
+
|
|
172
|
+
if toml_profiles and any(p in hardcoded_profiles for p in toml_profiles.keys()):
|
|
173
|
+
raise ValueError(
|
|
174
|
+
"Internal profiles cannot be redefined in external configuration file: "
|
|
175
|
+
f"{set(hardcoded_profiles.keys()).intersection(toml_profiles.keys())}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if toml_profile and profile == toml_profile:
|
|
179
|
+
return NeatConfig(**data)
|
|
180
|
+
elif (built_in_profiles := data.get("profiles")) and profile in built_in_profiles:
|
|
181
|
+
return NeatConfig(profile=profile, **data["profiles"][profile])
|
|
182
|
+
else:
|
|
183
|
+
raise ValueError(f"Profile '{profile}' not found in configuration file.")
|
|
184
|
+
else:
|
|
185
|
+
raise FileNotFoundError(f"Configuration file '{file_path}' not found.")
|
|
@@ -23,6 +23,7 @@ class DataModelMissingName(DataModelValidator):
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
code = f"{BASE_CODE}-001"
|
|
26
|
+
issue_type = Recommendation
|
|
26
27
|
|
|
27
28
|
def run(self) -> list[Recommendation]:
|
|
28
29
|
recommendations: list[Recommendation] = []
|
|
@@ -61,6 +62,7 @@ class DataModelMissingDescription(DataModelValidator):
|
|
|
61
62
|
"""
|
|
62
63
|
|
|
63
64
|
code = f"{BASE_CODE}-002"
|
|
65
|
+
issue_type = Recommendation
|
|
64
66
|
|
|
65
67
|
def run(self) -> list[Recommendation]:
|
|
66
68
|
recommendations: list[Recommendation] = []
|
|
@@ -95,6 +97,7 @@ class ViewMissingName(DataModelValidator):
|
|
|
95
97
|
"""
|
|
96
98
|
|
|
97
99
|
code = f"{BASE_CODE}-003"
|
|
100
|
+
issue_type = Recommendation
|
|
98
101
|
|
|
99
102
|
def run(self) -> list[Recommendation]:
|
|
100
103
|
recommendations: list[Recommendation] = []
|
|
@@ -145,6 +148,7 @@ class ViewMissingDescription(DataModelValidator):
|
|
|
145
148
|
"""
|
|
146
149
|
|
|
147
150
|
code = f"{BASE_CODE}-004"
|
|
151
|
+
issue_type = Recommendation
|
|
148
152
|
|
|
149
153
|
def run(self) -> list[Recommendation]:
|
|
150
154
|
recommendations: list[Recommendation] = []
|
|
@@ -185,6 +189,7 @@ class ViewPropertyMissingName(DataModelValidator):
|
|
|
185
189
|
"""
|
|
186
190
|
|
|
187
191
|
code = f"{BASE_CODE}-005"
|
|
192
|
+
issue_type = Recommendation
|
|
188
193
|
|
|
189
194
|
def run(self) -> list[Recommendation]:
|
|
190
195
|
recommendations: list[Recommendation] = []
|
|
@@ -233,6 +238,7 @@ class ViewPropertyMissingDescription(DataModelValidator):
|
|
|
233
238
|
"""
|
|
234
239
|
|
|
235
240
|
code = f"{BASE_CODE}-006"
|
|
241
|
+
issue_type = Recommendation
|
|
236
242
|
|
|
237
243
|
def run(self) -> list[Recommendation]:
|
|
238
244
|
recommendations: list[Recommendation] = []
|
|
@@ -31,6 +31,7 @@ class ConnectionValueTypeUnexisting(DataModelValidator):
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
code = f"{BASE_CODE}-001"
|
|
34
|
+
issue_type = ConsistencyError
|
|
34
35
|
|
|
35
36
|
def run(self) -> list[ConsistencyError]:
|
|
36
37
|
undefined_value_types = []
|
|
@@ -80,6 +81,7 @@ class ConnectionValueTypeUndefined(DataModelValidator):
|
|
|
80
81
|
"""
|
|
81
82
|
|
|
82
83
|
code = f"{BASE_CODE}-002"
|
|
84
|
+
issue_type = Recommendation
|
|
83
85
|
|
|
84
86
|
def run(self) -> list[Recommendation]:
|
|
85
87
|
missing_value_types = []
|
|
@@ -149,6 +151,7 @@ class ReverseConnectionSourceViewMissing(DataModelValidator):
|
|
|
149
151
|
"""
|
|
150
152
|
|
|
151
153
|
code = f"{BASE_CODE}-REVERSE-001"
|
|
154
|
+
issue_type = ConsistencyError
|
|
152
155
|
|
|
153
156
|
def run(self) -> list[ConsistencyError]:
|
|
154
157
|
errors: list[ConsistencyError] = []
|
|
@@ -193,6 +196,7 @@ class ReverseConnectionSourcePropertyMissing(DataModelValidator):
|
|
|
193
196
|
"""
|
|
194
197
|
|
|
195
198
|
code = f"{BASE_CODE}-REVERSE-002"
|
|
199
|
+
issue_type = ConsistencyError
|
|
196
200
|
|
|
197
201
|
def run(self) -> list[ConsistencyError]:
|
|
198
202
|
errors: list[ConsistencyError] = []
|
|
@@ -240,6 +244,7 @@ class ReverseConnectionSourcePropertyWrongType(DataModelValidator):
|
|
|
240
244
|
"""
|
|
241
245
|
|
|
242
246
|
code = f"{BASE_CODE}-REVERSE-003"
|
|
247
|
+
issue_type = ConsistencyError
|
|
243
248
|
|
|
244
249
|
def run(self) -> list[ConsistencyError]:
|
|
245
250
|
errors: list[ConsistencyError] = []
|
|
@@ -290,6 +295,7 @@ class ReverseConnectionContainerMissing(DataModelValidator):
|
|
|
290
295
|
"""
|
|
291
296
|
|
|
292
297
|
code = f"{BASE_CODE}-REVERSE-004"
|
|
298
|
+
issue_type = ConsistencyError
|
|
293
299
|
|
|
294
300
|
def run(self) -> list[ConsistencyError]:
|
|
295
301
|
errors: list[ConsistencyError] = []
|
|
@@ -347,6 +353,7 @@ class ReverseConnectionContainerPropertyMissing(DataModelValidator):
|
|
|
347
353
|
"""
|
|
348
354
|
|
|
349
355
|
code = f"{BASE_CODE}-REVERSE-005"
|
|
356
|
+
issue_type = ConsistencyError
|
|
350
357
|
|
|
351
358
|
def run(self) -> list[ConsistencyError]:
|
|
352
359
|
errors: list[ConsistencyError] = []
|
|
@@ -407,6 +414,7 @@ class ReverseConnectionContainerPropertyWrongType(DataModelValidator):
|
|
|
407
414
|
"""
|
|
408
415
|
|
|
409
416
|
code = f"{BASE_CODE}-REVERSE-006"
|
|
417
|
+
issue_type = ConsistencyError
|
|
410
418
|
|
|
411
419
|
def run(self) -> list[ConsistencyError]:
|
|
412
420
|
errors: list[ConsistencyError] = []
|
|
@@ -469,6 +477,7 @@ class ReverseConnectionTargetMissing(DataModelValidator):
|
|
|
469
477
|
"""
|
|
470
478
|
|
|
471
479
|
code = f"{BASE_CODE}-REVERSE-007"
|
|
480
|
+
issue_type = Recommendation
|
|
472
481
|
|
|
473
482
|
def run(self) -> list[Recommendation]:
|
|
474
483
|
recommendations: list[Recommendation] = []
|
|
@@ -525,6 +534,7 @@ class ReverseConnectionPointsToAncestor(DataModelValidator):
|
|
|
525
534
|
"""
|
|
526
535
|
|
|
527
536
|
code = f"{BASE_CODE}-REVERSE-008"
|
|
537
|
+
issue_type = Recommendation
|
|
528
538
|
|
|
529
539
|
def run(self) -> list[Recommendation]:
|
|
530
540
|
recommendations: list[Recommendation] = []
|
|
@@ -584,6 +594,7 @@ class ReverseConnectionTargetMismatch(DataModelValidator):
|
|
|
584
594
|
"""
|
|
585
595
|
|
|
586
596
|
code = f"{BASE_CODE}-REVERSE-009"
|
|
597
|
+
issue_type = ConsistencyError
|
|
587
598
|
|
|
588
599
|
def run(self) -> list[ConsistencyError]:
|
|
589
600
|
errors: list[ConsistencyError] = []
|
|
@@ -31,6 +31,7 @@ class ExternalContainerDoesNotExist(DataModelValidator):
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
code = f"{BASE_CODE}-001"
|
|
34
|
+
issue_type = ConsistencyError
|
|
34
35
|
|
|
35
36
|
def run(self) -> list[ConsistencyError]:
|
|
36
37
|
errors: list[ConsistencyError] = []
|
|
@@ -81,6 +82,7 @@ class ExternalContainerPropertyDoesNotExist(DataModelValidator):
|
|
|
81
82
|
"""
|
|
82
83
|
|
|
83
84
|
code = f"{BASE_CODE}-002"
|
|
85
|
+
issue_type = ConsistencyError
|
|
84
86
|
|
|
85
87
|
def run(self) -> list[ConsistencyError]:
|
|
86
88
|
errors: list[ConsistencyError] = []
|
|
@@ -137,6 +139,7 @@ class RequiredContainerDoesNotExist(DataModelValidator):
|
|
|
137
139
|
"""
|
|
138
140
|
|
|
139
141
|
code = f"{BASE_CODE}-003"
|
|
142
|
+
issue_type = ConsistencyError
|
|
140
143
|
|
|
141
144
|
def run(self) -> list[ConsistencyError]:
|
|
142
145
|
errors: list[ConsistencyError] = []
|
|
@@ -38,6 +38,7 @@ class DataModelViewCountIsOutOfLimits(DataModelValidator):
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
code = f"{BASE_CODE}-DATA-MODEL-001"
|
|
41
|
+
issue_type = ConsistencyError
|
|
41
42
|
|
|
42
43
|
def __init__(
|
|
43
44
|
self,
|
|
@@ -82,6 +83,7 @@ class ViewPropertyCountIsOutOfLimits(DataModelValidator):
|
|
|
82
83
|
"""
|
|
83
84
|
|
|
84
85
|
code = f"{BASE_CODE}-VIEW-001"
|
|
86
|
+
issue_type = ConsistencyError
|
|
85
87
|
|
|
86
88
|
def __init__(
|
|
87
89
|
self,
|
|
@@ -145,6 +147,7 @@ class ViewContainerCountIsOutOfLimits(DataModelValidator):
|
|
|
145
147
|
"""
|
|
146
148
|
|
|
147
149
|
code = f"{BASE_CODE}-VIEW-002"
|
|
150
|
+
issue_type = ConsistencyError
|
|
148
151
|
|
|
149
152
|
def __init__(
|
|
150
153
|
self,
|
|
@@ -204,6 +207,7 @@ class ViewImplementsCountIsOutOfLimits(DataModelValidator):
|
|
|
204
207
|
"""
|
|
205
208
|
|
|
206
209
|
code = f"{BASE_CODE}-VIEW-003"
|
|
210
|
+
issue_type = ConsistencyError
|
|
207
211
|
|
|
208
212
|
def __init__(
|
|
209
213
|
self,
|
|
@@ -258,6 +262,7 @@ class ContainerPropertyCountIsOutOfLimits(DataModelValidator):
|
|
|
258
262
|
"""
|
|
259
263
|
|
|
260
264
|
code = f"{BASE_CODE}-CONTAINER-001"
|
|
265
|
+
issue_type = ConsistencyError
|
|
261
266
|
|
|
262
267
|
def __init__(
|
|
263
268
|
self,
|
|
@@ -326,6 +331,7 @@ class ContainerPropertyListSizeIsOutOfLimits(DataModelValidator):
|
|
|
326
331
|
"""
|
|
327
332
|
|
|
328
333
|
code = f"{BASE_CODE}-CONTAINER-002"
|
|
334
|
+
issue_type = ConsistencyError
|
|
329
335
|
|
|
330
336
|
def __init__(
|
|
331
337
|
self,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from itertools import chain
|
|
2
3
|
|
|
3
4
|
from cognite.neat._client import NeatClient
|
|
@@ -56,12 +57,12 @@ class DmsDataModelValidation(OnSuccessIssuesChecker):
|
|
|
56
57
|
def __init__(
|
|
57
58
|
self,
|
|
58
59
|
client: NeatClient | None = None,
|
|
59
|
-
codes: list[str] | None = None,
|
|
60
60
|
modus_operandi: ModusOperandi = "additive",
|
|
61
|
+
can_run_validator: Callable[[str, type], bool] | None = None,
|
|
61
62
|
) -> None:
|
|
62
63
|
super().__init__()
|
|
63
64
|
self._client = client
|
|
64
|
-
self.
|
|
65
|
+
self._can_run_validator = can_run_validator or (lambda code, issue_type: True) # type: ignore
|
|
65
66
|
self._modus_operandi = modus_operandi
|
|
66
67
|
self._has_run = False
|
|
67
68
|
|
|
@@ -155,7 +156,7 @@ class DmsDataModelValidation(OnSuccessIssuesChecker):
|
|
|
155
156
|
|
|
156
157
|
# Run validators
|
|
157
158
|
for validator in validators:
|
|
158
|
-
if
|
|
159
|
+
if self._can_run_validator(validator.code, validator.issue_type):
|
|
159
160
|
self._issues.extend(validator.run())
|
|
160
161
|
|
|
161
162
|
self._has_run = True
|
|
@@ -24,6 +24,7 @@ class ViewToContainerMappingNotPossible(DataModelValidator):
|
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
code = f"{BASE_CODE}-001"
|
|
27
|
+
issue_type = ConsistencyError
|
|
27
28
|
|
|
28
29
|
def run(self) -> list[ConsistencyError]:
|
|
29
30
|
errors: list[ConsistencyError] = []
|
|
@@ -81,6 +82,7 @@ class ImplementedViewNotExisting(DataModelValidator):
|
|
|
81
82
|
"""
|
|
82
83
|
|
|
83
84
|
code = f"{BASE_CODE}-002"
|
|
85
|
+
issue_type = ConsistencyError
|
|
84
86
|
|
|
85
87
|
def run(self) -> list[ConsistencyError]:
|
|
86
88
|
errors: list[ConsistencyError] = []
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Any, Literal
|
|
2
2
|
|
|
3
3
|
from cognite.neat._client import NeatClient
|
|
4
|
+
from cognite.neat._config import NeatConfig
|
|
4
5
|
from cognite.neat._data_model.deployer.deployer import DeploymentOptions, SchemaDeployer
|
|
5
6
|
from cognite.neat._data_model.exporters import (
|
|
6
7
|
DMSAPIExporter,
|
|
@@ -19,7 +20,6 @@ from cognite.neat._exceptions import UserInputError
|
|
|
19
20
|
from cognite.neat._state_machine import PhysicalState
|
|
20
21
|
from cognite.neat._store._store import NeatStore
|
|
21
22
|
from cognite.neat._utils._reader import NeatReader
|
|
22
|
-
from cognite.neat._utils.useful_types import ModusOperandi
|
|
23
23
|
|
|
24
24
|
from ._wrappers import session_wrapper
|
|
25
25
|
|
|
@@ -27,11 +27,12 @@ from ._wrappers import session_wrapper
|
|
|
27
27
|
class PhysicalDataModel:
|
|
28
28
|
"""Read from a data source into NeatSession graph store."""
|
|
29
29
|
|
|
30
|
-
def __init__(self, store: NeatStore, client: NeatClient,
|
|
30
|
+
def __init__(self, store: NeatStore, client: NeatClient, config: NeatConfig) -> None:
|
|
31
31
|
self._store = store
|
|
32
32
|
self._client = client
|
|
33
|
-
self.
|
|
34
|
-
self.
|
|
33
|
+
self._config = config
|
|
34
|
+
self.read = ReadPhysicalDataModel(self._store, self._client, self._config)
|
|
35
|
+
self.write = WritePhysicalDataModel(self._store, self._client, self._config)
|
|
35
36
|
|
|
36
37
|
def _repr_html_(self) -> str:
|
|
37
38
|
if not isinstance(self._store.state, PhysicalState):
|
|
@@ -69,9 +70,10 @@ class PhysicalDataModel:
|
|
|
69
70
|
class ReadPhysicalDataModel:
|
|
70
71
|
"""Read physical data model from various sources into NeatSession graph store."""
|
|
71
72
|
|
|
72
|
-
def __init__(self, store: NeatStore, client: NeatClient) -> None:
|
|
73
|
+
def __init__(self, store: NeatStore, client: NeatClient, config: NeatConfig) -> None:
|
|
73
74
|
self._store = store
|
|
74
75
|
self._client = client
|
|
76
|
+
self._config = config
|
|
75
77
|
|
|
76
78
|
def yaml(self, io: Any, format: Literal["neat", "toolkit"] = "neat") -> None:
|
|
77
79
|
"""Read physical data model from YAML file(s)
|
|
@@ -92,7 +94,11 @@ class ReadPhysicalDataModel:
|
|
|
92
94
|
reader = DMSAPIImporter.from_yaml(path)
|
|
93
95
|
else:
|
|
94
96
|
raise UserInputError(f"Unsupported format: {format}. Supported formats are 'neat' and 'toolkit'.")
|
|
95
|
-
on_success = DmsDataModelValidation(
|
|
97
|
+
on_success = DmsDataModelValidation(
|
|
98
|
+
self._client,
|
|
99
|
+
modus_operandi=self._config.modeling.mode,
|
|
100
|
+
can_run_validator=self._config.validation.can_run_validator,
|
|
101
|
+
)
|
|
96
102
|
|
|
97
103
|
return self._store.read_physical(reader, on_success)
|
|
98
104
|
|
|
@@ -115,7 +121,11 @@ class ReadPhysicalDataModel:
|
|
|
115
121
|
reader = DMSAPIImporter.from_json(path)
|
|
116
122
|
else:
|
|
117
123
|
raise UserInputError(f"Unsupported format: {format}. Supported formats are 'neat' and 'toolkit'.")
|
|
118
|
-
on_success = DmsDataModelValidation(
|
|
124
|
+
on_success = DmsDataModelValidation(
|
|
125
|
+
self._client,
|
|
126
|
+
modus_operandi=self._config.modeling.mode,
|
|
127
|
+
can_run_validator=self._config.validation.can_run_validator,
|
|
128
|
+
)
|
|
119
129
|
|
|
120
130
|
return self._store.read_physical(reader, on_success)
|
|
121
131
|
|
|
@@ -124,7 +134,11 @@ class ReadPhysicalDataModel:
|
|
|
124
134
|
|
|
125
135
|
path = NeatReader.create(io).materialize_path()
|
|
126
136
|
reader = DMSTableImporter.from_excel(path)
|
|
127
|
-
on_success = DmsDataModelValidation(
|
|
137
|
+
on_success = DmsDataModelValidation(
|
|
138
|
+
self._client,
|
|
139
|
+
modus_operandi=self._config.modeling.mode,
|
|
140
|
+
can_run_validator=self._config.validation.can_run_validator,
|
|
141
|
+
)
|
|
128
142
|
|
|
129
143
|
return self._store.read_physical(reader, on_success)
|
|
130
144
|
|
|
@@ -140,7 +154,11 @@ class ReadPhysicalDataModel:
|
|
|
140
154
|
reader = DMSAPIImporter.from_cdf(
|
|
141
155
|
DataModelReference(space=space, external_id=external_id, version=version), self._client
|
|
142
156
|
)
|
|
143
|
-
on_success = DmsDataModelValidation(
|
|
157
|
+
on_success = DmsDataModelValidation(
|
|
158
|
+
self._client,
|
|
159
|
+
modus_operandi=self._config.modeling.mode,
|
|
160
|
+
can_run_validator=self._config.validation.can_run_validator,
|
|
161
|
+
)
|
|
144
162
|
|
|
145
163
|
return self._store.read_physical(reader, on_success)
|
|
146
164
|
|
|
@@ -149,10 +167,10 @@ class ReadPhysicalDataModel:
|
|
|
149
167
|
class WritePhysicalDataModel:
|
|
150
168
|
"""Write physical data model to various sources from NeatSession graph store."""
|
|
151
169
|
|
|
152
|
-
def __init__(self, store: NeatStore, client: NeatClient,
|
|
170
|
+
def __init__(self, store: NeatStore, client: NeatClient, config: NeatConfig) -> None:
|
|
153
171
|
self._store = store
|
|
154
172
|
self._client = client
|
|
155
|
-
self.
|
|
173
|
+
self._config = config
|
|
156
174
|
|
|
157
175
|
def yaml(self, io: Any, format: Literal["neat", "toolkit"] = "neat") -> None:
|
|
158
176
|
"""Write physical data model to YAML file
|
|
@@ -215,7 +233,7 @@ class WritePhysicalDataModel:
|
|
|
215
233
|
"""Write physical data model with views, containers, and spaces that are in the same space as the data model
|
|
216
234
|
to CDF.
|
|
217
235
|
|
|
218
|
-
This method depends on the session
|
|
236
|
+
This method depends on the session governance profile for data modeling set when creating the NeatSession.
|
|
219
237
|
- In 'additive' mode, only new or updates to data models/views/containers will be applied.
|
|
220
238
|
You cannot remove views from data models, properties from views or containers, or
|
|
221
239
|
indexes or constraints from containers.
|
|
@@ -234,7 +252,10 @@ class WritePhysicalDataModel:
|
|
|
234
252
|
"""
|
|
235
253
|
writer = DMSAPIExporter()
|
|
236
254
|
options = DeploymentOptions(
|
|
237
|
-
dry_run=dry_run,
|
|
255
|
+
dry_run=dry_run,
|
|
256
|
+
auto_rollback=rollback,
|
|
257
|
+
drop_data=drop_data,
|
|
258
|
+
modus_operandi=self._config.modeling.mode,
|
|
238
259
|
)
|
|
239
260
|
on_success = SchemaDeployer(self._client, options)
|
|
240
261
|
return self._store.write_physical(writer, on_success)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from typing import Literal
|
|
2
3
|
|
|
3
4
|
from cognite.client import ClientConfig, CogniteClient
|
|
4
5
|
|
|
5
6
|
from cognite.neat import _version
|
|
6
7
|
from cognite.neat._client import NeatClient
|
|
8
|
+
from cognite.neat._config import internal_profiles
|
|
7
9
|
from cognite.neat._state_machine import EmptyState, PhysicalState
|
|
8
10
|
from cognite.neat._store import NeatStore
|
|
9
11
|
from cognite.neat._utils.http_client import ParametersRequest, SuccessResponse
|
|
10
|
-
from cognite.neat._utils.useful_types import ModusOperandi
|
|
11
12
|
|
|
12
13
|
from ._issues import Issues
|
|
13
14
|
from ._opt import Opt
|
|
@@ -16,21 +17,38 @@ from ._result import Result
|
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class NeatSession:
|
|
19
|
-
"""A session is an interface for neat operations.
|
|
20
|
-
a manager for handling user interactions and orchestrating
|
|
21
|
-
the state machine for data model and instance operations.
|
|
22
|
-
"""
|
|
20
|
+
"""A session is an interface for neat operations."""
|
|
23
21
|
|
|
24
|
-
def __init__(
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
client: CogniteClient | ClientConfig,
|
|
25
|
+
config: Literal["legacy-additive", "legacy-rebuild", "deep-additive", "deep-rebuild"] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize a Neat session.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
client (CogniteClient | ClientConfig): The Cognite client or client configuration to use for the session.
|
|
31
|
+
config (Literal["legacy-additive", "legacy-rebuild", "deep-additive", "deep-rebuild"] | None):
|
|
32
|
+
The configuration profile to use for the session.
|
|
33
|
+
If None, the default profile "legacy-additive" is used. Meaning that Neat will perform additive modeling
|
|
34
|
+
and apply only validations that were part of the legacy Neat version."""
|
|
35
|
+
|
|
36
|
+
# Load configuration
|
|
37
|
+
if config and config not in internal_profiles():
|
|
38
|
+
raise ValueError(f"Profile '{config}' not found among internal profiles.")
|
|
39
|
+
|
|
40
|
+
self._config = internal_profiles()[config or "legacy-additive"]
|
|
41
|
+
|
|
42
|
+
# Use configuration for physical data model
|
|
25
43
|
self._store = NeatStore()
|
|
26
44
|
self._client = NeatClient(client)
|
|
27
|
-
self.physical_data_model = PhysicalDataModel(self._store, self._client,
|
|
45
|
+
self.physical_data_model = PhysicalDataModel(self._store, self._client, self._config)
|
|
28
46
|
self.issues = Issues(self._store)
|
|
29
47
|
self.result = Result(self._store)
|
|
30
48
|
self.opt = Opt(self._store)
|
|
31
49
|
|
|
32
50
|
if self.opt._collector.can_collect:
|
|
33
|
-
self.opt._collector.collect("initSession", {"mode": mode})
|
|
51
|
+
self.opt._collector.collect("initSession", {"mode": self._config.modeling.mode})
|
|
34
52
|
|
|
35
53
|
self._welcome_message()
|
|
36
54
|
|
|
@@ -50,6 +68,7 @@ class NeatSession:
|
|
|
50
68
|
message += f" (Organization: '{organization}')"
|
|
51
69
|
|
|
52
70
|
print(message)
|
|
71
|
+
print(self._config)
|
|
53
72
|
|
|
54
73
|
@property
|
|
55
74
|
def version(self) -> str:
|
cognite/neat/_version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "0.127.
|
|
1
|
+
__version__ = "0.127.29"
|
|
2
2
|
__engine__ = "^2.0.4"
|
|
@@ -243,17 +243,21 @@ class _DMSExporter:
|
|
|
243
243
|
connection = cast(EdgeEntity, prop.connection)
|
|
244
244
|
|
|
245
245
|
if connection.direction == "inwards" and isinstance(prop.value_type, ViewEntity):
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
# Warning in validation, should not have an inwards connection without an outwards connection
|
|
249
|
-
edge_type = cls._default_edge_type_from_view_id(prop.view.as_id(), prop_id)
|
|
250
|
-
elif len(edge_type_candidates) == 1:
|
|
251
|
-
edge_type = edge_type_candidates[0]
|
|
246
|
+
if connection.edge_type is not None:
|
|
247
|
+
edge_type = connection.edge_type.as_reference()
|
|
252
248
|
else:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
249
|
+
edge_type_candidates = outwards_type_by_view_value_type.get((prop.view, prop.value_type), [])
|
|
250
|
+
if len(edge_type_candidates) == 0:
|
|
251
|
+
# Warning in validation, should not have an inwards connection without an outwards connection
|
|
252
|
+
edge_type = cls._default_edge_type_from_view_id(prop.view.as_id(), prop_id)
|
|
253
|
+
elif len(edge_type_candidates) == 1:
|
|
254
|
+
edge_type = edge_type_candidates[0]
|
|
255
|
+
else:
|
|
256
|
+
raise NeatValueError(
|
|
257
|
+
f"Cannot infer edge type for {view_id}.{prop_id}, multiple candidates: "
|
|
258
|
+
f"{edge_type_candidates}. "
|
|
259
|
+
"Please specify edge type explicitly, i.e., edge(type=<YOUR_TYPE>)."
|
|
260
|
+
)
|
|
257
261
|
view_property_id = (prop.view, prop.view_property)
|
|
258
262
|
edge_types_by_view_property_id[view_property_id] = edge_type
|
|
259
263
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cognite-neat
|
|
3
|
-
Version: 0.127.
|
|
3
|
+
Version: 0.127.29
|
|
4
4
|
Summary: Knowledge graph transformation
|
|
5
5
|
Project-URL: Documentation, https://cognite-neat.readthedocs-hosted.com/
|
|
6
6
|
Project-URL: Homepage, https://cognite-neat.readthedocs-hosted.com/
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
cognite/neat/__init__.py,sha256=Lo4DbjDOwnhCYUoAgPp5RG1fDdF7OlnomalTe7n1ydw,211
|
|
2
|
+
cognite/neat/_config.py,sha256=W42Lj5PNR9X1is9vIPHLHvkl_rpU5PCrDuQI4zSNTJQ,6359
|
|
2
3
|
cognite/neat/_exceptions.py,sha256=ox-5hXpee4UJlPE7HpuEHV2C96aLbLKo-BhPDoOAzhA,1650
|
|
3
4
|
cognite/neat/_issues.py,sha256=wH1mnkrpBsHUkQMGUHFLUIQWQlfJ_qMfdF7q0d9wNhY,1871
|
|
4
|
-
cognite/neat/_version.py,sha256=
|
|
5
|
+
cognite/neat/_version.py,sha256=HnkgU0oEwuWFxHjFpJFKDNKAV0Zn342c-qMWtztHfKE,47
|
|
5
6
|
cognite/neat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
7
|
cognite/neat/v1.py,sha256=owqW5Mml2DSZx1AvPvwNRTBngfhBNrQ6EH-7CKL7Jp0,61
|
|
7
8
|
cognite/neat/_client/__init__.py,sha256=75Bh7eGhaN4sOt3ZcRzHl7pXaheu1z27kmTHeaI05vo,114
|
|
@@ -74,20 +75,20 @@ cognite/neat/_data_model/models/entities/_identifiers.py,sha256=uBiK4ot3V0b_LGXu
|
|
|
74
75
|
cognite/neat/_data_model/models/entities/_parser.py,sha256=zef_pSDZYMZrJl4IKreFDR577KutfhtN1xpH3Ayjt2o,7669
|
|
75
76
|
cognite/neat/_data_model/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
77
|
cognite/neat/_data_model/validation/dms/__init__.py,sha256=Ps26r8bY4sLPsceaS6CpgS-s6JxN_zKbiteF5RrQYYw,2512
|
|
77
|
-
cognite/neat/_data_model/validation/dms/_ai_readiness.py,sha256=
|
|
78
|
-
cognite/neat/_data_model/validation/dms/_base.py,sha256=
|
|
79
|
-
cognite/neat/_data_model/validation/dms/_connections.py,sha256=
|
|
80
|
-
cognite/neat/_data_model/validation/dms/_consistency.py,sha256=
|
|
81
|
-
cognite/neat/_data_model/validation/dms/_containers.py,sha256=
|
|
82
|
-
cognite/neat/_data_model/validation/dms/_limits.py,sha256=
|
|
83
|
-
cognite/neat/_data_model/validation/dms/_orchestrator.py,sha256=
|
|
84
|
-
cognite/neat/_data_model/validation/dms/_views.py,sha256=
|
|
78
|
+
cognite/neat/_data_model/validation/dms/_ai_readiness.py,sha256=yKT9KHS4iD7ijKBjjAGbREKWDGunBuDTl9FCX0RiW2Q,10717
|
|
79
|
+
cognite/neat/_data_model/validation/dms/_base.py,sha256=3Lqs8pzMQZznxhUr8esEe9H7F2wM3EKEl2H64GLS5OA,13344
|
|
80
|
+
cognite/neat/_data_model/validation/dms/_connections.py,sha256=6ea8WZNqYT9SPluwXvg0mgDJpzZMqpPC5xonbdLrT4E,27305
|
|
81
|
+
cognite/neat/_data_model/validation/dms/_consistency.py,sha256=uJ6coAVupD3WfeeXxoCIebM8WSnR78GXBXIBnN59aao,2477
|
|
82
|
+
cognite/neat/_data_model/validation/dms/_containers.py,sha256=UuvzrBBw45F5f9uzCh97lW4t7m7XIIT1I9FnzzYUYv4,7533
|
|
83
|
+
cognite/neat/_data_model/validation/dms/_limits.py,sha256=UDJ3oY2Xp96Zw2QpsGM4MQ6gZCrDuoKhhuEY2uQJLP4,16186
|
|
84
|
+
cognite/neat/_data_model/validation/dms/_orchestrator.py,sha256=0KpXMWlofpELCE3QXQh6SDyP3IoATPrR1412HJzAN6w,10811
|
|
85
|
+
cognite/neat/_data_model/validation/dms/_views.py,sha256=M4egIa7UAMGtZlqzIxx6ZzL4e_qo8GbDGh7vs9wywD8,4266
|
|
85
86
|
cognite/neat/_session/__init__.py,sha256=owqW5Mml2DSZx1AvPvwNRTBngfhBNrQ6EH-7CKL7Jp0,61
|
|
86
87
|
cognite/neat/_session/_issues.py,sha256=E8UQeSJURg2dm4MF1pfD9dp-heSRT7pgQZgKlD1-FGs,2723
|
|
87
88
|
cognite/neat/_session/_opt.py,sha256=QcVK08JMmVzJpD0GKHelbljMOQi6CMD1w-maQOlbyZQ,1350
|
|
88
|
-
cognite/neat/_session/_physical.py,sha256=
|
|
89
|
+
cognite/neat/_session/_physical.py,sha256=0EdpFS5v4pk4XJju8vZm3pHIM4AlSaCjxGRKeMAcq9g,11304
|
|
89
90
|
cognite/neat/_session/_result.py,sha256=po2X4s-Tioe0GQAGCfK862hKXNRX5YjJZsEzNcTC8nI,7879
|
|
90
|
-
cognite/neat/_session/_session.py,sha256=
|
|
91
|
+
cognite/neat/_session/_session.py,sha256=tWEvzi_85Ji52i2q5vJ7xIg3yR-8ywVCH8FjgncdlnU,3471
|
|
91
92
|
cognite/neat/_session/_wrappers.py,sha256=9t_MnJ0Sw_v-f6oTIh8dtAT-3oEbqumGuND97aPCC3M,3581
|
|
92
93
|
cognite/neat/_session/_html/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
93
94
|
cognite/neat/_session/_html/_render.py,sha256=fD8iee4ql50CrHGH41SSh9Tw1lM0tHt-NF0OnnxHosg,1193
|
|
@@ -190,7 +191,7 @@ cognite/neat/v0/core/_data_model/models/mapping/__init__.py,sha256=T68Hf7rhiXa7b
|
|
|
190
191
|
cognite/neat/v0/core/_data_model/models/mapping/_classic2core.py,sha256=F0zusTh9pPR4z-RExPw3o4EMBSU2si6FJLuej2a3JzM,1430
|
|
191
192
|
cognite/neat/v0/core/_data_model/models/mapping/_classic2core.yaml,sha256=ei-nuivNWVW9HmvzDBKIPF6ZdgaMq64XHw_rKm0CMxg,22584
|
|
192
193
|
cognite/neat/v0/core/_data_model/models/physical/__init__.py,sha256=pH5ZF8jiW0A2w7VCSoHUsXxe894QFvTtgjxXNGVVaxk,990
|
|
193
|
-
cognite/neat/v0/core/_data_model/models/physical/_exporter.py,sha256=
|
|
194
|
+
cognite/neat/v0/core/_data_model/models/physical/_exporter.py,sha256=slECWjHinQscoR86dDmvB4OalyCvE9D_bSKp4v_GINg,30555
|
|
194
195
|
cognite/neat/v0/core/_data_model/models/physical/_unverified.py,sha256=eE2D2DPfqEfOnYaUmOTPhBpHp9oN-gxjn8CTe_GKtz0,22419
|
|
195
196
|
cognite/neat/v0/core/_data_model/models/physical/_validation.py,sha256=g0uSZImYaGmMZEwHUpb8KF9LTPiztz7k7cFQT3jrs4Y,41326
|
|
196
197
|
cognite/neat/v0/core/_data_model/models/physical/_verified.py,sha256=49dn3K_gWO3VgfS-SK4kHpbSJywtj4mwES3iy-43z4M,29824
|
|
@@ -312,7 +313,7 @@ cognite/neat/v0/session/engine/__init__.py,sha256=D3MxUorEs6-NtgoICqtZ8PISQrjrr4
|
|
|
312
313
|
cognite/neat/v0/session/engine/_import.py,sha256=1QxA2_EK613lXYAHKQbZyw2yjo5P9XuiX4Z6_6-WMNQ,169
|
|
313
314
|
cognite/neat/v0/session/engine/_interface.py,sha256=3W-cYr493c_mW3P5O6MKN1xEQg3cA7NHR_ev3zdF9Vk,533
|
|
314
315
|
cognite/neat/v0/session/engine/_load.py,sha256=u0x7vuQCRoNcPt25KJBJRn8sJabonYK4vtSZpiTdP4k,5201
|
|
315
|
-
cognite_neat-0.127.
|
|
316
|
-
cognite_neat-0.127.
|
|
317
|
-
cognite_neat-0.127.
|
|
318
|
-
cognite_neat-0.127.
|
|
316
|
+
cognite_neat-0.127.29.dist-info/METADATA,sha256=BODhl8sBHt2YtVcTIr7NK7Pcew1A68onQGB63HaKYDo,9150
|
|
317
|
+
cognite_neat-0.127.29.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
318
|
+
cognite_neat-0.127.29.dist-info/licenses/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
|
|
319
|
+
cognite_neat-0.127.29.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|