DataExcept 0.3.0__tar.gz → 0.4.1__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 (39) hide show
  1. dataexcept-0.4.1/CHANGELOG.md +394 -0
  2. {dataexcept-0.3.0 → dataexcept-0.4.1}/CITATION.cff +2 -2
  3. {dataexcept-0.3.0 → dataexcept-0.4.1}/PKG-INFO +14 -14
  4. {dataexcept-0.3.0 → dataexcept-0.4.1}/README.md +11 -12
  5. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/__init__.py +16 -1
  6. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/__main__.py +7 -1
  7. dataexcept-0.4.1/dataexcept/_validation.py +28 -0
  8. dataexcept-0.4.1/dataexcept/base.py +190 -0
  9. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/database_exceptions.py +7 -3
  10. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/dataengineering_exceptions.py +5 -2
  11. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/datascience_exceptions/base.py +3 -1
  12. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/datascience_exceptions/ingestion.py +7 -5
  13. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/datascience_exceptions/operations.py +6 -4
  14. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/datascience_exceptions/training.py +9 -8
  15. dataexcept-0.4.1/dataexcept/exceptions/base.py +7 -0
  16. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/exceptions/notification.py +11 -4
  17. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/io_exceptions.py +7 -4
  18. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/logging_helpers.py +41 -1
  19. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/network_exceptions.py +3 -1
  20. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/pandas_exceptions.py +10 -6
  21. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/pipeline_exceptions.py +8 -5
  22. dataexcept-0.4.1/dataexcept/redaction.py +201 -0
  23. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/security_exceptions.py +11 -4
  24. {dataexcept-0.3.0 → dataexcept-0.4.1}/pyproject.toml +27 -3
  25. dataexcept-0.3.0/CHANGELOG.md +0 -175
  26. dataexcept-0.3.0/dataexcept/exceptions/base.py +0 -4
  27. {dataexcept-0.3.0 → dataexcept-0.4.1}/LICENSE +0 -0
  28. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/_deprecation.py +0 -0
  29. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/datascience_exceptions/__init__.py +0 -0
  30. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/exceptions/__init__.py +0 -0
  31. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/exceptions/authentication.py +0 -0
  32. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/exceptions/configuration.py +0 -0
  33. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/exceptions/external.py +0 -0
  34. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/exceptions/lifecycle.py +0 -0
  35. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/exceptions/parsing.py +0 -0
  36. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/exceptions/scheduling.py +0 -0
  37. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/exceptions/validation.py +0 -0
  38. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/job_exceptions.py +0 -0
  39. {dataexcept-0.3.0 → dataexcept-0.4.1}/dataexcept/py.typed +0 -0
@@ -0,0 +1,394 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.1] - 2026-08-25
11
+
12
+ ### Added
13
+
14
+ - Tests that derive the facts stated in more than one file, instead of trusting
15
+ them to be copied correctly: the citation, security policy, changelog and
16
+ checklist must all name the current version, checklist sections must be
17
+ numbered uniquely, no document may quote a test count, and the supported
18
+ Python range must agree across `requires-python`, the classifiers, the CI
19
+ matrix and the release gate.
20
+ - A contract-coverage test. The whole-hierarchy suites called `pytest.skip()`
21
+ when they could not build a class, so a class could sit outside the pickling,
22
+ message and inheritance guarantees with CI still green — and two were doing
23
+ exactly that. Skipping now requires an explicit, reasoned entry in an
24
+ exclusion table, which is empty.
25
+
26
+ - **Python 3.14 support.** `requires-python` capped at `<3.14`, so the package
27
+ refused to install on the current stable interpreter — 3.14 was released in
28
+ October 2025 and is no longer upcoming. The range is now `>=3.10,<3.15`, 3.14
29
+ is in the classifiers and the CI matrix, and `test (3.14)` joins the checks
30
+ the release workflow requires before it will build.
31
+
32
+ ### Changed
33
+
34
+ - `SECURITY.md` states the boundary precisely, including what is **not**
35
+ covered: a bare non-URL secret written into free-form text cannot be
36
+ recognised. The previous wording claimed credentials "never appear ...
37
+ whatever you pass in", which was broader than the implementation.
38
+
39
+ - The serialization and hierarchy guarantees are stated more precisely.
40
+ "Survives a process boundary intact" now says what happens to state that
41
+ cannot be serialized, and "catches anything this package raises" is now
42
+ "every operational exception" — constructors deliberately raise plain
43
+ `TypeError` for invalid arguments, which sits outside the hierarchy.
44
+
45
+ ### Fixed
46
+
47
+ - **An exception pickled by 0.4.0 could not be loaded by 0.4.1.** Restoring the
48
+ chain added three parameters to the private `_rebuild`, and an older payload
49
+ passes only three arguments — so a queued exception that outlived an upgrade,
50
+ or one sent by a worker on the previous release, raised `TypeError` on
51
+ unpickling. The new parameters carry defaults.
52
+
53
+ - The test probe fed a bare string to `Sequence[str]` parameters, so
54
+ `DataFormatError` and `DtypeMismatchError` correctly rejected it and were
55
+ silently skipped. The probe builds them properly now; nothing is skipped.
56
+ - `is_number` accepted booleans, because `bool` subclasses `int` and so
57
+ satisfies `numbers.Real`. A boolean is never a meaningful metric, threshold
58
+ or ratio, and accepting one hid a caller passing the wrong variable.
59
+ - `MergeKeyError("id", "cust_id")` silently became `['i', 'd']` — a bare string
60
+ is a sequence of strings. It now raises `TypeError`, matching
61
+ `DtypeMismatchError`, which already rejected this.
62
+ - `SECURITY.md` named 0.3.x as supported at 0.4.0, `CHECKLIST.md` was dated to
63
+ the previous release and numbered two sections 12, and the README quoted a
64
+ test count that no longer matched. `scripts/bump_version.py` now writes every
65
+ file that states the version, so they cannot drift apart by hand again.
66
+ - `black`'s target version is pinned rather than inferred from
67
+ `requires-python`. Adding 3.14 made inference pick `py314`, and black then
68
+ refused to verify its own output when run on an older interpreter — which is
69
+ what CI does.
70
+
71
+ - **Exception chaining was lost on a pickle round trip.** `__reduce__` saved
72
+ `args` and `__dict__`, but `__cause__`, `__context__` and
73
+ `__suppress_context__` are special exception state rather than `__dict__`
74
+ entries — so a wrapped exception rebuilt in another process no longer showed
75
+ what actually failed. All three are now restored explicitly. The 0.4.0 tests
76
+ did not catch this because they compared `args`, rendered text and `__dict__`
77
+ and never the chain; they now check it.
78
+ - **An exception carrying unpickleable state could not cross a process
79
+ boundary at all.** Several classes accept arbitrary caller state, so a
80
+ lambda, generator, open file or locally defined class made the whole
81
+ exception unserializable — replacing the real failure with a serialization
82
+ error about it. Such values are now replaced by an `UnpicklableValue`
83
+ describing what was there, and an unpickleable cause by an
84
+ `UnpicklableCause`, so the exception still arrives.
85
+
86
+ ### Security
87
+
88
+ - **Redaction is no longer defeated by the traceback.** `log_exception` passed
89
+ `exc_info`, so logging rendered the whole exception chain — including a
90
+ wrapped third-party exception whose own message still quoted the
91
+ credential-bearing URL. Redacting what DataExcept renders did nothing about
92
+ that. When the chain contains a URL, `log_exception` now formats the
93
+ traceback and scrubs it; every other exception keeps the structured
94
+ `exc_info` path, so nothing changes for them.
95
+ - `SECURITY.md` documents what remains outside that boundary: the wrapped
96
+ exception object is still reachable, so `traceback.print_exc()`,
97
+ `repr(exc.__dict__)` or `logger.error(..., exc_info=True)` will render its
98
+ text. A third-party exception's message is not ours to rewrite.
99
+
100
+ - **Every GitHub Action is pinned to a full-length commit SHA.** All 34
101
+ references used mutable tags, including `pypa/gh-action-pypi-publish` in the
102
+ OIDC publishing job — the step that holds the credential which uploads to
103
+ PyPI. Whoever controls an action repository can move a tag to different code
104
+ at any time; a commit SHA is the only immutable reference. Each pin carries
105
+ the version in a trailing comment so it stays reviewable and Dependabot can
106
+ still update it, and a test fails if any mutable reference reappears.
107
+
108
+ - **Credentials in a URL path are now redacted.** `redact_url` kept the whole
109
+ path, so `WebhookError` logged a Slack webhook URL — which Slack documents as
110
+ a secret in its entirety — unchanged. Where the path *is* the credential the
111
+ path is now dropped, keeping the host, which is what makes the error
112
+ actionable.
113
+ - **Sensitive parameters are matched by substring, not an exact allowlist.**
114
+ `X-Amz-Signature`, `X-Amz-Credential`, `auth_token` and `refresh_token` all
115
+ passed through before. Fragments are covered too, so an OAuth
116
+ `#access_token=` no longer survives.
117
+ - **A secret can no longer be reintroduced after redaction.** Redacting only
118
+ the structured argument left two open routes: a caller-supplied `message`,
119
+ and the text of a wrapped exception quoting the original URL. Every message
120
+ in the hierarchy now passes through one scrubbing boundary, so a URL is
121
+ redacted wherever it appears. Where the library was handed the secret
122
+ explicitly, that exact value is removed from the message as well.
123
+ - **URL-bearing fields beyond the four patched classes are redacted.**
124
+ `DataLoadingError.source` documents itself as "file path, URL" and rendered
125
+ a presigned S3 URL verbatim. Nine such fields now use `redact_if_url`, which
126
+ leaves ordinary file paths untouched.
127
+
128
+ ## [0.4.0] - 2026-08-24
129
+
130
+ ### Added
131
+
132
+ - **`DataExceptError`, the root of the hierarchy.** Every exception the package
133
+ raises now derives from it, so one clause catches the whole library:
134
+ ```python
135
+ except DataExceptError:
136
+ ...
137
+ ```
138
+ The nine domain roots (`JobError`, `DataScienceError`, `PipelineError` and
139
+ the rest) sit beneath it and still catch only their own domain, so granular
140
+ handling is unchanged.
141
+
142
+ ### Changed
143
+
144
+ - `SECURITY.md` claimed 0.1.x was the supported version, and `CHECKLIST.md`
145
+ still described an uncommitted lockfile, 30 of 96 top-level exports, 109 tests
146
+ and 85% coverage. Both now match the project.
147
+ - **Coverage is now measured honestly, and gated.** `coverage run -m pytest`
148
+ had no `source` setting, so it counted the tests and examples in the
149
+ denominator — and test files are by definition fully executed. The reported
150
+ figure was inflated: 86% when the package alone was at 79%.
151
+ `[tool.coverage.run]` now restricts measurement to `dataexcept` and enables
152
+ branch coverage, and `fail_under = 91` stops it regressing. The honest number
153
+ is **92%**; the README, checklist and roadmap now quote that rather than the
154
+ inflated one.
155
+ - The CLI is exercised in-process as well as through a subprocess. The
156
+ subprocess tests verify real invocation but coverage cannot see inside them,
157
+ which left `__main__.py` reporting 23% despite being tested. It now reports
158
+ 93%.
159
+
160
+ ### Fixed
161
+
162
+ - **NumPy scalars are accepted where a number is expected.** Eleven validations
163
+ used `isinstance(value, (int, float))`, which rejects `numpy.float32` and
164
+ `numpy.int64` — a poor answer from a library aimed at data science. They now
165
+ use `numbers.Real`, which NumPy registers its scalar types with, so this needs
166
+ no dependency on NumPy. Arrays and non-numbers are still rejected.
167
+ - **A wrapped exception is now chained.** Constructors that take an underlying
168
+ exception recorded it on an attribute but never set `__cause__`, so a
169
+ traceback did not show what actually failed. `DataExceptError` now mirrors it,
170
+ and Python prints "The above exception was the direct cause of the following
171
+ exception" as if `raise ... from` had been used.
172
+ - `from dataexcept import *` failed under `-W error::DeprecationWarning`,
173
+ because `__all__` listed the deprecated `job_exceptions` module. It is no
174
+ longer advertised there; it remains importable until 1.0.0.
175
+ - A redundant `global` declaration in `examples/lambda_main.py` raised three
176
+ `F824` warnings. CI linted only `dataexcept` and `tests` with flake8 while the
177
+ formatters covered `examples` and `scripts`; flake8 now covers all four.
178
+ - **Four exceptions discarded the reason for the failure.**
179
+ `DataLoadingError`, `MissingDataError`, `ModelSerializationError` and
180
+ `DeploymentError` rendered only their identifying attribute —
181
+ `[DataLoadingError] orders.csv` — while "invalid utf-8", "disk full" or
182
+ "permission denied" sat unseen in `args[0]`. Since logging uses `str(exc)`,
183
+ the part a reader needs never reached the log. All four now render the full
184
+ message, and a test asserts no class drops it.
185
+ - **Exceptions can now cross a process boundary.** They could not be pickled:
186
+ most constructors take several arguments while `Exception.args` holds only
187
+ the rendered message, and the default protocol replays `args` through
188
+ `__init__`. Of 98 classes, 39 raised `TypeError` on unpickling and a further
189
+ 47 came back with different state — only 10 round-tripped exactly. Raising
190
+ one inside a `ProcessPoolExecutor` killed the pool with `BrokenProcessPool`.
191
+ `DataExceptError.__reduce__` restores `args` and `__dict__` directly instead
192
+ of replaying `__init__`. All 97 constructible classes now round-trip with
193
+ identical type, message and attributes, covered by a test per class plus a
194
+ real process-pool test.
195
+ - The stability policy and README both claimed `except JobError:` catches
196
+ "anything else this library raises". It did not — there were nine
197
+ disconnected trees under `Exception`, so `JobError` caught neither
198
+ `ModelTrainingError` nor `PipelineError` nor `DatabaseError`. The claim is
199
+ now true of `DataExceptError`, and the docs say which base covers what.
200
+
201
+ ### Security
202
+
203
+ - **A release now has to prove where it came from.** The release workflow
204
+ checked only that the tag text matched `pyproject.toml`, so a tag pushed to
205
+ an unreviewed branch could reach the PyPI publishing job. It now refuses to
206
+ build unless the tagged commit is reachable from `main` and the same checks
207
+ branch protection requires are green on that exact commit.
208
+ - The built wheel is tested before it is published. A new `verify-wheel` job
209
+ installs the artifact, deletes the source package so nothing can import it by
210
+ accident, and runs the whole suite against what will actually be uploaded.
211
+ `scripts/check_wheel.py` then asserts the distribution ships `py.typed` and a
212
+ complete `__all__` — a file can be present in the repository and missing from
213
+ the artifact.
214
+ - **Credentials are no longer written into exception messages.** `log_exception`
215
+ logs `str(exc)`, so a failed connection put the database password in the log.
216
+ `InvalidTokenError` embedded the whole token; `DatabaseConnectionError` the
217
+ whole connection URL including username and password; `WebhookError` and
218
+ `ApiError` the URL including any signing or key parameter.
219
+ These are now redacted before being stored or rendered, so the raw value is
220
+ absent from the message, the attributes and a pickle of the exception. A
221
+ secret renders as `***(1a2b3c4d)` — a truncated SHA-256, so the same bad
222
+ credential failing repeatedly stays correlatable in a log without appearing
223
+ in it. Host, port and path survive, because those are what make the error
224
+ actionable.
225
+ `QueryExecutionError` still embeds the SQL it is given; SECURITY.md now says
226
+ so explicitly rather than leaving it to be discovered.
227
+
228
+ ## [0.3.0] - 2026-08-24
229
+
230
+ ### Added
231
+
232
+ - **Every exception the package defines is now importable from `dataexcept`
233
+ directly.** All 98 classes are exported at the top level, so callers no
234
+ longer need to know which domain module a class lives in. The domain modules
235
+ export the same objects, so `from dataexcept import ValidationError` and
236
+ `from dataexcept.exceptions import ValidationError` are interchangeable and
237
+ `except` behaves identically either way.
238
+
239
+ This was only safe because 0.2.0 removed the two hazards that make a flat
240
+ namespace dangerous: there are no duplicate class names left, and nothing
241
+ shadows a Python builtin. Imports are explicit rather than generated at
242
+ runtime, so type checkers and IDEs see the full surface — the package ships
243
+ `py.typed`.
244
+ - `dataexcept.exceptions` and `dataexcept.logging_helpers` are now named in
245
+ `__all__` alongside the other domain modules; the stability policy already
246
+ described them as public.
247
+ - Tests covering the public surface: every exception the package defines must
248
+ be exported and resolve, each top-level export must be the *same object* as
249
+ the submodule one, no exported name may shadow a builtin, no two exceptions
250
+ may share a name, and `from dataexcept import *` must expose the documented
251
+ surface. Verified these fail when an unexported exception is introduced.
252
+ - Tests that `poetry.lock` is committed, and that `docs/requirements.txt`
253
+ agrees with the Poetry `docs` group — the two list the same packages and
254
+ could drift apart silently.
255
+
256
+ ### Changed
257
+
258
+ - `poetry.lock` is committed, and CI installs from it. Every job previously ran
259
+ `pip install black flake8 isort mypy ruff` unpinned, so a new release of any
260
+ linter could turn the build red with no commit to point at. `poetry check
261
+ --lock` now fails the build if the lock and `pyproject.toml` disagree.
262
+ - The coverage badge is generated by `scripts/coverage_badge.py` instead of
263
+ the `coverage-badge` package. That package still imports `pkg_resources`,
264
+ removed in setuptools 81, so using it required pinning `setuptools<81` — and
265
+ dependency review flagged a moderate-severity advisory against the version
266
+ that pin selected. Its last release was August 2024. Generating the SVG
267
+ directly removes the dependency, the pin and the advisory together; the
268
+ output is byte-identical to the published badge apart from the percentage.
269
+
270
+ ## [0.2.1] - 2026-08-24
271
+
272
+ Documentation and CLI fixes. 0.2.0's README is what PyPI renders as the
273
+ project description, and its quick-start example did not run.
274
+
275
+ ### Fixed
276
+
277
+ - `python -m dataexcept --version` reported `__main__.py` as the program name
278
+ instead of `dataexcept`, because argparse defaults `prog` to `sys.argv[0]`.
279
+ - `dataexcept list` imported the deprecated `job_exceptions` shim to build its
280
+ output, so it emitted a `DeprecationWarning` at anyone who merely wanted to
281
+ see what the package offers, and it advertised `ConnectionError` and
282
+ `TimeoutError` alongside their replacements. Deprecated modules are now
283
+ skipped; the listing is 98 names, matching the classes the package defines.
284
+ - README's quick-start example began `from dataexcept import ValidationError,
285
+ ModelTrainingError`, which raises `ImportError` — `ModelTrainingError` is in
286
+ `dataexcept.datascience_exceptions` and is not re-exported at the top level.
287
+ - `docs/advanced_usage.md` taught `from dataexcept.job_exceptions import
288
+ JobError`, the deprecated path.
289
+ - README's comparison table showed exception messages without the
290
+ `[ClassName]` prefix the classes actually emit, its sample `dataexcept list`
291
+ output did not match the real alphabetical listing, its exception count and
292
+ CLI version were stale, and its end-to-end example used `np.log` without
293
+ importing numpy.
294
+
295
+ ### Added
296
+
297
+ - Tests that read the documentation: every `from dataexcept... import ...` in
298
+ README and `docs/` must resolve, no example may import a deprecated module,
299
+ and the README's exception count must match the package. Checked that these
300
+ fail when the original defects are reintroduced.
301
+
302
+ ## [0.2.0] - 2026-08-24
303
+
304
+ ### Changed
305
+
306
+ - **`ConnectionError` is now `ServiceConnectionError`, and `TimeoutError` is
307
+ now `OperationTimeoutError`.** The old names shadowed Python builtins without
308
+ inheriting from them, so after `from dataexcept import ConnectionError` an
309
+ `except ConnectionError:` in that module silently stopped catching real
310
+ socket failures.
311
+ - **`datascience_exceptions.SerializationError` is now
312
+ `ModelSerializationError`**, and **`pipeline_exceptions.FeatureEngineeringError`
313
+ is now `FeaturePreprocessingError`.** Each of those names previously referred
314
+ to two different classes in different modules, so catching one silently
315
+ missed the other.
316
+ - Project metadata moved from Poetry's `[tool.poetry]` table to the standard
317
+ PEP 621 `[project]` table, clearing every `poetry check` deprecation. The
318
+ license is now an SPDX expression (PEP 639), so the built metadata carries
319
+ `License-Expression: MIT` and the redundant license classifier is gone.
320
+ Wheel and sdist contents are otherwise unchanged.
321
+ - `dataexcept.job_exceptions` now names its removal version, 1.0.0, in both the
322
+ warning and the module docstring.
323
+
324
+ ### Deprecated
325
+
326
+ - `ConnectionError`, `TimeoutError`, `datascience_exceptions.SerializationError`
327
+ and `pipeline_exceptions.FeatureEngineeringError`. All four still resolve, to
328
+ the **same class object** as their replacement, so existing `except` clauses
329
+ keep working; touching one emits a `DeprecationWarning` naming the
330
+ replacement and 1.0.0 as the removal. Importing the package does not warn.
331
+ Find remaining uses with `python -W error::DeprecationWarning -m pytest`.
332
+
333
+ ### Added
334
+
335
+ - A published [API stability policy](https://diogoribeiro7.github.io/DataExcept/stability/)
336
+ stating what is public, what each kind of change costs in version terms, the
337
+ deprecation process, and how to migrate off the 0.2.0 renames.
338
+ - Security scanning: CodeQL, and pip-audit against the runtime and
339
+ documentation dependency sets, on push, pull request and weekly — advisories
340
+ are published against code that has not changed. Pull requests also get a
341
+ dependency review failing at moderate severity.
342
+ - Complexity and security linting via ruff's mccabe (`C90`) and flake8-bandit
343
+ (`S`) rule sets, so neither needs a separate tool. Complexity is capped at 8;
344
+ the highest score in the package is 6.
345
+ - A regression guard that fails if any exported name ever shadows a builtin
346
+ again.
347
+ - `__all__` on `dataexcept.logging_helpers`, the one public module without one.
348
+
349
+ ### Fixed
350
+
351
+ - `dataexcept.__version__` and `tests/test_version.py` read the version out of
352
+ `pyproject.toml` when the package is not installed, and were still looking in
353
+ `[tool.poetry]`. They now read `[project]`.
354
+ - `examples/example_usage.py` raised `TimeoutError` with keyword arguments the
355
+ builtin does not accept — a live instance of the shadowing hazard.
356
+
357
+ ## [0.1.0] - 2026-08-24
358
+
359
+ First public release.
360
+
361
+ ### Added
362
+
363
+ - Hierarchical exception classes for data science, machine learning and data
364
+ engineering workflows. Catch a specific failure or a broad category, and get
365
+ a message that names the value that caused it rather than a bare
366
+ `ValueError`.
367
+ - Domain modules for validation, configuration, authentication, parsing,
368
+ serialization, scheduling, notification, lifecycle and external-service
369
+ errors, plus dedicated pandas, database, network, I/O, pipeline and security
370
+ exception groups.
371
+ - `dataexcept.logging_helpers` with `log_exception`, `log_and_raise` and
372
+ `log_then_raise`, for logging exceptions with structured context and
373
+ re-raising without losing the traceback.
374
+ - A `dataexcept` command-line entry point that lists the exported exception
375
+ classes and reports the installed version.
376
+ - A `py.typed` marker, backed by a mypy-clean codebase that CI enforces, so
377
+ downstream type checkers get annotations that are actually correct.
378
+ - Documentation at
379
+ [diogoribeiro7.github.io/DataExcept](https://diogoribeiro7.github.io/DataExcept/),
380
+ including an API reference generated from the docstrings.
381
+
382
+ ### Notes
383
+
384
+ - Supports Python 3.10 through 3.13.
385
+ - Published to PyPI via OIDC trusted publishing; no long-lived API token is
386
+ involved in a release.
387
+
388
+ [Unreleased]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.1...HEAD
389
+ [0.4.1]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.4.0...v0.4.1
390
+ [0.4.0]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.3.0...v0.4.0
391
+ [0.3.0]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.2.1...v0.3.0
392
+ [0.2.1]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.2.0...v0.2.1
393
+ [0.2.0]: https://github.com/DiogoRibeiro7/DataExcept/compare/v0.1.0...v0.2.0
394
+ [0.1.0]: https://github.com/DiogoRibeiro7/DataExcept/releases/tag/v0.1.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.3.0"
4
+ version: "0.4.1"
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-08-24"
14
+ date-released: "2026-08-25"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DataExcept
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: A Python package providing structured, easily-extendable custom exception types.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -9,13 +9,14 @@ Author: Diogo Ribeiro
9
9
  Author-email: dfr@esmad.ipp.pt
10
10
  Maintainer: Diogo Ribeiro
11
11
  Maintainer-email: diogo.debastos.ribeiro@gmail.com
12
- Requires-Python: >=3.10,<3.14
12
+ Requires-Python: >=3.10,<3.15
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
19
20
  Requires-Dist: tomli ; python_version < "3.11"
20
21
  Project-URL: Changelog, https://github.com/DiogoRibeiro7/DataExcept/releases
21
22
  Project-URL: Documentation, https://diogoribeiro7.github.io/DataExcept/
@@ -41,13 +42,13 @@ Description-Content-Type: text/markdown
41
42
 
42
43
  ## 🎯 Key Features
43
44
 
44
- - **🏗️ Hierarchical Structure**: Catch specific errors or broad categories
45
+ - **🏗️ Hierarchical Structure**: Catch one specific error, a whole domain, or every operational error via `DataExceptError`
45
46
  - **📦 One Import**: Every exception is available from `dataexcept` directly, or from its domain module — same objects either way
46
- - **📊 Data Science Focused**: 98 exception classes covering ML pipelines, feature engineering, model training
47
- - **🔧 Production Ready**: Comprehensive logging helpers and error context
47
+ - **📊 Data Science Focused**: 100 exception classes covering ML pipelines, feature engineering, model training
48
+ - **🔧 Production Ready**: Logging helpers, error context, and exceptions that pickle — so they cross a process boundary with their message, attributes and cause intact
48
49
  - **📚 Academic Quality**: Proper documentation, type hints, and citation support
49
- - **🐍 Python 3.10+**: Modern Python with full type safety
50
- - **🧪 Well Tested**: Broad test suite with comprehensive edge case handling (see the coverage badge above)
50
+ - **🐍 Python 3.10 – 3.14**: Every supported version tested in CI, with full type safety
51
+ - **🧪 Well Tested**: Full branch coverage of the package gated in CI, with contract tests over every exception class
51
52
 
52
53
  ## 📦 Quick Installation
53
54
 
@@ -105,8 +106,7 @@ def load_dataset(file_path: str) -> pd.DataFrame:
105
106
  ### Exception Hierarchies
106
107
 
107
108
  ```python
108
- from dataexcept import JobError
109
- from dataexcept.datascience_exceptions import ModelTrainingError, ConvergenceError
109
+ from dataexcept import ConvergenceError, DataExceptError, ModelTrainingError
110
110
 
111
111
  try:
112
112
  # Your ML pipeline
@@ -119,8 +119,8 @@ except ModelTrainingError:
119
119
  # Handle any training-related error
120
120
  logger.error("Training failed, falling back to simpler model")
121
121
  train_simple_model()
122
- except JobError:
123
- # Handle any job-related error
122
+ except DataExceptError:
123
+ # Handle anything else DataExcept raised
124
124
  logger.error("Job failed, notifying administrators")
125
125
  send_alert()
126
126
  ```
@@ -214,7 +214,7 @@ except Exception as exc:
214
214
  ### Command Line Interface
215
215
 
216
216
  ```bash
217
- # List every exception class the package exports (98 of them, alphabetically)
217
+ # List every exception class the package exports (100 of them, alphabetically)
218
218
  $ dataexcept list
219
219
  ApiError
220
220
  AuthenticationError
@@ -225,7 +225,7 @@ BiasDetectionError
225
225
 
226
226
  # Check version
227
227
  $ dataexcept --version
228
- dataexcept 0.3.0
228
+ dataexcept 0.4.1
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.3.0},
378
+ version = {0.4.1},
379
379
  year = {2026},
380
380
  publisher = {GitHub}
381
381
  }
@@ -15,13 +15,13 @@
15
15
 
16
16
  ## 🎯 Key Features
17
17
 
18
- - **🏗️ Hierarchical Structure**: Catch specific errors or broad categories
18
+ - **🏗️ Hierarchical Structure**: Catch one specific error, a whole domain, or every operational error via `DataExceptError`
19
19
  - **📦 One Import**: Every exception is available from `dataexcept` directly, or from its domain module — same objects either way
20
- - **📊 Data Science Focused**: 98 exception classes covering ML pipelines, feature engineering, model training
21
- - **🔧 Production Ready**: Comprehensive logging helpers and error context
20
+ - **📊 Data Science Focused**: 100 exception classes covering ML pipelines, feature engineering, model training
21
+ - **🔧 Production Ready**: Logging helpers, error context, and exceptions that pickle — so they cross a process boundary with their message, attributes and cause intact
22
22
  - **📚 Academic Quality**: Proper documentation, type hints, and citation support
23
- - **🐍 Python 3.10+**: Modern Python with full type safety
24
- - **🧪 Well Tested**: Broad test suite with comprehensive edge case handling (see the coverage badge above)
23
+ - **🐍 Python 3.10 – 3.14**: Every supported version tested in CI, with full type safety
24
+ - **🧪 Well Tested**: Full branch coverage of the package gated in CI, with contract tests over every exception class
25
25
 
26
26
  ## 📦 Quick Installation
27
27
 
@@ -79,8 +79,7 @@ def load_dataset(file_path: str) -> pd.DataFrame:
79
79
  ### Exception Hierarchies
80
80
 
81
81
  ```python
82
- from dataexcept import JobError
83
- from dataexcept.datascience_exceptions import ModelTrainingError, ConvergenceError
82
+ from dataexcept import ConvergenceError, DataExceptError, ModelTrainingError
84
83
 
85
84
  try:
86
85
  # Your ML pipeline
@@ -93,8 +92,8 @@ except ModelTrainingError:
93
92
  # Handle any training-related error
94
93
  logger.error("Training failed, falling back to simpler model")
95
94
  train_simple_model()
96
- except JobError:
97
- # Handle any job-related error
95
+ except DataExceptError:
96
+ # Handle anything else DataExcept raised
98
97
  logger.error("Job failed, notifying administrators")
99
98
  send_alert()
100
99
  ```
@@ -188,7 +187,7 @@ except Exception as exc:
188
187
  ### Command Line Interface
189
188
 
190
189
  ```bash
191
- # List every exception class the package exports (98 of them, alphabetically)
190
+ # List every exception class the package exports (100 of them, alphabetically)
192
191
  $ dataexcept list
193
192
  ApiError
194
193
  AuthenticationError
@@ -199,7 +198,7 @@ BiasDetectionError
199
198
 
200
199
  # Check version
201
200
  $ dataexcept --version
202
- dataexcept 0.3.0
201
+ dataexcept 0.4.1
203
202
  ```
204
203
 
205
204
  ## 🎯 Use Cases
@@ -349,7 +348,7 @@ If you use DataExcept in your research, please cite it:
349
348
  author = {Ribeiro, Diogo},
350
349
  title = {DataExcept: Structured Exception Handling for Data Science},
351
350
  url = {https://github.com/DiogoRibeiro7/DataExcept},
352
- version = {0.3.0},
351
+ version = {0.4.1},
353
352
  year = {2026},
354
353
  publisher = {GitHub}
355
354
  }
@@ -4,6 +4,12 @@ Every exception the package defines is importable straight from here::
4
4
 
5
5
  from dataexcept import ValidationError, ModelTrainingError
6
6
 
7
+ They all derive from :class:`DataExceptError`, so one clause catches anything
8
+ this package raises::
9
+
10
+ except DataExceptError:
11
+ ...
12
+
7
13
  The domain modules (``datascience_exceptions``, ``pipeline_exceptions`` and so
8
14
  on) remain importable and export the same objects, so both spellings work and
9
15
  refer to the same classes.
@@ -32,6 +38,7 @@ from . import ( # noqa: F401
32
38
  security_exceptions,
33
39
  )
34
40
  from ._deprecation import resolve_deprecated
41
+ from .base import DataExceptError, UnpicklableCause, UnpicklableValue
35
42
  from .database_exceptions import (
36
43
  DatabaseConnectionError,
37
44
  DatabaseError,
@@ -156,6 +163,12 @@ from .security_exceptions import (
156
163
  )
157
164
 
158
165
  __all__ = [
166
+ # The root of the hierarchy: catches every operational exception the
167
+ # package raises.
168
+ "DataExceptError",
169
+ # Placeholders for state that could not survive serialization.
170
+ "UnpicklableCause",
171
+ "UnpicklableValue",
159
172
  # Every exception class the package defines.
160
173
  "ApiError",
161
174
  "AuthenticationError",
@@ -261,12 +274,14 @@ __all__ = [
261
274
  "log_exception",
262
275
  "log_then_raise",
263
276
  # Domain modules, for callers who prefer a qualified import.
277
+ # job_exceptions is deliberately absent: it is deprecated, and listing it
278
+ # makes `from dataexcept import *` emit a DeprecationWarning, which turns
279
+ # into an error under -W error::DeprecationWarning. It stays importable.
264
280
  "database_exceptions",
265
281
  "dataengineering_exceptions",
266
282
  "datascience_exceptions",
267
283
  "exceptions",
268
284
  "io_exceptions",
269
- "job_exceptions",
270
285
  "logging_helpers",
271
286
  "network_exceptions",
272
287
  "pandas_exceptions",
@@ -21,11 +21,17 @@ _DEPRECATED_MODULES = frozenset({"dataexcept.job_exceptions"})
21
21
  def _iter_exception_modules() -> Iterable[ModuleType]:
22
22
  """Yield every non-deprecated submodule that explicitly defines ``__all__``."""
23
23
  allowed_suffixes = ("exceptions", "_exceptions")
24
+ # dataexcept.base holds DataExceptError, the root of the hierarchy, and
25
+ # does not match the suffix rule.
26
+ always_include = {"dataexcept.base"}
24
27
 
25
28
  for module_info in pkgutil.walk_packages(
26
29
  _PKG_PATH, prefix="dataexcept.", onerror=lambda name: None
27
30
  ):
28
- if not module_info.name.endswith(allowed_suffixes):
31
+ if (
32
+ not module_info.name.endswith(allowed_suffixes)
33
+ and module_info.name not in always_include
34
+ ):
29
35
  continue
30
36
  if module_info.name in _DEPRECATED_MODULES:
31
37
  continue
@@ -0,0 +1,28 @@
1
+ """Small runtime checks shared by the exception constructors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numbers
6
+
7
+ __all__ = ["is_number"]
8
+
9
+
10
+ def is_number(value: object) -> bool:
11
+ """Return True for any real number, including NumPy scalars.
12
+
13
+ ``isinstance(value, (int, float))`` rejects ``numpy.float32`` and
14
+ ``numpy.int64``, which is a poor answer from a library aimed at data
15
+ science. ``numbers.Real`` accepts them because NumPy registers its scalar
16
+ types with the ABC, and it needs no dependency on NumPy to do so.
17
+
18
+ Booleans are excluded: ``bool`` subclasses ``int``, so ``numbers.Real``
19
+ would accept ``True`` as a metric.
20
+
21
+ This is a function rather than an inline ``isinstance`` because narrowing a
22
+ value to ``numbers.Real`` defeats mypy's inference for the rest of the
23
+ enclosing class.
24
+ """
25
+ # bool subclasses int, so numbers.Real accepts True and False. A boolean
26
+ # is never a meaningful metric, threshold or ratio, and accepting one hides
27
+ # a caller passing the wrong variable.
28
+ return not isinstance(value, bool) and isinstance(value, numbers.Real)