Skip to content

FastAPI integration #738

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/integrations/fastapi.rst
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.
1 change: 1 addition & 0 deletions docs/integrations/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Openapi-core integrates with your popular libraries and frameworks. Each integra
bottle
django
falcon
fastapi
flask
pyramid
requests
Expand Down
9 changes: 9 additions & 0 deletions openapi_core/contrib/fastapi/__init__.py
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",
]
5 changes: 5 additions & 0 deletions openapi_core/contrib/fastapi/middlewares.py
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"]
8 changes: 8 additions & 0 deletions openapi_core/contrib/fastapi/requests.py
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)
10 changes: 10 additions & 0 deletions openapi_core/contrib/fastapi/responses.py
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)
46 changes: 33 additions & 13 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ jsonschema-path = "^0.3.1"
jsonschema = "^4.18.0"
multidict = {version = "^6.0.4", optional = true}
aioitertools = {version = "^0.11.0", optional = true}
fastapi = {version = "^0.108.0", optional = true}

[tool.poetry.extras]
django = ["django"]
falcon = ["falcon"]
fastapi = ["fastapi"]
flask = ["flask"]
requests = ["requests"]
aiohttp = ["aiohttp", "multidict"]
Expand Down Expand Up @@ -108,6 +110,7 @@ aiohttp = "^3.8.4"
pytest-aiohttp = "^1.0.4"
bump2version = "^1.0.1"
pyflakes = "^3.1.0"
fastapi = "^0.108.0"

[tool.poetry.group.docs.dependencies]
sphinx = ">=5.3,<8.0"
Expand Down
Empty file.
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)
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)
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)
Loading