-
-
Notifications
You must be signed in to change notification settings - Fork 131
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #738 from python-openapi/feature/fastapi-integration
FastAPI integration
- Loading branch information
Showing
14 changed files
with
637 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
FastAPI | ||
========= | ||
|
||
This section describes integration with `FastAPI <https://fastapi.tiangolo.com>`__ ASGI framework. | ||
|
||
.. note:: | ||
|
||
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/). | ||
|
||
Middleware | ||
---------- | ||
|
||
FastAPI can be integrated by `middleware <https://fastapi.tiangolo.com/tutorial/middleware/>`__ to apply OpenAPI validation to your entire application. | ||
|
||
Add ``FastAPIOpenAPIMiddleware`` with OpenAPI object to your ``middleware`` list. | ||
|
||
.. code-block:: python | ||
:emphasize-lines: 2,5 | ||
from fastapi import FastAPI | ||
from openapi_core.contrib.fastapi.middlewares import FastAPIOpenAPIMiddleware | ||
app = FastAPI() | ||
app.add_middleware(FastAPIOpenAPIMiddleware, openapi=openapi) | ||
After that all your requests and responses will be validated. | ||
|
||
Also you have access to unmarshal result object with all unmarshalled request data through ``openapi`` scope of request object. | ||
|
||
.. code-block:: python | ||
async def homepage(request): | ||
# get parameters object with path, query, cookies and headers parameters | ||
unmarshalled_params = request.scope["openapi"].parameters | ||
# or specific location parameters | ||
unmarshalled_path_params = request.scope["openapi"].parameters.path | ||
# get body | ||
unmarshalled_body = request.scope["openapi"].body | ||
# get security data | ||
unmarshalled_security = request.scope["openapi"].security | ||
Response validation | ||
^^^^^^^^^^^^^^^^^^^ | ||
|
||
You can skip response validation process: by setting ``response_cls`` to ``None`` | ||
|
||
.. code-block:: python | ||
:emphasize-lines: 2 | ||
app = FastAPI() | ||
app.add_middleware(FastAPIOpenAPIMiddleware, openapi=openapi, response_cls=None) | ||
Low level | ||
--------- | ||
|
||
For low level integration see `Starlette <starlette.rst>`_ integration. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from openapi_core.contrib.fastapi.middlewares import FastAPIOpenAPIMiddleware | ||
from openapi_core.contrib.fastapi.requests import FastAPIOpenAPIRequest | ||
from openapi_core.contrib.fastapi.responses import FastAPIOpenAPIResponse | ||
|
||
__all__ = [ | ||
"FastAPIOpenAPIMiddleware", | ||
"FastAPIOpenAPIRequest", | ||
"FastAPIOpenAPIResponse", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from openapi_core.contrib.starlette.middlewares import ( | ||
StarletteOpenAPIMiddleware as FastAPIOpenAPIMiddleware, | ||
) | ||
|
||
__all__ = ["FastAPIOpenAPIMiddleware"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from fastapi import Request | ||
|
||
from openapi_core.contrib.starlette.requests import StarletteOpenAPIRequest | ||
|
||
|
||
class FastAPIOpenAPIRequest(StarletteOpenAPIRequest): | ||
def __init__(self, request: Request): | ||
super().__init__(request) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from typing import Optional | ||
|
||
from fastapi import Response | ||
|
||
from openapi_core.contrib.starlette.responses import StarletteOpenAPIResponse | ||
|
||
|
||
class FastAPIOpenAPIResponse(StarletteOpenAPIResponse): | ||
def __init__(self, response: Response, data: Optional[bytes] = None): | ||
super().__init__(response, data=data) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
9 changes: 9 additions & 0 deletions
9
tests/integration/contrib/fastapi/data/v3.0/fastapiproject/__main__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from fastapi import FastAPI | ||
from fastapiproject.openapi import openapi | ||
from fastapiproject.routers import pets | ||
|
||
from openapi_core.contrib.fastapi.middlewares import FastAPIOpenAPIMiddleware | ||
|
||
app = FastAPI() | ||
app.add_middleware(FastAPIOpenAPIMiddleware, openapi=openapi) | ||
app.include_router(pets.router) |
9 changes: 9 additions & 0 deletions
9
tests/integration/contrib/fastapi/data/v3.0/fastapiproject/openapi.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from pathlib import Path | ||
|
||
import yaml | ||
|
||
from openapi_core import OpenAPI | ||
|
||
openapi_spec_path = Path("tests/integration/data/v3.0/petstore.yaml") | ||
spec_dict = yaml.load(openapi_spec_path.read_text(), yaml.Loader) | ||
openapi = OpenAPI.from_dict(spec_dict) |
Empty file.
109 changes: 109 additions & 0 deletions
109
tests/integration/contrib/fastapi/data/v3.0/fastapiproject/routers/pets.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
from base64 import b64decode | ||
|
||
from fastapi import APIRouter | ||
from fastapi import Body | ||
from fastapi import Request | ||
from fastapi import Response | ||
from fastapi import status | ||
|
||
try: | ||
from typing import Annotated | ||
except ImportError: | ||
from typing_extensions import Annotated | ||
|
||
OPENID_LOGO = b64decode( | ||
""" | ||
R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d | ||
3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA | ||
AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg | ||
EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD | ||
Fzk0lpcjIQA7 | ||
""" | ||
) | ||
|
||
|
||
router = APIRouter( | ||
prefix="/v1/pets", | ||
tags=["pets"], | ||
responses={404: {"description": "Not found"}}, | ||
) | ||
|
||
|
||
@router.get("") | ||
async def list_pets(request: Request, response: Response): | ||
assert request.scope["openapi"] | ||
assert not request.scope["openapi"].errors | ||
assert request.scope["openapi"].parameters.query == { | ||
"page": 1, | ||
"limit": 12, | ||
"search": "", | ||
} | ||
data = [ | ||
{ | ||
"id": 12, | ||
"name": "Cat", | ||
"ears": { | ||
"healthy": True, | ||
}, | ||
}, | ||
] | ||
response.headers["X-Rate-Limit"] = "12" | ||
return {"data": data} | ||
|
||
|
||
@router.post("") | ||
async def create_pet(request: Request): | ||
assert request.scope["openapi"].parameters.cookie == { | ||
"user": 1, | ||
} | ||
assert request.scope["openapi"].parameters.header == { | ||
"api-key": "12345", | ||
} | ||
assert request.scope["openapi"].body.__class__.__name__ == "PetCreate" | ||
assert request.scope["openapi"].body.name in ["Cat", "Bird"] | ||
if request.scope["openapi"].body.name == "Cat": | ||
assert request.scope["openapi"].body.ears.__class__.__name__ == "Ears" | ||
assert request.scope["openapi"].body.ears.healthy is True | ||
if request.scope["openapi"].body.name == "Bird": | ||
assert ( | ||
request.scope["openapi"].body.wings.__class__.__name__ == "Wings" | ||
) | ||
assert request.scope["openapi"].body.wings.healthy is True | ||
|
||
headers = { | ||
"X-Rate-Limit": "12", | ||
} | ||
return Response(status_code=status.HTTP_201_CREATED, headers=headers) | ||
|
||
|
||
@router.get("/{petId}") | ||
async def detail_pet(request: Request, response: Response): | ||
assert request.scope["openapi"] | ||
assert not request.scope["openapi"].errors | ||
assert request.scope["openapi"].parameters.path == { | ||
"petId": 12, | ||
} | ||
data = { | ||
"id": 12, | ||
"name": "Cat", | ||
"ears": { | ||
"healthy": True, | ||
}, | ||
} | ||
response.headers["X-Rate-Limit"] = "12" | ||
return { | ||
"data": data, | ||
} | ||
|
||
|
||
@router.get("/{petId}/photo") | ||
async def download_pet_photo(): | ||
return Response(content=OPENID_LOGO, media_type="image/gif") | ||
|
||
|
||
@router.post("/{petId}/photo") | ||
async def upload_pet_photo( | ||
image: Annotated[bytes, Body(media_type="image/jpg")], | ||
): | ||
assert image == OPENID_LOGO | ||
return Response(status_code=status.HTTP_201_CREATED) |
Oops, something went wrong.