Skip to content

Commit

Permalink
Merge pull request #738 from python-openapi/feature/fastapi-integration
Browse files Browse the repository at this point in the history
FastAPI integration
  • Loading branch information
p1c2u authored Feb 13, 2024
2 parents 383d097 + 64f4dd0 commit fcddf6b
Show file tree
Hide file tree
Showing 14 changed files with 637 additions and 13 deletions.
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)
Empty file.
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

0 comments on commit fcddf6b

Please sign in to comment.