cognite-neat 0.88.1__py3-none-any.whl → 0.88.3__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/_version.py +1 -1
- cognite/neat/graph/__init__.py +0 -3
- cognite/neat/graph/loaders/_base.py +6 -6
- cognite/neat/graph/loaders/_rdf2asset.py +28 -31
- cognite/neat/graph/loaders/_rdf2dms.py +24 -15
- cognite/neat/issues/__init__.py +14 -0
- cognite/neat/issues/_base.py +415 -0
- cognite/neat/issues/errors/__init__.py +72 -0
- cognite/neat/issues/errors/_external.py +67 -0
- cognite/neat/issues/errors/_general.py +28 -0
- cognite/neat/issues/errors/_properties.py +62 -0
- cognite/neat/issues/errors/_resources.py +111 -0
- cognite/neat/issues/errors/_workflow.py +36 -0
- cognite/neat/{rules/issues → issues}/formatters.py +10 -10
- cognite/neat/issues/warnings/__init__.py +66 -0
- cognite/neat/issues/warnings/_external.py +40 -0
- cognite/neat/issues/warnings/_general.py +29 -0
- cognite/neat/issues/warnings/_models.py +92 -0
- cognite/neat/issues/warnings/_properties.py +44 -0
- cognite/neat/issues/warnings/_resources.py +55 -0
- cognite/neat/issues/warnings/user_modeling.py +113 -0
- cognite/neat/rules/_shared.py +10 -2
- cognite/neat/rules/exporters/_base.py +6 -6
- cognite/neat/rules/exporters/_rules2dms.py +19 -11
- cognite/neat/rules/exporters/_rules2excel.py +4 -4
- cognite/neat/rules/exporters/_rules2ontology.py +74 -51
- cognite/neat/rules/exporters/_rules2yaml.py +3 -3
- cognite/neat/rules/exporters/_validation.py +11 -96
- cognite/neat/rules/importers/__init__.py +7 -3
- cognite/neat/rules/importers/_base.py +9 -13
- cognite/neat/rules/importers/_dms2rules.py +42 -24
- cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +49 -53
- cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +31 -23
- cognite/neat/rules/importers/_dtdl2rules/spec.py +7 -0
- cognite/neat/rules/importers/_rdf/_imf2rules/__init__.py +3 -0
- cognite/neat/rules/importers/_rdf/_imf2rules/_imf2classes.py +82 -0
- cognite/neat/rules/importers/_rdf/_imf2rules/_imf2metadata.py +34 -0
- cognite/neat/rules/importers/_rdf/_imf2rules/_imf2properties.py +123 -0
- cognite/neat/rules/importers/{_owl2rules/_owl2rules.py → _rdf/_imf2rules/_imf2rules.py} +24 -18
- cognite/neat/rules/importers/{_inference2rules.py → _rdf/_inference2rules.py} +9 -9
- cognite/neat/rules/importers/_rdf/_owl2rules/_owl2classes.py +58 -0
- cognite/neat/rules/importers/_rdf/_owl2rules/_owl2metadata.py +68 -0
- cognite/neat/rules/importers/_rdf/_owl2rules/_owl2properties.py +60 -0
- cognite/neat/rules/importers/_rdf/_owl2rules/_owl2rules.py +76 -0
- cognite/neat/rules/importers/_rdf/_shared.py +586 -0
- cognite/neat/rules/importers/_spreadsheet2rules.py +35 -22
- cognite/neat/rules/importers/_yaml2rules.py +23 -21
- cognite/neat/rules/models/_constants.py +2 -1
- cognite/neat/rules/models/_rdfpath.py +4 -4
- cognite/neat/rules/models/_types/_field.py +9 -11
- cognite/neat/rules/models/asset/_rules.py +1 -3
- cognite/neat/rules/models/asset/_validation.py +14 -10
- cognite/neat/rules/models/dms/_converter.py +2 -4
- cognite/neat/rules/models/dms/_exporter.py +30 -8
- cognite/neat/rules/models/dms/_rules.py +23 -7
- cognite/neat/rules/models/dms/_schema.py +94 -62
- cognite/neat/rules/models/dms/_validation.py +105 -66
- cognite/neat/rules/models/entities.py +3 -0
- cognite/neat/rules/models/information/_converter.py +2 -2
- cognite/neat/rules/models/information/_rules.py +7 -8
- cognite/neat/rules/models/information/_validation.py +48 -25
- cognite/neat/rules/transformers/__init__.py +0 -0
- cognite/neat/rules/transformers/_base.py +15 -0
- cognite/neat/utils/auxiliary.py +2 -35
- cognite/neat/utils/text.py +17 -0
- cognite/neat/workflows/base.py +4 -4
- cognite/neat/workflows/cdf_store.py +3 -3
- cognite/neat/workflows/steps/data_contracts.py +1 -1
- cognite/neat/workflows/steps/lib/current/graph_extractor.py +3 -3
- cognite/neat/workflows/steps/lib/current/graph_loader.py +2 -2
- cognite/neat/workflows/steps/lib/current/graph_store.py +1 -1
- cognite/neat/workflows/steps/lib/current/rules_exporter.py +10 -10
- cognite/neat/workflows/steps/lib/current/rules_importer.py +78 -6
- cognite/neat/workflows/steps/lib/current/rules_validator.py +20 -9
- cognite/neat/workflows/steps/lib/io/io_steps.py +5 -5
- cognite/neat/workflows/steps_registry.py +4 -5
- {cognite_neat-0.88.1.dist-info → cognite_neat-0.88.3.dist-info}/METADATA +1 -1
- {cognite_neat-0.88.1.dist-info → cognite_neat-0.88.3.dist-info}/RECORD +86 -77
- cognite/neat/exceptions.py +0 -145
- cognite/neat/graph/exceptions.py +0 -90
- cognite/neat/graph/issues/loader.py +0 -104
- cognite/neat/issues.py +0 -158
- cognite/neat/rules/importers/_owl2rules/_owl2classes.py +0 -215
- cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +0 -209
- cognite/neat/rules/importers/_owl2rules/_owl2properties.py +0 -203
- cognite/neat/rules/issues/__init__.py +0 -26
- cognite/neat/rules/issues/base.py +0 -82
- cognite/neat/rules/issues/dms.py +0 -683
- cognite/neat/rules/issues/fileread.py +0 -197
- cognite/neat/rules/issues/importing.py +0 -423
- cognite/neat/rules/issues/ontology.py +0 -298
- cognite/neat/rules/issues/spreadsheet.py +0 -563
- cognite/neat/rules/issues/spreadsheet_file.py +0 -151
- cognite/neat/rules/issues/tables.py +0 -72
- cognite/neat/workflows/_exceptions.py +0 -41
- /cognite/neat/{graph/issues → rules/importers/_rdf}/__init__.py +0 -0
- /cognite/neat/rules/importers/{_owl2rules → _rdf/_owl2rules}/__init__.py +0 -0
- /cognite/neat/{graph/stores → store}/__init__.py +0 -0
- /cognite/neat/{graph/stores → store}/_base.py +0 -0
- /cognite/neat/{graph/stores → store}/_provenance.py +0 -0
- {cognite_neat-0.88.1.dist-info → cognite_neat-0.88.3.dist-info}/LICENSE +0 -0
- {cognite_neat-0.88.1.dist-info → cognite_neat-0.88.3.dist-info}/WHEEL +0 -0
- {cognite_neat-0.88.1.dist-info → cognite_neat-0.88.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import warnings
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from collections import UserList
|
|
5
|
+
from collections.abc import Collection, Hashable, Iterable, Sequence
|
|
6
|
+
from dataclasses import dataclass, fields
|
|
7
|
+
from functools import total_ordering
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import UnionType
|
|
10
|
+
from typing import Any, ClassVar, Literal, TypeAlias, TypeVar, get_args, get_origin
|
|
11
|
+
from warnings import WarningMessage
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from cognite.client.data_classes.data_modeling import ContainerId, ViewId
|
|
15
|
+
from pydantic_core import ErrorDetails
|
|
16
|
+
|
|
17
|
+
from cognite.neat.utils.spreadsheet import SpreadsheetRead
|
|
18
|
+
from cognite.neat.utils.text import humanize_collection, to_camel, to_snake
|
|
19
|
+
|
|
20
|
+
if sys.version_info < (3, 11):
|
|
21
|
+
from exceptiongroup import ExceptionGroup
|
|
22
|
+
from typing_extensions import Self
|
|
23
|
+
else:
|
|
24
|
+
from typing import Self
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"NeatIssue",
|
|
29
|
+
"NeatError",
|
|
30
|
+
"NeatWarning",
|
|
31
|
+
"DefaultWarning",
|
|
32
|
+
"NeatIssueList",
|
|
33
|
+
"MultiValueError",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
T_Identifier = TypeVar("T_Identifier", bound=Hashable)
|
|
37
|
+
|
|
38
|
+
T_ReferenceIdentifier = TypeVar("T_ReferenceIdentifier", bound=Hashable)
|
|
39
|
+
|
|
40
|
+
ResourceType: TypeAlias = (
|
|
41
|
+
Literal[
|
|
42
|
+
"view",
|
|
43
|
+
"container",
|
|
44
|
+
"view property",
|
|
45
|
+
"container property",
|
|
46
|
+
"space",
|
|
47
|
+
"class",
|
|
48
|
+
"asset",
|
|
49
|
+
"relationship",
|
|
50
|
+
"data model",
|
|
51
|
+
"edge",
|
|
52
|
+
"node",
|
|
53
|
+
"unknown",
|
|
54
|
+
]
|
|
55
|
+
# String to handle all unknown types in different importers.
|
|
56
|
+
| str
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@total_ordering
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class NeatIssue:
|
|
63
|
+
"""This is the base class for all exceptions and warnings (issues) used in Neat."""
|
|
64
|
+
|
|
65
|
+
extra: ClassVar[str | None] = None
|
|
66
|
+
fix: ClassVar[str | None] = None
|
|
67
|
+
|
|
68
|
+
def as_message(self) -> str:
|
|
69
|
+
"""Return a human-readable message for the issue."""
|
|
70
|
+
template = self.__doc__
|
|
71
|
+
if not template:
|
|
72
|
+
return "Missing"
|
|
73
|
+
variables, has_all_optional = self._get_variables()
|
|
74
|
+
|
|
75
|
+
msg = template.format(**variables)
|
|
76
|
+
if self.extra and has_all_optional:
|
|
77
|
+
msg += "\n" + self.extra.format(**variables)
|
|
78
|
+
if self.fix:
|
|
79
|
+
msg += f"\nFix: {self.fix.format(**variables)}"
|
|
80
|
+
name = type(self).__name__
|
|
81
|
+
return f"{name}: {msg}"
|
|
82
|
+
|
|
83
|
+
def _get_variables(self) -> tuple[dict[str, str], bool]:
|
|
84
|
+
variables: dict[str, str] = {}
|
|
85
|
+
has_all_optional = True
|
|
86
|
+
for name, var_ in vars(self).items():
|
|
87
|
+
if var_ is None:
|
|
88
|
+
has_all_optional = False
|
|
89
|
+
elif isinstance(var_, str):
|
|
90
|
+
variables[name] = var_
|
|
91
|
+
elif isinstance(var_, Path):
|
|
92
|
+
variables[name] = var_.as_posix()
|
|
93
|
+
elif isinstance(var_, Collection):
|
|
94
|
+
variables[name] = humanize_collection(var_)
|
|
95
|
+
else:
|
|
96
|
+
variables[name] = repr(var_)
|
|
97
|
+
return variables, has_all_optional
|
|
98
|
+
|
|
99
|
+
def dump(self) -> dict[str, Any]:
|
|
100
|
+
"""Return a dictionary representation of the issue."""
|
|
101
|
+
variables = vars(self)
|
|
102
|
+
output = {to_camel(key): self._dump_value(value) for key, value in variables.items() if value is not None}
|
|
103
|
+
output["NeatIssue"] = type(self).__name__
|
|
104
|
+
return output
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def _dump_value(cls, value: Any) -> list | int | bool | float | str | dict:
|
|
108
|
+
if isinstance(value, str | int | bool | float):
|
|
109
|
+
return value
|
|
110
|
+
elif isinstance(value, frozenset):
|
|
111
|
+
return [cls._dump_value(item) for item in value]
|
|
112
|
+
elif isinstance(value, Path):
|
|
113
|
+
return value.as_posix()
|
|
114
|
+
elif isinstance(value, tuple):
|
|
115
|
+
return [cls._dump_value(item) for item in value]
|
|
116
|
+
elif isinstance(value, ViewId | ContainerId):
|
|
117
|
+
return value.dump(camel_case=True, include_type=True)
|
|
118
|
+
raise ValueError(f"Unsupported type: {type(value)}")
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def load(cls, data: dict[str, Any]) -> "NeatIssue":
|
|
122
|
+
"""Create an instance of the issue from a dictionary."""
|
|
123
|
+
from cognite.neat.issues.errors import _NEAT_ERRORS_BY_NAME, NeatValueError
|
|
124
|
+
from cognite.neat.issues.warnings import _NEAT_WARNINGS_BY_NAME
|
|
125
|
+
|
|
126
|
+
if "NeatIssue" not in data:
|
|
127
|
+
raise NeatValueError("The data does not contain a NeatIssue key.")
|
|
128
|
+
issue_type = data.pop("NeatIssue")
|
|
129
|
+
args = {to_snake(key): value for key, value in data.items()}
|
|
130
|
+
if issue_type in _NEAT_ERRORS_BY_NAME:
|
|
131
|
+
return cls._load_values(_NEAT_ERRORS_BY_NAME[issue_type], args)
|
|
132
|
+
elif issue_type in _NEAT_WARNINGS_BY_NAME:
|
|
133
|
+
return cls._load_values(_NEAT_WARNINGS_BY_NAME[issue_type], args)
|
|
134
|
+
else:
|
|
135
|
+
raise NeatValueError(f"Unknown issue type: {issue_type}")
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def _load_values(cls, neat_issue_cls: "type[NeatIssue]", data: dict[str, Any]) -> "NeatIssue":
|
|
139
|
+
args: dict[str, Any] = {}
|
|
140
|
+
for f in fields(neat_issue_cls):
|
|
141
|
+
if f.name not in data:
|
|
142
|
+
continue
|
|
143
|
+
value = data[f.name]
|
|
144
|
+
args[f.name] = cls._load_value(f.type, value)
|
|
145
|
+
return neat_issue_cls(**args)
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def _load_value(cls, type_: type, value: Any) -> Any:
|
|
149
|
+
if isinstance(type_, UnionType) or get_origin(type_) is UnionType:
|
|
150
|
+
args = get_args(type_)
|
|
151
|
+
return cls._load_value(args[0], value)
|
|
152
|
+
elif type_ is frozenset or get_origin(type_) is frozenset:
|
|
153
|
+
subtype = get_args(type_)[0]
|
|
154
|
+
return frozenset(cls._load_value(subtype, item) for item in value)
|
|
155
|
+
elif type_ is Path:
|
|
156
|
+
return Path(value)
|
|
157
|
+
elif type_ is tuple or get_origin(type_) is tuple:
|
|
158
|
+
subtype = get_args(type_)[0]
|
|
159
|
+
return tuple(cls._load_value(subtype, item) for item in value)
|
|
160
|
+
elif type_ is ViewId:
|
|
161
|
+
return ViewId.load(value)
|
|
162
|
+
elif type_ is ContainerId:
|
|
163
|
+
return ContainerId.load(value)
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
def __lt__(self, other: "NeatIssue") -> bool:
|
|
167
|
+
if not isinstance(other, NeatIssue):
|
|
168
|
+
return NotImplemented
|
|
169
|
+
return (type(self).__name__, self.as_message()) < (type(other).__name__, other.as_message())
|
|
170
|
+
|
|
171
|
+
def __eq__(self, other: object) -> bool:
|
|
172
|
+
if not isinstance(other, NeatIssue):
|
|
173
|
+
return NotImplemented
|
|
174
|
+
return (type(self).__name__, self.as_message()) == (type(other).__name__, other.as_message())
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class NeatError(NeatIssue, Exception):
|
|
179
|
+
"""This is the base class for all exceptions (errors) used in Neat."""
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def from_pydantic_errors(cls, errors: list[ErrorDetails], **kwargs) -> "list[NeatError]":
|
|
183
|
+
"""Convert a list of pydantic errors to a list of Error instances.
|
|
184
|
+
|
|
185
|
+
This is intended to be overridden in subclasses to handle specific error types.
|
|
186
|
+
"""
|
|
187
|
+
all_errors: list[NeatError] = []
|
|
188
|
+
read_info_by_sheet = kwargs.get("read_info_by_sheet")
|
|
189
|
+
|
|
190
|
+
for error in errors:
|
|
191
|
+
ctx = error.get("ctx")
|
|
192
|
+
if isinstance(ctx, dict) and isinstance(multi_error := ctx.get("error"), MultiValueError):
|
|
193
|
+
if read_info_by_sheet:
|
|
194
|
+
for caught_error in multi_error.errors:
|
|
195
|
+
cls._adjust_row_numbers(caught_error, read_info_by_sheet) # type: ignore[arg-type]
|
|
196
|
+
all_errors.extend(multi_error.errors) # type: ignore[arg-type]
|
|
197
|
+
elif isinstance(ctx, dict) and isinstance(single_error := ctx.get("error"), NeatError):
|
|
198
|
+
if read_info_by_sheet:
|
|
199
|
+
cls._adjust_row_numbers(single_error, read_info_by_sheet)
|
|
200
|
+
all_errors.append(single_error)
|
|
201
|
+
elif len(error["loc"]) >= 4 and read_info_by_sheet:
|
|
202
|
+
all_errors.append(RowError.from_pydantic_error(error, read_info_by_sheet))
|
|
203
|
+
else:
|
|
204
|
+
all_errors.append(DefaultPydanticError.from_pydantic_error(error))
|
|
205
|
+
return all_errors
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def _adjust_row_numbers(caught_error: "NeatError", read_info_by_sheet: dict[str, SpreadsheetRead]) -> None:
|
|
209
|
+
from cognite.neat.issues.errors._properties import PropertyDefinitionDuplicatedError
|
|
210
|
+
from cognite.neat.issues.errors._resources import ResourceNotDefinedError
|
|
211
|
+
|
|
212
|
+
reader = read_info_by_sheet.get("Properties", SpreadsheetRead())
|
|
213
|
+
|
|
214
|
+
if isinstance(caught_error, PropertyDefinitionDuplicatedError) and caught_error.location_name == "rows":
|
|
215
|
+
adjusted_row_number = (
|
|
216
|
+
tuple(
|
|
217
|
+
reader.adjusted_row_number(row_no) if isinstance(row_no, int) else row_no
|
|
218
|
+
for row_no in caught_error.locations or []
|
|
219
|
+
)
|
|
220
|
+
or None
|
|
221
|
+
)
|
|
222
|
+
# The error is frozen, so we have to use __setattr__ to change the row number
|
|
223
|
+
object.__setattr__(caught_error, "locations", adjusted_row_number)
|
|
224
|
+
elif isinstance(caught_error, RowError):
|
|
225
|
+
# Adjusting the row number to the actual row number in the spreadsheet
|
|
226
|
+
new_row = reader.adjusted_row_number(caught_error.row)
|
|
227
|
+
# The error is frozen, so we have to use __setattr__ to change the row number
|
|
228
|
+
object.__setattr__(caught_error, "row", new_row)
|
|
229
|
+
elif isinstance(caught_error, ResourceNotDefinedError):
|
|
230
|
+
if isinstance(caught_error.row_number, int) and caught_error.sheet_name == "Properties":
|
|
231
|
+
new_row = reader.adjusted_row_number(caught_error.row_number)
|
|
232
|
+
object.__setattr__(caught_error, "row_number", new_row)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True)
|
|
236
|
+
class DefaultPydanticError(NeatError, ValueError):
|
|
237
|
+
"""{type}: {msg} [loc={loc}]"""
|
|
238
|
+
|
|
239
|
+
type: str
|
|
240
|
+
loc: tuple[int | str, ...]
|
|
241
|
+
msg: str
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def from_pydantic_error(cls, error: ErrorDetails) -> "DefaultPydanticError":
|
|
245
|
+
return cls(
|
|
246
|
+
type=error["type"],
|
|
247
|
+
loc=error["loc"],
|
|
248
|
+
msg=error["msg"],
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def as_message(self) -> str:
|
|
252
|
+
if self.loc and len(self.loc) == 1:
|
|
253
|
+
return f"{self.loc[0]} sheet: {self.msg}"
|
|
254
|
+
elif self.loc and len(self.loc) == 2:
|
|
255
|
+
return f"{self.loc[0]} sheet field/column <{self.loc[1]}>: {self.msg}"
|
|
256
|
+
else:
|
|
257
|
+
return self.msg
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass(frozen=True)
|
|
261
|
+
class RowError(NeatError, ValueError):
|
|
262
|
+
"""In {sheet_name}, row={row}, column={column}: {msg}. [type={type}, input_value={input}]"""
|
|
263
|
+
|
|
264
|
+
extra = "For further information visit {url}"
|
|
265
|
+
|
|
266
|
+
sheet_name: str
|
|
267
|
+
column: str
|
|
268
|
+
row: int
|
|
269
|
+
type: str
|
|
270
|
+
msg: str
|
|
271
|
+
input: Any
|
|
272
|
+
url: str | None = None
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def from_pydantic_error(
|
|
276
|
+
cls,
|
|
277
|
+
error: ErrorDetails,
|
|
278
|
+
read_info_by_sheet: dict[str, SpreadsheetRead] | None = None,
|
|
279
|
+
) -> Self:
|
|
280
|
+
sheet_name, _, row, column, *__ = error["loc"]
|
|
281
|
+
reader = (read_info_by_sheet or {}).get(str(sheet_name), SpreadsheetRead())
|
|
282
|
+
return cls(
|
|
283
|
+
sheet_name=str(sheet_name),
|
|
284
|
+
column=str(column),
|
|
285
|
+
row=reader.adjusted_row_number(int(row)),
|
|
286
|
+
type=error["type"],
|
|
287
|
+
msg=error["msg"],
|
|
288
|
+
input=error.get("input"),
|
|
289
|
+
url=str(url) if (url := error.get("url")) else None,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def as_message(self) -> str:
|
|
293
|
+
input_str = str(self.input) if self.input is not None else ""
|
|
294
|
+
input_str = input_str[:50] + "..." if len(input_str) > 50 else input_str
|
|
295
|
+
output = (
|
|
296
|
+
f"In {self.sheet_name}, row={self.row}, column={self.column}: {self.msg}. "
|
|
297
|
+
f"[type={self.type}, input_value={input_str}]"
|
|
298
|
+
)
|
|
299
|
+
if self.url:
|
|
300
|
+
output += f" For further information visit {self.url}"
|
|
301
|
+
return output
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass(frozen=True)
|
|
305
|
+
class NeatWarning(NeatIssue, UserWarning):
|
|
306
|
+
"""This is the base class for all warnings used in Neat."""
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def from_warning(cls, warning: WarningMessage) -> "NeatWarning":
|
|
310
|
+
"""Create a NeatWarning from a WarningMessage."""
|
|
311
|
+
return DefaultWarning.from_warning_message(warning)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass(frozen=True)
|
|
315
|
+
class DefaultWarning(NeatWarning):
|
|
316
|
+
"""{category}: {warning}"""
|
|
317
|
+
|
|
318
|
+
extra = "Source: {source}"
|
|
319
|
+
|
|
320
|
+
warning: str
|
|
321
|
+
category: str
|
|
322
|
+
source: str | None = None
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def from_warning_message(cls, warning: WarningMessage) -> NeatWarning:
|
|
326
|
+
if isinstance(warning.message, NeatWarning):
|
|
327
|
+
return warning.message
|
|
328
|
+
|
|
329
|
+
return cls(
|
|
330
|
+
warning=str(warning.message),
|
|
331
|
+
category=warning.category.__name__,
|
|
332
|
+
source=warning.source,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def as_message(self) -> str:
|
|
336
|
+
return str(self.warning)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
T_NeatIssue = TypeVar("T_NeatIssue", bound=NeatIssue)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class NeatIssueList(UserList[T_NeatIssue], ABC):
|
|
343
|
+
"""This is a generic list of NeatIssues."""
|
|
344
|
+
|
|
345
|
+
def __init__(self, issues: Sequence[T_NeatIssue] | None = None, title: str | None = None):
|
|
346
|
+
super().__init__(issues or [])
|
|
347
|
+
self.title = title
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def errors(self) -> Self:
|
|
351
|
+
"""Return all the errors in this list."""
|
|
352
|
+
return type(self)([issue for issue in self if isinstance(issue, NeatError)]) # type: ignore[misc]
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def has_errors(self) -> bool:
|
|
356
|
+
"""Return True if this list contains any errors."""
|
|
357
|
+
return any(isinstance(issue, NeatError) for issue in self)
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def warnings(self) -> Self:
|
|
361
|
+
"""Return all the warnings in this list."""
|
|
362
|
+
return type(self)([issue for issue in self if isinstance(issue, NeatWarning)]) # type: ignore[misc]
|
|
363
|
+
|
|
364
|
+
def as_errors(self) -> ExceptionGroup:
|
|
365
|
+
"""Return an ExceptionGroup with all the errors in this list."""
|
|
366
|
+
return ExceptionGroup(
|
|
367
|
+
"Operation failed",
|
|
368
|
+
[issue for issue in self if isinstance(issue, NeatError)],
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def trigger_warnings(self) -> None:
|
|
372
|
+
"""Trigger all warnings in this list."""
|
|
373
|
+
for warning in [issue for issue in self if isinstance(issue, NeatWarning)]:
|
|
374
|
+
warnings.warn(warning, stacklevel=2)
|
|
375
|
+
|
|
376
|
+
def to_pandas(self) -> pd.DataFrame:
|
|
377
|
+
"""Return a pandas DataFrame representation of this list."""
|
|
378
|
+
return pd.DataFrame([issue.dump() for issue in self])
|
|
379
|
+
|
|
380
|
+
def _repr_html_(self) -> str | None:
|
|
381
|
+
return self.to_pandas()._repr_html_() # type: ignore[operator]
|
|
382
|
+
|
|
383
|
+
def as_exception(self) -> "MultiValueError":
|
|
384
|
+
"""Return a MultiValueError with all the errors in this list."""
|
|
385
|
+
return MultiValueError(self.errors)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class MultiValueError(ValueError):
|
|
389
|
+
"""This is a container for multiple errors.
|
|
390
|
+
|
|
391
|
+
It is used in the pydantic field_validator/model_validator to collect multiple errors, which
|
|
392
|
+
can then be caught in a try-except block and returned as an IssueList.
|
|
393
|
+
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
def __init__(self, errors: Sequence[NeatIssue]):
|
|
397
|
+
self.errors = list(errors)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class IssueList(NeatIssueList[NeatIssue]):
|
|
401
|
+
"""This is a list of NeatIssues."""
|
|
402
|
+
|
|
403
|
+
...
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
T_Cls = TypeVar("T_Cls")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _get_subclasses(cls_: type[T_Cls], include_base: bool = False) -> Iterable[type[T_Cls]]:
|
|
410
|
+
"""Get all subclasses of a class."""
|
|
411
|
+
if include_base:
|
|
412
|
+
yield cls_
|
|
413
|
+
for s in cls_.__subclasses__():
|
|
414
|
+
yield s
|
|
415
|
+
yield from _get_subclasses(s, False)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from cognite.neat.issues._base import DefaultPydanticError, NeatError, RowError, _get_subclasses
|
|
2
|
+
|
|
3
|
+
from ._external import (
|
|
4
|
+
AuthorizationError,
|
|
5
|
+
FileMissingRequiredFieldError,
|
|
6
|
+
FileNotAFileError,
|
|
7
|
+
FileNotFoundNeatError,
|
|
8
|
+
FileReadError,
|
|
9
|
+
FileTypeUnexpectedError,
|
|
10
|
+
NeatYamlError,
|
|
11
|
+
)
|
|
12
|
+
from ._general import NeatImportError, NeatValueError, RegexViolationError
|
|
13
|
+
from ._properties import (
|
|
14
|
+
PropertyDefinitionDuplicatedError,
|
|
15
|
+
PropertyDefinitionError,
|
|
16
|
+
PropertyMappingDuplicatedError,
|
|
17
|
+
PropertyNotFoundError,
|
|
18
|
+
PropertyTypeNotSupportedError,
|
|
19
|
+
)
|
|
20
|
+
from ._resources import (
|
|
21
|
+
ResourceChangedError,
|
|
22
|
+
ResourceConvertionError,
|
|
23
|
+
ResourceCreationError,
|
|
24
|
+
ResourceDuplicatedError,
|
|
25
|
+
ResourceError,
|
|
26
|
+
ResourceMissingIdentifierError,
|
|
27
|
+
ResourceNotDefinedError,
|
|
28
|
+
ResourceNotFoundError,
|
|
29
|
+
ResourceRetrievalError,
|
|
30
|
+
)
|
|
31
|
+
from ._workflow import (
|
|
32
|
+
WorkflowConfigurationNotSetError,
|
|
33
|
+
WorkFlowMissingDataError,
|
|
34
|
+
WorkflowStepNotInitializedError,
|
|
35
|
+
WorkflowStepOutputError,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"NeatError",
|
|
40
|
+
"NeatValueError",
|
|
41
|
+
"NeatImportError",
|
|
42
|
+
"RegexViolationError",
|
|
43
|
+
"AuthorizationError",
|
|
44
|
+
"NeatYamlError",
|
|
45
|
+
"FileReadError",
|
|
46
|
+
"ResourceCreationError",
|
|
47
|
+
"FileNotFoundNeatError",
|
|
48
|
+
"FileMissingRequiredFieldError",
|
|
49
|
+
"PropertyDefinitionError",
|
|
50
|
+
"PropertyTypeNotSupportedError",
|
|
51
|
+
"PropertyNotFoundError",
|
|
52
|
+
"PropertyDefinitionDuplicatedError",
|
|
53
|
+
"ResourceChangedError",
|
|
54
|
+
"ResourceDuplicatedError",
|
|
55
|
+
"ResourceRetrievalError",
|
|
56
|
+
"ResourceNotFoundError",
|
|
57
|
+
"ResourceError",
|
|
58
|
+
"ResourceNotDefinedError",
|
|
59
|
+
"ResourceMissingIdentifierError",
|
|
60
|
+
"ResourceConvertionError",
|
|
61
|
+
"WorkflowConfigurationNotSetError",
|
|
62
|
+
"WorkFlowMissingDataError",
|
|
63
|
+
"WorkflowStepNotInitializedError",
|
|
64
|
+
"WorkflowStepOutputError",
|
|
65
|
+
"FileTypeUnexpectedError",
|
|
66
|
+
"FileNotAFileError",
|
|
67
|
+
"DefaultPydanticError",
|
|
68
|
+
"PropertyMappingDuplicatedError",
|
|
69
|
+
"RowError",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
_NEAT_ERRORS_BY_NAME = {error.__name__: error for error in _get_subclasses(NeatError, include_base=True)}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from yaml import YAMLError
|
|
5
|
+
|
|
6
|
+
from cognite.neat.issues import NeatError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class AuthorizationError(NeatError, RuntimeError):
|
|
11
|
+
"""Missing authorization for {action}: {reason}"""
|
|
12
|
+
|
|
13
|
+
action: str
|
|
14
|
+
reason: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class FileReadError(NeatError, RuntimeError):
|
|
19
|
+
"""Error when reading file, {filepath}: {reason}"""
|
|
20
|
+
|
|
21
|
+
fix = "Is the {filepath} open in another program? Is the file corrupted?"
|
|
22
|
+
filepath: Path
|
|
23
|
+
reason: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class FileNotFoundNeatError(NeatError, FileNotFoundError):
|
|
28
|
+
"""File {filepath} not found"""
|
|
29
|
+
|
|
30
|
+
fix = "Make sure to provide a valid file"
|
|
31
|
+
filepath: Path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class FileMissingRequiredFieldError(NeatError, ValueError):
|
|
36
|
+
"""Missing required {field_name} in {filepath}: {field}"""
|
|
37
|
+
|
|
38
|
+
filepath: Path
|
|
39
|
+
field_name: str
|
|
40
|
+
field: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class NeatYamlError(NeatError, YAMLError):
|
|
45
|
+
"""Invalid YAML: {reason}"""
|
|
46
|
+
|
|
47
|
+
extra = "Expected format: {expected_format}"
|
|
48
|
+
fix = "Check if the file is a valid YAML file"
|
|
49
|
+
|
|
50
|
+
reason: str
|
|
51
|
+
expected_format: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class FileTypeUnexpectedError(NeatError, TypeError):
|
|
56
|
+
"""Unexpected file type: {filepath}. Expected format: {expected_format}"""
|
|
57
|
+
|
|
58
|
+
filepath: Path
|
|
59
|
+
expected_format: frozenset[str]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class FileNotAFileError(NeatError, FileNotFoundError):
|
|
64
|
+
"""{filepath} is not a file"""
|
|
65
|
+
|
|
66
|
+
fix = "Make sure to provide a valid file"
|
|
67
|
+
filepath: Path
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from cognite.neat.issues import NeatError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class NeatValueError(NeatError, ValueError):
|
|
8
|
+
"""{raw_message}"""
|
|
9
|
+
|
|
10
|
+
raw_message: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class RegexViolationError(NeatError, ValueError):
|
|
15
|
+
"""Value, {value} failed regex, {regex}, validation. Make sure that the name follows the regex pattern."""
|
|
16
|
+
|
|
17
|
+
value: str
|
|
18
|
+
regex: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class NeatImportError(NeatError, ImportError):
|
|
23
|
+
"""The functionality requires {module}. You can include it
|
|
24
|
+
in your neat installation with `pip install "cognite-neat[{neat_extra}]"`.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
module: str
|
|
28
|
+
neat_extra: str
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Generic
|
|
3
|
+
|
|
4
|
+
from cognite.neat.issues._base import ResourceType
|
|
5
|
+
|
|
6
|
+
from ._resources import ResourceError, T_Identifier, T_ReferenceIdentifier
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class PropertyError(ResourceError[T_Identifier]):
|
|
11
|
+
"""Base class for property errors {resource_type} with identifier {identifier}.{property_name}"""
|
|
12
|
+
|
|
13
|
+
property_name: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PropertyNotFoundError(PropertyError, Generic[T_Identifier, T_ReferenceIdentifier]):
|
|
18
|
+
"""The {resource_type} with identifier {identifier} does not have a property {property_name}"""
|
|
19
|
+
|
|
20
|
+
extra = "referred to by {referred_type} {referred_by} does not exist"
|
|
21
|
+
fix = "Ensure the {resource_type} {identifier} has a {property_name} property"
|
|
22
|
+
|
|
23
|
+
referred_by: T_ReferenceIdentifier | None = None
|
|
24
|
+
referred_type: ResourceType | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class PropertyTypeNotSupportedError(PropertyError[T_Identifier]):
|
|
29
|
+
"""The {resource_type} with identifier {identifier} has a property {property_name}
|
|
30
|
+
of unsupported type {property_type}"""
|
|
31
|
+
|
|
32
|
+
property_type: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# This is a generic error that should be used sparingly
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class PropertyDefinitionError(PropertyError[T_Identifier]):
|
|
38
|
+
"""Invalid property definition for {resource_type} {identifier}.{property_name}: {reason}"""
|
|
39
|
+
|
|
40
|
+
reason: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class PropertyDefinitionDuplicatedError(PropertyError[T_Identifier]):
|
|
45
|
+
"""The {resource_type} with identifier {identifier} has multiple definitions for the property {property_name}
|
|
46
|
+
with values {property_values}
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
extra = "in locations {locations} with name {location_name}"
|
|
50
|
+
|
|
51
|
+
property_values: frozenset[str | int | float | bool | None | tuple[str | int | float | bool | None, ...]]
|
|
52
|
+
locations: tuple[str | int, ...] | None = None
|
|
53
|
+
location_name: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class PropertyMappingDuplicatedError(PropertyError[T_Identifier], Generic[T_Identifier, T_ReferenceIdentifier]):
|
|
58
|
+
"""The {resource_type} with identifier {identifier}.{property_name} is mapped to by: {mappings}. Ensure
|
|
59
|
+
that only one {mapping_type} maps to {resource_type} {identifier}.{property_name}"""
|
|
60
|
+
|
|
61
|
+
mappings: frozenset[T_ReferenceIdentifier]
|
|
62
|
+
mapping_type: ResourceType
|