fastapi-errors-plus 0.5.0__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/LICENSE +1 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/PKG-INFO +72 -5
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/README.md +71 -4
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/__init__.py +1 -1
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/base.py +1 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/protocol.py +1 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/PKG-INFO +72 -5
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/pyproject.toml +27 -1
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/tests/test_app.py +118 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/tests/test_base.py +1 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/tests/test_integration.py +95 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/errors.py +0 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/py.typed +0 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/SOURCES.txt +0 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/requires.txt +0 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/setup.cfg +0 -0
- {fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/tests/test_errors.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.1
|
|
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:
|
|
@@ -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:
|
{fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/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.1
|
|
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:
|
|
@@ -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.1"
|
|
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
|
+
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Test FastAPI application for integration testing."""
|
|
2
2
|
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
3
5
|
from fastapi import APIRouter, FastAPI, status
|
|
4
6
|
from fastapi_errors_plus import BaseErrorDTO, Errors, StandardErrorDTO
|
|
5
7
|
from tests.conftest import SimpleErrorDTO
|
|
@@ -11,6 +13,57 @@ app = FastAPI(title="Test API for fastapi-errors-plus")
|
|
|
11
13
|
router = APIRouter()
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
# Domain Exception pattern for testing (Best Practice example)
|
|
17
|
+
class DomainException(Exception):
|
|
18
|
+
"""Base exception implementing ErrorDTO protocol."""
|
|
19
|
+
status_code: int
|
|
20
|
+
message: str
|
|
21
|
+
|
|
22
|
+
def to_example(self) -> Dict[str, Any]:
|
|
23
|
+
"""Generate example for OpenAPI."""
|
|
24
|
+
return {
|
|
25
|
+
self.message: {
|
|
26
|
+
"value": {"detail": self.message},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def for_openapi(cls):
|
|
32
|
+
"""Returns instance for OpenAPI documentation."""
|
|
33
|
+
return cls()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestItemNotFoundError(DomainException):
|
|
37
|
+
"""Test item not found error."""
|
|
38
|
+
status_code = status.HTTP_404_NOT_FOUND
|
|
39
|
+
message = "Test item not found"
|
|
40
|
+
|
|
41
|
+
def __init__(self, item_id: str = ""):
|
|
42
|
+
self.item_id = item_id
|
|
43
|
+
super().__init__(self.message)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def for_openapi(cls):
|
|
47
|
+
"""Returns instance for OpenAPI documentation."""
|
|
48
|
+
return cls(item_id="test_id")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestItemAccessDeniedError(DomainException):
|
|
52
|
+
"""Test item access denied error."""
|
|
53
|
+
status_code = status.HTTP_403_FORBIDDEN
|
|
54
|
+
message = "Test item access denied"
|
|
55
|
+
|
|
56
|
+
def __init__(self, item_id: str = "", user_id: str = ""):
|
|
57
|
+
self.item_id = item_id
|
|
58
|
+
self.user_id = user_id
|
|
59
|
+
super().__init__(self.message)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def for_openapi(cls):
|
|
63
|
+
"""Returns instance for OpenAPI documentation."""
|
|
64
|
+
return cls(item_id="test_id", user_id="test_user")
|
|
65
|
+
|
|
66
|
+
|
|
14
67
|
# Example 1: Standard flags only
|
|
15
68
|
@router.get(
|
|
16
69
|
"/standard-flags",
|
|
@@ -220,6 +273,71 @@ def create_item_mixed_base_dto(item_id: int):
|
|
|
220
273
|
return {"message": f"Item {item_id} created"}
|
|
221
274
|
|
|
222
275
|
|
|
276
|
+
# Domain Exception pattern for testing (Best Practice example)
|
|
277
|
+
class DomainException(Exception):
|
|
278
|
+
"""Base exception implementing ErrorDTO protocol."""
|
|
279
|
+
status_code: int
|
|
280
|
+
message: str
|
|
281
|
+
|
|
282
|
+
def to_example(self) -> Dict[str, Any]:
|
|
283
|
+
"""Generate example for OpenAPI."""
|
|
284
|
+
return {
|
|
285
|
+
self.message: {
|
|
286
|
+
"value": {"detail": self.message},
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def for_openapi(cls):
|
|
292
|
+
"""Returns instance for OpenAPI documentation."""
|
|
293
|
+
return cls()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TestItemNotFoundError(DomainException):
|
|
297
|
+
"""Test item not found error."""
|
|
298
|
+
status_code = status.HTTP_404_NOT_FOUND
|
|
299
|
+
message = "Test item not found"
|
|
300
|
+
|
|
301
|
+
def __init__(self, item_id: str = ""):
|
|
302
|
+
self.item_id = item_id
|
|
303
|
+
super().__init__(self.message)
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def for_openapi(cls):
|
|
307
|
+
"""Returns instance for OpenAPI documentation."""
|
|
308
|
+
return cls(item_id="test_id")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class TestItemAccessDeniedError(DomainException):
|
|
312
|
+
"""Test item access denied error."""
|
|
313
|
+
status_code = status.HTTP_403_FORBIDDEN
|
|
314
|
+
message = "Test item access denied"
|
|
315
|
+
|
|
316
|
+
def __init__(self, item_id: str = "", user_id: str = ""):
|
|
317
|
+
self.item_id = item_id
|
|
318
|
+
self.user_id = user_id
|
|
319
|
+
super().__init__(self.message)
|
|
320
|
+
|
|
321
|
+
@classmethod
|
|
322
|
+
def for_openapi(cls):
|
|
323
|
+
"""Returns instance for OpenAPI documentation."""
|
|
324
|
+
return cls(item_id="test_id", user_id="test_user")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# Example 11: Domain Exception as ErrorDTO (Best Practice pattern)
|
|
328
|
+
@router.delete(
|
|
329
|
+
"/domain-exception/{item_id}",
|
|
330
|
+
responses=Errors(
|
|
331
|
+
TestItemNotFoundError.for_openapi(), # Using for_openapi() pattern
|
|
332
|
+
TestItemAccessDeniedError.for_openapi(), # Using for_openapi() pattern
|
|
333
|
+
unauthorized_401=True, # Standard flag
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
def delete_item_domain_exception(item_id: str):
|
|
337
|
+
"""Endpoint demonstrating Domain Exception as ErrorDTO pattern."""
|
|
338
|
+
return {"message": f"Item {item_id} deleted"}
|
|
339
|
+
|
|
340
|
+
|
|
223
341
|
# Register router
|
|
224
342
|
app.include_router(router, prefix="/api/v1", tags=["test"])
|
|
225
343
|
|
|
@@ -319,6 +319,93 @@ class TestMixedBaseDTOEndpoint:
|
|
|
319
319
|
assert "500" in responses # From flag
|
|
320
320
|
|
|
321
321
|
|
|
322
|
+
class TestDomainExceptionEndpoint:
|
|
323
|
+
"""Tests for Domain Exception as ErrorDTO endpoint (Best Practice pattern)."""
|
|
324
|
+
|
|
325
|
+
def test_responses_in_openapi(self):
|
|
326
|
+
"""Test that Domain Exception responses are in OpenAPI."""
|
|
327
|
+
client = TestClient(app)
|
|
328
|
+
response = client.get("/openapi.json")
|
|
329
|
+
schema = response.json()
|
|
330
|
+
|
|
331
|
+
endpoint = schema["paths"]["/api/v1/domain-exception/{item_id}"]["delete"]
|
|
332
|
+
responses = endpoint["responses"]
|
|
333
|
+
|
|
334
|
+
assert "401" in responses # From flag
|
|
335
|
+
assert "403" in responses # From TestItemAccessDeniedError.for_openapi()
|
|
336
|
+
assert "404" in responses # From TestItemNotFoundError.for_openapi()
|
|
337
|
+
|
|
338
|
+
def test_404_response_structure_from_domain_exception(self):
|
|
339
|
+
"""Test 404 response structure from Domain Exception."""
|
|
340
|
+
client = TestClient(app)
|
|
341
|
+
response = client.get("/openapi.json")
|
|
342
|
+
schema = response.json()
|
|
343
|
+
|
|
344
|
+
endpoint = schema["paths"]["/api/v1/domain-exception/{item_id}"]["delete"]
|
|
345
|
+
error_404 = endpoint["responses"]["404"]
|
|
346
|
+
|
|
347
|
+
assert error_404["description"] == "Test item not found"
|
|
348
|
+
assert "content" in error_404
|
|
349
|
+
assert "application/json" in error_404["content"]
|
|
350
|
+
assert "examples" in error_404["content"]["application/json"]
|
|
351
|
+
examples = error_404["content"]["application/json"]["examples"]
|
|
352
|
+
assert "Test item not found" in examples
|
|
353
|
+
|
|
354
|
+
def test_403_response_structure_from_domain_exception(self):
|
|
355
|
+
"""Test 403 response structure from Domain Exception."""
|
|
356
|
+
client = TestClient(app)
|
|
357
|
+
response = client.get("/openapi.json")
|
|
358
|
+
schema = response.json()
|
|
359
|
+
|
|
360
|
+
endpoint = schema["paths"]["/api/v1/domain-exception/{item_id}"]["delete"]
|
|
361
|
+
error_403 = endpoint["responses"]["403"]
|
|
362
|
+
|
|
363
|
+
assert error_403["description"] == "Test item access denied"
|
|
364
|
+
assert "content" in error_403
|
|
365
|
+
assert "application/json" in error_403["content"]
|
|
366
|
+
assert "examples" in error_403["content"]["application/json"]
|
|
367
|
+
examples = error_403["content"]["application/json"]["examples"]
|
|
368
|
+
assert "Test item access denied" in examples
|
|
369
|
+
|
|
370
|
+
def test_for_openapi_method_returns_error_dto(self):
|
|
371
|
+
"""Test that for_openapi() method returns instance implementing ErrorDTO."""
|
|
372
|
+
from tests.test_app import TestItemNotFoundError
|
|
373
|
+
|
|
374
|
+
error_instance = TestItemNotFoundError.for_openapi()
|
|
375
|
+
|
|
376
|
+
# Check that it implements ErrorDTO protocol
|
|
377
|
+
assert hasattr(error_instance, "status_code")
|
|
378
|
+
assert hasattr(error_instance, "message")
|
|
379
|
+
assert hasattr(error_instance, "to_example")
|
|
380
|
+
assert callable(error_instance.to_example)
|
|
381
|
+
|
|
382
|
+
# Check values
|
|
383
|
+
assert error_instance.status_code == 404
|
|
384
|
+
assert error_instance.message == "Test item not found"
|
|
385
|
+
assert error_instance.item_id == "test_id"
|
|
386
|
+
|
|
387
|
+
# Check to_example() works
|
|
388
|
+
example = error_instance.to_example()
|
|
389
|
+
assert isinstance(example, dict)
|
|
390
|
+
assert "Test item not found" in example
|
|
391
|
+
|
|
392
|
+
def test_domain_exception_works_with_errors_class(self):
|
|
393
|
+
"""Test that Domain Exception instances work with Errors class."""
|
|
394
|
+
from fastapi_errors_plus import Errors
|
|
395
|
+
from tests.test_app import TestItemNotFoundError, TestItemAccessDeniedError
|
|
396
|
+
|
|
397
|
+
errors = Errors(
|
|
398
|
+
TestItemNotFoundError.for_openapi(),
|
|
399
|
+
TestItemAccessDeniedError.for_openapi(),
|
|
400
|
+
)
|
|
401
|
+
responses = errors
|
|
402
|
+
|
|
403
|
+
assert 404 in responses
|
|
404
|
+
assert 403 in responses
|
|
405
|
+
assert responses[404]["description"] == "Test item not found"
|
|
406
|
+
assert responses[403]["description"] == "Test item access denied"
|
|
407
|
+
|
|
408
|
+
|
|
322
409
|
class TestRealEndpoints:
|
|
323
410
|
"""Tests for actual endpoint responses."""
|
|
324
411
|
|
|
@@ -361,5 +448,13 @@ class TestRealEndpoints:
|
|
|
361
448
|
|
|
362
449
|
assert response.status_code == 200
|
|
363
450
|
assert response.json() == {"message": "Item 1 deleted"}
|
|
451
|
+
|
|
452
|
+
def test_domain_exception_endpoint_works(self):
|
|
453
|
+
"""Test that Domain Exception endpoint returns 200."""
|
|
454
|
+
client = TestClient(app)
|
|
455
|
+
response = client.delete("/api/v1/domain-exception/1")
|
|
456
|
+
|
|
457
|
+
assert response.status_code == 200
|
|
458
|
+
assert response.json() == {"message": "Item 1 deleted"}
|
|
364
459
|
|
|
365
460
|
|
|
File without changes
|
|
File without changes
|
{fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_errors_plus-0.5.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|