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.
Files changed (37) hide show
  1. {dataexcept-1.1.0 → dataexcept-1.2.0}/CHANGELOG.md +18 -1
  2. {dataexcept-1.1.0 → dataexcept-1.2.0}/CITATION.cff +1 -1
  3. {dataexcept-1.1.0 → dataexcept-1.2.0}/PKG-INFO +1 -1
  4. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/__init__.py +4 -0
  5. dataexcept-1.2.0/dataexcept/serialization.py +216 -0
  6. {dataexcept-1.1.0 → dataexcept-1.2.0}/pyproject.toml +1 -1
  7. {dataexcept-1.1.0 → dataexcept-1.2.0}/LICENSE +0 -0
  8. {dataexcept-1.1.0 → dataexcept-1.2.0}/README.md +0 -0
  9. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/__main__.py +0 -0
  10. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/_validation.py +0 -0
  11. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/base.py +0 -0
  12. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/database_exceptions.py +0 -0
  13. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/dataengineering_exceptions.py +0 -0
  14. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/__init__.py +0 -0
  15. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/base.py +0 -0
  16. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/ingestion.py +0 -0
  17. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/operations.py +0 -0
  18. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/datascience_exceptions/training.py +0 -0
  19. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/__init__.py +0 -0
  20. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/authentication.py +0 -0
  21. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/base.py +0 -0
  22. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/configuration.py +0 -0
  23. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/external.py +0 -0
  24. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/lifecycle.py +0 -0
  25. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/notification.py +0 -0
  26. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/parsing.py +0 -0
  27. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/scheduling.py +0 -0
  28. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/exceptions/validation.py +0 -0
  29. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/io_exceptions.py +0 -0
  30. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/logging_helpers.py +0 -0
  31. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/network_exceptions.py +0 -0
  32. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/pandas_exceptions.py +0 -0
  33. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/pipeline_exceptions.py +0 -0
  34. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/py.typed +0 -0
  35. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/redaction.py +0 -0
  36. {dataexcept-1.1.0 → dataexcept-1.2.0}/dataexcept/security_exceptions.py +0 -0
  37. {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.1.0...HEAD
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
@@ -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.1.0"
4
+ version: "1.2.0"
5
5
  authors:
6
6
  - family-names: "Ribeiro"
7
7
  given-names: "Diogo"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DataExcept
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: A Python package providing structured, easily-extendable custom exception types.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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
+ )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "DataExcept"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  description = "A Python package providing structured, easily-extendable custom exception types."
5
5
  readme = "README.md"
6
6
  license = "MIT"
File without changes
File without changes