domain-errors 0.1.0__tar.gz → 0.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 (97) hide show
  1. {domain_errors-0.1.0 → domain_errors-0.2.0}/CHANGELOG.md +9 -3
  2. {domain_errors-0.1.0 → domain_errors-0.2.0}/CONTRIBUTING.md +2 -2
  3. {domain_errors-0.1.0 → domain_errors-0.2.0}/PKG-INFO +100 -26
  4. {domain_errors-0.1.0 → domain_errors-0.2.0}/README.md +99 -25
  5. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/apps/chain.md +10 -10
  6. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/apps/cloud.md +4 -4
  7. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/apps/diagrams.md +1 -1
  8. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/apps/http.md +4 -4
  9. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/apps/python.md +8 -8
  10. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/apps/wrap_errors.md +63 -10
  11. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/audits/2026-06-13-chain-security-audit.md +2 -2
  12. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/audits/2026-06-14-wrap_errors-security-audit.md +5 -5
  13. domain_errors-0.2.0/docs/reviews/2026-06-13-chain-review.md +49 -0
  14. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/reviews/2026-06-13-domain_error-review.md +1 -1
  15. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/reviews/2026-06-14-http-review.md +1 -1
  16. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/reviews/2026-06-14-python-review.md +1 -1
  17. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/reviews/2026-06-14-wrap_errors-review.md +38 -38
  18. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/config/_version.py +1 -1
  19. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/decorators/tests/test_wrap_errors/test_wrap_errors_client.py +342 -0
  20. domain_errors-0.2.0/domain_errors/decorators/wrap_errors/wrap_errors_client.py +165 -0
  21. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/tests/test_chain/conftest.py +1 -1
  22. {domain_errors-0.1.0 → domain_errors-0.2.0}/pyproject.toml +1 -0
  23. domain_errors-0.1.0/docs/reviews/2026-06-13-chain-review.md +0 -49
  24. domain_errors-0.1.0/domain_errors/decorators/wrap_errors/wrap_errors_client.py +0 -95
  25. {domain_errors-0.1.0 → domain_errors-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  26. {domain_errors-0.1.0 → domain_errors-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  27. {domain_errors-0.1.0 → domain_errors-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  28. {domain_errors-0.1.0 → domain_errors-0.2.0}/.github/workflows/ci.yml +0 -0
  29. {domain_errors-0.1.0 → domain_errors-0.2.0}/.github/workflows/cleanup-guard.yml +0 -0
  30. {domain_errors-0.1.0 → domain_errors-0.2.0}/.github/workflows/dto-strict.yml +0 -0
  31. {domain_errors-0.1.0 → domain_errors-0.2.0}/.github/workflows/no-ai-attribution.yml +0 -0
  32. {domain_errors-0.1.0 → domain_errors-0.2.0}/.github/workflows/publish.yml +0 -0
  33. {domain_errors-0.1.0 → domain_errors-0.2.0}/.github/workflows/ruff.yml +0 -0
  34. {domain_errors-0.1.0 → domain_errors-0.2.0}/.gitignore +0 -0
  35. {domain_errors-0.1.0 → domain_errors-0.2.0}/CODE_OF_CONDUCT.md +0 -0
  36. {domain_errors-0.1.0 → domain_errors-0.2.0}/LICENSE +0 -0
  37. {domain_errors-0.1.0 → domain_errors-0.2.0}/SECURITY.md +0 -0
  38. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/apps/domain_error.md +0 -0
  39. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/architecture/.gitkeep +0 -0
  40. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/audits/.gitkeep +0 -0
  41. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/audits/2026-06-13-domain_error-security-audit.md +0 -0
  42. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/audits/2026-06-14-cloud-security-audit.md +0 -0
  43. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/audits/2026-06-14-http-security-audit.md +0 -0
  44. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/audits/2026-06-14-python-security-audit.md +0 -0
  45. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/reviews/.gitkeep +0 -0
  46. {domain_errors-0.1.0 → domain_errors-0.2.0}/docs/reviews/2026-06-14-cloud-review.md +0 -0
  47. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/__init__.py +0 -0
  48. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/common/__init__.py +0 -0
  49. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/common/constants/__init__.py +0 -0
  50. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/common/constants/tests.py +0 -0
  51. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/common/tests/__init__.py +0 -0
  52. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/config/__init__.py +0 -0
  53. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/decorators/__init__.py +0 -0
  54. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/decorators/tests/__init__.py +0 -0
  55. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/decorators/tests/test_wrap_errors/__init__.py +0 -0
  56. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/decorators/tests/test_wrap_errors/conftest.py +0 -0
  57. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/decorators/wrap_errors/__init__.py +0 -0
  58. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/__init__.py +0 -0
  59. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/cloud/__init__.py +0 -0
  60. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/cloud/cloud_client.py +0 -0
  61. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/constants/__init__.py +0 -0
  62. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/constants/cloud.py +0 -0
  63. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/constants/domain_error.py +0 -0
  64. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/constants/http.py +0 -0
  65. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/constants/python.py +0 -0
  66. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/domain_error/__init__.py +0 -0
  67. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/domain_error/domain_error.py +0 -0
  68. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/http/__init__.py +0 -0
  69. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/http/http_client.py +0 -0
  70. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/python/__init__.py +0 -0
  71. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/python/python_client.py +0 -0
  72. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/__init__.py +0 -0
  73. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_cloud/__init__.py +0 -0
  74. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_cloud/conftest.py +0 -0
  75. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_cloud/test_cloud_client.py +0 -0
  76. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_domain_error/__init__.py +0 -0
  77. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_domain_error/conftest.py +0 -0
  78. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_domain_error/test_domain_error.py +0 -0
  79. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_http/__init__.py +0 -0
  80. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_http/conftest.py +0 -0
  81. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_http/test_http_client.py +0 -0
  82. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_python/__init__.py +0 -0
  83. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_python/conftest.py +0 -0
  84. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/domains/tests/test_python/test_python_client.py +0 -0
  85. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/py.typed +0 -0
  86. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/__init__.py +0 -0
  87. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/chain/__init__.py +0 -0
  88. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/chain/chain_client.py +0 -0
  89. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/chain/chain_objects.py +0 -0
  90. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/constants/__init__.py +0 -0
  91. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/constants/chain.py +0 -0
  92. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/constants/services.py +0 -0
  93. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/tests/__init__.py +0 -0
  94. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/tests/test_chain/__init__.py +0 -0
  95. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/tests/test_chain/test_chain_client.py +0 -0
  96. {domain_errors-0.1.0 → domain_errors-0.2.0}/domain_errors/services/tests/test_chain/test_chain_objects.py +0 -0
  97. {domain_errors-0.1.0 → domain_errors-0.2.0}/uv.lock +0 -0
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-07-06
11
+
12
+ ### Added
13
+
14
+ - **@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.
15
+
10
16
  ## [0.1.0] - 2026-06-15
11
17
 
12
18
  ### Added
@@ -14,9 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
20
  - **DomainError base class:** Typed exception hierarchy with class contract (code, domain, http_status, retryable, default_message).
15
21
  - **Instance state:** message (string) and context (dict) for structured logging.
16
22
  - **ErrorChain service:** Static methods for wrapping, walking, and analyzing exception chains.
17
- - `wrap(err, as_=ErrorType, message=None, **context)` construct typed domain errors.
18
- - `history(err, classifiers=())` walk exception cascade into ChainLink objects.
19
- - `crossings(err, classifiers=())` identify domain boundary crossings.
23
+ - `wrap(err, as_=ErrorType, message=None, **context)`: construct typed domain errors.
24
+ - `history(err, classifiers=())`: walk exception cascade into ChainLink objects.
25
+ - `crossings(err, classifiers=())`: identify domain boundary crossings.
20
26
  - **ChainLink value object:** Structured representation of one exception chain hop, with `to_log_extra()` for JSON-ready logging.
21
27
  - **DomainCrossing value object:** Causation hop where errors cross domain boundaries, with `to_log_extra()` for logging.
22
28
  - **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 containerno module-level functions. Containers keep methods organized.
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 codeno co-author or AI attribution trailers.
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.0
3
+ Version: 0.2.0
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
@@ -29,11 +29,11 @@ Description-Content-Type: text/markdown
29
29
 
30
30
  Typed domain error hierarchy with wrapping and chaining for Python services.
31
31
 
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 boundariesall ready for structured logging.
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: all ready for structured logging.
33
33
 
34
34
  ## Why?
35
35
 
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 domainsdatabase errors become API errors become payment errors. `domain-errors` makes this hierarchy explicit and traceable.
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: database errors become API errors become payment errors. `domain-errors` makes this hierarchy explicit and traceable.
37
37
 
38
38
  ```python
39
39
  from domain_errors import DomainError, ErrorChain
@@ -165,17 +165,17 @@ error = MyError(
165
165
  ```
166
166
 
167
167
  **Attributes:**
168
- - `message: str` the error message
169
- - `context: dict[str, object]` structured context data
168
+ - `message: str`: the error message
169
+ - `context: dict[str, object]`: structured context data
170
170
 
171
171
  ### ErrorChain
172
172
 
173
173
  Static methods for wrapping, walking, and analyzing exception chains.
174
174
 
175
175
  **Methods:**
176
- - `ErrorChain.wrap(err, as_=ErrorType, message=None, **context)` Construct a typed domain error for the caller to raise `from err`.
177
- - `ErrorChain.history(err, classifiers=())` Walk the exception chain; return a tuple of `ChainLink` objects.
178
- - `ErrorChain.crossings(err, classifiers=())` Find causation hops where errors crossed domains; return a tuple of `DomainCrossing` objects.
176
+ - `ErrorChain.wrap(err, as_=ErrorType, message=None, **context)`: Construct a typed domain error for the caller to raise `from err`.
177
+ - `ErrorChain.history(err, classifiers=())`: Walk the exception chain; return a tuple of `ChainLink` objects.
178
+ - `ErrorChain.crossings(err, classifiers=())`: Find causation hops where errors crossed domains; return a tuple of `DomainCrossing` objects.
179
179
 
180
180
  ### ChainLink
181
181
 
@@ -245,7 +245,9 @@ links = ErrorChain.history(err, classifiers=(python, http, cloud))
245
245
 
246
246
  ### @wrap_errors
247
247
 
248
- Decorator for automatic error wrapping. Catches specified exceptions, wraps them into a target DomainError, and captures function arguments for structured logging:
248
+ 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).
249
+
250
+ **Sync and async functions:**
249
251
 
250
252
  ```python
251
253
  from domain_errors import DomainError, wrap_errors
@@ -260,18 +262,6 @@ def read_config(path: str) -> str:
260
262
  with open(path) as f:
261
263
  return f.read()
262
264
 
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
265
  try:
276
266
  config = read_config("/etc/app.yaml")
277
267
  except StorageError as err:
@@ -280,18 +270,102 @@ except StorageError as err:
280
270
  pass
281
271
  ```
282
272
 
273
+ **Service classes (class-level wrapping):**
274
+
275
+ ```python
276
+ from domain_errors import DomainError, wrap_errors
277
+
278
+ class PaymentError(DomainError):
279
+ code = "payment_error"
280
+ domain = "payment"
281
+ http_status = 402
282
+
283
+ @wrap_errors(PaymentError, catch=(ValueError,))
284
+ class PaymentService:
285
+ def validate_amount(self, amount: int) -> bool:
286
+ if amount <= 0:
287
+ raise ValueError("Amount must be positive")
288
+ return True
289
+
290
+ async def process_payment(self, card: str, amount: int) -> str:
291
+ if not card:
292
+ raise ValueError("Card required")
293
+ return f"Processed {amount}"
294
+
295
+ service = PaymentService()
296
+
297
+ assert service.validate_amount(100)
298
+
299
+ try:
300
+ service.validate_amount(-50)
301
+ except PaymentError as err:
302
+ print(f"Error: {err.message}")
303
+ print(f"Context: {err.context}")
304
+ print(f"Cause: {err.__cause__}")
305
+ ```
306
+
283
307
  DomainError instances pass through unchanged; works on sync and async callables; set `capture=False` to omit arguments (e.g., for secrets).
284
308
 
309
+ ## Service Class Example
310
+
311
+ A complete service class pattern with error wrapping and context capture:
312
+
313
+ ```python
314
+ from domain_errors import DomainError, ErrorChain
315
+
316
+ class PaymentService:
317
+ @staticmethod
318
+ def process_payment(amount_cents: int) -> dict:
319
+ try:
320
+ if amount_cents <= 0:
321
+ raise ValueError("Amount must be positive")
322
+ if amount_cents > 999999:
323
+ raise ValueError("Amount exceeds maximum")
324
+ return {"status": "success", "amount_cents": amount_cents}
325
+ except ValueError as e:
326
+ raise ErrorChain.wrap(
327
+ e,
328
+ as_=PaymentError,
329
+ message=f"Payment validation failed: {e}",
330
+ amount_cents=amount_cents
331
+ ) from e
332
+
333
+ class PaymentError(DomainError):
334
+ code = "payment_error"
335
+ domain = "payment"
336
+ http_status = 400
337
+ default_message = "Payment processing failed."
338
+
339
+ # Usage
340
+ try:
341
+ result = PaymentService.process_payment(5000)
342
+ print(f"Success: {result}")
343
+ except PaymentError as err:
344
+ print(f"Error {err.code} in {err.domain}: {err.message}")
345
+ print(f"Context: {err.context}")
346
+ ```
347
+
348
+ Output:
349
+
350
+ ```
351
+ ✓ Successful payment: {'status': 'success', 'amount_cents': 5000}
352
+ ✓ Caught PaymentError: code=payment_error, domain=payment
353
+ Message: Payment validation failed: Amount must be positive
354
+ Context: {'amount_cents': -100}
355
+ Chain length: 2
356
+ ✓ Caught PaymentError: amount_cents=9999999
357
+ ```
358
+
285
359
  ## Documentation
286
360
 
287
- - **DomainError Base:** [docs/apps/domain_error.md](docs/apps/domain_error.md) class contract, subclassing, message/context semantics
288
- - **ErrorChain:** [docs/apps/chain.md](docs/apps/chain.md) wrap, history, crossings, logging integration
289
- - **@wrap_errors Decorator:** [docs/apps/wrap_errors.md](docs/apps/wrap_errors.md) declarative error wrapping for functions and async callables
290
- - **Architecture:** [docs/apps/diagrams.md](docs/apps/diagrams.md) data flow and domain relationships
361
+ - **DomainError Base:** [docs/apps/domain_error.md](docs/apps/domain_error.md): class contract, subclassing, message/context semantics
362
+ - **ErrorChain:** [docs/apps/chain.md](docs/apps/chain.md): wrap, history, crossings, logging integration
363
+ - **@wrap_errors Decorator:** [docs/apps/wrap_errors.md](docs/apps/wrap_errors.md): declarative error wrapping for functions and async callables
364
+ - **Architecture:** [docs/apps/diagrams.md](docs/apps/diagrams.md): data flow and domain relationships
291
365
 
292
366
  ## License
293
367
 
294
- Apache 2.0 see LICENSE file.
368
+ Apache 2.0; see LICENSE file.
295
369
 
296
370
  ## Contributing
297
371
 
@@ -7,11 +7,11 @@
7
7
 
8
8
  Typed domain error hierarchy with wrapping and chaining for Python services.
9
9
 
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 boundariesall ready for structured logging.
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: all ready for structured logging.
11
11
 
12
12
  ## Why?
13
13
 
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 domainsdatabase errors become API errors become payment errors. `domain-errors` makes this hierarchy explicit and traceable.
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: database errors become API errors become payment errors. `domain-errors` makes this hierarchy explicit and traceable.
15
15
 
16
16
  ```python
17
17
  from domain_errors import DomainError, ErrorChain
@@ -143,17 +143,17 @@ error = MyError(
143
143
  ```
144
144
 
145
145
  **Attributes:**
146
- - `message: str` the error message
147
- - `context: dict[str, object]` structured context data
146
+ - `message: str`: the error message
147
+ - `context: dict[str, object]`: structured context data
148
148
 
149
149
  ### ErrorChain
150
150
 
151
151
  Static methods for wrapping, walking, and analyzing exception chains.
152
152
 
153
153
  **Methods:**
154
- - `ErrorChain.wrap(err, as_=ErrorType, message=None, **context)` Construct a typed domain error for the caller to raise `from err`.
155
- - `ErrorChain.history(err, classifiers=())` Walk the exception chain; return a tuple of `ChainLink` objects.
156
- - `ErrorChain.crossings(err, classifiers=())` Find causation hops where errors crossed domains; return a tuple of `DomainCrossing` objects.
154
+ - `ErrorChain.wrap(err, as_=ErrorType, message=None, **context)`: Construct a typed domain error for the caller to raise `from err`.
155
+ - `ErrorChain.history(err, classifiers=())`: Walk the exception chain; return a tuple of `ChainLink` objects.
156
+ - `ErrorChain.crossings(err, classifiers=())`: Find causation hops where errors crossed domains; return a tuple of `DomainCrossing` objects.
157
157
 
158
158
  ### ChainLink
159
159
 
@@ -223,7 +223,9 @@ links = ErrorChain.history(err, classifiers=(python, http, cloud))
223
223
 
224
224
  ### @wrap_errors
225
225
 
226
- Decorator for automatic error wrapping. Catches specified exceptions, wraps them into a target DomainError, and captures function arguments for structured logging:
226
+ 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).
227
+
228
+ **Sync and async functions:**
227
229
 
228
230
  ```python
229
231
  from domain_errors import DomainError, wrap_errors
@@ -238,18 +240,6 @@ def read_config(path: str) -> str:
238
240
  with open(path) as f:
239
241
  return f.read()
240
242
 
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
243
  try:
254
244
  config = read_config("/etc/app.yaml")
255
245
  except StorageError as err:
@@ -258,18 +248,102 @@ except StorageError as err:
258
248
  pass
259
249
  ```
260
250
 
251
+ **Service classes (class-level wrapping):**
252
+
253
+ ```python
254
+ from domain_errors import DomainError, wrap_errors
255
+
256
+ class PaymentError(DomainError):
257
+ code = "payment_error"
258
+ domain = "payment"
259
+ http_status = 402
260
+
261
+ @wrap_errors(PaymentError, catch=(ValueError,))
262
+ class PaymentService:
263
+ def validate_amount(self, amount: int) -> bool:
264
+ if amount <= 0:
265
+ raise ValueError("Amount must be positive")
266
+ return True
267
+
268
+ async def process_payment(self, card: str, amount: int) -> str:
269
+ if not card:
270
+ raise ValueError("Card required")
271
+ return f"Processed {amount}"
272
+
273
+ service = PaymentService()
274
+
275
+ assert service.validate_amount(100)
276
+
277
+ try:
278
+ service.validate_amount(-50)
279
+ except PaymentError as err:
280
+ print(f"Error: {err.message}")
281
+ print(f"Context: {err.context}")
282
+ print(f"Cause: {err.__cause__}")
283
+ ```
284
+
261
285
  DomainError instances pass through unchanged; works on sync and async callables; set `capture=False` to omit arguments (e.g., for secrets).
262
286
 
287
+ ## Service Class Example
288
+
289
+ A complete service class pattern with error wrapping and context capture:
290
+
291
+ ```python
292
+ from domain_errors import DomainError, ErrorChain
293
+
294
+ class PaymentService:
295
+ @staticmethod
296
+ def process_payment(amount_cents: int) -> dict:
297
+ try:
298
+ if amount_cents <= 0:
299
+ raise ValueError("Amount must be positive")
300
+ if amount_cents > 999999:
301
+ raise ValueError("Amount exceeds maximum")
302
+ return {"status": "success", "amount_cents": amount_cents}
303
+ except ValueError as e:
304
+ raise ErrorChain.wrap(
305
+ e,
306
+ as_=PaymentError,
307
+ message=f"Payment validation failed: {e}",
308
+ amount_cents=amount_cents
309
+ ) from e
310
+
311
+ class PaymentError(DomainError):
312
+ code = "payment_error"
313
+ domain = "payment"
314
+ http_status = 400
315
+ default_message = "Payment processing failed."
316
+
317
+ # Usage
318
+ try:
319
+ result = PaymentService.process_payment(5000)
320
+ print(f"Success: {result}")
321
+ except PaymentError as err:
322
+ print(f"Error {err.code} in {err.domain}: {err.message}")
323
+ print(f"Context: {err.context}")
324
+ ```
325
+
326
+ Output:
327
+
328
+ ```
329
+ ✓ Successful payment: {'status': 'success', 'amount_cents': 5000}
330
+ ✓ Caught PaymentError: code=payment_error, domain=payment
331
+ Message: Payment validation failed: Amount must be positive
332
+ Context: {'amount_cents': -100}
333
+ Chain length: 2
334
+ ✓ Caught PaymentError: amount_cents=9999999
335
+ ```
336
+
263
337
  ## Documentation
264
338
 
265
- - **DomainError Base:** [docs/apps/domain_error.md](docs/apps/domain_error.md) class contract, subclassing, message/context semantics
266
- - **ErrorChain:** [docs/apps/chain.md](docs/apps/chain.md) wrap, history, crossings, logging integration
267
- - **@wrap_errors Decorator:** [docs/apps/wrap_errors.md](docs/apps/wrap_errors.md) declarative error wrapping for functions and async callables
268
- - **Architecture:** [docs/apps/diagrams.md](docs/apps/diagrams.md) data flow and domain relationships
339
+ - **DomainError Base:** [docs/apps/domain_error.md](docs/apps/domain_error.md): class contract, subclassing, message/context semantics
340
+ - **ErrorChain:** [docs/apps/chain.md](docs/apps/chain.md): wrap, history, crossings, logging integration
341
+ - **@wrap_errors Decorator:** [docs/apps/wrap_errors.md](docs/apps/wrap_errors.md): declarative error wrapping for functions and async callables
342
+ - **Architecture:** [docs/apps/diagrams.md](docs/apps/diagrams.md): data flow and domain relationships
269
343
 
270
344
  ## License
271
345
 
272
- Apache 2.0 see LICENSE file.
346
+ Apache 2.0; see LICENSE file.
273
347
 
274
348
  ## Contributing
275
349
 
@@ -33,10 +33,10 @@ def wrap(
33
33
  ```
34
34
 
35
35
  **Parameters:**
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`)
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` the root exception (typically the caught exception in an except block)
83
- - `classifiers` optional domain classifiers to resolve domains for foreign exceptions
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` the root exception
123
- - `classifiers` optional domain 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]` Convert to a JSON-ready dict for logger `extra` parameter.
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]` Convert to a JSON-ready dict for logger `extra` parameter.
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` any caught exception
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` 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
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 including httpx │
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` any caught exception
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` 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
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"`** 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`
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` any caught exception
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` 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
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
@@ -9,7 +9,7 @@
9
9
 
10
10
  `wrap_errors` is a function decorator that automatically wraps caught exceptions into a target `DomainError` via `ErrorChain.wrap()`, enabling declarative error handling without boilerplate try-except-raise-from blocks. Use it to convert foreign (non-DomainError) exceptions into domain-specific errors, capture function arguments into error context for structured logging, and let `DomainError` instances pass through unchanged.
11
11
 
12
- It is the suite-wide error-wrapping **standard** for all OSS packagesthe decorator analog of manual `ErrorChain.wrap(...) + raise ... from e`.
12
+ It is the suite-wide error-wrapping **standard** for all OSS packages: the decorator analog of manual `ErrorChain.wrap(...) + raise ... from e`.
13
13
 
14
14
  ## Core Design
15
15
 
@@ -26,7 +26,7 @@ The decorator:
26
26
  - Catches exceptions matching the `catch` tuple
27
27
  - Calls `ErrorChain.wrap(error, as_=..., message=..., **captured_args)` to construct a domain error
28
28
  - Raises the domain error with `raise ... from error` (so `__cause__` is the original exception)
29
- - **Passes `DomainError` (and subclasses) through unchanged**—never re-wraps
29
+ - **Passes `DomainError` (and subclasses) through unchanged**: never re-wraps
30
30
  - Captures the call's bound arguments (with defaults applied) into the domain error's `.context`, unless disabled with `capture=False`
31
31
  - Works transparently on async functions (awaits the coroutine)
32
32
  - Preserves the original function's `__name__`, `__doc__`, and signature via `functools.wraps`
@@ -61,10 +61,10 @@ The returned `WrapErrorsClient` instance is callable; when applied to a function
61
61
 
62
62
  ## Parameters
63
63
 
64
- - **`as_`** the target `DomainError` subclass to wrap into (required, positional). Exceptions matching `catch` will be wrapped into this type via `ErrorChain.wrap()`.
65
- - **`catch`** tuple of `Exception` types to catch (default `(Exception,)`). Only exceptions in this tuple (or subclasses) are wrapped; others propagate raw. Typed as `tuple[type[Exception], ...]`. Note: `BaseException` (e.g., `KeyboardInterrupt`, `SystemExit`) is intentionally NOT catchable.
66
- - **`message`** optional override message passed to the constructed `DomainError` (default `None`). If provided, used instead of `DomainError.default_message`.
67
- - **`capture`** when `True` (default), the decorated call's bound arguments are captured into the `DomainError`'s `.context` dict via `inspect.signature(func).bind(...) + apply_defaults()`. Set `False` to omit arguments (e.g., for functions handling secrets).
64
+ - **`as_`**: the target `DomainError` subclass to wrap into (required, positional). Exceptions matching `catch` will be wrapped into this type via `ErrorChain.wrap()`.
65
+ - **`catch`**: tuple of `Exception` types to catch (default `(Exception,)`). Only exceptions in this tuple (or subclasses) are wrapped; others propagate raw. Typed as `tuple[type[Exception], ...]`. Note: `BaseException` (e.g., `KeyboardInterrupt`, `SystemExit`) is intentionally NOT catchable.
66
+ - **`message`**: optional override message passed to the constructed `DomainError` (default `None`). If provided, used instead of `DomainError.default_message`.
67
+ - **`capture`**: when `True` (default), the decorated call's bound arguments are captured into the `DomainError`'s `.context` dict via `inspect.signature(func).bind(...) + apply_defaults()`. Set `False` to omit arguments (e.g., for functions handling secrets).
68
68
 
69
69
  ## Behavior
70
70
 
@@ -96,7 +96,7 @@ def authenticate(username: str, password: str) -> str:
96
96
  Guidance:
97
97
  - Default to `capture=False` on authentication, payment, and PII-handling functions.
98
98
  - Argument capture is all-or-nothing in v0.1.0; field-level redaction (e.g. via a sensitivity-mixin integration) is a planned future enhancement.
99
- - Captured context is emitted verbatim redaction is the consumer's logging-pipeline responsibility until field-level support ships.
99
+ - Captured context is emitted verbatim: redaction is the consumer's logging-pipeline responsibility until field-level support ships.
100
100
 
101
101
  ## Examples
102
102
 
@@ -245,6 +245,59 @@ except AuthError as err:
245
245
  # Context: {} (no args captured)
246
246
  ```
247
247
 
248
+ ### Example 6: Class-level decoration (fans out to all public methods)
249
+
250
+ ```python
251
+ from domain_errors import DomainError, wrap_errors
252
+ import asyncio
253
+
254
+ class PaymentError(DomainError):
255
+ code = "payment_error"
256
+ domain = "payment"
257
+ http_status = 402
258
+
259
+ @wrap_errors(PaymentError, catch=(ValueError, IOError))
260
+ class PaymentService:
261
+ def __init__(self, rate_limit: int = 100) -> None:
262
+ self.rate_limit = rate_limit
263
+
264
+ def validate_amount(self, amount: int) -> bool:
265
+ if amount <= 0:
266
+ raise ValueError("Amount must be positive")
267
+ if amount > self.rate_limit:
268
+ raise ValueError("Amount exceeds rate limit")
269
+ return True
270
+
271
+ async def charge_card(self, card_token: str, amount: int) -> str:
272
+ if not card_token:
273
+ raise ValueError("Card token required")
274
+ return f"Charged {amount} on {card_token}"
275
+
276
+ service = PaymentService(rate_limit=5000)
277
+
278
+ assert service.validate_amount(100)
279
+
280
+ with pytest.raises(PaymentError) as exc_info:
281
+ service.validate_amount(-50)
282
+ assert exc_info.value.__cause__.__class__ == ValueError
283
+ assert exc_info.value.context == {"amount": -50}
284
+
285
+ result = asyncio.run(service.charge_card("tok_visa", 1000))
286
+ assert "Charged 1000" in result
287
+
288
+ with pytest.raises(PaymentError):
289
+ asyncio.run(service.charge_card("", 1000))
290
+ ```
291
+
292
+ **Class-level behavior:**
293
+ - Decorating a class fans out the decorator over all public callables in `cls.__dict__`.
294
+ - Sync methods get the sync wrapper; async methods get the async wrapper (dispatch preserved per method).
295
+ - Private methods (prefixed with `_`), dunder methods (e.g., `__init__`), properties, and nested classes are skipped.
296
+ - Classmethod and staticmethod are unwrapped, decorated, and rewrapped to preserve their semantics.
297
+ - Methods already decorated with `@wrap_errors` (marked with `__wrap_errors_applied__`) are left untouched (override detection).
298
+ - The `capture`, `message`, and `catch` parameters apply uniformly to all fanned methods.
299
+ - Arguments `self` and `cls` are automatically filtered from the error context to avoid object serialization issues.
300
+
248
301
  ## Usage with ErrorChain
249
302
 
250
303
  After the decorator wraps an error, walk the chain to classify foreign exceptions and log structured data:
@@ -294,6 +347,6 @@ except ServiceError as err:
294
347
 
295
348
  ## See Also
296
349
 
297
- - **ErrorChain:** `docs/apps/chain.md` how to walk chains and use classifiers
298
- - **DomainError Base:** `docs/apps/domain_error.md` how to define domain-specific errors
299
- - **Architecture:** `docs/apps/diagrams.md` system diagram with decorator placement
350
+ - **ErrorChain:** `docs/apps/chain.md`: how to walk chains and use classifiers
351
+ - **DomainError Base:** `docs/apps/domain_error.md`: how to define domain-specific errors
352
+ - **Architecture:** `docs/apps/diagrams.md`: system diagram with decorator placement