Skip to content

Commit bd20ef7

Browse files
committed
FastAPI integration
1 parent 9762b79 commit bd20ef7

File tree

14 files changed

+635
-13
lines changed

14 files changed

+635
-13
lines changed

docs/integrations/fastapi.rst

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
FastAPI
2+
=========
3+
4+
This section describes integration with `FastAPI <https://fastapi.tiangolo.com>`__ ASGI framework.
5+
6+
.. note::
7+
8+
FastAPI also provides OpenAPI support. The main difference is that, unlike FastAPI's code-first approach, OpenAPI-core allows you to laverage your existing specification that alligns with API-First approach. You can read more about API-first vs. code-first in the [Guide to API-first](https://www.postman.com/api-first/).
9+
10+
Middleware
11+
----------
12+
13+
FastAPI can be integrated by `middleware <https://fastapi.tiangolo.com/tutorial/middleware/>`__ to apply OpenAPI validation to your entire application.
14+
15+
Add ``FastAPIOpenAPIMiddleware`` with OpenAPI object to your ``middleware`` list.
16+
17+
.. code-block:: python
18+
:emphasize-lines: 2,5
19+
20+
from fastapi import FastAPI
21+
from openapi_core.contrib.fastapi.middlewares import FastAPIOpenAPIMiddleware
22+
23+
app = FastAPI()
24+
app.add_middleware(FastAPIOpenAPIMiddleware, openapi=openapi)
25+
26+
After that all your requests and responses will be validated.
27+
28+
Also you have access to unmarshal result object with all unmarshalled request data through ``openapi`` scope of request object.
29+
30+
.. code-block:: python
31+
32+
async def homepage(request):
33+
# get parameters object with path, query, cookies and headers parameters
34+
unmarshalled_params = request.scope["openapi"].parameters
35+
# or specific location parameters
36+
unmarshalled_path_params = request.scope["openapi"].parameters.path
37+
38+
# get body
39+
unmarshalled_body = request.scope["openapi"].body
40+
41+
# get security data
42+
unmarshalled_security = request.scope["openapi"].security
43+
44+
Response validation
45+
^^^^^^^^^^^^^^^^^^^
46+
47+
You can skip response validation process: by setting ``response_cls`` to ``None``
48+
49+
.. code-block:: python
50+
:emphasize-lines: 2
51+
52+
app = FastAPI()
53+
app.add_middleware(FastAPIOpenAPIMiddleware, openapi=openapi, response_cls=None)
54+
55+
Low level
56+
---------
57+
58+
For low level integration see `Starlette <starlette.rst>`_ integration.

docs/integrations/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Openapi-core integrates with your popular libraries and frameworks. Each integra
1010
bottle
1111
django
1212
falcon
13+
fastapi
1314
flask
1415
pyramid
1516
requests
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from openapi_core.contrib.fastapi.middlewares import FastAPIOpenAPIMiddleware
2+
from openapi_core.contrib.fastapi.requests import FastAPIOpenAPIRequest
3+
from openapi_core.contrib.fastapi.responses import FastAPIOpenAPIResponse
4+
5+
__all__ = [
6+
"FastAPIOpenAPIMiddleware",
7+
"FastAPIOpenAPIRequest",
8+
"FastAPIOpenAPIResponse",
9+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from openapi_core.contrib.starlette.middlewares import (
2+
StarletteOpenAPIMiddleware as FastAPIOpenAPIMiddleware,
3+
)
4+
5+
__all__ = ["FastAPIOpenAPIMiddleware"]
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from fastapi import Request
2+
3+
from openapi_core.contrib.starlette.requests import (
4+
StarletteOpenAPIRequest,
5+
)
6+
7+
8+
class FastAPIOpenAPIRequest(StarletteOpenAPIRequest):
9+
def __init__(self, request: Request):
10+
super().__init__(request)
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from fastapi import Response
2+
3+
from openapi_core.contrib.starlette.responses import (
4+
StarletteOpenAPIResponse,
5+
)
6+
7+
8+
class FastAPIOpenAPIResponse(StarletteOpenAPIResponse):
9+
def __init__(self, response: Response, data: Optional[bytes] = None):
10+
super().__init__(response, data=data)

poetry.lock

+33-13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,12 @@ jsonschema-path = "^0.3.1"
7676
jsonschema = "^4.18.0"
7777
multidict = {version = "^6.0.4", optional = true}
7878
aioitertools = {version = "^0.11.0", optional = true}
79+
fastapi = {version = "^0.108.0", optional = true}
7980

8081
[tool.poetry.extras]
8182
django = ["django"]
8283
falcon = ["falcon"]
84+
fastapi = ["fastapi"]
8385
flask = ["flask"]
8486
requests = ["requests"]
8587
aiohttp = ["aiohttp", "multidict"]
@@ -108,6 +110,7 @@ aiohttp = "^3.8.4"
108110
pytest-aiohttp = "^1.0.4"
109111
bump2version = "^1.0.1"
110112
pyflakes = "^3.1.0"
113+
fastapi = "^0.108.0"
111114

112115
[tool.poetry.group.docs.dependencies]
113116
sphinx = ">=5.3,<8.0"

tests/integration/contrib/fastapi/data/v3.0/fastapiproject/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from fastapi import FastAPI
2+
from fastapiproject.openapi import openapi
3+
from fastapiproject.routers import pets
4+
5+
from openapi_core.contrib.fastapi.middlewares import FastAPIOpenAPIMiddleware
6+
7+
app = FastAPI()
8+
app.add_middleware(FastAPIOpenAPIMiddleware, openapi=openapi)
9+
app.include_router(pets.router)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pathlib import Path
2+
3+
import yaml
4+
5+
from openapi_core import OpenAPI
6+
7+
openapi_spec_path = Path("tests/integration/data/v3.0/petstore.yaml")
8+
spec_dict = yaml.load(openapi_spec_path.read_text(), yaml.Loader)
9+
openapi = OpenAPI.from_dict(spec_dict)

tests/integration/contrib/fastapi/data/v3.0/fastapiproject/routers/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from base64 import b64decode
2+
from typing import Annotated
3+
4+
from fastapi import APIRouter
5+
from fastapi import Body
6+
from fastapi import Request
7+
from fastapi import Response
8+
from fastapi import status
9+
10+
OPENID_LOGO = b64decode(
11+
"""
12+
R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d
13+
3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA
14+
AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg
15+
EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD
16+
Fzk0lpcjIQA7
17+
"""
18+
)
19+
20+
21+
router = APIRouter(
22+
prefix="/v1/pets",
23+
tags=["pets"],
24+
responses={404: {"description": "Not found"}},
25+
)
26+
27+
28+
@router.get("")
29+
async def list_pets(request: Request, response: Response):
30+
assert request.scope["openapi"]
31+
assert not request.scope["openapi"].errors
32+
assert request.scope["openapi"].parameters.query == {
33+
"page": 1,
34+
"limit": 12,
35+
"search": "",
36+
}
37+
data = [
38+
{
39+
"id": 12,
40+
"name": "Cat",
41+
"ears": {
42+
"healthy": True,
43+
},
44+
},
45+
]
46+
response.headers["X-Rate-Limit"] = "12"
47+
return {"data": data}
48+
49+
50+
@router.post("")
51+
async def create_pet(request: Request):
52+
assert request.scope["openapi"].parameters.cookie == {
53+
"user": 1,
54+
}
55+
assert request.scope["openapi"].parameters.header == {
56+
"api-key": "12345",
57+
}
58+
assert request.scope["openapi"].body.__class__.__name__ == "PetCreate"
59+
assert request.scope["openapi"].body.name in ["Cat", "Bird"]
60+
if request.scope["openapi"].body.name == "Cat":
61+
assert request.scope["openapi"].body.ears.__class__.__name__ == "Ears"
62+
assert request.scope["openapi"].body.ears.healthy is True
63+
if request.scope["openapi"].body.name == "Bird":
64+
assert (
65+
request.scope["openapi"].body.wings.__class__.__name__ == "Wings"
66+
)
67+
assert request.scope["openapi"].body.wings.healthy is True
68+
69+
headers = {
70+
"X-Rate-Limit": "12",
71+
}
72+
return Response(status_code=status.HTTP_201_CREATED, headers=headers)
73+
74+
75+
@router.get("/{petId}")
76+
async def detail_pet(request: Request, response: Response):
77+
assert request.scope["openapi"]
78+
assert not request.scope["openapi"].errors
79+
assert request.scope["openapi"].parameters.path == {
80+
"petId": 12,
81+
}
82+
data = {
83+
"id": 12,
84+
"name": "Cat",
85+
"ears": {
86+
"healthy": True,
87+
},
88+
}
89+
response.headers["X-Rate-Limit"] = "12"
90+
return {
91+
"data": data,
92+
}
93+
94+
95+
@router.get("/{petId}/photo")
96+
async def download_pet_photo():
97+
return Response(content=OPENID_LOGO, media_type="image/gif")
98+
99+
100+
@router.post("/{petId}/photo")
101+
async def upload_pet_photo(
102+
image: Annotated[bytes, Body(media_type="image/jpg")],
103+
):
104+
assert image == OPENID_LOGO
105+
return Response(status_code=status.HTTP_201_CREATED)

0 commit comments

Comments
 (0)