fastapi-errors-plus 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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.
@@ -10,5 +10,5 @@ __all__ = [
10
10
  "BaseErrorDTO", # Base implementation (optional)
11
11
  "StandardErrorDTO", # Extended implementation (optional)
12
12
  ]
13
- __version__ = "0.4.0"
13
+ __version__ = "0.6.0"
14
14
 
@@ -120,3 +120,5 @@ class StandardErrorDTO(BaseErrorDTO):
120
120
  for key, message in self.examples.items()
121
121
  }
122
122
 
123
+
124
+
@@ -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
- unauthorized_401=True, # 401 (explicit)
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. Defaults to True (None means True).
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). Defaults to True (None means True).
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
- content["examples"][example_key] = {"value": example}
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"]["CustomExample"] = {"value": response_json["example"]}
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", {})
@@ -48,3 +48,5 @@ class ErrorDTO(Protocol):
48
48
  ...
49
49
 
50
50
 
51
+
52
+
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-errors-plus
3
- Version: 0.4.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
- - 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
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 = True, # Defaults to True (FastAPI validates all parameters)
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 = True, # Defaults to True (FastAPI validates all parameters)
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). Defaults to `True` (FastAPI validates all parameters).
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). Defaults to `True` (FastAPI validates all parameters).
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
- - Callable object that returns `Dict[int, Dict[str, Any]]` in FastAPI responses format
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
 
@@ -0,0 +1,10 @@
1
+ fastapi_errors_plus/__init__.py,sha256=6xx4sQHjvhEHcxsenhJMZlR2RrH-FnahEPVoRvAjakI,454
2
+ fastapi_errors_plus/base.py,sha256=iTQ6WFxc5fLoY0Omi8aPNua7wBqQj20Z5ZPOOJhTIow,3580
3
+ fastapi_errors_plus/errors.py,sha256=7ikmrSphWOH_nnxLC0HPQ0RPiuR1u8olw3bxGxdLv50,20868
4
+ fastapi_errors_plus/protocol.py,sha256=466lfnVKWa1jjD0iWy3r_FIXFIqTuATJOT9sqXxbKZE,1218
5
+ fastapi_errors_plus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ fastapi_errors_plus-0.6.0.dist-info/licenses/LICENSE,sha256=W_dONuuw-DA1UPcP9NPnQbhCbtMkLjhUr3Ne96398VU,1093
7
+ fastapi_errors_plus-0.6.0.dist-info/METADATA,sha256=03r27kEKm4G10pYJ77aBQecEQOq_8hTlndEuYYWpzo4,21455
8
+ fastapi_errors_plus-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ fastapi_errors_plus-0.6.0.dist-info/top_level.txt,sha256=vmHLF1-DQYB2quqrvdBA0g6rDtgOWQBrA1m-X_WyNk8,20
10
+ fastapi_errors_plus-0.6.0.dist-info/RECORD,,
@@ -21,3 +21,5 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
23
 
24
+
25
+
@@ -1,9 +0,0 @@
1
- fastapi_errors_plus/__init__.py,sha256=0f8Avw6S8rxv_dq54spP7DsIrgcOlZ8TAMWPfd0XoRQ,454
2
- fastapi_errors_plus/base.py,sha256=SpZxS_avOr1kwcuJXjR-BQbeKX8Yp3pExCujR76E8Uk,3578
3
- fastapi_errors_plus/errors.py,sha256=pz7AJQoGc6vqVIA0ZA6agrq-QHxvL6wOMutR0gmB-tI,18899
4
- fastapi_errors_plus/protocol.py,sha256=pFIVjMB5foltP_BINLllo6wEDdVncXeNIpOEbd2_o4o,1216
5
- fastapi_errors_plus-0.4.0.dist-info/licenses/LICENSE,sha256=yQIQ4Uyt7lAeNcxDofCQTLZA7G5hkHqJc0BKHC4CDXk,1091
6
- fastapi_errors_plus-0.4.0.dist-info/METADATA,sha256=WDr9qBzVqBcte5CTQpUVl8zxXxQ1OkHw0UL3PXIpwec,19186
7
- fastapi_errors_plus-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- fastapi_errors_plus-0.4.0.dist-info/top_level.txt,sha256=vmHLF1-DQYB2quqrvdBA0g6rDtgOWQBrA1m-X_WyNk8,20
9
- fastapi_errors_plus-0.4.0.dist-info/RECORD,,