fastapi-easy-responses 0.1.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.
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fastapi-easy-responses
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple package to easily handle and document all HTTP exceptions and other errors in a FastAPI app.
|
|
5
|
+
Author: Bod Zol
|
|
6
|
+
Author-email: Bod Zol <5642056+bod-zol@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: fastapi[standard]>=0.126.0,<0.200.0
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Project-URL: Homepage, https://github.com/bod-zol/fastapi-easy-responses
|
|
10
|
+
Project-URL: Repository, https://github.com/bod-zol/fastapi-easy-responses
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# FastAPI Easy Responses
|
|
14
|
+
|
|
15
|
+
A simple package to easily handle and document all HTTP exceptions and other errors in a FastAPI app.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv add fastapi-easy-responses
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install fastapi-easy-responses
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# main
|
|
31
|
+
from fastapi_easy_responses import register_custom_exceptions
|
|
32
|
+
app = FastAPI()
|
|
33
|
+
register_custom_exceptions(app) # 1.
|
|
34
|
+
|
|
35
|
+
# exceptions
|
|
36
|
+
from fastapi_easy_responses import CustomAppException
|
|
37
|
+
class DuplicateItemError(CustomAppException): # 2.
|
|
38
|
+
status_code = 409
|
|
39
|
+
description = "Duplicate item already exists"
|
|
40
|
+
|
|
41
|
+
# crud
|
|
42
|
+
async def create_item(session: AsyncSession, item: Item) -> Item:
|
|
43
|
+
try:
|
|
44
|
+
session.add(item)
|
|
45
|
+
await session.commit()
|
|
46
|
+
await session.refresh(item)
|
|
47
|
+
return item
|
|
48
|
+
except IntegrityError as e:
|
|
49
|
+
await session.rollback()
|
|
50
|
+
raise DuplicateItemError() from e # 3.
|
|
51
|
+
|
|
52
|
+
# router
|
|
53
|
+
from fastapi_easy_responses import get_responses
|
|
54
|
+
@router.post(
|
|
55
|
+
"",
|
|
56
|
+
response_model=ItemRead,
|
|
57
|
+
status_code=201,
|
|
58
|
+
responses=get_responses(DuplicateItemError), # 4.
|
|
59
|
+
)
|
|
60
|
+
async def create_item_endpoint(item: ItemCreate, session: AsyncSession):
|
|
61
|
+
db_item = Item.model_validate(item)
|
|
62
|
+
return await create_item(session, db_item) # 5.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This gives you a centralized, more consistent and easier-to-maintain exception handling and documentation.
|
|
66
|
+
|
|
67
|
+
Note the following:
|
|
68
|
+
|
|
69
|
+
1. Call `register_custom_exceptions(app)` to activate the centralized exception handler for `CustomAppException`s.
|
|
70
|
+
1. Inherit your exception class from `CustomAppException`, and provide the required `status_code` and `description` as class variables: as such, these are static per exception class, not dynamic per raise. You'll use this exception class everywhere.
|
|
71
|
+
1. Raise your exception in any operation.
|
|
72
|
+
1. Use the same exception class to generate the OpenAPI documentation. No magic numbers and strings needed, so you have proper autocomplete.
|
|
73
|
+
1. No need to manually catch and convert your exception to HTTPException, the centralized exception handler does it for you. Or more precisely, it returns the same JSONResponse as the default exception handler for HTTPException would.
|
|
74
|
+
|
|
75
|
+
## Why
|
|
76
|
+
|
|
77
|
+
For comparison, this is something like what you would usually do in a FastAPI app.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# exceptions
|
|
81
|
+
class DuplicateItemError(ValueError):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
# crud
|
|
85
|
+
async def create_item(session: AsyncSession, item: Item) -> Item:
|
|
86
|
+
try:
|
|
87
|
+
session.add(item)
|
|
88
|
+
await session.commit()
|
|
89
|
+
await session.refresh(item)
|
|
90
|
+
return item
|
|
91
|
+
except IntegrityError as e:
|
|
92
|
+
await session.rollback()
|
|
93
|
+
raise DuplicateItemError("An item with this name already exists.") from e
|
|
94
|
+
|
|
95
|
+
# router
|
|
96
|
+
@router.post(
|
|
97
|
+
"",
|
|
98
|
+
response_model=ItemRead,
|
|
99
|
+
status_code=201,
|
|
100
|
+
responses={
|
|
101
|
+
409: {"model": Message, "description": "An item with this name already exists."}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
async def create_item_endpoint(item: ItemCreate, session: AsyncSession):
|
|
105
|
+
try:
|
|
106
|
+
db_item = Item.model_validate(item)
|
|
107
|
+
return await create_item(session, db_item)
|
|
108
|
+
except DuplicateItemError as e:
|
|
109
|
+
raise HTTPException(status_code=409, detail=str(e)) from e
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Which means you have to manually maintain all the original exception to HTTPException mappings. If you forget it, your unhandled exception will raise a generic internal server error. It also requires additional manual work to provide consistent documentation.
|
|
113
|
+
|
|
114
|
+
## Similar packages
|
|
115
|
+
|
|
116
|
+
There are similar packages with similar purpose, like
|
|
117
|
+
|
|
118
|
+
- [APIException](https://github.com/akutayural/APIException)
|
|
119
|
+
- [fastapi-problem](https://github.com/NRWLDev/fastapi-problem)
|
|
120
|
+
|
|
121
|
+
This package is simpler for basic use-cases, which may or may not be what you want. This package doesn't introduce new response schemas, custom error codes, doesn't provide logging (yet); but gives you the least amount of code you have to write.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
[MIT](/LICENSE.md)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# FastAPI Easy Responses
|
|
2
|
+
|
|
3
|
+
A simple package to easily handle and document all HTTP exceptions and other errors in a FastAPI app.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add fastapi-easy-responses
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install fastapi-easy-responses
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
# main
|
|
19
|
+
from fastapi_easy_responses import register_custom_exceptions
|
|
20
|
+
app = FastAPI()
|
|
21
|
+
register_custom_exceptions(app) # 1.
|
|
22
|
+
|
|
23
|
+
# exceptions
|
|
24
|
+
from fastapi_easy_responses import CustomAppException
|
|
25
|
+
class DuplicateItemError(CustomAppException): # 2.
|
|
26
|
+
status_code = 409
|
|
27
|
+
description = "Duplicate item already exists"
|
|
28
|
+
|
|
29
|
+
# crud
|
|
30
|
+
async def create_item(session: AsyncSession, item: Item) -> Item:
|
|
31
|
+
try:
|
|
32
|
+
session.add(item)
|
|
33
|
+
await session.commit()
|
|
34
|
+
await session.refresh(item)
|
|
35
|
+
return item
|
|
36
|
+
except IntegrityError as e:
|
|
37
|
+
await session.rollback()
|
|
38
|
+
raise DuplicateItemError() from e # 3.
|
|
39
|
+
|
|
40
|
+
# router
|
|
41
|
+
from fastapi_easy_responses import get_responses
|
|
42
|
+
@router.post(
|
|
43
|
+
"",
|
|
44
|
+
response_model=ItemRead,
|
|
45
|
+
status_code=201,
|
|
46
|
+
responses=get_responses(DuplicateItemError), # 4.
|
|
47
|
+
)
|
|
48
|
+
async def create_item_endpoint(item: ItemCreate, session: AsyncSession):
|
|
49
|
+
db_item = Item.model_validate(item)
|
|
50
|
+
return await create_item(session, db_item) # 5.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This gives you a centralized, more consistent and easier-to-maintain exception handling and documentation.
|
|
54
|
+
|
|
55
|
+
Note the following:
|
|
56
|
+
|
|
57
|
+
1. Call `register_custom_exceptions(app)` to activate the centralized exception handler for `CustomAppException`s.
|
|
58
|
+
1. Inherit your exception class from `CustomAppException`, and provide the required `status_code` and `description` as class variables: as such, these are static per exception class, not dynamic per raise. You'll use this exception class everywhere.
|
|
59
|
+
1. Raise your exception in any operation.
|
|
60
|
+
1. Use the same exception class to generate the OpenAPI documentation. No magic numbers and strings needed, so you have proper autocomplete.
|
|
61
|
+
1. No need to manually catch and convert your exception to HTTPException, the centralized exception handler does it for you. Or more precisely, it returns the same JSONResponse as the default exception handler for HTTPException would.
|
|
62
|
+
|
|
63
|
+
## Why
|
|
64
|
+
|
|
65
|
+
For comparison, this is something like what you would usually do in a FastAPI app.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# exceptions
|
|
69
|
+
class DuplicateItemError(ValueError):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# crud
|
|
73
|
+
async def create_item(session: AsyncSession, item: Item) -> Item:
|
|
74
|
+
try:
|
|
75
|
+
session.add(item)
|
|
76
|
+
await session.commit()
|
|
77
|
+
await session.refresh(item)
|
|
78
|
+
return item
|
|
79
|
+
except IntegrityError as e:
|
|
80
|
+
await session.rollback()
|
|
81
|
+
raise DuplicateItemError("An item with this name already exists.") from e
|
|
82
|
+
|
|
83
|
+
# router
|
|
84
|
+
@router.post(
|
|
85
|
+
"",
|
|
86
|
+
response_model=ItemRead,
|
|
87
|
+
status_code=201,
|
|
88
|
+
responses={
|
|
89
|
+
409: {"model": Message, "description": "An item with this name already exists."}
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
async def create_item_endpoint(item: ItemCreate, session: AsyncSession):
|
|
93
|
+
try:
|
|
94
|
+
db_item = Item.model_validate(item)
|
|
95
|
+
return await create_item(session, db_item)
|
|
96
|
+
except DuplicateItemError as e:
|
|
97
|
+
raise HTTPException(status_code=409, detail=str(e)) from e
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Which means you have to manually maintain all the original exception to HTTPException mappings. If you forget it, your unhandled exception will raise a generic internal server error. It also requires additional manual work to provide consistent documentation.
|
|
101
|
+
|
|
102
|
+
## Similar packages
|
|
103
|
+
|
|
104
|
+
There are similar packages with similar purpose, like
|
|
105
|
+
|
|
106
|
+
- [APIException](https://github.com/akutayural/APIException)
|
|
107
|
+
- [fastapi-problem](https://github.com/NRWLDev/fastapi-problem)
|
|
108
|
+
|
|
109
|
+
This package is simpler for basic use-cases, which may or may not be what you want. This package doesn't introduce new response schemas, custom error codes, doesn't provide logging (yet); but gives you the least amount of code you have to write.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
[MIT](/LICENSE.md)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastapi-easy-responses"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A simple package to easily handle and document all HTTP exceptions and other errors in a FastAPI app."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Bod Zol", email = "5642056+bod-zol@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fastapi[standard]>=0.126.0,<0.200.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://github.com/bod-zol/fastapi-easy-responses"
|
|
16
|
+
Repository = "https://github.com/bod-zol/fastapi-easy-responses"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.11.14,<0.12.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[tool.ruff.lint]
|
|
23
|
+
select = [
|
|
24
|
+
"E", # pycodestyle errors
|
|
25
|
+
"W", # pycodestyle warnings
|
|
26
|
+
"F", # pyflakes
|
|
27
|
+
"FAST", # fastapi
|
|
28
|
+
"I", # isort
|
|
29
|
+
"B", # flake8-bugbear
|
|
30
|
+
"C4", # flake8-comprehensions
|
|
31
|
+
"UP", # pyupgrade
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"pre-commit>=4.6.0",
|
|
37
|
+
"pytest>=9.0.3",
|
|
38
|
+
"pytest-cov>=7.1.0",
|
|
39
|
+
"ruff>=0.15.12",
|
|
40
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Convenience imports for the fastapi-easy-responses package."""
|
|
2
|
+
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
CustomAppException,
|
|
5
|
+
ErrorResponse,
|
|
6
|
+
get_responses,
|
|
7
|
+
register_custom_exceptions,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ErrorResponse",
|
|
12
|
+
"CustomAppException",
|
|
13
|
+
"register_custom_exceptions",
|
|
14
|
+
"get_responses",
|
|
15
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from fastapi import FastAPI, Request, status
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ErrorResponse(BaseModel):
|
|
7
|
+
"""Standardized error response model."""
|
|
8
|
+
|
|
9
|
+
detail: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CustomAppException(Exception):
|
|
13
|
+
"""Base class for custom application exceptions with HTTP metadata.
|
|
14
|
+
Subclasses will automatically register their status code and description.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
18
|
+
description: str = "Internal server error"
|
|
19
|
+
registry: dict[str, dict[str, object]] = {}
|
|
20
|
+
|
|
21
|
+
def __init_subclass__(cls, **kwargs):
|
|
22
|
+
super().__init_subclass__(**kwargs)
|
|
23
|
+
if cls is not CustomAppException:
|
|
24
|
+
CustomAppException.registry[cls.__name__] = {
|
|
25
|
+
"status_code": cls.status_code,
|
|
26
|
+
"description": cls.description,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_custom_exceptions(app: FastAPI) -> None:
|
|
31
|
+
"""Register exception handlers for CustomAppException subclasses
|
|
32
|
+
using FastAPI's decorator.
|
|
33
|
+
This function should be called in the main application setup to ensure
|
|
34
|
+
that all custom exceptions are properly handled.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@app.exception_handler(CustomAppException)
|
|
38
|
+
async def custom_app_exception_handler(
|
|
39
|
+
request: Request, exc: CustomAppException
|
|
40
|
+
) -> JSONResponse:
|
|
41
|
+
"""Convert CustomAppException to JSONResponse with appropriate status code."""
|
|
42
|
+
return JSONResponse(
|
|
43
|
+
status_code=exc.status_code,
|
|
44
|
+
content=ErrorResponse(detail=exc.description).model_dump(),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_responses(*exception_classes: type[Exception]) -> dict:
|
|
49
|
+
"""
|
|
50
|
+
Auto-generate FastAPI response documentation from exception registry.
|
|
51
|
+
|
|
52
|
+
Remarks:
|
|
53
|
+
- Handles only exceptions that are subclasses of CustomAppException.
|
|
54
|
+
- If multiple exceptions share the same status code,
|
|
55
|
+
only the last one will be documented for that code.
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
@router.post("...", responses=get_responses(DuplicateItemError))
|
|
59
|
+
async def my_endpoint(...): ...
|
|
60
|
+
"""
|
|
61
|
+
responses = {}
|
|
62
|
+
for exc_cls in exception_classes:
|
|
63
|
+
exc_name = exc_cls.__name__
|
|
64
|
+
if exc_name in CustomAppException.registry:
|
|
65
|
+
exc_info = CustomAppException.registry[exc_name]
|
|
66
|
+
responses[exc_info["status_code"]] = {
|
|
67
|
+
"description": exc_info["description"],
|
|
68
|
+
"model": ErrorResponse,
|
|
69
|
+
}
|
|
70
|
+
return responses
|