domain-errors 0.1.0__tar.gz → 0.2.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.
- {domain_errors-0.1.0 → domain_errors-0.2.1}/CHANGELOG.md +15 -3
- {domain_errors-0.1.0 → domain_errors-0.2.1}/CONTRIBUTING.md +2 -2
- {domain_errors-0.1.0 → domain_errors-0.2.1}/PKG-INFO +102 -26
- {domain_errors-0.1.0 → domain_errors-0.2.1}/README.md +101 -25
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/chain.md +10 -10
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/cloud.md +4 -4
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/diagrams.md +1 -1
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/http.md +4 -4
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/python.md +8 -8
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/wrap_errors.md +63 -10
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-13-chain-security-audit.md +2 -2
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-14-wrap_errors-security-audit.md +5 -5
- domain_errors-0.2.1/docs/reviews/2026-06-13-chain-review.md +49 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-13-domain_error-review.md +1 -1
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-14-http-review.md +1 -1
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-14-python-review.md +1 -1
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-14-wrap_errors-review.md +38 -38
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/config/_version.py +1 -1
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/tests/test_wrap_errors/test_wrap_errors_client.py +342 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/wrap_errors/wrap_errors_client.py +62 -6
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/test_chain/conftest.py +1 -1
- {domain_errors-0.1.0 → domain_errors-0.2.1}/pyproject.toml +1 -0
- domain_errors-0.1.0/docs/reviews/2026-06-13-chain-review.md +0 -49
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/ci.yml +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/cleanup-guard.yml +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/dto-strict.yml +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/no-ai-attribution.yml +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/publish.yml +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/ruff.yml +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/.gitignore +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/CODE_OF_CONDUCT.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/LICENSE +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/SECURITY.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/domain_error.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/architecture/.gitkeep +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/.gitkeep +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-13-domain_error-security-audit.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-14-cloud-security-audit.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-14-http-security-audit.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-14-python-security-audit.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/.gitkeep +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-14-cloud-review.md +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/common/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/common/constants/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/common/constants/tests.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/common/tests/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/config/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/tests/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/tests/test_wrap_errors/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/tests/test_wrap_errors/conftest.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/wrap_errors/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/cloud/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/cloud/cloud_client.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/cloud.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/domain_error.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/http.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/python.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/domain_error/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/domain_error/domain_error.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/http/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/http/http_client.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/python/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/python/python_client.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_cloud/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_cloud/conftest.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_cloud/test_cloud_client.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_domain_error/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_domain_error/conftest.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_domain_error/test_domain_error.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_http/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_http/conftest.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_http/test_http_client.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_python/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_python/conftest.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_python/test_python_client.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/py.typed +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/chain/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/chain/chain_client.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/chain/chain_objects.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/constants/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/constants/chain.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/constants/services.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/test_chain/__init__.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/test_chain/test_chain_client.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/test_chain/test_chain_objects.py +0 -0
- {domain_errors-0.1.0 → domain_errors-0.2.1}/uv.lock +0 -0
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-07-11
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Final pointer release: this package is consolidated into `domain-suite` (import roots unchanged there). This distribution remains installable but receives no further releases.
|
|
15
|
+
|
|
16
|
+
## [0.2.0] - 2026-07-06
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **@wrap_errors class-level decoration:** Decorator can now be applied to classes. Fans out over all public callables in `cls.__dict__`, preserving sync/async dispatch per method. Skips private methods, dunder methods, properties, nested classes, and already-decorated methods. Unwraps and rewraps classmethod/staticmethod to preserve semantics. Config (catch, as_, message, capture) applies uniformly to all fanned methods.
|
|
21
|
+
|
|
10
22
|
## [0.1.0] - 2026-06-15
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -14,9 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
26
|
- **DomainError base class:** Typed exception hierarchy with class contract (code, domain, http_status, retryable, default_message).
|
|
15
27
|
- **Instance state:** message (string) and context (dict) for structured logging.
|
|
16
28
|
- **ErrorChain service:** Static methods for wrapping, walking, and analyzing exception chains.
|
|
17
|
-
- `wrap(err, as_=ErrorType, message=None, **context)
|
|
18
|
-
- `history(err, classifiers=())
|
|
19
|
-
- `crossings(err, classifiers=())
|
|
29
|
+
- `wrap(err, as_=ErrorType, message=None, **context)`: construct typed domain errors.
|
|
30
|
+
- `history(err, classifiers=())`: walk exception cascade into ChainLink objects.
|
|
31
|
+
- `crossings(err, classifiers=())`: identify domain boundary crossings.
|
|
20
32
|
- **ChainLink value object:** Structured representation of one exception chain hop, with `to_log_extra()` for JSON-ready logging.
|
|
21
33
|
- **DomainCrossing value object:** Causation hop where errors cross domain boundaries, with `to_log_extra()` for logging.
|
|
22
34
|
- **ChainVia enum:** Tags for how a link entered the chain (ROOT, CAUSE, CONTEXT).
|
|
@@ -42,13 +42,13 @@ This project follows these conventions:
|
|
|
42
42
|
- **Dataclasses**: Use `@dataclass(frozen=True, slots=True)` for immutable value types.
|
|
43
43
|
- **Docstrings**: One-line docstrings for modules, classes, and functions. Use triple quotes even for single lines.
|
|
44
44
|
- **Imports**: Import abstract base classes from `collections.abc` (not `typing`). Use absolute imports (no relative imports).
|
|
45
|
-
- **Methods**: Every method lives in a class or container
|
|
45
|
+
- **Methods**: Every method lives in a class or container: no module-level functions. Containers keep methods organized.
|
|
46
46
|
- **Constants**: Extract magic constants to `constants/` directory only when they are semantically significant (shared across modules or define behavior).
|
|
47
47
|
|
|
48
48
|
## Pull Request Flow
|
|
49
49
|
|
|
50
50
|
1. **Branch**: Create a feature branch off `main` (e.g., `git checkout -b feature/my-feature`).
|
|
51
|
-
2. **Commit**: Make small, clear commits. Each commit author is the person who wrote the code
|
|
51
|
+
2. **Commit**: Make small, clear commits. Each commit author is the person who wrote the code: no co-author or AI attribution trailers.
|
|
52
52
|
3. **Push**: Push your branch and open a pull request on GitHub.
|
|
53
53
|
4. **Review**: The maintainer will review your code, request changes if needed, and merge when ready. Contributors do not merge their own PRs.
|
|
54
54
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: domain-errors
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Typed domain error hierarchy with wrapping and chaining for Python services
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/domain-errors/
|
|
6
6
|
Project-URL: Repository, https://github.com/jekhator/domain-errors
|
|
@@ -20,6 +20,8 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
|
+
> **This package is consolidated into [domain-suite](https://github.com/jekhator/domain-suite) (PyPI: `domain-suite`).** Import roots are unchanged there; this repository is frozen for new features and receives pointer releases only.
|
|
24
|
+
|
|
23
25
|
# domain-errors
|
|
24
26
|
|
|
25
27
|
[](https://pypi.org/project/domain-errors/)
|
|
@@ -29,11 +31,11 @@ Description-Content-Type: text/markdown
|
|
|
29
31
|
|
|
30
32
|
Typed domain error hierarchy with wrapping and chaining for Python services.
|
|
31
33
|
|
|
32
|
-
Define per-domain exception types with a structured contract (code, domain, HTTP status, retryability). Wrap foreign exceptions into domain errors, walk causation chains, and detect when errors cross domain boundaries
|
|
34
|
+
Define per-domain exception types with a structured contract (code, domain, HTTP status, retryability). Wrap foreign exceptions into domain errors, walk causation chains, and detect when errors cross domain boundaries: all ready for structured logging.
|
|
33
35
|
|
|
34
36
|
## Why?
|
|
35
37
|
|
|
36
|
-
Service errors should carry semantic meaning: *what* failed (code), *where* it failed (domain), *how* to handle it (HTTP status, retryability), and *why* it happened (context). Cross-service calls risk mixing domains
|
|
38
|
+
Service errors should carry semantic meaning: *what* failed (code), *where* it failed (domain), *how* to handle it (HTTP status, retryability), and *why* it happened (context). Cross-service calls risk mixing domains: database errors become API errors become payment errors. `domain-errors` makes this hierarchy explicit and traceable.
|
|
37
39
|
|
|
38
40
|
```python
|
|
39
41
|
from domain_errors import DomainError, ErrorChain
|
|
@@ -165,17 +167,17 @@ error = MyError(
|
|
|
165
167
|
```
|
|
166
168
|
|
|
167
169
|
**Attributes:**
|
|
168
|
-
- `message: str
|
|
169
|
-
- `context: dict[str, object]
|
|
170
|
+
- `message: str`: the error message
|
|
171
|
+
- `context: dict[str, object]`: structured context data
|
|
170
172
|
|
|
171
173
|
### ErrorChain
|
|
172
174
|
|
|
173
175
|
Static methods for wrapping, walking, and analyzing exception chains.
|
|
174
176
|
|
|
175
177
|
**Methods:**
|
|
176
|
-
- `ErrorChain.wrap(err, as_=ErrorType, message=None, **context)
|
|
177
|
-
- `ErrorChain.history(err, classifiers=())
|
|
178
|
-
- `ErrorChain.crossings(err, classifiers=())
|
|
178
|
+
- `ErrorChain.wrap(err, as_=ErrorType, message=None, **context)`: Construct a typed domain error for the caller to raise `from err`.
|
|
179
|
+
- `ErrorChain.history(err, classifiers=())`: Walk the exception chain; return a tuple of `ChainLink` objects.
|
|
180
|
+
- `ErrorChain.crossings(err, classifiers=())`: Find causation hops where errors crossed domains; return a tuple of `DomainCrossing` objects.
|
|
179
181
|
|
|
180
182
|
### ChainLink
|
|
181
183
|
|
|
@@ -245,7 +247,9 @@ links = ErrorChain.history(err, classifiers=(python, http, cloud))
|
|
|
245
247
|
|
|
246
248
|
### @wrap_errors
|
|
247
249
|
|
|
248
|
-
Decorator for automatic error wrapping. Catches specified exceptions, wraps them into a target DomainError, and captures function arguments for structured logging
|
|
250
|
+
Decorator for automatic error wrapping. Catches specified exceptions, wraps them into a target DomainError, and captures function arguments for structured logging. Works on both functions and classes (fans out to all public methods, preserving sync/async dispatch).
|
|
251
|
+
|
|
252
|
+
**Sync and async functions:**
|
|
249
253
|
|
|
250
254
|
```python
|
|
251
255
|
from domain_errors import DomainError, wrap_errors
|
|
@@ -260,18 +264,6 @@ def read_config(path: str) -> str:
|
|
|
260
264
|
with open(path) as f:
|
|
261
265
|
return f.read()
|
|
262
266
|
|
|
263
|
-
# Manual wrapping (before)
|
|
264
|
-
try:
|
|
265
|
-
config = read_config("/etc/app.yaml")
|
|
266
|
-
except FileNotFoundError as e:
|
|
267
|
-
raise ErrorChain.wrap(e, as_=StorageError, path=path) from e
|
|
268
|
-
|
|
269
|
-
# Declarative wrapping (after)
|
|
270
|
-
@wrap_errors(StorageError, catch=(FileNotFoundError, IOError))
|
|
271
|
-
def read_config(path: str) -> str:
|
|
272
|
-
with open(path) as f:
|
|
273
|
-
return f.read()
|
|
274
|
-
|
|
275
267
|
try:
|
|
276
268
|
config = read_config("/etc/app.yaml")
|
|
277
269
|
except StorageError as err:
|
|
@@ -280,18 +272,102 @@ except StorageError as err:
|
|
|
280
272
|
pass
|
|
281
273
|
```
|
|
282
274
|
|
|
275
|
+
**Service classes (class-level wrapping):**
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from domain_errors import DomainError, wrap_errors
|
|
279
|
+
|
|
280
|
+
class PaymentError(DomainError):
|
|
281
|
+
code = "payment_error"
|
|
282
|
+
domain = "payment"
|
|
283
|
+
http_status = 402
|
|
284
|
+
|
|
285
|
+
@wrap_errors(PaymentError, catch=(ValueError,))
|
|
286
|
+
class PaymentService:
|
|
287
|
+
def validate_amount(self, amount: int) -> bool:
|
|
288
|
+
if amount <= 0:
|
|
289
|
+
raise ValueError("Amount must be positive")
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
async def process_payment(self, card: str, amount: int) -> str:
|
|
293
|
+
if not card:
|
|
294
|
+
raise ValueError("Card required")
|
|
295
|
+
return f"Processed {amount}"
|
|
296
|
+
|
|
297
|
+
service = PaymentService()
|
|
298
|
+
|
|
299
|
+
assert service.validate_amount(100)
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
service.validate_amount(-50)
|
|
303
|
+
except PaymentError as err:
|
|
304
|
+
print(f"Error: {err.message}")
|
|
305
|
+
print(f"Context: {err.context}")
|
|
306
|
+
print(f"Cause: {err.__cause__}")
|
|
307
|
+
```
|
|
308
|
+
|
|
283
309
|
DomainError instances pass through unchanged; works on sync and async callables; set `capture=False` to omit arguments (e.g., for secrets).
|
|
284
310
|
|
|
311
|
+
## Service Class Example
|
|
312
|
+
|
|
313
|
+
A complete service class pattern with error wrapping and context capture:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
from domain_errors import DomainError, ErrorChain
|
|
317
|
+
|
|
318
|
+
class PaymentService:
|
|
319
|
+
@staticmethod
|
|
320
|
+
def process_payment(amount_cents: int) -> dict:
|
|
321
|
+
try:
|
|
322
|
+
if amount_cents <= 0:
|
|
323
|
+
raise ValueError("Amount must be positive")
|
|
324
|
+
if amount_cents > 999999:
|
|
325
|
+
raise ValueError("Amount exceeds maximum")
|
|
326
|
+
return {"status": "success", "amount_cents": amount_cents}
|
|
327
|
+
except ValueError as e:
|
|
328
|
+
raise ErrorChain.wrap(
|
|
329
|
+
e,
|
|
330
|
+
as_=PaymentError,
|
|
331
|
+
message=f"Payment validation failed: {e}",
|
|
332
|
+
amount_cents=amount_cents
|
|
333
|
+
) from e
|
|
334
|
+
|
|
335
|
+
class PaymentError(DomainError):
|
|
336
|
+
code = "payment_error"
|
|
337
|
+
domain = "payment"
|
|
338
|
+
http_status = 400
|
|
339
|
+
default_message = "Payment processing failed."
|
|
340
|
+
|
|
341
|
+
# Usage
|
|
342
|
+
try:
|
|
343
|
+
result = PaymentService.process_payment(5000)
|
|
344
|
+
print(f"Success: {result}")
|
|
345
|
+
except PaymentError as err:
|
|
346
|
+
print(f"Error {err.code} in {err.domain}: {err.message}")
|
|
347
|
+
print(f"Context: {err.context}")
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Output:
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
✓ Successful payment: {'status': 'success', 'amount_cents': 5000}
|
|
354
|
+
✓ Caught PaymentError: code=payment_error, domain=payment
|
|
355
|
+
Message: Payment validation failed: Amount must be positive
|
|
356
|
+
Context: {'amount_cents': -100}
|
|
357
|
+
Chain length: 2
|
|
358
|
+
✓ Caught PaymentError: amount_cents=9999999
|
|
359
|
+
```
|
|
360
|
+
|
|
285
361
|
## Documentation
|
|
286
362
|
|
|
287
|
-
- **DomainError Base:** [docs/apps/domain_error.md](docs/apps/domain_error.md)
|
|
288
|
-
- **ErrorChain:** [docs/apps/chain.md](docs/apps/chain.md)
|
|
289
|
-
- **@wrap_errors Decorator:** [docs/apps/wrap_errors.md](docs/apps/wrap_errors.md)
|
|
290
|
-
- **Architecture:** [docs/apps/diagrams.md](docs/apps/diagrams.md)
|
|
363
|
+
- **DomainError Base:** [docs/apps/domain_error.md](docs/apps/domain_error.md): class contract, subclassing, message/context semantics
|
|
364
|
+
- **ErrorChain:** [docs/apps/chain.md](docs/apps/chain.md): wrap, history, crossings, logging integration
|
|
365
|
+
- **@wrap_errors Decorator:** [docs/apps/wrap_errors.md](docs/apps/wrap_errors.md): declarative error wrapping for functions and async callables
|
|
366
|
+
- **Architecture:** [docs/apps/diagrams.md](docs/apps/diagrams.md): data flow and domain relationships
|
|
291
367
|
|
|
292
368
|
## License
|
|
293
369
|
|
|
294
|
-
Apache 2.0
|
|
370
|
+
Apache 2.0; see LICENSE file.
|
|
295
371
|
|
|
296
372
|
## Contributing
|
|
297
373
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
> **This package is consolidated into [domain-suite](https://github.com/jekhator/domain-suite) (PyPI: `domain-suite`).** Import roots are unchanged there; this repository is frozen for new features and receives pointer releases only.
|
|
2
|
+
|
|
1
3
|
# domain-errors
|
|
2
4
|
|
|
3
5
|
[](https://pypi.org/project/domain-errors/)
|
|
@@ -7,11 +9,11 @@
|
|
|
7
9
|
|
|
8
10
|
Typed domain error hierarchy with wrapping and chaining for Python services.
|
|
9
11
|
|
|
10
|
-
Define per-domain exception types with a structured contract (code, domain, HTTP status, retryability). Wrap foreign exceptions into domain errors, walk causation chains, and detect when errors cross domain boundaries
|
|
12
|
+
Define per-domain exception types with a structured contract (code, domain, HTTP status, retryability). Wrap foreign exceptions into domain errors, walk causation chains, and detect when errors cross domain boundaries: all ready for structured logging.
|
|
11
13
|
|
|
12
14
|
## Why?
|
|
13
15
|
|
|
14
|
-
Service errors should carry semantic meaning: *what* failed (code), *where* it failed (domain), *how* to handle it (HTTP status, retryability), and *why* it happened (context). Cross-service calls risk mixing domains
|
|
16
|
+
Service errors should carry semantic meaning: *what* failed (code), *where* it failed (domain), *how* to handle it (HTTP status, retryability), and *why* it happened (context). Cross-service calls risk mixing domains: database errors become API errors become payment errors. `domain-errors` makes this hierarchy explicit and traceable.
|
|
15
17
|
|
|
16
18
|
```python
|
|
17
19
|
from domain_errors import DomainError, ErrorChain
|
|
@@ -143,17 +145,17 @@ error = MyError(
|
|
|
143
145
|
```
|
|
144
146
|
|
|
145
147
|
**Attributes:**
|
|
146
|
-
- `message: str
|
|
147
|
-
- `context: dict[str, object]
|
|
148
|
+
- `message: str`: the error message
|
|
149
|
+
- `context: dict[str, object]`: structured context data
|
|
148
150
|
|
|
149
151
|
### ErrorChain
|
|
150
152
|
|
|
151
153
|
Static methods for wrapping, walking, and analyzing exception chains.
|
|
152
154
|
|
|
153
155
|
**Methods:**
|
|
154
|
-
- `ErrorChain.wrap(err, as_=ErrorType, message=None, **context)
|
|
155
|
-
- `ErrorChain.history(err, classifiers=())
|
|
156
|
-
- `ErrorChain.crossings(err, classifiers=())
|
|
156
|
+
- `ErrorChain.wrap(err, as_=ErrorType, message=None, **context)`: Construct a typed domain error for the caller to raise `from err`.
|
|
157
|
+
- `ErrorChain.history(err, classifiers=())`: Walk the exception chain; return a tuple of `ChainLink` objects.
|
|
158
|
+
- `ErrorChain.crossings(err, classifiers=())`: Find causation hops where errors crossed domains; return a tuple of `DomainCrossing` objects.
|
|
157
159
|
|
|
158
160
|
### ChainLink
|
|
159
161
|
|
|
@@ -223,7 +225,9 @@ links = ErrorChain.history(err, classifiers=(python, http, cloud))
|
|
|
223
225
|
|
|
224
226
|
### @wrap_errors
|
|
225
227
|
|
|
226
|
-
Decorator for automatic error wrapping. Catches specified exceptions, wraps them into a target DomainError, and captures function arguments for structured logging
|
|
228
|
+
Decorator for automatic error wrapping. Catches specified exceptions, wraps them into a target DomainError, and captures function arguments for structured logging. Works on both functions and classes (fans out to all public methods, preserving sync/async dispatch).
|
|
229
|
+
|
|
230
|
+
**Sync and async functions:**
|
|
227
231
|
|
|
228
232
|
```python
|
|
229
233
|
from domain_errors import DomainError, wrap_errors
|
|
@@ -238,18 +242,6 @@ def read_config(path: str) -> str:
|
|
|
238
242
|
with open(path) as f:
|
|
239
243
|
return f.read()
|
|
240
244
|
|
|
241
|
-
# Manual wrapping (before)
|
|
242
|
-
try:
|
|
243
|
-
config = read_config("/etc/app.yaml")
|
|
244
|
-
except FileNotFoundError as e:
|
|
245
|
-
raise ErrorChain.wrap(e, as_=StorageError, path=path) from e
|
|
246
|
-
|
|
247
|
-
# Declarative wrapping (after)
|
|
248
|
-
@wrap_errors(StorageError, catch=(FileNotFoundError, IOError))
|
|
249
|
-
def read_config(path: str) -> str:
|
|
250
|
-
with open(path) as f:
|
|
251
|
-
return f.read()
|
|
252
|
-
|
|
253
245
|
try:
|
|
254
246
|
config = read_config("/etc/app.yaml")
|
|
255
247
|
except StorageError as err:
|
|
@@ -258,18 +250,102 @@ except StorageError as err:
|
|
|
258
250
|
pass
|
|
259
251
|
```
|
|
260
252
|
|
|
253
|
+
**Service classes (class-level wrapping):**
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from domain_errors import DomainError, wrap_errors
|
|
257
|
+
|
|
258
|
+
class PaymentError(DomainError):
|
|
259
|
+
code = "payment_error"
|
|
260
|
+
domain = "payment"
|
|
261
|
+
http_status = 402
|
|
262
|
+
|
|
263
|
+
@wrap_errors(PaymentError, catch=(ValueError,))
|
|
264
|
+
class PaymentService:
|
|
265
|
+
def validate_amount(self, amount: int) -> bool:
|
|
266
|
+
if amount <= 0:
|
|
267
|
+
raise ValueError("Amount must be positive")
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
async def process_payment(self, card: str, amount: int) -> str:
|
|
271
|
+
if not card:
|
|
272
|
+
raise ValueError("Card required")
|
|
273
|
+
return f"Processed {amount}"
|
|
274
|
+
|
|
275
|
+
service = PaymentService()
|
|
276
|
+
|
|
277
|
+
assert service.validate_amount(100)
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
service.validate_amount(-50)
|
|
281
|
+
except PaymentError as err:
|
|
282
|
+
print(f"Error: {err.message}")
|
|
283
|
+
print(f"Context: {err.context}")
|
|
284
|
+
print(f"Cause: {err.__cause__}")
|
|
285
|
+
```
|
|
286
|
+
|
|
261
287
|
DomainError instances pass through unchanged; works on sync and async callables; set `capture=False` to omit arguments (e.g., for secrets).
|
|
262
288
|
|
|
289
|
+
## Service Class Example
|
|
290
|
+
|
|
291
|
+
A complete service class pattern with error wrapping and context capture:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from domain_errors import DomainError, ErrorChain
|
|
295
|
+
|
|
296
|
+
class PaymentService:
|
|
297
|
+
@staticmethod
|
|
298
|
+
def process_payment(amount_cents: int) -> dict:
|
|
299
|
+
try:
|
|
300
|
+
if amount_cents <= 0:
|
|
301
|
+
raise ValueError("Amount must be positive")
|
|
302
|
+
if amount_cents > 999999:
|
|
303
|
+
raise ValueError("Amount exceeds maximum")
|
|
304
|
+
return {"status": "success", "amount_cents": amount_cents}
|
|
305
|
+
except ValueError as e:
|
|
306
|
+
raise ErrorChain.wrap(
|
|
307
|
+
e,
|
|
308
|
+
as_=PaymentError,
|
|
309
|
+
message=f"Payment validation failed: {e}",
|
|
310
|
+
amount_cents=amount_cents
|
|
311
|
+
) from e
|
|
312
|
+
|
|
313
|
+
class PaymentError(DomainError):
|
|
314
|
+
code = "payment_error"
|
|
315
|
+
domain = "payment"
|
|
316
|
+
http_status = 400
|
|
317
|
+
default_message = "Payment processing failed."
|
|
318
|
+
|
|
319
|
+
# Usage
|
|
320
|
+
try:
|
|
321
|
+
result = PaymentService.process_payment(5000)
|
|
322
|
+
print(f"Success: {result}")
|
|
323
|
+
except PaymentError as err:
|
|
324
|
+
print(f"Error {err.code} in {err.domain}: {err.message}")
|
|
325
|
+
print(f"Context: {err.context}")
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Output:
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
✓ Successful payment: {'status': 'success', 'amount_cents': 5000}
|
|
332
|
+
✓ Caught PaymentError: code=payment_error, domain=payment
|
|
333
|
+
Message: Payment validation failed: Amount must be positive
|
|
334
|
+
Context: {'amount_cents': -100}
|
|
335
|
+
Chain length: 2
|
|
336
|
+
✓ Caught PaymentError: amount_cents=9999999
|
|
337
|
+
```
|
|
338
|
+
|
|
263
339
|
## Documentation
|
|
264
340
|
|
|
265
|
-
- **DomainError Base:** [docs/apps/domain_error.md](docs/apps/domain_error.md)
|
|
266
|
-
- **ErrorChain:** [docs/apps/chain.md](docs/apps/chain.md)
|
|
267
|
-
- **@wrap_errors Decorator:** [docs/apps/wrap_errors.md](docs/apps/wrap_errors.md)
|
|
268
|
-
- **Architecture:** [docs/apps/diagrams.md](docs/apps/diagrams.md)
|
|
341
|
+
- **DomainError Base:** [docs/apps/domain_error.md](docs/apps/domain_error.md): class contract, subclassing, message/context semantics
|
|
342
|
+
- **ErrorChain:** [docs/apps/chain.md](docs/apps/chain.md): wrap, history, crossings, logging integration
|
|
343
|
+
- **@wrap_errors Decorator:** [docs/apps/wrap_errors.md](docs/apps/wrap_errors.md): declarative error wrapping for functions and async callables
|
|
344
|
+
- **Architecture:** [docs/apps/diagrams.md](docs/apps/diagrams.md): data flow and domain relationships
|
|
269
345
|
|
|
270
346
|
## License
|
|
271
347
|
|
|
272
|
-
Apache 2.0
|
|
348
|
+
Apache 2.0; see LICENSE file.
|
|
273
349
|
|
|
274
350
|
## Contributing
|
|
275
351
|
|
|
@@ -33,10 +33,10 @@ def wrap(
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
**Parameters:**
|
|
36
|
-
- `err
|
|
37
|
-
- `as_
|
|
38
|
-
- `message
|
|
39
|
-
- `**context
|
|
36
|
+
- `err`: the caught exception to wrap
|
|
37
|
+
- `as_`: the target DomainError subclass
|
|
38
|
+
- `message`: optional custom message (defaults to the class `default_message`)
|
|
39
|
+
- `**context`: structured context data (e.g., `user_id=123, attempt=2`)
|
|
40
40
|
|
|
41
41
|
**Example:**
|
|
42
42
|
```python
|
|
@@ -79,8 +79,8 @@ def history(
|
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
**Parameters:**
|
|
82
|
-
- `err
|
|
83
|
-
- `classifiers
|
|
82
|
+
- `err`: the root exception (typically the caught exception in an except block)
|
|
83
|
+
- `classifiers`: optional domain classifiers to resolve domains for foreign exceptions
|
|
84
84
|
|
|
85
85
|
**Returns:** A tuple of `ChainLink` objects, root exception first, following `__cause__` (CAUSE via) then `__context__` (CONTEXT via) unless suppressed.
|
|
86
86
|
|
|
@@ -119,8 +119,8 @@ def crossings(
|
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
**Parameters:**
|
|
122
|
-
- `err
|
|
123
|
-
- `classifiers
|
|
122
|
+
- `err`: the root exception
|
|
123
|
+
- `classifiers`: optional domain classifiers
|
|
124
124
|
|
|
125
125
|
**Returns:** A tuple of `DomainCrossing` objects, one for each link-to-link transition where domains differ.
|
|
126
126
|
|
|
@@ -172,7 +172,7 @@ class ChainLink:
|
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
**Methods:**
|
|
175
|
-
- `to_log_extra() → dict[str, object]
|
|
175
|
+
- `to_log_extra() → dict[str, object]`: Convert to a JSON-ready dict for logger `extra` parameter.
|
|
176
176
|
|
|
177
177
|
### `DomainCrossing`
|
|
178
178
|
|
|
@@ -186,7 +186,7 @@ class DomainCrossing:
|
|
|
186
186
|
```
|
|
187
187
|
|
|
188
188
|
**Methods:**
|
|
189
|
-
- `to_log_extra() → dict[str, object]
|
|
189
|
+
- `to_log_extra() → dict[str, object]`: Convert to a JSON-ready dict for logger `extra` parameter.
|
|
190
190
|
|
|
191
191
|
### `DomainClassifier` (Protocol)
|
|
192
192
|
|
|
@@ -60,7 +60,7 @@ def classify(self, err: BaseException) -> str | None:
|
|
|
60
60
|
|
|
61
61
|
### Parameters
|
|
62
62
|
|
|
63
|
-
- `err
|
|
63
|
+
- `err`: any caught exception
|
|
64
64
|
|
|
65
65
|
### Returns
|
|
66
66
|
|
|
@@ -159,6 +159,6 @@ except Exception as e:
|
|
|
159
159
|
|
|
160
160
|
## See Also
|
|
161
161
|
|
|
162
|
-
- **ErrorChain:** `docs/apps/chain.md
|
|
163
|
-
- **DomainError Base:** `docs/apps/domain_error.md
|
|
164
|
-
- **Architecture:** `docs/apps/diagrams.md
|
|
162
|
+
- **ErrorChain:** `docs/apps/chain.md`: how to use classifiers with history/crossings
|
|
163
|
+
- **DomainError Base:** `docs/apps/domain_error.md`: how to define domain-specific errors
|
|
164
|
+
- **Architecture:** `docs/apps/diagrams.md`: system diagram with classifier placement
|
|
@@ -175,7 +175,7 @@ domains/http/http_client.py (imports: from __future__ import annotations
|
|
|
175
175
|
│ BY-ORIGIN: maps any exception DEFINED IN an HTTP-client library │
|
|
176
176
|
│ to const.HTTP. No import of the libs (string check on │
|
|
177
177
|
│ type(err).__module__), so NO optional-dep machinery AND complete │
|
|
178
|
-
│ coverage of each lib's whole exception surface
|
|
178
|
+
│ coverage of each lib's whole exception surface: including httpx │
|
|
179
179
|
│ InvalidURL / CookieConflict / StreamError, which sit OUTSIDE the │
|
|
180
180
|
│ httpx.HTTPError tree (a base-class catch would miss them). │
|
|
181
181
|
│ │
|
|
@@ -60,7 +60,7 @@ def classify(self, err: BaseException) -> str | None:
|
|
|
60
60
|
|
|
61
61
|
### Parameters
|
|
62
62
|
|
|
63
|
-
- `err
|
|
63
|
+
- `err`: any caught exception
|
|
64
64
|
|
|
65
65
|
### Returns
|
|
66
66
|
|
|
@@ -153,6 +153,6 @@ except Exception as e:
|
|
|
153
153
|
|
|
154
154
|
## See Also
|
|
155
155
|
|
|
156
|
-
- **ErrorChain:** `docs/apps/chain.md
|
|
157
|
-
- **DomainError Base:** `docs/apps/domain_error.md
|
|
158
|
-
- **Architecture:** `docs/apps/diagrams.md
|
|
156
|
+
- **ErrorChain:** `docs/apps/chain.md`: how to use classifiers with history/crossings
|
|
157
|
+
- **DomainError Base:** `docs/apps/domain_error.md`: how to define domain-specific errors
|
|
158
|
+
- **Architecture:** `docs/apps/diagrams.md`: system diagram with classifier placement
|
|
@@ -51,10 +51,10 @@ _FAMILIES: tuple[tuple[tuple[type[BaseException], ...], str], ...] = (
|
|
|
51
51
|
|
|
52
52
|
The classifier maps to four coarse domains:
|
|
53
53
|
|
|
54
|
-
- **`"network"
|
|
55
|
-
- **`"os"
|
|
56
|
-
- **`"logic"
|
|
57
|
-
- **`"assertion"
|
|
54
|
+
- **`"network"`**: Network-layer exceptions: `ConnectionError`, `TimeoutError`
|
|
55
|
+
- **`"os"`**: Operating system and I/O exceptions: `FileNotFoundError`, `PermissionError`, `OSError` (and subclasses)
|
|
56
|
+
- **`"logic"`**: Logical/programming errors: `ValueError`, `KeyError`, `TypeError`
|
|
57
|
+
- **`"assertion"`**: Assertion failures: `AssertionError`
|
|
58
58
|
|
|
59
59
|
These constants are exported from `domain_errors/domains/constants/python.py`:
|
|
60
60
|
|
|
@@ -71,7 +71,7 @@ def classify(self, err: BaseException) -> str | None:
|
|
|
71
71
|
|
|
72
72
|
### Parameters
|
|
73
73
|
|
|
74
|
-
- `err
|
|
74
|
+
- `err`: any caught exception
|
|
75
75
|
|
|
76
76
|
### Returns
|
|
77
77
|
|
|
@@ -161,6 +161,6 @@ except Exception as e:
|
|
|
161
161
|
|
|
162
162
|
## See Also
|
|
163
163
|
|
|
164
|
-
- **ErrorChain:** `docs/apps/chain.md
|
|
165
|
-
- **DomainError Base:** `docs/apps/domain_error.md
|
|
166
|
-
- **Architecture:** `docs/apps/diagrams.md
|
|
164
|
+
- **ErrorChain:** `docs/apps/chain.md`: how to use classifiers with history/crossings
|
|
165
|
+
- **DomainError Base:** `docs/apps/domain_error.md`: how to define domain-specific errors
|
|
166
|
+
- **Architecture:** `docs/apps/diagrams.md`: system diagram with classifier placement
|