DataExcept 0.4.2__tar.gz → 0.4.3__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-0.4.2 → dataexcept-0.4.3}/CHANGELOG.md +47 -1
  2. {dataexcept-0.4.2 → dataexcept-0.4.3}/CITATION.cff +1 -1
  3. {dataexcept-0.4.2 → dataexcept-0.4.3}/PKG-INFO +3 -3
  4. {dataexcept-0.4.2 → dataexcept-0.4.3}/README.md +2 -2
  5. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/logging_helpers.py +56 -7
  6. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/redaction.py +4 -1
  7. {dataexcept-0.4.2 → dataexcept-0.4.3}/pyproject.toml +2 -1
  8. {dataexcept-0.4.2 → dataexcept-0.4.3}/LICENSE +0 -0
  9. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/__init__.py +0 -0
  10. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/__main__.py +0 -0
  11. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/_deprecation.py +0 -0
  12. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/_validation.py +0 -0
  13. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/base.py +0 -0
  14. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/database_exceptions.py +0 -0
  15. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/dataengineering_exceptions.py +0 -0
  16. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/datascience_exceptions/__init__.py +0 -0
  17. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/datascience_exceptions/base.py +0 -0
  18. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/datascience_exceptions/ingestion.py +0 -0
  19. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/datascience_exceptions/operations.py +0 -0
  20. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/datascience_exceptions/training.py +0 -0
  21. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/__init__.py +0 -0
  22. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/authentication.py +0 -0
  23. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/base.py +0 -0
  24. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/configuration.py +0 -0
  25. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/external.py +0 -0
  26. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/lifecycle.py +0 -0
  27. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/notification.py +0 -0
  28. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/parsing.py +0 -0
  29. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/scheduling.py +0 -0
  30. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/exceptions/validation.py +0 -0
  31. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/io_exceptions.py +0 -0
  32. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/job_exceptions.py +0 -0
  33. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/network_exceptions.py +0 -0
  34. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/pandas_exceptions.py +0 -0
  35. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/pipeline_exceptions.py +0 -0
  36. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/py.typed +0 -0
  37. {dataexcept-0.4.2 → dataexcept-0.4.3}/dataexcept/security_exceptions.py +0 -0
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+
12
+ - The release workflow's `verify-wheel` job installed a hand-written list of
13
+ test requirements, which went stale the moment `hypothesis` was added — the
14
+ suite could not even be collected against the built wheel. It now installs
15
+ the dev group from the lock, so the list cannot drift again. The job did its
16
+ job: it failed, and publishing was skipped.
17
+ - `test_no_module_imports_tomllib_without_a_fallback` walked the whole tree, so
18
+ it picked up any virtualenv sitting in it. It now scans the project's own
19
+ source directories by name; site-packages is not ours to police.
20
+
21
+ ## [0.4.3] - 2026-08-25
22
+
23
+ ### Added
24
+
25
+ - Property-based tests over the logging helpers and the CLI: generated context
26
+ values, including hostile ones, must never make `log_exception` raise; the
27
+ normalised context must be strict-JSON encodable; `log_and_raise` must
28
+ re-raise the *same* exception with its traceback intact; and the CLI must
29
+ raise nothing but `SystemExit` for any argument list.
30
+ - **Property-based tests over the exception hierarchy**, using Hypothesis.
31
+ Generated text is fed to every class and four properties are asserted for
32
+ each: construction raises nothing but `TypeError`, a message built from real
33
+ input is non-empty, the exception survives a pickle round trip with its type,
34
+ message and cause intact, and a credential-bearing URL is never rendered.
35
+ Every constructor defect the reviews found — `MergeKeyError("id")` becoming
36
+ `['i', 'd']`, `is_number(True)`, and the one below — was in argument
37
+ handling and was found by inspection rather than by a test.
38
+
39
+ ### Fixed
40
+
41
+ - **`log_exception` could raise, replacing the caller's error with its own.**
42
+ A context value whose `__repr__` raises propagated straight out of the
43
+ logging helper — so a helper called while handling a failure made the failure
44
+ worse. Nothing in context normalisation can raise now; a value that cannot be
45
+ described at all becomes `<unrepresentable TypeName>`.
46
+ - **`NaN` and `Infinity` in a log context produced invalid JSON.**
47
+ `json.dumps` emits them bare, which a strict consumer downstream rejects.
48
+ They are stringified instead.
49
+ - `ApiError`, `DatabaseConnectionError` and `WebhookError` raised
50
+ `AttributeError: 'object' object has no attribute 'decode'` when given a
51
+ non-string, because the redaction helpers passed whatever they were given to
52
+ `urlsplit`. That masked the caller's actual mistake with a message about
53
+ `.decode`. The helpers now return a non-string untouched.
54
+
10
55
  ## [0.4.2] - 2026-08-25
11
56
 
12
57
  ### Changed
@@ -429,7 +474,8 @@ First public release.
429
474
  - Published to PyPI via OIDC trusted publishing; no long-lived API token is
430
475
  involved in a release.
431
476
 
432
- [Unreleased]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.2...HEAD
477
+ [Unreleased]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.3...HEAD
478
+ [0.4.3]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.2...v0.4.3
433
479
  [0.4.2]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.1...v0.4.2
434
480
  [0.4.1]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.0...v0.4.1
435
481
  [0.4.0]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.3.0...v0.4.0
@@ -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: "0.4.2"
4
+ version: "0.4.3"
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: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: A Python package providing structured, easily-extendable custom exception types.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -225,7 +225,7 @@ BiasDetectionError
225
225
 
226
226
  # Check version
227
227
  $ dataexcept --version
228
- dataexcept 0.4.2
228
+ dataexcept 0.4.3
229
229
  ```
230
230
 
231
231
  ## 🎯 Use Cases
@@ -375,7 +375,7 @@ If you use DataExcept in your research, please cite it:
375
375
  author = {Ribeiro, Diogo},
376
376
  title = {DataExcept: Structured Exception Handling for Data Science},
377
377
  url = {https://github.com/DiogoRibeiro7/DataExcept},
378
- version = {0.4.2},
378
+ version = {0.4.3},
379
379
  year = {2026},
380
380
  publisher = {GitHub}
381
381
  }
@@ -198,7 +198,7 @@ BiasDetectionError
198
198
 
199
199
  # Check version
200
200
  $ dataexcept --version
201
- dataexcept 0.4.2
201
+ dataexcept 0.4.3
202
202
  ```
203
203
 
204
204
  ## 🎯 Use Cases
@@ -348,7 +348,7 @@ If you use DataExcept in your research, please cite it:
348
348
  author = {Ribeiro, Diogo},
349
349
  title = {DataExcept: Structured Exception Handling for Data Science},
350
350
  url = {https://github.com/DiogoRibeiro7/DataExcept},
351
- version = {0.4.2},
351
+ version = {0.4.3},
352
352
  year = {2026},
353
353
  publisher = {GitHub}
354
354
  }
@@ -12,6 +12,9 @@ from .redaction import redact_urls_in_text
12
12
 
13
13
  Context = Mapping[str, Any]
14
14
 
15
+ #: Sentinel: the value could not be coerced into anything JSON will take.
16
+ _UNCOERCIBLE = object()
17
+
15
18
  __all__ = [
16
19
  "Context",
17
20
  "log_and_raise",
@@ -20,15 +23,61 @@ __all__ = [
20
23
  ]
21
24
 
22
25
 
23
- def _normalize_context_value(value: Any) -> Any:
26
+ def _is_json_safe(value: Any) -> bool:
27
+ """True if a strict JSON encoder will accept *value* as it stands.
28
+
29
+ ``allow_nan=False`` because ``json.dumps`` otherwise emits bare ``NaN`` and
30
+ ``Infinity``, which are not valid JSON and will be rejected downstream.
31
+ """
24
32
  try:
25
- json.dumps(value)
26
- return value
27
- except TypeError:
33
+ json.dumps(value, allow_nan=False)
34
+ except (TypeError, ValueError):
35
+ return False
36
+ return True
37
+
38
+
39
+ def _coerced(value: Any) -> Any:
40
+ """Round-trip *value* through JSON, stringifying whatever will not encode.
41
+
42
+ Returns ``_UNCOERCIBLE`` rather than raising: this runs while the caller is
43
+ already handling a failure.
44
+ """
45
+ try:
46
+ return json.loads(json.dumps(value, default=str, allow_nan=False))
47
+ except Exception:
48
+ return _UNCOERCIBLE
49
+
50
+
51
+ def _described(value: Any) -> str:
52
+ """Describe *value* without letting it raise.
53
+
54
+ An object may define a ``__repr__`` that raises. Naming the type is the
55
+ most that can be said without invoking anything the object controls.
56
+ """
57
+ try:
58
+ return repr(value)
59
+ except Exception:
28
60
  try:
29
- return json.loads(json.dumps(value, default=str))
30
- except Exception: # pragma: no cover - extremely defensive
31
- return repr(value)
61
+ return f"<unrepresentable {type(value).__name__}>"
62
+ except Exception: # pragma: no cover - a type with a hostile __name__
63
+ return "<unrepresentable>"
64
+
65
+
66
+ def _normalize_context_value(value: Any) -> Any:
67
+ """Return *value* in a form a strict JSON log encoder will accept.
68
+
69
+ Nothing here may raise. This runs while the caller is already handling a
70
+ failure, and an exception escaping would replace their error with one about
71
+ logging it -- so even a hostile ``__repr__`` has to be survivable.
72
+ """
73
+ if _is_json_safe(value):
74
+ return value
75
+
76
+ coerced = _coerced(value)
77
+ if coerced is not _UNCOERCIBLE:
78
+ return coerced
79
+
80
+ return _described(value)
32
81
 
33
82
 
34
83
  def _build_extra(context: Context | None) -> dict[str, Any] | None:
@@ -170,7 +170,10 @@ def redact_url(url: Optional[str], *, keep_path: bool = True) -> Optional[str]:
170
170
  incoming webhook URL is the common case -- Slack, Discord and others put
171
171
  the secret in the path, so preserving it would defeat the point.
172
172
  """
173
- if not url:
173
+ if not url or not isinstance(url, str):
174
+ # Anything that is not a string is handed back untouched. urlsplit
175
+ # would raise AttributeError from inside urllib, masking whatever the
176
+ # caller's real mistake was with a message about `.decode`.
174
177
  return url
175
178
 
176
179
  try:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "DataExcept"
3
- version = "0.4.2"
3
+ version = "0.4.3"
4
4
  description = "A Python package providing structured, easily-extendable custom exception types."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -43,6 +43,7 @@ include = [
43
43
  black = "*"
44
44
  coverage = "*"
45
45
  flake8 = "*"
46
+ hypothesis = "*"
46
47
  isort = "*"
47
48
  mypy = "*"
48
49
  pre-commit = "*"
File without changes