DataExcept 1.0.0__tar.gz → 1.2.0__tar.gz
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.
- {dataexcept-1.0.0 → dataexcept-1.2.0}/CHANGELOG.md +41 -1
- {dataexcept-1.0.0 → dataexcept-1.2.0}/CITATION.cff +2 -2
- {dataexcept-1.0.0 → dataexcept-1.2.0}/PKG-INFO +1 -1
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/__init__.py +8 -0
- dataexcept-1.2.0/dataexcept/serialization.py +216 -0
- dataexcept-1.2.0/dataexcept/wrapping.py +103 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/pyproject.toml +1 -1
- {dataexcept-1.0.0 → dataexcept-1.2.0}/LICENSE +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/README.md +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/__main__.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/_validation.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/base.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/database_exceptions.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/dataengineering_exceptions.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/__init__.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/base.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/ingestion.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/operations.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/training.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/__init__.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/authentication.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/base.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/configuration.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/external.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/lifecycle.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/notification.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/parsing.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/scheduling.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/exceptions/validation.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/io_exceptions.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/logging_helpers.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/network_exceptions.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/pandas_exceptions.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/pipeline_exceptions.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/py.typed +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/redaction.py +0 -0
- {dataexcept-1.0.0 → dataexcept-1.2.0}/dataexcept/security_exceptions.py +0 -0
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.0] - 2026-09-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`exception_to_dict` and `exception_to_json`** provide a stable, strict
|
|
15
|
+
JSON-safe envelope for exceptions crossing API, queue, telemetry and
|
|
16
|
+
structured-log boundaries. The envelope preserves type, module, rendered
|
|
17
|
+
message, public attributes and bounded cause/context chains.
|
|
18
|
+
- Structured export degrades hostile or unserialisable values safely, converts
|
|
19
|
+
non-finite floats and byte payloads to text, marks cycles and depth
|
|
20
|
+
truncation, and excludes traceback frames and private attributes.
|
|
21
|
+
- Export uses a stricter redaction boundary than ordinary exception rendering:
|
|
22
|
+
credential-bearing URL paths are removed as well as sensitive query and
|
|
23
|
+
fragment values, including in third-party causes, mapping keys and arbitrary
|
|
24
|
+
attribute representations.
|
|
25
|
+
|
|
26
|
+
## [1.1.0] - 2026-09-02
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`wrap` and `wrapping`**, for turning a third-party exception into a
|
|
31
|
+
DataExcept one:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
with wrapping(OSError, DataLoadingError, source=path):
|
|
35
|
+
frame = pd.read_csv(path)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The hand-written form is easy to get subtly wrong: a missing `from exc` loses
|
|
39
|
+
the traceback, the original passed to the wrong parameter is not recorded,
|
|
40
|
+
and a broad `except` relabels a `KeyboardInterrupt` as a data error. These
|
|
41
|
+
settle the wiring — the original goes to whichever constructor parameter
|
|
42
|
+
takes a cause (`original`, `original_exception` or `cause`) and is set as
|
|
43
|
+
`__cause__` either way, which matters because only 17 of the 100 classes
|
|
44
|
+
record one on an attribute.
|
|
45
|
+
- Guidance in the advanced usage guide on building a project-specific
|
|
46
|
+
hierarchy on these bases, and what deriving from a domain root gets you.
|
|
47
|
+
|
|
10
48
|
## [1.0.0] - 2026-08-25
|
|
11
49
|
|
|
12
50
|
First stable release. The public API is frozen; see the
|
|
@@ -509,7 +547,9 @@ First public release.
|
|
|
509
547
|
- Published to PyPI via OIDC trusted publishing; no long-lived API token is
|
|
510
548
|
involved in a release.
|
|
511
549
|
|
|
512
|
-
[Unreleased]: https://github.com/DiogoRibeiro7/DataExcept/compare/v1.
|
|
550
|
+
[Unreleased]: https://github.com/DiogoRibeiro7/DataExcept/compare/v1.2.0...HEAD
|
|
551
|
+
[1.2.0]: https://github.com/DiogoRibeiro7/DataExcept/compare/v1.1.0...v1.2.0
|
|
552
|
+
[1.1.0]: https://github.com/DiogoRibeiro7/DataExcept/compare/v1.0.0...v1.1.0
|
|
513
553
|
[1.0.0]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.3...v1.0.0
|
|
514
554
|
[0.4.3]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.2...v0.4.3
|
|
515
555
|
[0.4.2]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.1...v0.4.2
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
cff-version: 1.2.0
|
|
2
2
|
message: "If you use this software, please cite it using the following metadata."
|
|
3
3
|
title: "DataExcept"
|
|
4
|
-
version: "1.
|
|
4
|
+
version: "1.2.0"
|
|
5
5
|
authors:
|
|
6
6
|
- family-names: "Ribeiro"
|
|
7
7
|
given-names: "Diogo"
|
|
@@ -11,4 +11,4 @@ authors:
|
|
|
11
11
|
type: software
|
|
12
12
|
url: "https://github.com/DiogoRibeiro7/DataExcept"
|
|
13
13
|
repository-code: "https://github.com/DiogoRibeiro7/DataExcept"
|
|
14
|
-
date-released: "2026-
|
|
14
|
+
date-released: "2026-09-02"
|
|
@@ -159,6 +159,8 @@ from .security_exceptions import (
|
|
|
159
159
|
InvalidTokenError,
|
|
160
160
|
SecurityError,
|
|
161
161
|
)
|
|
162
|
+
from .serialization import exception_to_dict, exception_to_json
|
|
163
|
+
from .wrapping import wrap, wrapping
|
|
162
164
|
|
|
163
165
|
__all__ = [
|
|
164
166
|
# The root of the hierarchy: catches every operational exception the
|
|
@@ -266,6 +268,12 @@ __all__ = [
|
|
|
266
268
|
"UnderfittingError",
|
|
267
269
|
"ValidationError",
|
|
268
270
|
"WebhookError",
|
|
271
|
+
# Turning a third-party exception into one of these.
|
|
272
|
+
"wrap",
|
|
273
|
+
"wrapping",
|
|
274
|
+
# Structured serialization for APIs, queues and telemetry.
|
|
275
|
+
"exception_to_dict",
|
|
276
|
+
"exception_to_json",
|
|
269
277
|
# Logging helpers.
|
|
270
278
|
"Context",
|
|
271
279
|
"log_and_raise",
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Strict JSON-safe structured representations of exceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from collections.abc import Mapping, Sequence, Set
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .redaction import redact_urls_in_text
|
|
11
|
+
|
|
12
|
+
__all__ = ["exception_to_dict", "exception_to_json"]
|
|
13
|
+
|
|
14
|
+
_MAX_VALUE_DEPTH = 8
|
|
15
|
+
_NOT_SCALAR = object()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _redact_export_text(text: str) -> str:
|
|
19
|
+
"""Scrub URLs for export, including their paths.
|
|
20
|
+
|
|
21
|
+
Normal DataExcept messages preserve URL paths because paths are commonly
|
|
22
|
+
useful debugging context. Structured envelopes have a stricter boundary:
|
|
23
|
+
third-party errors and arbitrary caller state may put credentials in the
|
|
24
|
+
path itself, so exported text never preserves URL paths.
|
|
25
|
+
"""
|
|
26
|
+
return redact_urls_in_text(text, keep_path=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _safe_text(value: Any) -> str:
|
|
30
|
+
"""Render *value* without raising and scrub credential-bearing URLs."""
|
|
31
|
+
try:
|
|
32
|
+
text = str(value)
|
|
33
|
+
except Exception:
|
|
34
|
+
try:
|
|
35
|
+
text = f"<unrepresentable {type(value).__name__}>"
|
|
36
|
+
except Exception: # pragma: no cover - hostile type metadata
|
|
37
|
+
text = "<unrepresentable>"
|
|
38
|
+
return _redact_export_text(text)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _safe_key(value: Any) -> str:
|
|
42
|
+
if isinstance(value, str):
|
|
43
|
+
return _redact_export_text(value)
|
|
44
|
+
return _safe_text(value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _safe_scalar(value: Any) -> Any:
|
|
48
|
+
if value is None or isinstance(value, (bool, int)):
|
|
49
|
+
return value
|
|
50
|
+
if isinstance(value, float):
|
|
51
|
+
return value if math.isfinite(value) else str(value)
|
|
52
|
+
if isinstance(value, str):
|
|
53
|
+
return _redact_export_text(value)
|
|
54
|
+
if isinstance(value, (bytes, bytearray)):
|
|
55
|
+
return _safe_text(value)
|
|
56
|
+
return _NOT_SCALAR
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _safe_mapping(value: Mapping[Any, Any], *, depth: int, seen: set[int]) -> Any:
|
|
60
|
+
identity = id(value)
|
|
61
|
+
seen.add(identity)
|
|
62
|
+
try:
|
|
63
|
+
return {
|
|
64
|
+
_safe_key(key): _json_safe(item, depth=depth + 1, seen=seen)
|
|
65
|
+
for key, item in value.items()
|
|
66
|
+
}
|
|
67
|
+
except Exception:
|
|
68
|
+
return _safe_text(value)
|
|
69
|
+
finally:
|
|
70
|
+
seen.discard(identity)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _safe_collection(
|
|
74
|
+
value: Sequence[Any] | Set[Any], *, depth: int, seen: set[int]
|
|
75
|
+
) -> Any:
|
|
76
|
+
identity = id(value)
|
|
77
|
+
seen.add(identity)
|
|
78
|
+
try:
|
|
79
|
+
return [_json_safe(item, depth=depth + 1, seen=seen) for item in value]
|
|
80
|
+
except Exception:
|
|
81
|
+
return _safe_text(value)
|
|
82
|
+
finally:
|
|
83
|
+
seen.discard(identity)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _json_safe(value: Any, *, depth: int = 0, seen: set[int] | None = None) -> Any:
|
|
87
|
+
"""Return *value* in a strict JSON-safe form without raising."""
|
|
88
|
+
if seen is None:
|
|
89
|
+
seen = set()
|
|
90
|
+
scalar = _safe_scalar(value)
|
|
91
|
+
if scalar is not _NOT_SCALAR:
|
|
92
|
+
return scalar
|
|
93
|
+
if depth >= _MAX_VALUE_DEPTH:
|
|
94
|
+
return "<truncated>"
|
|
95
|
+
if id(value) in seen:
|
|
96
|
+
return "<cycle>"
|
|
97
|
+
if isinstance(value, Mapping):
|
|
98
|
+
return _safe_mapping(value, depth=depth, seen=seen)
|
|
99
|
+
if isinstance(value, (Sequence, Set)):
|
|
100
|
+
return _safe_collection(value, depth=depth, seen=seen)
|
|
101
|
+
return _safe_text(value)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _attributes(exc: BaseException) -> dict[str, Any]:
|
|
105
|
+
"""Return public instance attributes in a JSON-safe representation."""
|
|
106
|
+
try:
|
|
107
|
+
state = vars(exc)
|
|
108
|
+
result: dict[str, Any] = {}
|
|
109
|
+
for name, value in state.items():
|
|
110
|
+
if not isinstance(name, str) or name.startswith("_"):
|
|
111
|
+
continue
|
|
112
|
+
result[name] = _json_safe(value)
|
|
113
|
+
return result
|
|
114
|
+
except Exception:
|
|
115
|
+
# This code runs at an error-transport boundary. A hostile __dict__,
|
|
116
|
+
# custom mapping, iterator or key must not replace the original failure
|
|
117
|
+
# with a serialization failure.
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _exception_record(
|
|
122
|
+
exc: BaseException,
|
|
123
|
+
*,
|
|
124
|
+
include_attributes: bool,
|
|
125
|
+
max_depth: int,
|
|
126
|
+
depth: int,
|
|
127
|
+
seen: set[int],
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
if depth > max_depth:
|
|
130
|
+
return {"truncated": True}
|
|
131
|
+
|
|
132
|
+
identity = id(exc)
|
|
133
|
+
if identity in seen:
|
|
134
|
+
return {
|
|
135
|
+
"type": type(exc).__name__,
|
|
136
|
+
"module": type(exc).__module__,
|
|
137
|
+
"message": _safe_text(exc),
|
|
138
|
+
"cycle": True,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
seen.add(identity)
|
|
142
|
+
record: dict[str, Any] = {
|
|
143
|
+
"type": type(exc).__name__,
|
|
144
|
+
"module": type(exc).__module__,
|
|
145
|
+
"message": _safe_text(exc),
|
|
146
|
+
}
|
|
147
|
+
if include_attributes:
|
|
148
|
+
attributes = _attributes(exc)
|
|
149
|
+
if attributes:
|
|
150
|
+
record["attributes"] = attributes
|
|
151
|
+
|
|
152
|
+
if exc.__cause__ is not None:
|
|
153
|
+
record["cause"] = _exception_record(
|
|
154
|
+
exc.__cause__,
|
|
155
|
+
include_attributes=include_attributes,
|
|
156
|
+
max_depth=max_depth,
|
|
157
|
+
depth=depth + 1,
|
|
158
|
+
seen=seen,
|
|
159
|
+
)
|
|
160
|
+
if exc.__context__ is not None and not exc.__suppress_context__:
|
|
161
|
+
record["context"] = _exception_record(
|
|
162
|
+
exc.__context__,
|
|
163
|
+
include_attributes=include_attributes,
|
|
164
|
+
max_depth=max_depth,
|
|
165
|
+
depth=depth + 1,
|
|
166
|
+
seen=seen,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
seen.discard(identity)
|
|
170
|
+
return record
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def exception_to_dict(
|
|
174
|
+
exc: BaseException,
|
|
175
|
+
*,
|
|
176
|
+
include_attributes: bool = True,
|
|
177
|
+
max_depth: int = 8,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
"""Return a strict JSON-safe structured representation of *exc*.
|
|
180
|
+
|
|
181
|
+
The representation contains the exception type, module and rendered
|
|
182
|
+
message, optionally public instance attributes, and bounded cause/context
|
|
183
|
+
chains. Traceback frames and private attributes are deliberately excluded.
|
|
184
|
+
"""
|
|
185
|
+
if not isinstance(exc, BaseException):
|
|
186
|
+
raise TypeError("exc must be an exception instance")
|
|
187
|
+
if not isinstance(max_depth, int) or isinstance(max_depth, bool):
|
|
188
|
+
raise TypeError("max_depth must be an integer")
|
|
189
|
+
if max_depth < 0:
|
|
190
|
+
raise ValueError("max_depth must be non-negative")
|
|
191
|
+
return _exception_record(
|
|
192
|
+
exc,
|
|
193
|
+
include_attributes=include_attributes,
|
|
194
|
+
max_depth=max_depth,
|
|
195
|
+
depth=0,
|
|
196
|
+
seen=set(),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def exception_to_json(
|
|
201
|
+
exc: BaseException,
|
|
202
|
+
*,
|
|
203
|
+
include_attributes: bool = True,
|
|
204
|
+
max_depth: int = 8,
|
|
205
|
+
**json_kwargs: Any,
|
|
206
|
+
) -> str:
|
|
207
|
+
"""Return :func:`exception_to_dict` encoded as strict JSON."""
|
|
208
|
+
json_kwargs["allow_nan"] = False
|
|
209
|
+
return json.dumps(
|
|
210
|
+
exception_to_dict(
|
|
211
|
+
exc,
|
|
212
|
+
include_attributes=include_attributes,
|
|
213
|
+
max_depth=max_depth,
|
|
214
|
+
),
|
|
215
|
+
**json_kwargs,
|
|
216
|
+
)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Turning a third-party exception into a DataExcept one.
|
|
2
|
+
|
|
3
|
+
The pattern this replaces is everywhere in pipeline code::
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
frame = pd.read_csv(path)
|
|
7
|
+
except OSError as exc:
|
|
8
|
+
raise DataLoadingError(path, exc) from exc
|
|
9
|
+
|
|
10
|
+
It is easy to write and easy to get subtly wrong: forget the ``from exc`` and
|
|
11
|
+
the traceback stops showing what actually failed; pass the original to the
|
|
12
|
+
wrong parameter and it is not recorded at all; catch too broadly and a
|
|
13
|
+
``KeyboardInterrupt`` becomes a data-loading error.
|
|
14
|
+
|
|
15
|
+
:func:`wrap` and :func:`wrapping` do the same thing with the wiring settled.
|
|
16
|
+
The original is passed to whichever constructor parameter takes a cause --
|
|
17
|
+
``original``, ``original_exception`` or ``cause``, whichever that class uses --
|
|
18
|
+
and set as ``__cause__`` either way, so a traceback always shows both.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import contextlib
|
|
24
|
+
import inspect
|
|
25
|
+
from typing import Any, Iterator, Tuple, Type, Union
|
|
26
|
+
|
|
27
|
+
from .base import DataExceptError
|
|
28
|
+
|
|
29
|
+
__all__ = ["wrap", "wrapping"]
|
|
30
|
+
|
|
31
|
+
#: Constructor parameter names used across the package for a wrapped
|
|
32
|
+
#: exception. Checked in this order; the first the target accepts wins.
|
|
33
|
+
_CAUSE_PARAMETERS = ("original", "original_exception", "cause")
|
|
34
|
+
|
|
35
|
+
Catchable = Union[Type[BaseException], Tuple[Type[BaseException], ...]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _cause_parameter(target: Type[DataExceptError]) -> str | None:
|
|
39
|
+
"""Return the parameter of *target* that takes a wrapped exception."""
|
|
40
|
+
try:
|
|
41
|
+
parameters = inspect.signature(target.__init__).parameters
|
|
42
|
+
except (TypeError, ValueError): # pragma: no cover - builtins and C types
|
|
43
|
+
return None
|
|
44
|
+
for name in _CAUSE_PARAMETERS:
|
|
45
|
+
if name in parameters:
|
|
46
|
+
return name
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def wrap(
|
|
51
|
+
original: BaseException,
|
|
52
|
+
target: Type[DataExceptError],
|
|
53
|
+
/,
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
) -> DataExceptError:
|
|
56
|
+
"""Build *target* from *original*, recording it as the cause.
|
|
57
|
+
|
|
58
|
+
Extra keyword arguments go to the constructor::
|
|
59
|
+
|
|
60
|
+
raise wrap(exc, DataLoadingError, source=path) from exc
|
|
61
|
+
|
|
62
|
+
If *target* accepts a cause parameter, *original* is passed to it. Either
|
|
63
|
+
way ``__cause__`` is set, so a traceback shows the underlying failure even
|
|
64
|
+
for a class that records nothing.
|
|
65
|
+
|
|
66
|
+
An explicit ``original``/``cause`` keyword wins, so a caller can still say
|
|
67
|
+
exactly what they mean.
|
|
68
|
+
"""
|
|
69
|
+
parameter = _cause_parameter(target)
|
|
70
|
+
if parameter is not None and parameter not in kwargs:
|
|
71
|
+
kwargs[parameter] = original
|
|
72
|
+
|
|
73
|
+
exception = target(**kwargs)
|
|
74
|
+
# Set unconditionally: the target may record nothing, and the point is that
|
|
75
|
+
# the traceback shows what actually failed.
|
|
76
|
+
exception.__cause__ = original
|
|
77
|
+
return exception
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@contextlib.contextmanager
|
|
81
|
+
def wrapping(
|
|
82
|
+
catch: Catchable,
|
|
83
|
+
target: Type[DataExceptError],
|
|
84
|
+
/,
|
|
85
|
+
**kwargs: Any,
|
|
86
|
+
) -> Iterator[None]:
|
|
87
|
+
"""Translate *catch* raised inside the block into *target*.
|
|
88
|
+
|
|
89
|
+
::
|
|
90
|
+
|
|
91
|
+
with wrapping(OSError, DataLoadingError, source=path):
|
|
92
|
+
frame = pd.read_csv(path)
|
|
93
|
+
|
|
94
|
+
Only exceptions matching *catch* are translated; everything else propagates
|
|
95
|
+
untouched, including anything already raised by this package. Because
|
|
96
|
+
*catch* is given explicitly there is no default broad ``except``, so a
|
|
97
|
+
``KeyboardInterrupt`` or a bug in the block is never relabelled as a data
|
|
98
|
+
error.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
yield
|
|
102
|
+
except catch as exc:
|
|
103
|
+
raise wrap(exc, target, **kwargs) from exc
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|