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