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.
Files changed (96) hide show
  1. {domain_errors-0.1.0 → domain_errors-0.2.1}/CHANGELOG.md +15 -3
  2. {domain_errors-0.1.0 → domain_errors-0.2.1}/CONTRIBUTING.md +2 -2
  3. {domain_errors-0.1.0 → domain_errors-0.2.1}/PKG-INFO +102 -26
  4. {domain_errors-0.1.0 → domain_errors-0.2.1}/README.md +101 -25
  5. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/chain.md +10 -10
  6. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/cloud.md +4 -4
  7. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/diagrams.md +1 -1
  8. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/http.md +4 -4
  9. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/python.md +8 -8
  10. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/wrap_errors.md +63 -10
  11. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-13-chain-security-audit.md +2 -2
  12. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-14-wrap_errors-security-audit.md +5 -5
  13. domain_errors-0.2.1/docs/reviews/2026-06-13-chain-review.md +49 -0
  14. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-13-domain_error-review.md +1 -1
  15. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-14-http-review.md +1 -1
  16. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-14-python-review.md +1 -1
  17. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-14-wrap_errors-review.md +38 -38
  18. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/config/_version.py +1 -1
  19. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/tests/test_wrap_errors/test_wrap_errors_client.py +342 -0
  20. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/wrap_errors/wrap_errors_client.py +62 -6
  21. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/test_chain/conftest.py +1 -1
  22. {domain_errors-0.1.0 → domain_errors-0.2.1}/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-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  25. {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/ci.yml +0 -0
  28. {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/cleanup-guard.yml +0 -0
  29. {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/dto-strict.yml +0 -0
  30. {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/no-ai-attribution.yml +0 -0
  31. {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/publish.yml +0 -0
  32. {domain_errors-0.1.0 → domain_errors-0.2.1}/.github/workflows/ruff.yml +0 -0
  33. {domain_errors-0.1.0 → domain_errors-0.2.1}/.gitignore +0 -0
  34. {domain_errors-0.1.0 → domain_errors-0.2.1}/CODE_OF_CONDUCT.md +0 -0
  35. {domain_errors-0.1.0 → domain_errors-0.2.1}/LICENSE +0 -0
  36. {domain_errors-0.1.0 → domain_errors-0.2.1}/SECURITY.md +0 -0
  37. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/apps/domain_error.md +0 -0
  38. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/architecture/.gitkeep +0 -0
  39. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/.gitkeep +0 -0
  40. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-13-domain_error-security-audit.md +0 -0
  41. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-14-cloud-security-audit.md +0 -0
  42. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-14-http-security-audit.md +0 -0
  43. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/audits/2026-06-14-python-security-audit.md +0 -0
  44. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/.gitkeep +0 -0
  45. {domain_errors-0.1.0 → domain_errors-0.2.1}/docs/reviews/2026-06-14-cloud-review.md +0 -0
  46. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/__init__.py +0 -0
  47. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/common/__init__.py +0 -0
  48. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/common/constants/__init__.py +0 -0
  49. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/common/constants/tests.py +0 -0
  50. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/common/tests/__init__.py +0 -0
  51. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/config/__init__.py +0 -0
  52. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/__init__.py +0 -0
  53. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/tests/__init__.py +0 -0
  54. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/tests/test_wrap_errors/__init__.py +0 -0
  55. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/tests/test_wrap_errors/conftest.py +0 -0
  56. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/decorators/wrap_errors/__init__.py +0 -0
  57. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/__init__.py +0 -0
  58. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/cloud/__init__.py +0 -0
  59. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/cloud/cloud_client.py +0 -0
  60. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/__init__.py +0 -0
  61. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/cloud.py +0 -0
  62. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/domain_error.py +0 -0
  63. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/http.py +0 -0
  64. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/constants/python.py +0 -0
  65. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/domain_error/__init__.py +0 -0
  66. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/domain_error/domain_error.py +0 -0
  67. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/http/__init__.py +0 -0
  68. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/http/http_client.py +0 -0
  69. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/python/__init__.py +0 -0
  70. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/python/python_client.py +0 -0
  71. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/__init__.py +0 -0
  72. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_cloud/__init__.py +0 -0
  73. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_cloud/conftest.py +0 -0
  74. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_cloud/test_cloud_client.py +0 -0
  75. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_domain_error/__init__.py +0 -0
  76. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_domain_error/conftest.py +0 -0
  77. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_domain_error/test_domain_error.py +0 -0
  78. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_http/__init__.py +0 -0
  79. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_http/conftest.py +0 -0
  80. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_http/test_http_client.py +0 -0
  81. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_python/__init__.py +0 -0
  82. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_python/conftest.py +0 -0
  83. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/domains/tests/test_python/test_python_client.py +0 -0
  84. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/py.typed +0 -0
  85. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/__init__.py +0 -0
  86. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/chain/__init__.py +0 -0
  87. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/chain/chain_client.py +0 -0
  88. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/chain/chain_objects.py +0 -0
  89. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/constants/__init__.py +0 -0
  90. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/constants/chain.py +0 -0
  91. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/constants/services.py +0 -0
  92. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/__init__.py +0 -0
  93. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/test_chain/__init__.py +0 -0
  94. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/test_chain/test_chain_client.py +0 -0
  95. {domain_errors-0.1.0 → domain_errors-0.2.1}/domain_errors/services/tests/test_chain/test_chain_objects.py +0 -0
  96. {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)` construct typed domain errors.
18
- - `history(err, classifiers=())` walk exception cascade into ChainLink objects.
19
- - `crossings(err, classifiers=())` identify domain boundary crossings.
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 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.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
  [![PyPI](https://img.shields.io/pypi/v/domain-errors)](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 boundariesall ready for structured logging.
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 domainsdatabase errors become API errors become payment errors. `domain-errors` makes this hierarchy explicit and traceable.
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` the error message
169
- - `context: dict[str, object]` structured context data
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)` 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.
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) 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
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 see LICENSE file.
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
  [![PyPI](https://img.shields.io/pypi/v/domain-errors)](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 boundariesall ready for structured logging.
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 domainsdatabase errors become API errors become payment errors. `domain-errors` makes this hierarchy explicit and traceable.
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` the error message
147
- - `context: dict[str, object]` structured context data
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)` 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.
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) 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
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 see LICENSE file.
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` 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