fastapi-errors-plus 0.4.0__tar.gz → 0.6.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.4.0 → fastapi_errors_plus-0.6.0}/LICENSE +2 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/PKG-INFO +84 -10
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/README.md +83 -9
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus/__init__.py +1 -1
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus/base.py +2 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus/errors.py +57 -11
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus/protocol.py +2 -0
- fastapi_errors_plus-0.6.0/fastapi_errors_plus/py.typed +0 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus.egg-info/PKG-INFO +84 -10
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus.egg-info/SOURCES.txt +1 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/pyproject.toml +27 -1
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/tests/test_base.py +2 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/tests/test_errors.py +123 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus.egg-info/requires.txt +0 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/setup.cfg +0 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/tests/test_app.py +0 -0
- {fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/tests/test_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-errors-plus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -246,10 +246,10 @@ def delete_item(id: int):
|
|
|
246
246
|
```
|
|
247
247
|
|
|
248
248
|
**Benefits:**
|
|
249
|
-
-
|
|
250
|
-
-
|
|
251
|
-
-
|
|
252
|
-
-
|
|
249
|
+
- No need to write ErrorDTO classes from scratch
|
|
250
|
+
- Correct implementation out of the box
|
|
251
|
+
- Reusable across all endpoints
|
|
252
|
+
- Supports inheritance for custom logic
|
|
253
253
|
|
|
254
254
|
### 5. Mixed Usage
|
|
255
255
|
|
|
@@ -319,6 +319,8 @@ class ErrorDTO(Protocol):
|
|
|
319
319
|
|
|
320
320
|
Any class implementing this protocol (through structural typing) can be used with `Errors()`.
|
|
321
321
|
|
|
322
|
+
**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.
|
|
323
|
+
|
|
322
324
|
### When to Use Protocol vs BaseErrorDTO
|
|
323
325
|
|
|
324
326
|
**Use Protocol (structural typing)** when:
|
|
@@ -333,6 +335,71 @@ Any class implementing this protocol (through structural typing) can be used wit
|
|
|
333
335
|
|
|
334
336
|
Both approaches work together — you can mix them in the same `Errors()` call!
|
|
335
337
|
|
|
338
|
+
## Best Practice: Connecting Exceptions and ErrorDTO
|
|
339
|
+
|
|
340
|
+
### Problem
|
|
341
|
+
|
|
342
|
+
It's not always clear which exception corresponds to which ErrorDTO:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
# Not clear which exception this documents
|
|
346
|
+
responses=Errors(notification_not_found_error)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Solution: Domain Exception as ErrorDTO
|
|
350
|
+
|
|
351
|
+
**Recommended approach** — make your exceptions implement ErrorDTO protocol:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
# domain/exceptions.py
|
|
355
|
+
from typing import Dict, Any
|
|
356
|
+
|
|
357
|
+
class DomainException(Exception):
|
|
358
|
+
"""Base exception implementing ErrorDTO protocol."""
|
|
359
|
+
status_code: int
|
|
360
|
+
message: str
|
|
361
|
+
|
|
362
|
+
def to_example(self) -> Dict[str, Any]:
|
|
363
|
+
return {self.message: {"value": {"detail": self.message}}}
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def for_openapi(cls):
|
|
367
|
+
"""Returns instance for OpenAPI documentation."""
|
|
368
|
+
return cls()
|
|
369
|
+
|
|
370
|
+
class NotificationNotFoundError(DomainException):
|
|
371
|
+
status_code = 404
|
|
372
|
+
message = "Notification not found"
|
|
373
|
+
|
|
374
|
+
def __init__(self, notification_id: str = ""):
|
|
375
|
+
self.notification_id = notification_id
|
|
376
|
+
super().__init__(self.message)
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
def for_openapi(cls):
|
|
380
|
+
return cls(notification_id="example_id")
|
|
381
|
+
|
|
382
|
+
# In endpoint
|
|
383
|
+
@router.delete(
|
|
384
|
+
"/{notificationId}",
|
|
385
|
+
responses=Errors(
|
|
386
|
+
NotificationNotFoundError.for_openapi(), # Clear connection!
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
async def delete_notification(notification_id: str):
|
|
390
|
+
if not notification:
|
|
391
|
+
raise NotificationNotFoundError(notification_id) # Same exception!
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Benefits:**
|
|
395
|
+
- Exception and ErrorDTO are one class
|
|
396
|
+
- Clear connection visible in endpoint
|
|
397
|
+
- No duplication
|
|
398
|
+
- Type-safe
|
|
399
|
+
- Works with any project architecture
|
|
400
|
+
|
|
401
|
+
See [examples/domain_exceptions.py](examples/domain_exceptions.py) for complete example.
|
|
402
|
+
|
|
336
403
|
## Compatibility with Existing Projects
|
|
337
404
|
|
|
338
405
|
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol:
|
|
@@ -375,11 +442,11 @@ Errors(
|
|
|
375
442
|
*errors: Union[Dict[int, Dict[str, Any]], ErrorDTO],
|
|
376
443
|
unauthorized: bool = False,
|
|
377
444
|
forbidden: bool = False,
|
|
378
|
-
validation_error: bool =
|
|
445
|
+
validation_error: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
|
|
379
446
|
internal_server_error: bool = False,
|
|
380
447
|
unauthorized_401: bool = False,
|
|
381
448
|
forbidden_403: bool = False,
|
|
382
|
-
validation_error_422: bool =
|
|
449
|
+
validation_error_422: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
|
|
383
450
|
internal_server_error_500: bool = False,
|
|
384
451
|
)
|
|
385
452
|
```
|
|
@@ -388,18 +455,25 @@ Errors(
|
|
|
388
455
|
- `*errors`: Arbitrary errors as dict or ErrorDTO objects
|
|
389
456
|
- `unauthorized_401`: Add 401 Unauthorized error (recommended, explicit). Defaults to `False`.
|
|
390
457
|
- `forbidden_403`: Add 403 Forbidden error (recommended, explicit). Defaults to `False`.
|
|
391
|
-
- `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit).
|
|
458
|
+
- `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit).
|
|
459
|
+
- `None` (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
460
|
+
- `False`: Explicitly disable 422
|
|
461
|
+
- `True`: Explicitly enable 422
|
|
392
462
|
- `internal_server_error_500`: Add 500 Internal Server Error (recommended, explicit). Defaults to `False`.
|
|
393
463
|
- `unauthorized`: Add 401 Unauthorized error (legacy, for backward compatibility). Defaults to `False`.
|
|
394
464
|
- `forbidden`: Add 403 Forbidden error (legacy, for backward compatibility). Defaults to `False`.
|
|
395
|
-
- `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility).
|
|
465
|
+
- `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility).
|
|
466
|
+
- `None` (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
467
|
+
- `False`: Explicitly disable 422
|
|
468
|
+
- `True`: Explicitly enable 422
|
|
396
469
|
- `internal_server_error`: Add 500 Internal Server Error (legacy, for backward compatibility). Defaults to `False`.
|
|
397
470
|
|
|
398
471
|
**Why `validation_error=True` by default?**
|
|
399
472
|
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`.
|
|
400
473
|
|
|
401
474
|
**Returns:**
|
|
402
|
-
-
|
|
475
|
+
- Mapping object (dict-like) that implements `Dict[int, Dict[str, Any]]` for FastAPI responses
|
|
476
|
+
- Can be used directly in `responses` parameter without calling: `responses=Errors(...)`
|
|
403
477
|
|
|
404
478
|
#### Usage
|
|
405
479
|
|
|
@@ -220,10 +220,10 @@ def delete_item(id: int):
|
|
|
220
220
|
```
|
|
221
221
|
|
|
222
222
|
**Benefits:**
|
|
223
|
-
-
|
|
224
|
-
-
|
|
225
|
-
-
|
|
226
|
-
-
|
|
223
|
+
- No need to write ErrorDTO classes from scratch
|
|
224
|
+
- Correct implementation out of the box
|
|
225
|
+
- Reusable across all endpoints
|
|
226
|
+
- Supports inheritance for custom logic
|
|
227
227
|
|
|
228
228
|
### 5. Mixed Usage
|
|
229
229
|
|
|
@@ -293,6 +293,8 @@ class ErrorDTO(Protocol):
|
|
|
293
293
|
|
|
294
294
|
Any class implementing this protocol (through structural typing) can be used with `Errors()`.
|
|
295
295
|
|
|
296
|
+
**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.
|
|
297
|
+
|
|
296
298
|
### When to Use Protocol vs BaseErrorDTO
|
|
297
299
|
|
|
298
300
|
**Use Protocol (structural typing)** when:
|
|
@@ -307,6 +309,71 @@ Any class implementing this protocol (through structural typing) can be used wit
|
|
|
307
309
|
|
|
308
310
|
Both approaches work together — you can mix them in the same `Errors()` call!
|
|
309
311
|
|
|
312
|
+
## Best Practice: Connecting Exceptions and ErrorDTO
|
|
313
|
+
|
|
314
|
+
### Problem
|
|
315
|
+
|
|
316
|
+
It's not always clear which exception corresponds to which ErrorDTO:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
# Not clear which exception this documents
|
|
320
|
+
responses=Errors(notification_not_found_error)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Solution: Domain Exception as ErrorDTO
|
|
324
|
+
|
|
325
|
+
**Recommended approach** — make your exceptions implement ErrorDTO protocol:
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
# domain/exceptions.py
|
|
329
|
+
from typing import Dict, Any
|
|
330
|
+
|
|
331
|
+
class DomainException(Exception):
|
|
332
|
+
"""Base exception implementing ErrorDTO protocol."""
|
|
333
|
+
status_code: int
|
|
334
|
+
message: str
|
|
335
|
+
|
|
336
|
+
def to_example(self) -> Dict[str, Any]:
|
|
337
|
+
return {self.message: {"value": {"detail": self.message}}}
|
|
338
|
+
|
|
339
|
+
@classmethod
|
|
340
|
+
def for_openapi(cls):
|
|
341
|
+
"""Returns instance for OpenAPI documentation."""
|
|
342
|
+
return cls()
|
|
343
|
+
|
|
344
|
+
class NotificationNotFoundError(DomainException):
|
|
345
|
+
status_code = 404
|
|
346
|
+
message = "Notification not found"
|
|
347
|
+
|
|
348
|
+
def __init__(self, notification_id: str = ""):
|
|
349
|
+
self.notification_id = notification_id
|
|
350
|
+
super().__init__(self.message)
|
|
351
|
+
|
|
352
|
+
@classmethod
|
|
353
|
+
def for_openapi(cls):
|
|
354
|
+
return cls(notification_id="example_id")
|
|
355
|
+
|
|
356
|
+
# In endpoint
|
|
357
|
+
@router.delete(
|
|
358
|
+
"/{notificationId}",
|
|
359
|
+
responses=Errors(
|
|
360
|
+
NotificationNotFoundError.for_openapi(), # Clear connection!
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
async def delete_notification(notification_id: str):
|
|
364
|
+
if not notification:
|
|
365
|
+
raise NotificationNotFoundError(notification_id) # Same exception!
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Benefits:**
|
|
369
|
+
- Exception and ErrorDTO are one class
|
|
370
|
+
- Clear connection visible in endpoint
|
|
371
|
+
- No duplication
|
|
372
|
+
- Type-safe
|
|
373
|
+
- Works with any project architecture
|
|
374
|
+
|
|
375
|
+
See [examples/domain_exceptions.py](examples/domain_exceptions.py) for complete example.
|
|
376
|
+
|
|
310
377
|
## Compatibility with Existing Projects
|
|
311
378
|
|
|
312
379
|
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol:
|
|
@@ -349,11 +416,11 @@ Errors(
|
|
|
349
416
|
*errors: Union[Dict[int, Dict[str, Any]], ErrorDTO],
|
|
350
417
|
unauthorized: bool = False,
|
|
351
418
|
forbidden: bool = False,
|
|
352
|
-
validation_error: bool =
|
|
419
|
+
validation_error: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
|
|
353
420
|
internal_server_error: bool = False,
|
|
354
421
|
unauthorized_401: bool = False,
|
|
355
422
|
forbidden_403: bool = False,
|
|
356
|
-
validation_error_422: bool =
|
|
423
|
+
validation_error_422: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
|
|
357
424
|
internal_server_error_500: bool = False,
|
|
358
425
|
)
|
|
359
426
|
```
|
|
@@ -362,18 +429,25 @@ Errors(
|
|
|
362
429
|
- `*errors`: Arbitrary errors as dict or ErrorDTO objects
|
|
363
430
|
- `unauthorized_401`: Add 401 Unauthorized error (recommended, explicit). Defaults to `False`.
|
|
364
431
|
- `forbidden_403`: Add 403 Forbidden error (recommended, explicit). Defaults to `False`.
|
|
365
|
-
- `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit).
|
|
432
|
+
- `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit).
|
|
433
|
+
- `None` (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
434
|
+
- `False`: Explicitly disable 422
|
|
435
|
+
- `True`: Explicitly enable 422
|
|
366
436
|
- `internal_server_error_500`: Add 500 Internal Server Error (recommended, explicit). Defaults to `False`.
|
|
367
437
|
- `unauthorized`: Add 401 Unauthorized error (legacy, for backward compatibility). Defaults to `False`.
|
|
368
438
|
- `forbidden`: Add 403 Forbidden error (legacy, for backward compatibility). Defaults to `False`.
|
|
369
|
-
- `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility).
|
|
439
|
+
- `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility).
|
|
440
|
+
- `None` (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
441
|
+
- `False`: Explicitly disable 422
|
|
442
|
+
- `True`: Explicitly enable 422
|
|
370
443
|
- `internal_server_error`: Add 500 Internal Server Error (legacy, for backward compatibility). Defaults to `False`.
|
|
371
444
|
|
|
372
445
|
**Why `validation_error=True` by default?**
|
|
373
446
|
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`.
|
|
374
447
|
|
|
375
448
|
**Returns:**
|
|
376
|
-
-
|
|
449
|
+
- Mapping object (dict-like) that implements `Dict[int, Dict[str, Any]]` for FastAPI responses
|
|
450
|
+
- Can be used directly in `responses` parameter without calling: `responses=Errors(...)`
|
|
377
451
|
|
|
378
452
|
#### Usage
|
|
379
453
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Main Errors class for documenting errors in FastAPI endpoints."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
|
-
from typing import Any, Dict, Iterator, Union
|
|
4
|
+
from typing import Any, Dict, Iterator, Optional, Union
|
|
5
5
|
|
|
6
6
|
from fastapi import status
|
|
7
7
|
|
|
@@ -29,10 +29,7 @@ class Errors(Mapping):
|
|
|
29
29
|
@router.delete(
|
|
30
30
|
"/{id}",
|
|
31
31
|
responses=Errors(
|
|
32
|
-
|
|
33
|
-
forbidden_403=True, # 403 (explicit)
|
|
34
|
-
validation_error_422=True, # 422 (explicit)
|
|
35
|
-
{404: { # 404 via dict
|
|
32
|
+
{404: { # 404 via dict (positional first)
|
|
36
33
|
"description": "Not found",
|
|
37
34
|
"content": {
|
|
38
35
|
"application/json": {
|
|
@@ -40,6 +37,9 @@ class Errors(Mapping):
|
|
|
40
37
|
},
|
|
41
38
|
},
|
|
42
39
|
}},
|
|
40
|
+
unauthorized_401=True, # 401 (explicit, named after positional)
|
|
41
|
+
forbidden_403=True, # 403 (explicit)
|
|
42
|
+
# validation_error_422=True - not needed, defaults to True
|
|
43
43
|
),
|
|
44
44
|
)
|
|
45
45
|
def delete_item(id: int):
|
|
@@ -55,12 +55,12 @@ class Errors(Mapping):
|
|
|
55
55
|
# Old parameters (for backward compatibility)
|
|
56
56
|
unauthorized: bool = False, # 401
|
|
57
57
|
forbidden: bool = False, # 403
|
|
58
|
-
validation_error: bool = None, # 422 (None = use default True, False = disable, True = enable)
|
|
58
|
+
validation_error: Optional[bool] = None, # 422 (None = use default True, False = disable, True = enable)
|
|
59
59
|
internal_server_error: bool = False, # 500
|
|
60
60
|
# New parameters with explicit status codes (recommended)
|
|
61
61
|
unauthorized_401: bool = False, # 401 (explicit)
|
|
62
62
|
forbidden_403: bool = False, # 403 (explicit)
|
|
63
|
-
validation_error_422: bool = None, # 422 (explicit, None = use default True, False = disable, True = enable)
|
|
63
|
+
validation_error_422: Optional[bool] = None, # 422 (explicit, None = use default True, False = disable, True = enable)
|
|
64
64
|
internal_server_error_500: bool = False, # 500 (explicit)
|
|
65
65
|
) -> None:
|
|
66
66
|
"""Initialize Errors instance.
|
|
@@ -73,7 +73,10 @@ class Errors(Mapping):
|
|
|
73
73
|
Deprecated: Use `unauthorized_401` instead for explicit status code.
|
|
74
74
|
forbidden: Add 403 Forbidden error. Defaults to False.
|
|
75
75
|
Deprecated: Use `forbidden_403` instead for explicit status code.
|
|
76
|
-
validation_error: Add 422 Unprocessable Entity error.
|
|
76
|
+
validation_error: Add 422 Unprocessable Entity error.
|
|
77
|
+
- None (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
78
|
+
- False: Explicitly disable 422
|
|
79
|
+
- True: Explicitly enable 422
|
|
77
80
|
FastAPI automatically validates all parameters (Path, Query, Body), so 422 is relevant
|
|
78
81
|
in 95%+ of endpoints. Set to False only for endpoints without parameters.
|
|
79
82
|
Deprecated: Use `validation_error_422` instead for explicit status code.
|
|
@@ -81,7 +84,10 @@ class Errors(Mapping):
|
|
|
81
84
|
Deprecated: Use `internal_server_error_500` instead for explicit status code.
|
|
82
85
|
unauthorized_401: Add 401 Unauthorized error (explicit). Defaults to False.
|
|
83
86
|
forbidden_403: Add 403 Forbidden error (explicit). Defaults to False.
|
|
84
|
-
validation_error_422: Add 422 Unprocessable Entity error (explicit).
|
|
87
|
+
validation_error_422: Add 422 Unprocessable Entity error (explicit).
|
|
88
|
+
- None (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
89
|
+
- False: Explicitly disable 422
|
|
90
|
+
- True: Explicitly enable 422
|
|
85
91
|
FastAPI automatically validates all parameters (Path, Query, Body), so 422 is relevant
|
|
86
92
|
in 95%+ of endpoints. Set to False only for endpoints without parameters.
|
|
87
93
|
internal_server_error_500: Add 500 Internal Server Error (explicit). Defaults to False.
|
|
@@ -205,6 +211,38 @@ class Errors(Mapping):
|
|
|
205
211
|
f"Got {type(error).__name__}"
|
|
206
212
|
)
|
|
207
213
|
|
|
214
|
+
def _unique_key(self, examples: Dict[str, Any], base: str) -> str:
|
|
215
|
+
"""Generate unique key for examples dict.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
examples: Existing examples dict
|
|
219
|
+
base: Base key name
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Unique key that doesn't exist in examples
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
>>> errors = Errors()
|
|
226
|
+
>>> errors._unique_key({"default": {}}, "default")
|
|
227
|
+
"default_2"
|
|
228
|
+
>>> errors._unique_key({"default": {}, "default_2": {}}, "default")
|
|
229
|
+
"default_3"
|
|
230
|
+
"""
|
|
231
|
+
key = base
|
|
232
|
+
i = 2
|
|
233
|
+
while key in examples:
|
|
234
|
+
key = f"{base}_{i}"
|
|
235
|
+
i += 1
|
|
236
|
+
return key
|
|
237
|
+
|
|
238
|
+
# Standard descriptions for priority checking
|
|
239
|
+
STANDARD_DESCRIPTIONS = {
|
|
240
|
+
status.HTTP_401_UNAUTHORIZED: "Unauthorized",
|
|
241
|
+
status.HTTP_403_FORBIDDEN: "Forbidden",
|
|
242
|
+
status.HTTP_422_UNPROCESSABLE_ENTITY: "Validation Error",
|
|
243
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error",
|
|
244
|
+
}
|
|
245
|
+
|
|
208
246
|
def _add_standard_error(
|
|
209
247
|
self,
|
|
210
248
|
status_code: int,
|
|
@@ -257,7 +295,9 @@ class Errors(Mapping):
|
|
|
257
295
|
content["examples"] = {}
|
|
258
296
|
|
|
259
297
|
# Add new example with unique key (don't overwrite existing!)
|
|
260
|
-
|
|
298
|
+
# Check if example_key already exists in examples, use unique key if needed
|
|
299
|
+
unique_key = self._unique_key(content["examples"], example_key)
|
|
300
|
+
content["examples"][unique_key] = {"value": example}
|
|
261
301
|
|
|
262
302
|
# Don't overwrite description if it already exists
|
|
263
303
|
# Priority: dict > DTO > standard flags
|
|
@@ -334,7 +374,8 @@ class Errors(Mapping):
|
|
|
334
374
|
existing_json["examples"]["default"] = {"value": response_json["example"]}
|
|
335
375
|
else:
|
|
336
376
|
# If default exists, add with a unique key
|
|
337
|
-
existing_json["examples"]
|
|
377
|
+
unique_key = self._unique_key(existing_json["examples"], "CustomExample")
|
|
378
|
+
existing_json["examples"][unique_key] = {"value": response_json["example"]}
|
|
338
379
|
|
|
339
380
|
def _add_error_dto(self, error_dto: ErrorDTO) -> None:
|
|
340
381
|
"""Add error from ErrorDTO (via Protocol).
|
|
@@ -373,6 +414,11 @@ class Errors(Mapping):
|
|
|
373
414
|
# Merge examples for the same status code
|
|
374
415
|
# Safely access content/application/json with defaults
|
|
375
416
|
existing = self._responses[status_code]
|
|
417
|
+
|
|
418
|
+
# If description is standard, replace it with DTO message (priority: DTO > flags)
|
|
419
|
+
if existing.get("description") == self.STANDARD_DESCRIPTIONS.get(status_code):
|
|
420
|
+
existing["description"] = error_dto.message
|
|
421
|
+
|
|
376
422
|
existing_content = existing.setdefault("content", {})
|
|
377
423
|
content_json = existing_content.setdefault("application/json", {})
|
|
378
424
|
existing_examples = content_json.get("examples", {})
|
|
File without changes
|
{fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-errors-plus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -246,10 +246,10 @@ def delete_item(id: int):
|
|
|
246
246
|
```
|
|
247
247
|
|
|
248
248
|
**Benefits:**
|
|
249
|
-
-
|
|
250
|
-
-
|
|
251
|
-
-
|
|
252
|
-
-
|
|
249
|
+
- No need to write ErrorDTO classes from scratch
|
|
250
|
+
- Correct implementation out of the box
|
|
251
|
+
- Reusable across all endpoints
|
|
252
|
+
- Supports inheritance for custom logic
|
|
253
253
|
|
|
254
254
|
### 5. Mixed Usage
|
|
255
255
|
|
|
@@ -319,6 +319,8 @@ class ErrorDTO(Protocol):
|
|
|
319
319
|
|
|
320
320
|
Any class implementing this protocol (through structural typing) can be used with `Errors()`.
|
|
321
321
|
|
|
322
|
+
**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.
|
|
323
|
+
|
|
322
324
|
### When to Use Protocol vs BaseErrorDTO
|
|
323
325
|
|
|
324
326
|
**Use Protocol (structural typing)** when:
|
|
@@ -333,6 +335,71 @@ Any class implementing this protocol (through structural typing) can be used wit
|
|
|
333
335
|
|
|
334
336
|
Both approaches work together — you can mix them in the same `Errors()` call!
|
|
335
337
|
|
|
338
|
+
## Best Practice: Connecting Exceptions and ErrorDTO
|
|
339
|
+
|
|
340
|
+
### Problem
|
|
341
|
+
|
|
342
|
+
It's not always clear which exception corresponds to which ErrorDTO:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
# Not clear which exception this documents
|
|
346
|
+
responses=Errors(notification_not_found_error)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Solution: Domain Exception as ErrorDTO
|
|
350
|
+
|
|
351
|
+
**Recommended approach** — make your exceptions implement ErrorDTO protocol:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
# domain/exceptions.py
|
|
355
|
+
from typing import Dict, Any
|
|
356
|
+
|
|
357
|
+
class DomainException(Exception):
|
|
358
|
+
"""Base exception implementing ErrorDTO protocol."""
|
|
359
|
+
status_code: int
|
|
360
|
+
message: str
|
|
361
|
+
|
|
362
|
+
def to_example(self) -> Dict[str, Any]:
|
|
363
|
+
return {self.message: {"value": {"detail": self.message}}}
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def for_openapi(cls):
|
|
367
|
+
"""Returns instance for OpenAPI documentation."""
|
|
368
|
+
return cls()
|
|
369
|
+
|
|
370
|
+
class NotificationNotFoundError(DomainException):
|
|
371
|
+
status_code = 404
|
|
372
|
+
message = "Notification not found"
|
|
373
|
+
|
|
374
|
+
def __init__(self, notification_id: str = ""):
|
|
375
|
+
self.notification_id = notification_id
|
|
376
|
+
super().__init__(self.message)
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
def for_openapi(cls):
|
|
380
|
+
return cls(notification_id="example_id")
|
|
381
|
+
|
|
382
|
+
# In endpoint
|
|
383
|
+
@router.delete(
|
|
384
|
+
"/{notificationId}",
|
|
385
|
+
responses=Errors(
|
|
386
|
+
NotificationNotFoundError.for_openapi(), # Clear connection!
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
async def delete_notification(notification_id: str):
|
|
390
|
+
if not notification:
|
|
391
|
+
raise NotificationNotFoundError(notification_id) # Same exception!
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Benefits:**
|
|
395
|
+
- Exception and ErrorDTO are one class
|
|
396
|
+
- Clear connection visible in endpoint
|
|
397
|
+
- No duplication
|
|
398
|
+
- Type-safe
|
|
399
|
+
- Works with any project architecture
|
|
400
|
+
|
|
401
|
+
See [examples/domain_exceptions.py](examples/domain_exceptions.py) for complete example.
|
|
402
|
+
|
|
336
403
|
## Compatibility with Existing Projects
|
|
337
404
|
|
|
338
405
|
If your project already has error DTOs (like `ApiErrorDTO`), they can work with `fastapi-errors-plus` if they implement the `ErrorDTO` protocol:
|
|
@@ -375,11 +442,11 @@ Errors(
|
|
|
375
442
|
*errors: Union[Dict[int, Dict[str, Any]], ErrorDTO],
|
|
376
443
|
unauthorized: bool = False,
|
|
377
444
|
forbidden: bool = False,
|
|
378
|
-
validation_error: bool =
|
|
445
|
+
validation_error: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
|
|
379
446
|
internal_server_error: bool = False,
|
|
380
447
|
unauthorized_401: bool = False,
|
|
381
448
|
forbidden_403: bool = False,
|
|
382
|
-
validation_error_422: bool =
|
|
449
|
+
validation_error_422: Optional[bool] = None, # None (default) => True (FastAPI validates all parameters)
|
|
383
450
|
internal_server_error_500: bool = False,
|
|
384
451
|
)
|
|
385
452
|
```
|
|
@@ -388,18 +455,25 @@ Errors(
|
|
|
388
455
|
- `*errors`: Arbitrary errors as dict or ErrorDTO objects
|
|
389
456
|
- `unauthorized_401`: Add 401 Unauthorized error (recommended, explicit). Defaults to `False`.
|
|
390
457
|
- `forbidden_403`: Add 403 Forbidden error (recommended, explicit). Defaults to `False`.
|
|
391
|
-
- `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit).
|
|
458
|
+
- `validation_error_422`: Add 422 Unprocessable Entity error (recommended, explicit).
|
|
459
|
+
- `None` (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
460
|
+
- `False`: Explicitly disable 422
|
|
461
|
+
- `True`: Explicitly enable 422
|
|
392
462
|
- `internal_server_error_500`: Add 500 Internal Server Error (recommended, explicit). Defaults to `False`.
|
|
393
463
|
- `unauthorized`: Add 401 Unauthorized error (legacy, for backward compatibility). Defaults to `False`.
|
|
394
464
|
- `forbidden`: Add 403 Forbidden error (legacy, for backward compatibility). Defaults to `False`.
|
|
395
|
-
- `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility).
|
|
465
|
+
- `validation_error`: Add 422 Unprocessable Entity error (legacy, for backward compatibility).
|
|
466
|
+
- `None` (default): Add 422 (True by default, FastAPI validates all parameters)
|
|
467
|
+
- `False`: Explicitly disable 422
|
|
468
|
+
- `True`: Explicitly enable 422
|
|
396
469
|
- `internal_server_error`: Add 500 Internal Server Error (legacy, for backward compatibility). Defaults to `False`.
|
|
397
470
|
|
|
398
471
|
**Why `validation_error=True` by default?**
|
|
399
472
|
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`.
|
|
400
473
|
|
|
401
474
|
**Returns:**
|
|
402
|
-
-
|
|
475
|
+
- Mapping object (dict-like) that implements `Dict[int, Dict[str, Any]]` for FastAPI responses
|
|
476
|
+
- Can be used directly in `responses` parameter without calling: `responses=Errors(...)`
|
|
403
477
|
|
|
404
478
|
#### Usage
|
|
405
479
|
|
{fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus.egg-info/SOURCES.txt
RENAMED
|
@@ -5,6 +5,7 @@ fastapi_errors_plus/__init__.py
|
|
|
5
5
|
fastapi_errors_plus/base.py
|
|
6
6
|
fastapi_errors_plus/errors.py
|
|
7
7
|
fastapi_errors_plus/protocol.py
|
|
8
|
+
fastapi_errors_plus/py.typed
|
|
8
9
|
fastapi_errors_plus.egg-info/PKG-INFO
|
|
9
10
|
fastapi_errors_plus.egg-info/SOURCES.txt
|
|
10
11
|
fastapi_errors_plus.egg-info/dependency_links.txt
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-errors-plus"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Universal library for documenting errors in FastAPI endpoints"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -46,3 +46,29 @@ python_files = "test_*.py"
|
|
|
46
46
|
python_classes = "Test*"
|
|
47
47
|
python_functions = "test_*"
|
|
48
48
|
|
|
49
|
+
[tool.coverage.run]
|
|
50
|
+
source = ["fastapi_errors_plus"]
|
|
51
|
+
omit = [
|
|
52
|
+
"*/tests/*",
|
|
53
|
+
"*/__pycache__/*",
|
|
54
|
+
"*/venv/*",
|
|
55
|
+
"*/.venv/*",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.coverage.report]
|
|
59
|
+
exclude_lines = [
|
|
60
|
+
"pragma: no cover",
|
|
61
|
+
"def __repr__",
|
|
62
|
+
"raise AssertionError",
|
|
63
|
+
"raise NotImplementedError",
|
|
64
|
+
"if __name__ == .__main__.:",
|
|
65
|
+
"if TYPE_CHECKING:",
|
|
66
|
+
"@abstractmethod",
|
|
67
|
+
]
|
|
68
|
+
precision = 2
|
|
69
|
+
show_missing = true
|
|
70
|
+
skip_covered = false
|
|
71
|
+
|
|
72
|
+
[tool.coverage.html]
|
|
73
|
+
directory = "tests/htmlcov"
|
|
74
|
+
|
|
@@ -446,6 +446,82 @@ class TestErrorsEdgeCases:
|
|
|
446
446
|
assert len(errors) == 1
|
|
447
447
|
|
|
448
448
|
|
|
449
|
+
class TestUniqueKeys:
|
|
450
|
+
"""Tests for unique key generation to prevent collisions."""
|
|
451
|
+
|
|
452
|
+
def test_unique_keys_for_standard_errors(self):
|
|
453
|
+
"""Test that standard error keys are unique when merging."""
|
|
454
|
+
# First add flag (creates example), then add dict with examples containing same key
|
|
455
|
+
errors = Errors(
|
|
456
|
+
{401: { # Positional first - adds "StandardUnauthorized" to examples
|
|
457
|
+
"content": {
|
|
458
|
+
"application/json": {
|
|
459
|
+
"examples": {
|
|
460
|
+
"StandardUnauthorized": {"value": {"detail": "Custom"}},
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
}},
|
|
465
|
+
unauthorized_401=True, # Named after positional - should use unique key
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Should have both examples
|
|
469
|
+
examples = errors[401]["content"]["application/json"]["examples"]
|
|
470
|
+
# When dict is added first with "StandardUnauthorized", then flag is added
|
|
471
|
+
# Flag creates "example" which gets converted to "default" (not "StandardUnauthorized_2")
|
|
472
|
+
# This is because flag creates "example" first, then it's converted
|
|
473
|
+
assert "StandardUnauthorized" in examples # From dict
|
|
474
|
+
assert "default" in examples # From flag (converted from example)
|
|
475
|
+
|
|
476
|
+
# Both examples should be present
|
|
477
|
+
assert len(examples) == 2
|
|
478
|
+
assert examples["StandardUnauthorized"]["value"]["detail"] == "Custom"
|
|
479
|
+
assert examples["default"]["value"]["detail"] == "Unauthorized"
|
|
480
|
+
|
|
481
|
+
def test_unique_keys_for_dict_errors(self):
|
|
482
|
+
"""Test that dict error keys are unique when merging."""
|
|
483
|
+
errors = Errors(
|
|
484
|
+
{401: {
|
|
485
|
+
"content": {
|
|
486
|
+
"application/json": {
|
|
487
|
+
"example": {"detail": "First"},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
}},
|
|
491
|
+
{401: {
|
|
492
|
+
"content": {
|
|
493
|
+
"application/json": {
|
|
494
|
+
"example": {"detail": "Second"},
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
}},
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
examples = errors[401]["content"]["application/json"]["examples"]
|
|
501
|
+
assert "default" in examples
|
|
502
|
+
# Second example should have unique key
|
|
503
|
+
assert "CustomExample" in examples or "CustomExample_2" in examples
|
|
504
|
+
assert len(examples) == 2
|
|
505
|
+
|
|
506
|
+
def test_unique_keys_multiple_collisions(self):
|
|
507
|
+
"""Test unique key generation with multiple collisions."""
|
|
508
|
+
errors = Errors()
|
|
509
|
+
examples = {
|
|
510
|
+
"CustomExample": {"value": {"detail": "First"}},
|
|
511
|
+
"CustomExample_2": {"value": {"detail": "Second"}},
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# Add another CustomExample - should become CustomExample_3
|
|
515
|
+
unique_key = errors._unique_key(examples, "CustomExample")
|
|
516
|
+
assert unique_key == "CustomExample_3"
|
|
517
|
+
|
|
518
|
+
examples[unique_key] = {"value": {"detail": "Third"}}
|
|
519
|
+
|
|
520
|
+
# Add another - should become CustomExample_4
|
|
521
|
+
unique_key = errors._unique_key(examples, "CustomExample")
|
|
522
|
+
assert unique_key == "CustomExample_4"
|
|
523
|
+
|
|
524
|
+
|
|
449
525
|
class TestErrorsValidation:
|
|
450
526
|
"""Tests for ErrorDTO validation."""
|
|
451
527
|
|
|
@@ -531,3 +607,50 @@ class TestErrorsValidation:
|
|
|
531
607
|
assert "to_example" in error_msg
|
|
532
608
|
assert "BadObject" in error_msg
|
|
533
609
|
|
|
610
|
+
|
|
611
|
+
class TestDTODescriptionPriority:
|
|
612
|
+
"""Tests for DTO description priority over standard flags."""
|
|
613
|
+
|
|
614
|
+
def test_dto_overrides_standard_description(self):
|
|
615
|
+
"""Test that ErrorDTO description overrides standard flag description."""
|
|
616
|
+
from fastapi_errors_plus import StandardErrorDTO
|
|
617
|
+
|
|
618
|
+
custom_401 = StandardErrorDTO(
|
|
619
|
+
status_code=401,
|
|
620
|
+
message="Custom Unauthorized",
|
|
621
|
+
examples={"Custom": "Custom auth error"},
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
errors = Errors(
|
|
625
|
+
custom_401, # Positional first
|
|
626
|
+
unauthorized_401=True, # Named after - adds "Unauthorized"
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# DTO should override standard description
|
|
630
|
+
assert errors[401]["description"] == "Custom Unauthorized"
|
|
631
|
+
|
|
632
|
+
def test_dto_does_not_override_custom_description(self):
|
|
633
|
+
"""Test that DTO does not override non-standard description."""
|
|
634
|
+
from fastapi_errors_plus import StandardErrorDTO
|
|
635
|
+
|
|
636
|
+
custom_401 = StandardErrorDTO(
|
|
637
|
+
status_code=401,
|
|
638
|
+
message="Another Custom",
|
|
639
|
+
examples={"Custom": "Custom auth error"},
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
errors = Errors(
|
|
643
|
+
{401: { # Positional first - custom description
|
|
644
|
+
"description": "My Custom Description",
|
|
645
|
+
"content": {
|
|
646
|
+
"application/json": {
|
|
647
|
+
"example": {"detail": "Custom"},
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
}},
|
|
651
|
+
custom_401, # DTO should not override dict description
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
# Dict description should be preserved (dict > DTO)
|
|
655
|
+
assert errors[401]["description"] == "My Custom Description"
|
|
656
|
+
|
|
File without changes
|
{fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_errors_plus-0.4.0 → fastapi_errors_plus-0.6.0}/fastapi_errors_plus.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|