DataExcept 1.1.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.1.0 → dataexcept-1.2.0}/CHANGELOG.md +18 -1
- {dataexcept-1.1.0 → dataexcept-1.2.0}/CITATION.cff +1 -1
- {dataexcept-1.1.0 → dataexcept-1.2.0}/PKG-INFO +1 -1
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/__init__.py +4 -0
- dataexcept-1.2.0/dataexcept/serialization.py +216 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/pyproject.toml +1 -1
- {dataexcept-1.1.0 → dataexcept-1.2.0}/LICENSE +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/README.md +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/__main__.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/_validation.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/base.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/database_exceptions.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/dataengineering_exceptions.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/__init__.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/base.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/ingestion.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/operations.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/training.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/__init__.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/authentication.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/base.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/configuration.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/external.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/lifecycle.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/notification.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/parsing.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/scheduling.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/validation.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/io_exceptions.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/logging_helpers.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/network_exceptions.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/pandas_exceptions.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/pipeline_exceptions.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/py.typed +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/redaction.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/security_exceptions.py +0 -0
- {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/wrapping.py +0 -0
|
@@ -7,6 +7,22 @@ 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
|
+
|
|
10
26
|
## [1.1.0] - 2026-09-02
|
|
11
27
|
|
|
12
28
|
### Added
|
|
@@ -531,7 +547,8 @@ First public release.
|
|
|
531
547
|
- Published to PyPI via OIDC trusted publishing; no long-lived API token is
|
|
532
548
|
involved in a release.
|
|
533
549
|
|
|
534
|
-
[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
|
|
535
552
|
[1.1.0]: https://github.com/DiogoRibeiro7/DataExcept/compare/v1.0.0...v1.1.0
|
|
536
553
|
[1.0.0]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.3...v1.0.0
|
|
537
554
|
[0.4.3]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.2...v0.4.3
|
|
@@ -159,6 +159,7 @@ from .security_exceptions import (
|
|
|
159
159
|
InvalidTokenError,
|
|
160
160
|
SecurityError,
|
|
161
161
|
)
|
|
162
|
+
from .serialization import exception_to_dict, exception_to_json
|
|
162
163
|
from .wrapping import wrap, wrapping
|
|
163
164
|
|
|
164
165
|
__all__ = [
|
|
@@ -270,6 +271,9 @@ __all__ = [
|
|
|
270
271
|
# Turning a third-party exception into one of these.
|
|
271
272
|
"wrap",
|
|
272
273
|
"wrapping",
|
|
274
|
+
# Structured serialization for APIs, queues and telemetry.
|
|
275
|
+
"exception_to_dict",
|
|
276
|
+
"exception_to_json",
|
|
273
277
|
# Logging helpers.
|
|
274
278
|
"Context",
|
|
275
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
|
+
)
|
|
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
|
|
File without changes
|