-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathexceptions.py
More file actions
325 lines (238 loc) · 11.3 KB
/
exceptions.py
File metadata and controls
325 lines (238 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
"""Errors that are designed to result in more useful critical error messages."""
import logging
from abc import ABC, abstractmethod
from collections.abc import Mapping
from typing import Any, Optional
from dve.core_engine.message import FeedbackMessage
from dve.core_engine.type_hints import EntityName, ErrorCategory, ErrorLocation, Messages
PipelineErrorLocation = str
"""A location within the pipeline execution, for context within the error."""
FieldName = str
"""The name of a field within the data."""
FieldType = str
"""A representation of the type of the field as a string."""
ExpectedFieldType = FieldType
"""A representation of the expected type of the field as a string."""
ActualFieldType = str
"""A representation of the actual type of the field as a string."""
class BackendError(Exception):
"""A base exception type for all backend errors."""
class MessageBearingError(BackendError):
"""A backend error that comes with pre-created messages."""
def __init__(self, *args: object, messages: Messages) -> None:
super().__init__(*args)
self.messages = messages
"""The messages to be returned as part of the error."""
class BackendErrorMixin(ABC, BackendError):
"""A mixin used to create backend error type."""
@abstractmethod
def get_message_preamble(self) -> str:
"""Get the start of the message string to be used for logging and feedback.
This will be joined to the location string with the joiner.
"""
def get_joiner(self):
"""The joiner between the preamble and the context string."""
return "in"
def get_message_epilogue(self) -> Optional[str]:
"""Get the end of the message string to be used for logging and feedback.
This will be appended straight after the location string. Avoid using this
unless necessary, since this leads to very verbose errors.
"""
return None
def to_message_string(self, location: PipelineErrorLocation) -> str:
"""Create a message from the error."""
epilogue = self.get_message_epilogue() # pylint: disable=assignment-from-none
if epilogue:
location = "".join((location, epilogue))
return " ".join((self.get_message_preamble(), self.get_joiner(), location))
class BackendMissingFunctionality(NotImplementedError, BackendErrorMixin):
"""An error raised when a backend is missing the functionality
required by a given rule config.
"""
def __init__(self, *args: object, operation: str) -> None:
super().__init__(*args)
self.operation = operation
"""The operation which has not been implemented by the backend."""
def get_message_preamble(self) -> str:
"""Get the start of the message string to be used for logging and feedback.
This will be joined to the location string with the joiner.
"""
return f"The running backend does not support the {self.operation!r} operation"
# pylint: disable=unused-argument
def get_joiner(self):
"""The joiner between the preamble and the context string."""
return "specified in"
class MissingEntity(KeyError, BackendErrorMixin):
"""An error to be emitted when a required entity is missing."""
def __init__(self, *args: object, entity_name: str) -> None:
super().__init__(*args)
self.entity_name = entity_name
"""The name of the missing entity."""
def get_message_preamble(self) -> str:
"""Get the start of the message string to be used for logging and feedback.
This will be joined to the location string with the joiner.
"""
return f"Missing entity {self.entity_name!r}"
def get_joiner(self):
"""The joiner between the preamble and the context string."""
return "required by"
class MissingRefDataEntity(MissingEntity, BackendErrorMixin): # pylint: disable=too-many-ancestors
"""An error to be emitted when a required refdata entity is missing."""
def get_message_preamble(self) -> str:
"""Get the start of the message string to be used for logging and feedback.
This will be joined to the location string with the joiner.
"""
return f"Missing reference data entity {self.entity_name!r}"
class NoRefDataConfigSupplied(BackendError):
"""An error raised when trying to load a refdata entity when no refdata
config has been supplied.
"""
def __init__(self, *args: object) -> None:
super().__init__(*args)
def get_message_preamble(self) -> EntityName:
"""Message for logging purposes"""
return "Refdata loader not supplied with refdata config - unable to load refdata entities"
class ConstraintError(ValueError, BackendErrorMixin):
"""Raised when a given constraint is violated."""
def __init__(self, *args: object, constraint: str) -> None:
super().__init__(*args)
self.constraint = constraint
"""The constraint that was violated. This should reference the relevant entities."""
def get_message_preamble(self) -> str:
"""Get the start of the message string to be used for logging and feedback.
This will be joined to the location string with the joiner.
"""
return "Constraint violated"
def get_message_epilogue(self) -> str:
"""Get the end of the message string to be used for logging and feedback.
This will be appended straight after the location string.
Avoid using this unless necessary, since this leads to very verbose errors.
"""
return f": {self.constraint}"
class ReaderErrorMixin(BackendErrorMixin):
"""A mixin to be used by the reader errors."""
def get_joiner(self) -> str:
return "for"
class ReaderLacksEntityTypeSupport(ReaderErrorMixin, TypeError):
"""An error raised when a reader class lacks direct support for
a given entity type.
"""
def __init__(self, *args: object, entity_type: Any) -> None:
super().__init__(*args)
self.entity_type = entity_type
"""The entity type that is not supported directly by the reader."""
def get_message_preamble(self) -> EntityName:
return f"Reader does not support reading directly to entity type {self.entity_type!r}"
class RefdataLacksFileExtensionSupport(BackendError):
"""An error raised when trying to load a refdata file where the loader
lacks support for the given file type
"""
def __init__(self, *args: object, file_extension: str) -> None:
super().__init__(*args)
self.file_extension = file_extension
"""The file extension that is not supported directly by the
refdata loader"""
def get_message_preamble(self) -> EntityName:
"""Message for logging purposes"""
return f"Refdata loader does not support reading refdata from {self.file_extension} files"
class EmptyFileError(ReaderErrorMixin, ValueError):
"""The read file was empty."""
def get_message_preamble(self) -> str:
return "File location provided is empty"
class MissingHeaderError(ReaderErrorMixin, ValueError):
"""A header is expected, but was not parsed."""
def get_message_preamble(self) -> str:
return "No header was provided"
class FieldCountMismatch(ReaderErrorMixin, ValueError):
"""An error raised when the number of expected fields does not match
the number of parsed fields.
"""
def __init__(self, *args: object, n_expected_fields: int, n_actual_fields: int) -> None:
super().__init__(*args)
self.n_expected_fields = n_expected_fields
"""The number of fields that should have been in the file."""
self.n_actual_fields = n_actual_fields
"""The number of fields that were actually in the file."""
def get_message_preamble(self) -> str:
return (
f"Number of fields expected ({self.n_expected_fields}) does not "
+ f"match the number in the provided data ({self.n_actual_fields})"
)
class SchemaMismatch(ReaderErrorMixin, ValueError):
"""An error raised when a file does not match a given schema (e.g. fields
are missing).
"""
def __init__(
self,
*args: object,
missing_fields: Optional[set[FieldName]] = None,
extra_fields: Optional[set[FieldName]] = None,
wrong_types: Optional[Mapping[FieldName, tuple[ActualFieldType, ExpectedFieldType]]] = None,
):
self.missing_fields = missing_fields or set()
"""Fields that are missing from the expected schema."""
self.extra_fields = extra_fields or set()
"""Fields that are additional to the expected schema."""
self.wrong_types = wrong_types or {}
"""
Fields that are the wrong type in the expected schema (a mapping of field name to a
tuple of actual and expected field type, both as strings).
"""
super().__init__(*args)
def get_message_preamble(self) -> str:
return "Schema mismatch"
def get_message_epilogue(self) -> str:
message_components = []
if self.missing_fields:
fields_str = ", ".join(map(repr, sorted(self.missing_fields)))
message_components.append(f"The following fields are missing: {fields_str}")
if self.extra_fields:
fields_str = ", ".join(map(repr, sorted(self.extra_fields)))
message_components.append(f"The following extra fields were provided: {fields_str}")
if self.wrong_types:
type_strings = []
for field_name in sorted(self.wrong_types):
actual_type, expected_type = self.wrong_types[field_name]
type_strings.append(
f"{field_name!r} (got {actual_type!r}, expected {expected_type!r})"
)
fields_str = ", ".join(type_strings)
message_components.append(f"The following fields had the wrong types: {fields_str}")
return ". ".join(("", *message_components))
class NoComplexSchemaSupport(ReaderErrorMixin, ValueError):
"""An error raised when a schema is invalid for a given file reader
(e.g. the reader does not support complex types).
"""
def get_message_preamble(self) -> str:
return "The specified reader does not support complex data types"
def render_error(
error: Exception,
location: PipelineErrorLocation,
logger: Optional[logging.Logger] = None,
entity_name: Optional[EntityName] = None,
error_location: Optional[ErrorLocation] = None,
error_category: Optional[ErrorCategory] = None,
) -> Messages:
"""Convert a generic error to a message string."""
if isinstance(error, MessageBearingError):
return error.messages
if isinstance(error, BackendErrorMixin):
msg = error.to_message_string(location)
entity_name = entity_name or getattr(error, "entity_name", None)
error_location = error_location or getattr(error, "error_location", None)
error_category = error_category or getattr(error, "error_category", None)
else:
msg = f"Unexpected error ({type(error).__name__}: {error}) in {location}"
if logger:
logger.error(msg)
logger.exception(error)
return [
FeedbackMessage(
entity_name,
None,
failure_type="integrity",
error_message=msg,
error_location=error_location,
category=error_category,
)
]