fastapi-errors-plus 0.7.0__tar.gz → 0.8.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.
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/PKG-INFO +88 -24
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/README.md +84 -23
- fastapi_errors_plus-0.8.0/fastapi_errors_plus/__init__.py +17 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/fastapi_errors_plus/base.py +43 -57
- fastapi_errors_plus-0.8.0/fastapi_errors_plus/error_doc.py +59 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/fastapi_errors_plus/errors.py +196 -105
- fastapi_errors_plus-0.8.0/fastapi_errors_plus/example_utils.py +52 -0
- fastapi_errors_plus-0.8.0/fastapi_errors_plus/protocol.py +40 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/fastapi_errors_plus.egg-info/PKG-INFO +88 -24
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/fastapi_errors_plus.egg-info/SOURCES.txt +5 -1
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/fastapi_errors_plus.egg-info/requires.txt +3 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/pyproject.toml +19 -1
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/tests/test_app.py +26 -18
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/tests/test_base.py +72 -61
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/tests/test_errors.py +246 -174
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/tests/test_integration.py +93 -92
- fastapi_errors_plus-0.8.0/tests/test_release_08.py +131 -0
- fastapi_errors_plus-0.8.0/tests/test_release_08_edge_cases.py +243 -0
- fastapi_errors_plus-0.7.0/fastapi_errors_plus/__init__.py +0 -14
- fastapi_errors_plus-0.7.0/fastapi_errors_plus/protocol.py +0 -60
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/LICENSE +0 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/fastapi_errors_plus/py.typed +0 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
- {fastapi_errors_plus-0.7.0 → fastapi_errors_plus-0.8.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-errors-plus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Universal library for documenting errors in FastAPI endpoints
|
|
5
5
|
Author-email: Igor Selivanov <seligoroff@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -22,6 +22,9 @@ Provides-Extra: dev
|
|
|
22
22
|
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
23
23
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
24
24
|
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
25
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
25
28
|
Dynamic: license-file
|
|
26
29
|
|
|
27
30
|
# fastapi-errors-plus
|
|
@@ -30,7 +33,7 @@ Dynamic: license-file
|
|
|
30
33
|
[](https://opensource.org/licenses/MIT)
|
|
31
34
|
[](https://www.python.org/downloads/)
|
|
32
35
|
[](https://fastapi.tiangolo.com/)
|
|
33
|
-
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
34
37
|
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
35
38
|
|
|
36
39
|
Universal library for documenting errors in FastAPI endpoints.
|
|
@@ -164,7 +167,7 @@ class MyErrorDTO:
|
|
|
164
167
|
status_code = 404
|
|
165
168
|
message = "Not found"
|
|
166
169
|
|
|
167
|
-
def
|
|
170
|
+
def to_examples(self):
|
|
168
171
|
return {
|
|
169
172
|
"Not found": {
|
|
170
173
|
"value": {"detail": "Not found"},
|
|
@@ -209,7 +212,7 @@ def delete_item(id: int):
|
|
|
209
212
|
|
|
210
213
|
#### OpenAPI extras (`schema`) next to examples
|
|
211
214
|
|
|
212
|
-
ADR-style payloads (`code`, `detail`, optional `context`) need a **`schema`** in the spec besides **`examples`**. On **`BaseErrorDTO`** / **`StandardErrorDTO`** use **`openapi_json_extras`** (a dict merged under `content["application/json"]` — omit `example` / `examples` there; **`
|
|
215
|
+
ADR-style payloads (`code`, `detail`, optional `context`) need a **`schema`** in the spec besides **`examples`**. On **`BaseErrorDTO`** / **`StandardErrorDTO`** / **`ErrorDoc`** use **`openapi_json_extras`** (a dict merged under `content["application/json"]` — omit `example` / `examples` there; **`to_examples()`** still defines examples):
|
|
213
216
|
|
|
214
217
|
```python
|
|
215
218
|
from fastapi import APIRouter, status
|
|
@@ -284,6 +287,37 @@ def delete_item(id: int):
|
|
|
284
287
|
- Reusable across all endpoints
|
|
285
288
|
- Supports inheritance for custom logic
|
|
286
289
|
|
|
290
|
+
`examples` values may be **strings** (shorthand for `{"detail": text}`) or full OpenAPI Example Objects with **`summary`** and **`value`**.
|
|
291
|
+
|
|
292
|
+
#### ErrorDoc
|
|
293
|
+
|
|
294
|
+
For arbitrary response bodies (ADR `code` / `detail` / `context`, not only `detail` strings), use **`ErrorDoc`**:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from fastapi_errors_plus import ErrorDoc, Errors
|
|
298
|
+
|
|
299
|
+
permission_denied = ErrorDoc(
|
|
300
|
+
status_code=403,
|
|
301
|
+
message="Insufficient permissions",
|
|
302
|
+
examples={
|
|
303
|
+
"MissingRole": {
|
|
304
|
+
"summary": "User lacks required role",
|
|
305
|
+
"value": {
|
|
306
|
+
"code": "FORBIDDEN",
|
|
307
|
+
"detail": "Role admin required",
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
openapi_json_extras={"schema": ADR_ERROR_BODY_SCHEMA},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@router.delete("/{id}", responses=Errors(permission_denied))
|
|
315
|
+
def delete_item(id: int):
|
|
316
|
+
...
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
A **plain dict** as an example value is treated as the full response **body** unless it looks like an OpenAPI Example Object (keys only among `value`, `summary`, `description`, `externalValue`).
|
|
320
|
+
|
|
287
321
|
### 5. Mixed Usage
|
|
288
322
|
|
|
289
323
|
Combine flags, dict, and ErrorDTO:
|
|
@@ -363,7 +397,7 @@ Order matters only for overlaps: whichever **`dict`** is applied **later** in th
|
|
|
363
397
|
|
|
364
398
|
## ErrorDTO Protocol
|
|
365
399
|
|
|
366
|
-
The
|
|
400
|
+
The canonical **`ErrorDTO`** protocol defines the interface for error objects compatible with the library:
|
|
367
401
|
|
|
368
402
|
```python
|
|
369
403
|
from typing import Protocol, Dict, Any
|
|
@@ -372,27 +406,30 @@ class ErrorDTO(Protocol):
|
|
|
372
406
|
status_code: int
|
|
373
407
|
message: str
|
|
374
408
|
|
|
375
|
-
def
|
|
376
|
-
"""
|
|
377
|
-
|
|
378
|
-
Returns:
|
|
379
|
-
Dict in format: {"key": {"value": {"detail": "message"}}}
|
|
380
|
-
"""
|
|
409
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
410
|
+
"""OpenAPI ``examples`` map: {"Key": {"value": {...}, "summary": "..."}}."""
|
|
381
411
|
...
|
|
382
412
|
```
|
|
383
413
|
|
|
384
414
|
Any class implementing this protocol (through structural typing) can be used with `Errors()`.
|
|
385
415
|
|
|
416
|
+
**Legacy migration:** classes that only define **`to_example()`** still work at **runtime** (`DeprecationWarning` once per class per `Errors()` call). For static typing use **`LegacyErrorDTO`** or the union alias **`ErrorDTOLike`** (`ErrorDTO | LegacyErrorDTO`).
|
|
417
|
+
|
|
386
418
|
**Best Practice:** For maximum clarity, consider making your domain exceptions implement the ErrorDTO protocol directly. See [Best Practice: Connecting Exceptions and ErrorDTO](#best-practice-connecting-exceptions-and-errordto) for details.
|
|
387
419
|
|
|
388
420
|
### When to Use Protocol vs BaseErrorDTO
|
|
389
421
|
|
|
422
|
+
Optional on custom DTOs (OpenAPI media-type extras without duplicating a status `dict`):
|
|
423
|
+
|
|
424
|
+
- attribute **`openapi_json_extras`**: fragment for **`content["application/json"]`** (often `{"schema": ...}` — not for `example` / `examples`);
|
|
425
|
+
- or method **`to_openapi_json_media_type_extras() -> Optional[dict]`** — when non-empty, overrides **`openapi_json_extras`**.
|
|
426
|
+
|
|
390
427
|
**Use Protocol (structural typing)** when:
|
|
391
428
|
- Your project already has error DTOs that implement the protocol
|
|
392
429
|
- You need maximum flexibility and custom implementations
|
|
393
430
|
- You want to keep your existing error infrastructure
|
|
394
431
|
|
|
395
|
-
**Use BaseErrorDTO/StandardErrorDTO** when:
|
|
432
|
+
**Use BaseErrorDTO/StandardErrorDTO/ErrorDoc** when:
|
|
396
433
|
- Starting a new project or adding error documentation
|
|
397
434
|
- You want a ready-to-use implementation without boilerplate
|
|
398
435
|
- You need multiple examples for standard HTTP errors (401, 403, etc.)
|
|
@@ -403,7 +440,7 @@ Both approaches work together — you can mix them in the same `Errors()` call!
|
|
|
403
440
|
|
|
404
441
|
**Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
|
|
405
442
|
|
|
406
|
-
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `
|
|
443
|
+
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_examples()`) will work, including Pydantic models. Legacy `to_example()` is still accepted at runtime.
|
|
407
444
|
|
|
408
445
|
### Simple Pydantic Model as ErrorDTO
|
|
409
446
|
|
|
@@ -417,8 +454,8 @@ class PydanticErrorDTO(BaseModel):
|
|
|
417
454
|
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
|
|
418
455
|
message: str = Field(..., min_length=1, description="Error message")
|
|
419
456
|
|
|
420
|
-
def
|
|
421
|
-
"""Generate
|
|
457
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
458
|
+
"""Generate examples for OpenAPI."""
|
|
422
459
|
return {
|
|
423
460
|
self.message: {
|
|
424
461
|
"value": {"detail": self.message},
|
|
@@ -461,8 +498,8 @@ class DetailedErrorDTO(BaseModel):
|
|
|
461
498
|
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
462
499
|
timestamp: Optional[str] = Field(None, description="Error timestamp")
|
|
463
500
|
|
|
464
|
-
def
|
|
465
|
-
"""Generate
|
|
501
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
502
|
+
"""Generate examples for OpenAPI."""
|
|
466
503
|
example = {"detail": self.message}
|
|
467
504
|
if self.error_code:
|
|
468
505
|
example["error_code"] = self.error_code
|
|
@@ -519,7 +556,7 @@ class DomainException(Exception):
|
|
|
519
556
|
status_code: int
|
|
520
557
|
message: str
|
|
521
558
|
|
|
522
|
-
def
|
|
559
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
523
560
|
return {self.message: {"value": {"detail": self.message}}}
|
|
524
561
|
|
|
525
562
|
@classmethod
|
|
@@ -562,7 +599,7 @@ See [examples/domain_exceptions.py](examples/domain_exceptions.py) for complete
|
|
|
562
599
|
|
|
563
600
|
## Compatibility with Existing Projects
|
|
564
601
|
|
|
565
|
-
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol:
|
|
602
|
+
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol (or legacy `to_example()` during migration):
|
|
566
603
|
|
|
567
604
|
```python
|
|
568
605
|
# Your existing ApiErrorDTO
|
|
@@ -571,7 +608,7 @@ class ApiErrorDTO:
|
|
|
571
608
|
status_code: int
|
|
572
609
|
message: str
|
|
573
610
|
|
|
574
|
-
def
|
|
611
|
+
def to_examples(self) -> dict:
|
|
575
612
|
return {
|
|
576
613
|
self.message: {
|
|
577
614
|
"value": {"detail": self.message},
|
|
@@ -640,9 +677,13 @@ FastAPI automatically validates all parameters (Path, Query, Body), so 422 is re
|
|
|
640
677
|
```python
|
|
641
678
|
# Instances are Mapping keyed by HTTP status codes
|
|
642
679
|
error_responses = Errors(unauthorized_401=True, forbidden_403=True)
|
|
643
|
-
documented = error_responses[401] #
|
|
680
|
+
documented = error_responses[401] # deep copy — safe to read, won't mutate internal state
|
|
644
681
|
```
|
|
645
682
|
|
|
683
|
+
**Isolation (0.8+):** incoming response **dicts** are **deep-copied** on ingest; **`errors[status]`** returns a **deep copy** so callers cannot corrupt shared registry templates or internal merge state.
|
|
684
|
+
|
|
685
|
+
**Descriptions:** response blocks with only **`model`** (no `description`) get a default from **`HTTPStatus.phrase`** so OpenAPI generation does not fail.
|
|
686
|
+
|
|
646
687
|
### `ErrorDTO`
|
|
647
688
|
|
|
648
689
|
Protocol for error objects compatible with the library.
|
|
@@ -652,9 +693,32 @@ Protocol for error objects compatible with the library.
|
|
|
652
693
|
- `message: str` — Error message description
|
|
653
694
|
|
|
654
695
|
**Required methods:**
|
|
655
|
-
- `
|
|
696
|
+
- `to_examples() -> Dict[str, Any]` — OpenAPI `examples` map for `application/json`
|
|
697
|
+
|
|
698
|
+
During `Errors(...)` initialization, non-`dict` objects in `*errors` missing `status_code`, `message`, or a callable **`to_examples()`** / **`to_example()`** raise **`TypeError`** naming what was missing.
|
|
699
|
+
|
|
700
|
+
### `LegacyErrorDTO` / `ErrorDTOLike`
|
|
701
|
+
|
|
702
|
+
- **`LegacyErrorDTO`** — typing helper for classes that only implement deprecated **`to_example()`**.
|
|
703
|
+
- **`ErrorDTOLike`** — `Union[ErrorDTO, LegacyErrorDTO]` for transitional annotations.
|
|
704
|
+
|
|
705
|
+
### `ErrorDoc`
|
|
706
|
+
|
|
707
|
+
Declarative DTO for arbitrary example bodies and per-example **`summary`**.
|
|
708
|
+
|
|
709
|
+
**Constructor:**
|
|
710
|
+
```python
|
|
711
|
+
ErrorDoc(
|
|
712
|
+
status_code: int,
|
|
713
|
+
message: str,
|
|
714
|
+
examples: Optional[Dict[str, str | dict]] = None,
|
|
715
|
+
body: Optional[Dict[str, Any]] = None,
|
|
716
|
+
example_key: Optional[str] = None,
|
|
717
|
+
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
718
|
+
)
|
|
719
|
+
```
|
|
656
720
|
|
|
657
|
-
|
|
721
|
+
When **`examples`** is omitted, a single example is built from **`body`** or `{"detail": message}`.
|
|
658
722
|
|
|
659
723
|
### `BaseErrorDTO`
|
|
660
724
|
|
|
@@ -793,7 +857,7 @@ class DomainException(Exception):
|
|
|
793
857
|
def __init__(self) -> None:
|
|
794
858
|
super().__init__(self.message)
|
|
795
859
|
|
|
796
|
-
def
|
|
860
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
797
861
|
return {
|
|
798
862
|
self.message: {
|
|
799
863
|
"value": {"detail": self.message},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://www.python.org/downloads/)
|
|
6
6
|
[](https://fastapi.tiangolo.com/)
|
|
7
|
-
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
8
8
|
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
9
9
|
|
|
10
10
|
Universal library for documenting errors in FastAPI endpoints.
|
|
@@ -138,7 +138,7 @@ class MyErrorDTO:
|
|
|
138
138
|
status_code = 404
|
|
139
139
|
message = "Not found"
|
|
140
140
|
|
|
141
|
-
def
|
|
141
|
+
def to_examples(self):
|
|
142
142
|
return {
|
|
143
143
|
"Not found": {
|
|
144
144
|
"value": {"detail": "Not found"},
|
|
@@ -183,7 +183,7 @@ def delete_item(id: int):
|
|
|
183
183
|
|
|
184
184
|
#### OpenAPI extras (`schema`) next to examples
|
|
185
185
|
|
|
186
|
-
ADR-style payloads (`code`, `detail`, optional `context`) need a **`schema`** in the spec besides **`examples`**. On **`BaseErrorDTO`** / **`StandardErrorDTO`** use **`openapi_json_extras`** (a dict merged under `content["application/json"]` — omit `example` / `examples` there; **`
|
|
186
|
+
ADR-style payloads (`code`, `detail`, optional `context`) need a **`schema`** in the spec besides **`examples`**. On **`BaseErrorDTO`** / **`StandardErrorDTO`** / **`ErrorDoc`** use **`openapi_json_extras`** (a dict merged under `content["application/json"]` — omit `example` / `examples` there; **`to_examples()`** still defines examples):
|
|
187
187
|
|
|
188
188
|
```python
|
|
189
189
|
from fastapi import APIRouter, status
|
|
@@ -258,6 +258,37 @@ def delete_item(id: int):
|
|
|
258
258
|
- Reusable across all endpoints
|
|
259
259
|
- Supports inheritance for custom logic
|
|
260
260
|
|
|
261
|
+
`examples` values may be **strings** (shorthand for `{"detail": text}`) or full OpenAPI Example Objects with **`summary`** and **`value`**.
|
|
262
|
+
|
|
263
|
+
#### ErrorDoc
|
|
264
|
+
|
|
265
|
+
For arbitrary response bodies (ADR `code` / `detail` / `context`, not only `detail` strings), use **`ErrorDoc`**:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from fastapi_errors_plus import ErrorDoc, Errors
|
|
269
|
+
|
|
270
|
+
permission_denied = ErrorDoc(
|
|
271
|
+
status_code=403,
|
|
272
|
+
message="Insufficient permissions",
|
|
273
|
+
examples={
|
|
274
|
+
"MissingRole": {
|
|
275
|
+
"summary": "User lacks required role",
|
|
276
|
+
"value": {
|
|
277
|
+
"code": "FORBIDDEN",
|
|
278
|
+
"detail": "Role admin required",
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
openapi_json_extras={"schema": ADR_ERROR_BODY_SCHEMA},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
@router.delete("/{id}", responses=Errors(permission_denied))
|
|
286
|
+
def delete_item(id: int):
|
|
287
|
+
...
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
A **plain dict** as an example value is treated as the full response **body** unless it looks like an OpenAPI Example Object (keys only among `value`, `summary`, `description`, `externalValue`).
|
|
291
|
+
|
|
261
292
|
### 5. Mixed Usage
|
|
262
293
|
|
|
263
294
|
Combine flags, dict, and ErrorDTO:
|
|
@@ -337,7 +368,7 @@ Order matters only for overlaps: whichever **`dict`** is applied **later** in th
|
|
|
337
368
|
|
|
338
369
|
## ErrorDTO Protocol
|
|
339
370
|
|
|
340
|
-
The
|
|
371
|
+
The canonical **`ErrorDTO`** protocol defines the interface for error objects compatible with the library:
|
|
341
372
|
|
|
342
373
|
```python
|
|
343
374
|
from typing import Protocol, Dict, Any
|
|
@@ -346,27 +377,30 @@ class ErrorDTO(Protocol):
|
|
|
346
377
|
status_code: int
|
|
347
378
|
message: str
|
|
348
379
|
|
|
349
|
-
def
|
|
350
|
-
"""
|
|
351
|
-
|
|
352
|
-
Returns:
|
|
353
|
-
Dict in format: {"key": {"value": {"detail": "message"}}}
|
|
354
|
-
"""
|
|
380
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
381
|
+
"""OpenAPI ``examples`` map: {"Key": {"value": {...}, "summary": "..."}}."""
|
|
355
382
|
...
|
|
356
383
|
```
|
|
357
384
|
|
|
358
385
|
Any class implementing this protocol (through structural typing) can be used with `Errors()`.
|
|
359
386
|
|
|
387
|
+
**Legacy migration:** classes that only define **`to_example()`** still work at **runtime** (`DeprecationWarning` once per class per `Errors()` call). For static typing use **`LegacyErrorDTO`** or the union alias **`ErrorDTOLike`** (`ErrorDTO | LegacyErrorDTO`).
|
|
388
|
+
|
|
360
389
|
**Best Practice:** For maximum clarity, consider making your domain exceptions implement the ErrorDTO protocol directly. See [Best Practice: Connecting Exceptions and ErrorDTO](#best-practice-connecting-exceptions-and-errordto) for details.
|
|
361
390
|
|
|
362
391
|
### When to Use Protocol vs BaseErrorDTO
|
|
363
392
|
|
|
393
|
+
Optional on custom DTOs (OpenAPI media-type extras without duplicating a status `dict`):
|
|
394
|
+
|
|
395
|
+
- attribute **`openapi_json_extras`**: fragment for **`content["application/json"]`** (often `{"schema": ...}` — not for `example` / `examples`);
|
|
396
|
+
- or method **`to_openapi_json_media_type_extras() -> Optional[dict]`** — when non-empty, overrides **`openapi_json_extras`**.
|
|
397
|
+
|
|
364
398
|
**Use Protocol (structural typing)** when:
|
|
365
399
|
- Your project already has error DTOs that implement the protocol
|
|
366
400
|
- You need maximum flexibility and custom implementations
|
|
367
401
|
- You want to keep your existing error infrastructure
|
|
368
402
|
|
|
369
|
-
**Use BaseErrorDTO/StandardErrorDTO** when:
|
|
403
|
+
**Use BaseErrorDTO/StandardErrorDTO/ErrorDoc** when:
|
|
370
404
|
- Starting a new project or adding error documentation
|
|
371
405
|
- You want a ready-to-use implementation without boilerplate
|
|
372
406
|
- You need multiple examples for standard HTTP errors (401, 403, etc.)
|
|
@@ -377,7 +411,7 @@ Both approaches work together — you can mix them in the same `Errors()` call!
|
|
|
377
411
|
|
|
378
412
|
**Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
|
|
379
413
|
|
|
380
|
-
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `
|
|
414
|
+
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_examples()`) will work, including Pydantic models. Legacy `to_example()` is still accepted at runtime.
|
|
381
415
|
|
|
382
416
|
### Simple Pydantic Model as ErrorDTO
|
|
383
417
|
|
|
@@ -391,8 +425,8 @@ class PydanticErrorDTO(BaseModel):
|
|
|
391
425
|
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
|
|
392
426
|
message: str = Field(..., min_length=1, description="Error message")
|
|
393
427
|
|
|
394
|
-
def
|
|
395
|
-
"""Generate
|
|
428
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
429
|
+
"""Generate examples for OpenAPI."""
|
|
396
430
|
return {
|
|
397
431
|
self.message: {
|
|
398
432
|
"value": {"detail": self.message},
|
|
@@ -435,8 +469,8 @@ class DetailedErrorDTO(BaseModel):
|
|
|
435
469
|
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
436
470
|
timestamp: Optional[str] = Field(None, description="Error timestamp")
|
|
437
471
|
|
|
438
|
-
def
|
|
439
|
-
"""Generate
|
|
472
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
473
|
+
"""Generate examples for OpenAPI."""
|
|
440
474
|
example = {"detail": self.message}
|
|
441
475
|
if self.error_code:
|
|
442
476
|
example["error_code"] = self.error_code
|
|
@@ -493,7 +527,7 @@ class DomainException(Exception):
|
|
|
493
527
|
status_code: int
|
|
494
528
|
message: str
|
|
495
529
|
|
|
496
|
-
def
|
|
530
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
497
531
|
return {self.message: {"value": {"detail": self.message}}}
|
|
498
532
|
|
|
499
533
|
@classmethod
|
|
@@ -536,7 +570,7 @@ See [examples/domain_exceptions.py](examples/domain_exceptions.py) for complete
|
|
|
536
570
|
|
|
537
571
|
## Compatibility with Existing Projects
|
|
538
572
|
|
|
539
|
-
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol:
|
|
573
|
+
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol (or legacy `to_example()` during migration):
|
|
540
574
|
|
|
541
575
|
```python
|
|
542
576
|
# Your existing ApiErrorDTO
|
|
@@ -545,7 +579,7 @@ class ApiErrorDTO:
|
|
|
545
579
|
status_code: int
|
|
546
580
|
message: str
|
|
547
581
|
|
|
548
|
-
def
|
|
582
|
+
def to_examples(self) -> dict:
|
|
549
583
|
return {
|
|
550
584
|
self.message: {
|
|
551
585
|
"value": {"detail": self.message},
|
|
@@ -614,9 +648,13 @@ FastAPI automatically validates all parameters (Path, Query, Body), so 422 is re
|
|
|
614
648
|
```python
|
|
615
649
|
# Instances are Mapping keyed by HTTP status codes
|
|
616
650
|
error_responses = Errors(unauthorized_401=True, forbidden_403=True)
|
|
617
|
-
documented = error_responses[401] #
|
|
651
|
+
documented = error_responses[401] # deep copy — safe to read, won't mutate internal state
|
|
618
652
|
```
|
|
619
653
|
|
|
654
|
+
**Isolation (0.8+):** incoming response **dicts** are **deep-copied** on ingest; **`errors[status]`** returns a **deep copy** so callers cannot corrupt shared registry templates or internal merge state.
|
|
655
|
+
|
|
656
|
+
**Descriptions:** response blocks with only **`model`** (no `description`) get a default from **`HTTPStatus.phrase`** so OpenAPI generation does not fail.
|
|
657
|
+
|
|
620
658
|
### `ErrorDTO`
|
|
621
659
|
|
|
622
660
|
Protocol for error objects compatible with the library.
|
|
@@ -626,9 +664,32 @@ Protocol for error objects compatible with the library.
|
|
|
626
664
|
- `message: str` — Error message description
|
|
627
665
|
|
|
628
666
|
**Required methods:**
|
|
629
|
-
- `
|
|
667
|
+
- `to_examples() -> Dict[str, Any]` — OpenAPI `examples` map for `application/json`
|
|
668
|
+
|
|
669
|
+
During `Errors(...)` initialization, non-`dict` objects in `*errors` missing `status_code`, `message`, or a callable **`to_examples()`** / **`to_example()`** raise **`TypeError`** naming what was missing.
|
|
670
|
+
|
|
671
|
+
### `LegacyErrorDTO` / `ErrorDTOLike`
|
|
672
|
+
|
|
673
|
+
- **`LegacyErrorDTO`** — typing helper for classes that only implement deprecated **`to_example()`**.
|
|
674
|
+
- **`ErrorDTOLike`** — `Union[ErrorDTO, LegacyErrorDTO]` for transitional annotations.
|
|
675
|
+
|
|
676
|
+
### `ErrorDoc`
|
|
677
|
+
|
|
678
|
+
Declarative DTO for arbitrary example bodies and per-example **`summary`**.
|
|
679
|
+
|
|
680
|
+
**Constructor:**
|
|
681
|
+
```python
|
|
682
|
+
ErrorDoc(
|
|
683
|
+
status_code: int,
|
|
684
|
+
message: str,
|
|
685
|
+
examples: Optional[Dict[str, str | dict]] = None,
|
|
686
|
+
body: Optional[Dict[str, Any]] = None,
|
|
687
|
+
example_key: Optional[str] = None,
|
|
688
|
+
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
689
|
+
)
|
|
690
|
+
```
|
|
630
691
|
|
|
631
|
-
|
|
692
|
+
When **`examples`** is omitted, a single example is built from **`body`** or `{"detail": message}`.
|
|
632
693
|
|
|
633
694
|
### `BaseErrorDTO`
|
|
634
695
|
|
|
@@ -767,7 +828,7 @@ class DomainException(Exception):
|
|
|
767
828
|
def __init__(self) -> None:
|
|
768
829
|
super().__init__(self.message)
|
|
769
830
|
|
|
770
|
-
def
|
|
831
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
771
832
|
return {
|
|
772
833
|
self.message: {
|
|
773
834
|
"value": {"detail": self.message},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Universal library for documenting errors in FastAPI endpoints."""
|
|
2
|
+
|
|
3
|
+
from fastapi_errors_plus.base import BaseErrorDTO, StandardErrorDTO
|
|
4
|
+
from fastapi_errors_plus.error_doc import ErrorDoc
|
|
5
|
+
from fastapi_errors_plus.errors import Errors
|
|
6
|
+
from fastapi_errors_plus.protocol import ErrorDTO, ErrorDTOLike, LegacyErrorDTO
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Errors",
|
|
10
|
+
"ErrorDTO",
|
|
11
|
+
"LegacyErrorDTO",
|
|
12
|
+
"ErrorDTOLike",
|
|
13
|
+
"ErrorDoc",
|
|
14
|
+
"BaseErrorDTO",
|
|
15
|
+
"StandardErrorDTO",
|
|
16
|
+
]
|
|
17
|
+
__version__ = "0.8.0"
|
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
"""Base implementations of ErrorDTO protocol for convenience."""
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from typing import Any, Dict, Optional
|
|
5
6
|
|
|
7
|
+
from fastapi_errors_plus.example_utils import ExampleSpec, _normalize_example_specs
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
@dataclass
|
|
8
11
|
class BaseErrorDTO:
|
|
9
12
|
"""Base implementation of ErrorDTO Protocol.
|
|
10
|
-
|
|
13
|
+
|
|
11
14
|
Projects can use this class directly or create their own implementation
|
|
12
15
|
that implements ErrorDTO Protocol through structural typing.
|
|
13
|
-
|
|
16
|
+
|
|
14
17
|
This class provides a simple way to create error DTOs without writing
|
|
15
18
|
boilerplate code in every project.
|
|
16
|
-
|
|
19
|
+
|
|
17
20
|
Example:
|
|
18
21
|
```python
|
|
19
22
|
from fastapi_errors_plus import Errors, BaseErrorDTO
|
|
20
|
-
|
|
23
|
+
|
|
21
24
|
notification_error = BaseErrorDTO(
|
|
22
25
|
status_code=404,
|
|
23
26
|
message="Notification not found",
|
|
24
27
|
)
|
|
25
|
-
|
|
28
|
+
|
|
26
29
|
@router.delete(
|
|
27
30
|
"/{id}",
|
|
28
31
|
responses=Errors(notification_error),
|
|
@@ -31,51 +34,48 @@ class BaseErrorDTO:
|
|
|
31
34
|
pass
|
|
32
35
|
```
|
|
33
36
|
"""
|
|
37
|
+
|
|
34
38
|
status_code: int
|
|
35
39
|
"""HTTP status code for the error."""
|
|
36
|
-
|
|
40
|
+
|
|
37
41
|
message: str
|
|
38
42
|
"""Error message description."""
|
|
39
|
-
|
|
43
|
+
|
|
40
44
|
openapi_json_extras: Optional[Dict[str, Any]] = field(default=None)
|
|
41
45
|
"""Optional OpenAPI fragment under ``content['application/json']`` besides examples,
|
|
42
46
|
typically ``{\"schema\": ...}`` or ``encoding``. Do **not** put ``example`` / ``examples``
|
|
43
47
|
here — use :meth:`to_example`. Arbitrary implementations may instead implement
|
|
44
48
|
:meth:`to_openapi_json_media_type_extras`; that return value wins over this attribute."""
|
|
45
|
-
|
|
46
|
-
def
|
|
47
|
-
"""Generate
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
Dict in format: {"key": {"value": {"detail": "message"}}}
|
|
51
|
-
|
|
52
|
-
Example:
|
|
53
|
-
```python
|
|
54
|
-
{
|
|
55
|
-
"Notification not found": {
|
|
56
|
-
"value": {"detail": "Notification not found"},
|
|
57
|
-
},
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
"""
|
|
49
|
+
|
|
50
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
51
|
+
"""Generate OpenAPI ``examples`` for this error."""
|
|
61
52
|
return {
|
|
62
53
|
self.message: {
|
|
63
54
|
"value": {"detail": self.message},
|
|
64
55
|
},
|
|
65
56
|
}
|
|
66
57
|
|
|
58
|
+
def to_example(self) -> Dict[str, Any]:
|
|
59
|
+
"""Deprecated alias for :meth:`to_examples`."""
|
|
60
|
+
warnings.warn(
|
|
61
|
+
"to_example() is deprecated; use to_examples() instead.",
|
|
62
|
+
DeprecationWarning,
|
|
63
|
+
stacklevel=2,
|
|
64
|
+
)
|
|
65
|
+
return self.to_examples()
|
|
66
|
+
|
|
67
67
|
|
|
68
68
|
@dataclass
|
|
69
69
|
class StandardErrorDTO(BaseErrorDTO):
|
|
70
70
|
"""Extended implementation for errors with multiple examples.
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
Useful for standard HTTP errors (401, 403) that can have different
|
|
73
73
|
causes with different messages.
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
Example:
|
|
76
76
|
```python
|
|
77
77
|
from fastapi_errors_plus import Errors, StandardErrorDTO
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
unauthorized_error = StandardErrorDTO(
|
|
80
80
|
status_code=401,
|
|
81
81
|
message="Unauthorized",
|
|
@@ -84,7 +84,7 @@ class StandardErrorDTO(BaseErrorDTO):
|
|
|
84
84
|
"SessionNotFound": "Сессия пользователя не была найдена.",
|
|
85
85
|
},
|
|
86
86
|
)
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
@router.delete(
|
|
89
89
|
"/{id}",
|
|
90
90
|
responses=Errors(unauthorized_error),
|
|
@@ -93,39 +93,25 @@ class StandardErrorDTO(BaseErrorDTO):
|
|
|
93
93
|
pass
|
|
94
94
|
```
|
|
95
95
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
|
|
97
|
+
examples: Optional[Dict[str, ExampleSpec]] = field(default=None)
|
|
98
|
+
"""Examples: ``str`` detail text or full OpenAPI example object (with ``summary``)."""
|
|
99
|
+
|
|
99
100
|
def __post_init__(self) -> None:
|
|
100
101
|
"""Initialize examples with default value if not provided."""
|
|
101
102
|
if self.examples is None:
|
|
102
103
|
self.examples = {self.message: self.message}
|
|
103
|
-
|
|
104
|
-
def to_example(self) -> Dict[str, Any]:
|
|
105
|
-
"""Generate examples for OpenAPI with multiple examples.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
Dict in format: {"key": {"value": {"detail": "message"}}, ...}
|
|
109
|
-
|
|
110
|
-
Example:
|
|
111
|
-
```python
|
|
112
|
-
{
|
|
113
|
-
"InvalidToken": {
|
|
114
|
-
"value": {"detail": "Ошибка декодирования токена."},
|
|
115
|
-
},
|
|
116
|
-
"SessionNotFound": {
|
|
117
|
-
"value": {"detail": "Сессия пользователя не была найдена."},
|
|
118
|
-
},
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
"""
|
|
122
|
-
return {
|
|
123
|
-
key: {
|
|
124
|
-
"value": {"detail": message},
|
|
125
|
-
}
|
|
126
|
-
for key, message in self.examples.items()
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
104
|
|
|
105
|
+
def to_examples(self) -> Dict[str, Any]:
|
|
106
|
+
"""Generate OpenAPI ``examples`` with optional summaries."""
|
|
107
|
+
assert self.examples is not None
|
|
108
|
+
return _normalize_example_specs(self.examples)
|
|
131
109
|
|
|
110
|
+
def to_example(self) -> Dict[str, Any]:
|
|
111
|
+
"""Deprecated alias for :meth:`to_examples`."""
|
|
112
|
+
warnings.warn(
|
|
113
|
+
"to_example() is deprecated; use to_examples() instead.",
|
|
114
|
+
DeprecationWarning,
|
|
115
|
+
stacklevel=2,
|
|
116
|
+
)
|
|
117
|
+
return self.to_examples()
|