fastapi-errors-plus 0.6.1__tar.gz → 0.7.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.6.1 → fastapi_errors_plus-0.7.0}/LICENSE +1 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/PKG-INFO +190 -33
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/README.md +189 -32
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/__init__.py +1 -1
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/base.py +7 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/errors.py +56 -8
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/protocol.py +8 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/PKG-INFO +190 -33
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/pyproject.toml +15 -1
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/tests/test_app.py +4 -55
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/tests/test_base.py +39 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/tests/test_errors.py +267 -15
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/tests/test_integration.py +21 -7
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus/py.typed +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/SOURCES.txt +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/requires.txt +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.0}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.7.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.7.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
|
|
@@ -30,12 +30,12 @@ Dynamic: license-file
|
|
|
30
30
|
[](https://opensource.org/licenses/MIT)
|
|
31
31
|
[](https://www.python.org/downloads/)
|
|
32
32
|
[](https://fastapi.tiangolo.com/)
|
|
33
|
-
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
34
|
+
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
35
35
|
|
|
36
36
|
Universal library for documenting errors in FastAPI endpoints.
|
|
37
37
|
|
|
38
|
-
> [Русская версия README](README.ru.md)
|
|
38
|
+
> [Русская версия README](https://github.com/seligoroff/fastapi-errors-plus/blob/main/README.ru.md)
|
|
39
39
|
|
|
40
40
|
## Philosophy
|
|
41
41
|
|
|
@@ -207,6 +207,39 @@ def delete_item(id: int):
|
|
|
207
207
|
pass
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
#### OpenAPI extras (`schema`) next to examples
|
|
211
|
+
|
|
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; **`to_example()`** still defines examples):
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from fastapi import APIRouter, status
|
|
216
|
+
from fastapi_errors_plus import BaseErrorDTO, Errors
|
|
217
|
+
|
|
218
|
+
ADR_ERROR_BODY_SCHEMA = {
|
|
219
|
+
"type": "object",
|
|
220
|
+
"required": ["code", "detail"],
|
|
221
|
+
"properties": {
|
|
222
|
+
"code": {"type": "string"},
|
|
223
|
+
"detail": {"type": "string"},
|
|
224
|
+
"context": {"type": "object"},
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
business_conflict = BaseErrorDTO(
|
|
229
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
230
|
+
message="BusinessRuleViolation",
|
|
231
|
+
openapi_json_extras={"schema": ADR_ERROR_BODY_SCHEMA},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
router = APIRouter()
|
|
235
|
+
|
|
236
|
+
@router.post("/items", responses=Errors(business_conflict, validation_error=False))
|
|
237
|
+
def create_item():
|
|
238
|
+
...
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Custom **`ErrorDTO`** classes may instead define **`to_openapi_json_media_type_extras()`** returning a **`dict`**; if present and non-empty, it overrides **`openapi_json_extras`**. Any later **`dict`** in **`Errors`** for that status still overrides the same **`application/json`** keys (same precedence as merging two dicts).
|
|
242
|
+
|
|
210
243
|
#### StandardErrorDTO
|
|
211
244
|
|
|
212
245
|
Extended implementation for errors with multiple examples (useful for standard HTTP errors):
|
|
@@ -297,6 +330,37 @@ def update_item(id: int):
|
|
|
297
330
|
|
|
298
331
|
The OpenAPI spec will contain both examples under the 404 status code.
|
|
299
332
|
|
|
333
|
+
When merging the same status code: a **dict** wins for **`description`** over the bundled standard-flag wording; an **ErrorDTO**’s `message` can replace the description only while it still matches the library’s default label for that code (custom descriptions coming from dicts are not overwritten by DTOs).
|
|
334
|
+
|
|
335
|
+
Under `content["application/json"]`, `example` / `examples` are merged as before; any **other** OpenAPI Media Type fields from a later **`dict`** (for example **`schema`**, **`encoding`**) are **copied in** as well—the later dict wins on conflict (**same rule as `description`**).
|
|
336
|
+
|
|
337
|
+
You can combine one **`ErrorDTO`** (examples) with a **`dict`** for the **same numeric status code** listing only **`schema`** (or other non-example keys) without repeating the boilerplate examples block:
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
Errors(
|
|
341
|
+
conflict_error_doc, # implements ErrorDTO, e.g. .for_openapi() for ADR-shaped examples
|
|
342
|
+
{
|
|
343
|
+
status.HTTP_409_CONFLICT: {
|
|
344
|
+
"description": "Business rule violation",
|
|
345
|
+
"content": {
|
|
346
|
+
"application/json": {
|
|
347
|
+
"schema": {
|
|
348
|
+
"type": "object",
|
|
349
|
+
"properties": {
|
|
350
|
+
"code": {"type": "string"},
|
|
351
|
+
"detail": {"type": "string"},
|
|
352
|
+
"context": {"type": "object"},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Order matters only for overlaps: whichever **`dict`** is applied **later** in the **`Errors`** argument list overwrites **`schema`** / **`encoding`** (and **`model`** on the outer response dict, if provided) when the same keys appear again.
|
|
363
|
+
|
|
300
364
|
## ErrorDTO Protocol
|
|
301
365
|
|
|
302
366
|
The `ErrorDTO` protocol defines the interface for error objects compatible with the library:
|
|
@@ -335,6 +399,102 @@ Any class implementing this protocol (through structural typing) can be used wit
|
|
|
335
399
|
|
|
336
400
|
Both approaches work together — you can mix them in the same `Errors()` call!
|
|
337
401
|
|
|
402
|
+
## Using Pydantic with ErrorDTO
|
|
403
|
+
|
|
404
|
+
**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
|
+
|
|
406
|
+
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
|
|
407
|
+
|
|
408
|
+
### Simple Pydantic Model as ErrorDTO
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
from pydantic import BaseModel, Field
|
|
412
|
+
from fastapi_errors_plus import Errors
|
|
413
|
+
from typing import Dict, Any
|
|
414
|
+
|
|
415
|
+
class PydanticErrorDTO(BaseModel):
|
|
416
|
+
"""Pydantic model implementing ErrorDTO Protocol."""
|
|
417
|
+
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
|
|
418
|
+
message: str = Field(..., min_length=1, description="Error message")
|
|
419
|
+
|
|
420
|
+
def to_example(self) -> Dict[str, Any]:
|
|
421
|
+
"""Generate example for OpenAPI."""
|
|
422
|
+
return {
|
|
423
|
+
self.message: {
|
|
424
|
+
"value": {"detail": self.message},
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# Usage
|
|
429
|
+
notification_error = PydanticErrorDTO(
|
|
430
|
+
status_code=404,
|
|
431
|
+
message="Notification not found",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
@router.delete(
|
|
435
|
+
"/{id}",
|
|
436
|
+
responses=Errors(notification_error),
|
|
437
|
+
)
|
|
438
|
+
def delete_item(id: int):
|
|
439
|
+
pass
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Benefits:**
|
|
443
|
+
- Runtime validation through Pydantic
|
|
444
|
+
- Type safety
|
|
445
|
+
- Automatic field documentation
|
|
446
|
+
- Works with ErrorDTO Protocol through structural typing
|
|
447
|
+
|
|
448
|
+
### Complex ErrorDTO with Pydantic
|
|
449
|
+
|
|
450
|
+
For errors with additional fields:
|
|
451
|
+
|
|
452
|
+
```python
|
|
453
|
+
from pydantic import BaseModel, Field
|
|
454
|
+
from fastapi_errors_plus import Errors
|
|
455
|
+
from typing import Dict, Any, Optional
|
|
456
|
+
|
|
457
|
+
class DetailedErrorDTO(BaseModel):
|
|
458
|
+
"""Pydantic model for errors with additional fields."""
|
|
459
|
+
status_code: int = Field(..., ge=400, le=599)
|
|
460
|
+
message: str = Field(..., min_length=1)
|
|
461
|
+
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
462
|
+
timestamp: Optional[str] = Field(None, description="Error timestamp")
|
|
463
|
+
|
|
464
|
+
def to_example(self) -> Dict[str, Any]:
|
|
465
|
+
"""Generate example for OpenAPI."""
|
|
466
|
+
example = {"detail": self.message}
|
|
467
|
+
if self.error_code:
|
|
468
|
+
example["error_code"] = self.error_code
|
|
469
|
+
if self.timestamp:
|
|
470
|
+
example["timestamp"] = self.timestamp
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
self.message: {
|
|
474
|
+
"value": example,
|
|
475
|
+
},
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
# Usage
|
|
479
|
+
validation_error = DetailedErrorDTO(
|
|
480
|
+
status_code=422,
|
|
481
|
+
message="Validation failed",
|
|
482
|
+
error_code="VALIDATION_ERROR",
|
|
483
|
+
timestamp="2025-01-15T10:30:00Z",
|
|
484
|
+
)
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**When to use Pydantic with ErrorDTO:**
|
|
488
|
+
- Your project already uses Pydantic extensively
|
|
489
|
+
- You need runtime validation for error objects
|
|
490
|
+
- You want automatic field documentation
|
|
491
|
+
- You have complex error structures with multiple fields
|
|
492
|
+
|
|
493
|
+
**When not to use Pydantic:**
|
|
494
|
+
- Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
|
|
495
|
+
- You need simple error objects (dataclasses are sufficient)
|
|
496
|
+
- You want to keep dependencies minimal
|
|
497
|
+
|
|
338
498
|
## Best Practice: Connecting Exceptions and ErrorDTO
|
|
339
499
|
|
|
340
500
|
### Problem
|
|
@@ -472,14 +632,15 @@ Errors(
|
|
|
472
632
|
FastAPI automatically validates all parameters (Path, Query, Body), so 422 is relevant in 95%+ of endpoints. For endpoints without parameters, explicitly set `validation_error=False` or `validation_error_422=False`.
|
|
473
633
|
|
|
474
634
|
**Returns:**
|
|
475
|
-
-
|
|
476
|
-
-
|
|
635
|
+
- A dict-like `Mapping[int, …]` suitable for FastAPI’s `responses` / OpenAPI
|
|
636
|
+
- Pass the instance as-is — **do not** call it like a function: `responses=Errors(...)`
|
|
477
637
|
|
|
478
638
|
#### Usage
|
|
479
639
|
|
|
480
640
|
```python
|
|
481
|
-
#
|
|
482
|
-
|
|
641
|
+
# Instances are Mapping keyed by HTTP status codes
|
|
642
|
+
error_responses = Errors(unauthorized_401=True, forbidden_403=True)
|
|
643
|
+
documented = error_responses[401] # response block picked up by OpenAPI
|
|
483
644
|
```
|
|
484
645
|
|
|
485
646
|
### `ErrorDTO`
|
|
@@ -493,6 +654,8 @@ Protocol for error objects compatible with the library.
|
|
|
493
654
|
**Required methods:**
|
|
494
655
|
- `to_example() -> Dict[str, Any]` — Generate example for OpenAPI
|
|
495
656
|
|
|
657
|
+
During `Errors(...)` initialization, non-`dict` objects in `*errors` missing `status_code`, `message`, or a callable `to_example` raise **`TypeError`** naming what was missing.
|
|
658
|
+
|
|
496
659
|
### `BaseErrorDTO`
|
|
497
660
|
|
|
498
661
|
Base implementation of ErrorDTO Protocol for convenience.
|
|
@@ -502,6 +665,7 @@ Base implementation of ErrorDTO Protocol for convenience.
|
|
|
502
665
|
BaseErrorDTO(
|
|
503
666
|
status_code: int,
|
|
504
667
|
message: str,
|
|
668
|
+
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
505
669
|
)
|
|
506
670
|
```
|
|
507
671
|
|
|
@@ -519,6 +683,7 @@ Extended implementation for errors with multiple examples.
|
|
|
519
683
|
StandardErrorDTO(
|
|
520
684
|
status_code: int,
|
|
521
685
|
message: str,
|
|
686
|
+
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
522
687
|
examples: Optional[Dict[str, str]] = None,
|
|
523
688
|
)
|
|
524
689
|
```
|
|
@@ -570,7 +735,7 @@ def delete_item(id: int):
|
|
|
570
735
|
```python
|
|
571
736
|
from fastapi import APIRouter
|
|
572
737
|
from fastapi_errors_plus import Errors
|
|
573
|
-
from api.exceptions.dto import
|
|
738
|
+
from api.exceptions.dto import notification_not_found_error # ErrorDTO-compatible instance
|
|
574
739
|
|
|
575
740
|
router = APIRouter()
|
|
576
741
|
|
|
@@ -579,7 +744,7 @@ router = APIRouter()
|
|
|
579
744
|
responses=Errors(
|
|
580
745
|
unauthorized=True,
|
|
581
746
|
forbidden=True,
|
|
582
|
-
|
|
747
|
+
notification_not_found_error,
|
|
583
748
|
),
|
|
584
749
|
)
|
|
585
750
|
async def delete_notification(notification_id: int):
|
|
@@ -618,40 +783,32 @@ This example shows how to use `fastapi-errors-plus` in a FastAPI project with Cl
|
|
|
618
783
|
|
|
619
784
|
**Domain Layer** (`domain/errors.py`):
|
|
620
785
|
```python
|
|
621
|
-
from typing import
|
|
786
|
+
from typing import Dict, Any
|
|
622
787
|
|
|
623
|
-
class
|
|
624
|
-
"""Domain error
|
|
788
|
+
class DomainException(Exception):
|
|
789
|
+
"""Domain exception usable as runtime error and shaped like ErrorDTO for OpenAPI."""
|
|
625
790
|
status_code: int
|
|
626
791
|
message: str
|
|
627
|
-
|
|
628
|
-
def to_example(self) -> Dict[str, Any]:
|
|
629
|
-
"""Generate example for OpenAPI."""
|
|
630
|
-
...
|
|
631
792
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
message = "Item not found"
|
|
636
|
-
|
|
793
|
+
def __init__(self) -> None:
|
|
794
|
+
super().__init__(self.message)
|
|
795
|
+
|
|
637
796
|
def to_example(self) -> Dict[str, Any]:
|
|
638
797
|
return {
|
|
639
|
-
|
|
640
|
-
"value": {"detail":
|
|
798
|
+
self.message: {
|
|
799
|
+
"value": {"detail": self.message},
|
|
641
800
|
},
|
|
642
801
|
}
|
|
643
802
|
|
|
644
|
-
|
|
645
|
-
|
|
803
|
+
|
|
804
|
+
class ItemNotFoundError(DomainException):
|
|
805
|
+
status_code = 404
|
|
806
|
+
message = "Item not found"
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
class ItemAlreadyExistsError(DomainException):
|
|
646
810
|
status_code = 409
|
|
647
811
|
message = "Item already exists"
|
|
648
|
-
|
|
649
|
-
def to_example(self) -> Dict[str, Any]:
|
|
650
|
-
return {
|
|
651
|
-
"ItemAlreadyExists": {
|
|
652
|
-
"value": {"detail": "Item already exists"},
|
|
653
|
-
},
|
|
654
|
-
}
|
|
655
812
|
```
|
|
656
813
|
|
|
657
814
|
**Application Layer** (`application/use_cases.py`):
|
|
@@ -4,12 +4,12 @@
|
|
|
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
|
+
[](https://github.com/seligoroff/fastapi-errors-plus)
|
|
9
9
|
|
|
10
10
|
Universal library for documenting errors in FastAPI endpoints.
|
|
11
11
|
|
|
12
|
-
> [Русская версия README](README.ru.md)
|
|
12
|
+
> [Русская версия README](https://github.com/seligoroff/fastapi-errors-plus/blob/main/README.ru.md)
|
|
13
13
|
|
|
14
14
|
## Philosophy
|
|
15
15
|
|
|
@@ -181,6 +181,39 @@ def delete_item(id: int):
|
|
|
181
181
|
pass
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
#### OpenAPI extras (`schema`) next to examples
|
|
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; **`to_example()`** still defines examples):
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from fastapi import APIRouter, status
|
|
190
|
+
from fastapi_errors_plus import BaseErrorDTO, Errors
|
|
191
|
+
|
|
192
|
+
ADR_ERROR_BODY_SCHEMA = {
|
|
193
|
+
"type": "object",
|
|
194
|
+
"required": ["code", "detail"],
|
|
195
|
+
"properties": {
|
|
196
|
+
"code": {"type": "string"},
|
|
197
|
+
"detail": {"type": "string"},
|
|
198
|
+
"context": {"type": "object"},
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
business_conflict = BaseErrorDTO(
|
|
203
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
204
|
+
message="BusinessRuleViolation",
|
|
205
|
+
openapi_json_extras={"schema": ADR_ERROR_BODY_SCHEMA},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
router = APIRouter()
|
|
209
|
+
|
|
210
|
+
@router.post("/items", responses=Errors(business_conflict, validation_error=False))
|
|
211
|
+
def create_item():
|
|
212
|
+
...
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Custom **`ErrorDTO`** classes may instead define **`to_openapi_json_media_type_extras()`** returning a **`dict`**; if present and non-empty, it overrides **`openapi_json_extras`**. Any later **`dict`** in **`Errors`** for that status still overrides the same **`application/json`** keys (same precedence as merging two dicts).
|
|
216
|
+
|
|
184
217
|
#### StandardErrorDTO
|
|
185
218
|
|
|
186
219
|
Extended implementation for errors with multiple examples (useful for standard HTTP errors):
|
|
@@ -271,6 +304,37 @@ def update_item(id: int):
|
|
|
271
304
|
|
|
272
305
|
The OpenAPI spec will contain both examples under the 404 status code.
|
|
273
306
|
|
|
307
|
+
When merging the same status code: a **dict** wins for **`description`** over the bundled standard-flag wording; an **ErrorDTO**’s `message` can replace the description only while it still matches the library’s default label for that code (custom descriptions coming from dicts are not overwritten by DTOs).
|
|
308
|
+
|
|
309
|
+
Under `content["application/json"]`, `example` / `examples` are merged as before; any **other** OpenAPI Media Type fields from a later **`dict`** (for example **`schema`**, **`encoding`**) are **copied in** as well—the later dict wins on conflict (**same rule as `description`**).
|
|
310
|
+
|
|
311
|
+
You can combine one **`ErrorDTO`** (examples) with a **`dict`** for the **same numeric status code** listing only **`schema`** (or other non-example keys) without repeating the boilerplate examples block:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
Errors(
|
|
315
|
+
conflict_error_doc, # implements ErrorDTO, e.g. .for_openapi() for ADR-shaped examples
|
|
316
|
+
{
|
|
317
|
+
status.HTTP_409_CONFLICT: {
|
|
318
|
+
"description": "Business rule violation",
|
|
319
|
+
"content": {
|
|
320
|
+
"application/json": {
|
|
321
|
+
"schema": {
|
|
322
|
+
"type": "object",
|
|
323
|
+
"properties": {
|
|
324
|
+
"code": {"type": "string"},
|
|
325
|
+
"detail": {"type": "string"},
|
|
326
|
+
"context": {"type": "object"},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Order matters only for overlaps: whichever **`dict`** is applied **later** in the **`Errors`** argument list overwrites **`schema`** / **`encoding`** (and **`model`** on the outer response dict, if provided) when the same keys appear again.
|
|
337
|
+
|
|
274
338
|
## ErrorDTO Protocol
|
|
275
339
|
|
|
276
340
|
The `ErrorDTO` protocol defines the interface for error objects compatible with the library:
|
|
@@ -309,6 +373,102 @@ Any class implementing this protocol (through structural typing) can be used wit
|
|
|
309
373
|
|
|
310
374
|
Both approaches work together — you can mix them in the same `Errors()` call!
|
|
311
375
|
|
|
376
|
+
## Using Pydantic with ErrorDTO
|
|
377
|
+
|
|
378
|
+
**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
|
+
|
|
380
|
+
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
|
|
381
|
+
|
|
382
|
+
### Simple Pydantic Model as ErrorDTO
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
from pydantic import BaseModel, Field
|
|
386
|
+
from fastapi_errors_plus import Errors
|
|
387
|
+
from typing import Dict, Any
|
|
388
|
+
|
|
389
|
+
class PydanticErrorDTO(BaseModel):
|
|
390
|
+
"""Pydantic model implementing ErrorDTO Protocol."""
|
|
391
|
+
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
|
|
392
|
+
message: str = Field(..., min_length=1, description="Error message")
|
|
393
|
+
|
|
394
|
+
def to_example(self) -> Dict[str, Any]:
|
|
395
|
+
"""Generate example for OpenAPI."""
|
|
396
|
+
return {
|
|
397
|
+
self.message: {
|
|
398
|
+
"value": {"detail": self.message},
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
# Usage
|
|
403
|
+
notification_error = PydanticErrorDTO(
|
|
404
|
+
status_code=404,
|
|
405
|
+
message="Notification not found",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
@router.delete(
|
|
409
|
+
"/{id}",
|
|
410
|
+
responses=Errors(notification_error),
|
|
411
|
+
)
|
|
412
|
+
def delete_item(id: int):
|
|
413
|
+
pass
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Benefits:**
|
|
417
|
+
- Runtime validation through Pydantic
|
|
418
|
+
- Type safety
|
|
419
|
+
- Automatic field documentation
|
|
420
|
+
- Works with ErrorDTO Protocol through structural typing
|
|
421
|
+
|
|
422
|
+
### Complex ErrorDTO with Pydantic
|
|
423
|
+
|
|
424
|
+
For errors with additional fields:
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
from pydantic import BaseModel, Field
|
|
428
|
+
from fastapi_errors_plus import Errors
|
|
429
|
+
from typing import Dict, Any, Optional
|
|
430
|
+
|
|
431
|
+
class DetailedErrorDTO(BaseModel):
|
|
432
|
+
"""Pydantic model for errors with additional fields."""
|
|
433
|
+
status_code: int = Field(..., ge=400, le=599)
|
|
434
|
+
message: str = Field(..., min_length=1)
|
|
435
|
+
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
436
|
+
timestamp: Optional[str] = Field(None, description="Error timestamp")
|
|
437
|
+
|
|
438
|
+
def to_example(self) -> Dict[str, Any]:
|
|
439
|
+
"""Generate example for OpenAPI."""
|
|
440
|
+
example = {"detail": self.message}
|
|
441
|
+
if self.error_code:
|
|
442
|
+
example["error_code"] = self.error_code
|
|
443
|
+
if self.timestamp:
|
|
444
|
+
example["timestamp"] = self.timestamp
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
self.message: {
|
|
448
|
+
"value": example,
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# Usage
|
|
453
|
+
validation_error = DetailedErrorDTO(
|
|
454
|
+
status_code=422,
|
|
455
|
+
message="Validation failed",
|
|
456
|
+
error_code="VALIDATION_ERROR",
|
|
457
|
+
timestamp="2025-01-15T10:30:00Z",
|
|
458
|
+
)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**When to use Pydantic with ErrorDTO:**
|
|
462
|
+
- Your project already uses Pydantic extensively
|
|
463
|
+
- You need runtime validation for error objects
|
|
464
|
+
- You want automatic field documentation
|
|
465
|
+
- You have complex error structures with multiple fields
|
|
466
|
+
|
|
467
|
+
**When not to use Pydantic:**
|
|
468
|
+
- Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
|
|
469
|
+
- You need simple error objects (dataclasses are sufficient)
|
|
470
|
+
- You want to keep dependencies minimal
|
|
471
|
+
|
|
312
472
|
## Best Practice: Connecting Exceptions and ErrorDTO
|
|
313
473
|
|
|
314
474
|
### Problem
|
|
@@ -446,14 +606,15 @@ Errors(
|
|
|
446
606
|
FastAPI automatically validates all parameters (Path, Query, Body), so 422 is relevant in 95%+ of endpoints. For endpoints without parameters, explicitly set `validation_error=False` or `validation_error_422=False`.
|
|
447
607
|
|
|
448
608
|
**Returns:**
|
|
449
|
-
-
|
|
450
|
-
-
|
|
609
|
+
- A dict-like `Mapping[int, …]` suitable for FastAPI’s `responses` / OpenAPI
|
|
610
|
+
- Pass the instance as-is — **do not** call it like a function: `responses=Errors(...)`
|
|
451
611
|
|
|
452
612
|
#### Usage
|
|
453
613
|
|
|
454
614
|
```python
|
|
455
|
-
#
|
|
456
|
-
|
|
615
|
+
# Instances are Mapping keyed by HTTP status codes
|
|
616
|
+
error_responses = Errors(unauthorized_401=True, forbidden_403=True)
|
|
617
|
+
documented = error_responses[401] # response block picked up by OpenAPI
|
|
457
618
|
```
|
|
458
619
|
|
|
459
620
|
### `ErrorDTO`
|
|
@@ -467,6 +628,8 @@ Protocol for error objects compatible with the library.
|
|
|
467
628
|
**Required methods:**
|
|
468
629
|
- `to_example() -> Dict[str, Any]` — Generate example for OpenAPI
|
|
469
630
|
|
|
631
|
+
During `Errors(...)` initialization, non-`dict` objects in `*errors` missing `status_code`, `message`, or a callable `to_example` raise **`TypeError`** naming what was missing.
|
|
632
|
+
|
|
470
633
|
### `BaseErrorDTO`
|
|
471
634
|
|
|
472
635
|
Base implementation of ErrorDTO Protocol for convenience.
|
|
@@ -476,6 +639,7 @@ Base implementation of ErrorDTO Protocol for convenience.
|
|
|
476
639
|
BaseErrorDTO(
|
|
477
640
|
status_code: int,
|
|
478
641
|
message: str,
|
|
642
|
+
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
479
643
|
)
|
|
480
644
|
```
|
|
481
645
|
|
|
@@ -493,6 +657,7 @@ Extended implementation for errors with multiple examples.
|
|
|
493
657
|
StandardErrorDTO(
|
|
494
658
|
status_code: int,
|
|
495
659
|
message: str,
|
|
660
|
+
openapi_json_extras: Optional[Dict[str, Any]] = None,
|
|
496
661
|
examples: Optional[Dict[str, str]] = None,
|
|
497
662
|
)
|
|
498
663
|
```
|
|
@@ -544,7 +709,7 @@ def delete_item(id: int):
|
|
|
544
709
|
```python
|
|
545
710
|
from fastapi import APIRouter
|
|
546
711
|
from fastapi_errors_plus import Errors
|
|
547
|
-
from api.exceptions.dto import
|
|
712
|
+
from api.exceptions.dto import notification_not_found_error # ErrorDTO-compatible instance
|
|
548
713
|
|
|
549
714
|
router = APIRouter()
|
|
550
715
|
|
|
@@ -553,7 +718,7 @@ router = APIRouter()
|
|
|
553
718
|
responses=Errors(
|
|
554
719
|
unauthorized=True,
|
|
555
720
|
forbidden=True,
|
|
556
|
-
|
|
721
|
+
notification_not_found_error,
|
|
557
722
|
),
|
|
558
723
|
)
|
|
559
724
|
async def delete_notification(notification_id: int):
|
|
@@ -592,40 +757,32 @@ This example shows how to use `fastapi-errors-plus` in a FastAPI project with Cl
|
|
|
592
757
|
|
|
593
758
|
**Domain Layer** (`domain/errors.py`):
|
|
594
759
|
```python
|
|
595
|
-
from typing import
|
|
760
|
+
from typing import Dict, Any
|
|
596
761
|
|
|
597
|
-
class
|
|
598
|
-
"""Domain error
|
|
762
|
+
class DomainException(Exception):
|
|
763
|
+
"""Domain exception usable as runtime error and shaped like ErrorDTO for OpenAPI."""
|
|
599
764
|
status_code: int
|
|
600
765
|
message: str
|
|
601
|
-
|
|
602
|
-
def to_example(self) -> Dict[str, Any]:
|
|
603
|
-
"""Generate example for OpenAPI."""
|
|
604
|
-
...
|
|
605
766
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
message = "Item not found"
|
|
610
|
-
|
|
767
|
+
def __init__(self) -> None:
|
|
768
|
+
super().__init__(self.message)
|
|
769
|
+
|
|
611
770
|
def to_example(self) -> Dict[str, Any]:
|
|
612
771
|
return {
|
|
613
|
-
|
|
614
|
-
"value": {"detail":
|
|
772
|
+
self.message: {
|
|
773
|
+
"value": {"detail": self.message},
|
|
615
774
|
},
|
|
616
775
|
}
|
|
617
776
|
|
|
618
|
-
|
|
619
|
-
|
|
777
|
+
|
|
778
|
+
class ItemNotFoundError(DomainException):
|
|
779
|
+
status_code = 404
|
|
780
|
+
message = "Item not found"
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
class ItemAlreadyExistsError(DomainException):
|
|
620
784
|
status_code = 409
|
|
621
785
|
message = "Item already exists"
|
|
622
|
-
|
|
623
|
-
def to_example(self) -> Dict[str, Any]:
|
|
624
|
-
return {
|
|
625
|
-
"ItemAlreadyExists": {
|
|
626
|
-
"value": {"detail": "Item already exists"},
|
|
627
|
-
},
|
|
628
|
-
}
|
|
629
786
|
```
|
|
630
787
|
|
|
631
788
|
**Application Layer** (`application/use_cases.py`):
|
|
@@ -37,6 +37,12 @@ class BaseErrorDTO:
|
|
|
37
37
|
message: str
|
|
38
38
|
"""Error message description."""
|
|
39
39
|
|
|
40
|
+
openapi_json_extras: Optional[Dict[str, Any]] = field(default=None)
|
|
41
|
+
"""Optional OpenAPI fragment under ``content['application/json']`` besides examples,
|
|
42
|
+
typically ``{\"schema\": ...}`` or ``encoding``. Do **not** put ``example`` / ``examples``
|
|
43
|
+
here — use :meth:`to_example`. Arbitrary implementations may instead implement
|
|
44
|
+
:meth:`to_openapi_json_media_type_extras`; that return value wins over this attribute."""
|
|
45
|
+
|
|
40
46
|
def to_example(self) -> Dict[str, Any]:
|
|
41
47
|
"""Generate example for OpenAPI.
|
|
42
48
|
|
|
@@ -122,3 +128,4 @@ class StandardErrorDTO(BaseErrorDTO):
|
|
|
122
128
|
|
|
123
129
|
|
|
124
130
|
|
|
131
|
+
|