Skip to content

Starlette support #427

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
Sep 29, 2022
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
4 changes: 2 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ Table of contents

installation
usage
extensions
customizations
integrations
customizations
extensions


Related projects
Expand Down
57 changes: 43 additions & 14 deletions docs/integrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The integration supports Django from version 3.0 and above.
Middleware
~~~~~~~~~~

Django can be integrated by middleware. Add `DjangoOpenAPIMiddleware` to your `MIDDLEWARE` list and define `OPENAPI_SPEC`.
Django can be integrated by middleware. Add ``DjangoOpenAPIMiddleware`` to your ``MIDDLEWARE`` list and define ``OPENAPI_SPEC``.

.. code-block:: python

Expand Down Expand Up @@ -52,7 +52,7 @@ After that you have access to validation result object with all validated reques
Low level
~~~~~~~~~

You can use `DjangoOpenAPIRequest` as a Django request factory:
You can use ``DjangoOpenAPIRequest`` as a Django request factory:

.. code-block:: python

Expand All @@ -62,7 +62,7 @@ You can use `DjangoOpenAPIRequest` as a Django request factory:
openapi_request = DjangoOpenAPIRequest(django_request)
result = openapi_request_validator.validate(spec, openapi_request)

You can use `DjangoOpenAPIResponse` as a Django response factory:
You can use ``DjangoOpenAPIResponse`` as a Django response factory:

.. code-block:: python

Expand All @@ -82,7 +82,7 @@ The integration supports Falcon from version 3.0 and above.
Middleware
~~~~~~~~~~

The Falcon API can be integrated by `FalconOpenAPIMiddleware` middleware.
The Falcon API can be integrated by ``FalconOpenAPIMiddleware`` middleware.

.. code-block:: python

Expand Down Expand Up @@ -111,7 +111,7 @@ After that you will have access to validation result object with all validated r
Low level
~~~~~~~~~

You can use `FalconOpenAPIRequest` as a Falcon request factory:
You can use ``FalconOpenAPIRequest`` as a Falcon request factory:

.. code-block:: python

Expand All @@ -121,7 +121,7 @@ You can use `FalconOpenAPIRequest` as a Falcon request factory:
openapi_request = FalconOpenAPIRequest(falcon_request)
result = openapi_request_validator.validate(spec, openapi_request)

You can use `FalconOpenAPIResponse` as a Falcon response factory:
You can use ``FalconOpenAPIResponse`` as a Falcon response factory:

.. code-block:: python

Expand All @@ -140,7 +140,7 @@ This section describes integration with `Flask <https://flask.palletsprojects.co
Decorator
~~~~~~~~~

Flask views can be integrated by `FlaskOpenAPIViewDecorator` decorator.
Flask views can be integrated by ``FlaskOpenAPIViewDecorator`` decorator.

.. code-block:: python

Expand All @@ -163,7 +163,7 @@ If you want to decorate class based view you can use the decorators attribute:
View
~~~~

As an alternative to the decorator-based integration, a Flask method based views can be integrated by inheritance from `FlaskOpenAPIView` class.
As an alternative to the decorator-based integration, a Flask method based views can be integrated by inheritance from ``FlaskOpenAPIView`` class.

.. code-block:: python

Expand All @@ -177,7 +177,7 @@ As an alternative to the decorator-based integration, a Flask method based views
Request parameters
~~~~~~~~~~~~~~~~~~

In Flask, all unmarshalled request data are provided as Flask request object's `openapi.parameters` attribute
In Flask, all unmarshalled request data are provided as Flask request object's ``openapi.parameters`` attribute

.. code-block:: python

Expand All @@ -192,7 +192,7 @@ In Flask, all unmarshalled request data are provided as Flask request object's `
Low level
~~~~~~~~~

You can use `FlaskOpenAPIRequest` as a Flask request factory:
You can use ``FlaskOpenAPIRequest`` as a Flask request factory:

.. code-block:: python

Expand All @@ -219,7 +219,7 @@ This section describes integration with `Requests <https://requests.readthedocs.
Low level
~~~~~~~~~

You can use `RequestsOpenAPIRequest` as a Requests request factory:
You can use ``RequestsOpenAPIRequest`` as a Requests request factory:

.. code-block:: python

Expand All @@ -229,7 +229,7 @@ You can use `RequestsOpenAPIRequest` as a Requests request factory:
openapi_request = RequestsOpenAPIRequest(requests_request)
result = openapi_request_validator.validate(spec, openapi_request)

You can use `RequestsOpenAPIResponse` as a Requests response factory:
You can use ``RequestsOpenAPIResponse`` as a Requests response factory:

.. code-block:: python

Expand All @@ -240,6 +240,35 @@ You can use `RequestsOpenAPIResponse` as a Requests response factory:
result = openapi_respose_validator.validate(spec, openapi_request, openapi_response)


Starlette
---------

This section describes integration with `Starlette <https://www.starlette.io>`__ ASGI framework.

Low level
~~~~~~~~~

You can use ``StarletteOpenAPIRequest`` as a Starlette request factory:

.. code-block:: python

from openapi_core.validation.request import openapi_request_validator
from openapi_core.contrib.starlette import StarletteOpenAPIRequest

openapi_request = StarletteOpenAPIRequest(starlette_request)
result = openapi_request_validator.validate(spec, openapi_request)

You can use ``StarletteOpenAPIResponse`` as a Starlette response factory:

.. code-block:: python

from openapi_core.validation.response import openapi_respose_validator
from openapi_core.contrib.starlette import StarletteOpenAPIResponse

openapi_response = StarletteOpenAPIResponse(starlette_response)
result = openapi_respose_validator.validate(spec, openapi_request, openapi_response)


Tornado
-------

Expand All @@ -254,7 +283,7 @@ This section describes integration with `Werkzeug <https://werkzeug.palletsproje
Low level
~~~~~~~~~

You can use `WerkzeugOpenAPIRequest` as a Werkzeug request factory:
You can use ``WerkzeugOpenAPIRequest`` as a Werkzeug request factory:

.. code-block:: python

Expand All @@ -264,7 +293,7 @@ You can use `WerkzeugOpenAPIRequest` as a Werkzeug request factory:
openapi_request = WerkzeugOpenAPIRequest(werkzeug_request)
result = openapi_request_validator.validate(spec, openapi_request)

You can use `WerkzeugOpenAPIResponse` as a Werkzeug response factory:
You can use ``WerkzeugOpenAPIResponse`` as a Werkzeug response factory:

.. code-block:: python

Expand Down
7 changes: 7 additions & 0 deletions openapi_core/contrib/starlette/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from openapi_core.contrib.starlette.requests import StarletteOpenAPIRequest
from openapi_core.contrib.starlette.responses import StarletteOpenAPIResponse

__all__ = [
"StarletteOpenAPIRequest",
"StarletteOpenAPIResponse",
]
50 changes: 50 additions & 0 deletions openapi_core/contrib/starlette/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""OpenAPI core contrib starlette requests module"""
from typing import Optional

from asgiref.sync import AsyncToSync
from starlette.requests import Request

from openapi_core.validation.request.datatypes import RequestParameters


class StarletteOpenAPIRequest:
def __init__(self, request: Request):
self.request = request

self.parameters = RequestParameters(
query=self.request.query_params,
header=self.request.headers,
cookie=self.request.cookies,
)

self._get_body = AsyncToSync(self.request.body, force_new_loop=True) # type: ignore

@property
def host_url(self) -> str:
return self.request.base_url._url

@property
def path(self) -> str:
return self.request.url.path

@property
def method(self) -> str:
return self.request.method.lower()

@property
def body(self) -> Optional[str]:
body = self._get_body()
if body is None:
return None
if isinstance(body, bytes):
return body.decode("utf-8")
assert isinstance(body, str)
return body

@property
def mimetype(self) -> str:
content_type = self.request.headers["Content-Type"]
if content_type:
return content_type.partition(";")[0]

return ""
27 changes: 27 additions & 0 deletions openapi_core/contrib/starlette/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""OpenAPI core contrib starlette responses module"""
from starlette.datastructures import Headers
from starlette.responses import Response


class StarletteOpenAPIResponse:
def __init__(self, response: Response):
self.response = response

@property
def data(self) -> str:
if isinstance(self.response.body, bytes):
return self.response.body.decode("utf-8")
assert isinstance(self.response.body, str)
return self.response.body

@property
def status_code(self) -> int:
return self.response.status_code

@property
def mimetype(self) -> str:
return self.response.media_type or ""

@property
def headers(self) -> Headers:
return self.response.headers
8 changes: 3 additions & 5 deletions openapi_core/schema/parameters.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import re
from typing import Any
from typing import Dict
from typing import Mapping
from typing import Optional
from typing import Union

from werkzeug.datastructures import Headers

from openapi_core.schema.protocols import SuportsGetAll
from openapi_core.schema.protocols import SuportsGetList
Expand Down Expand Up @@ -49,7 +47,7 @@ def get_explode(param_or_header: Spec) -> bool:

def get_value(
param_or_header: Spec,
location: Union[Headers, Dict[str, Any]],
location: Mapping[str, Any],
name: Optional[str] = None,
) -> Any:
"""Returns parameter/header value from specific location"""
Expand Down Expand Up @@ -80,7 +78,7 @@ def get_value(


def get_deep_object_value(
location: Union[Headers, Dict[str, Any]],
location: Mapping[str, Any],
name: Optional[str] = None,
) -> Dict[str, Any]:
values = {}
Expand Down
23 changes: 9 additions & 14 deletions openapi_core/validation/request/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from dataclasses import dataclass
from dataclasses import field
from typing import Any
from typing import Dict
from typing import Optional
from typing import Mapping

from werkzeug.datastructures import Headers
from werkzeug.datastructures import ImmutableMultiDict
Expand All @@ -28,25 +27,21 @@ class RequestParameters:
Path parameters as dict. Gets resolved against spec if empty.
"""

query: ImmutableMultiDict[str, Any] = field(
default_factory=ImmutableMultiDict
)
header: Headers = field(default_factory=Headers)
cookie: ImmutableMultiDict[str, Any] = field(
default_factory=ImmutableMultiDict
)
path: dict[str, Any] = field(default_factory=dict)
query: Mapping[str, Any] = field(default_factory=ImmutableMultiDict)
header: Mapping[str, Any] = field(default_factory=Headers)
cookie: Mapping[str, Any] = field(default_factory=ImmutableMultiDict)
path: Mapping[str, Any] = field(default_factory=dict)

def __getitem__(self, location: str) -> Any:
return getattr(self, location)


@dataclass
class Parameters:
query: dict[str, Any] = field(default_factory=dict)
header: dict[str, Any] = field(default_factory=dict)
cookie: dict[str, Any] = field(default_factory=dict)
path: dict[str, Any] = field(default_factory=dict)
query: Mapping[str, Any] = field(default_factory=dict)
header: Mapping[str, Any] = field(default_factory=dict)
cookie: Mapping[str, Any] = field(default_factory=dict)
path: Mapping[str, Any] = field(default_factory=dict)


@dataclass
Expand Down
6 changes: 3 additions & 3 deletions openapi_core/validation/response/protocols.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""OpenAPI core validation response protocols module"""
from typing import TYPE_CHECKING
from typing import Any
from typing import Mapping
from typing import Optional

if TYPE_CHECKING:
Expand All @@ -13,8 +15,6 @@
from typing_extensions import Protocol
from typing_extensions import runtime_checkable

from werkzeug.datastructures import Headers

from openapi_core.spec import Spec
from openapi_core.validation.request.protocols import Request
from openapi_core.validation.response.datatypes import ResponseValidationResult
Expand Down Expand Up @@ -48,7 +48,7 @@ def mimetype(self) -> str:
...

@property
def headers(self) -> Headers:
def headers(self) -> Mapping[str, Any]:
...


Expand Down
8 changes: 2 additions & 6 deletions openapi_core/validation/validators.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
"""OpenAPI core validation validators module"""
from typing import Any
from typing import Dict
from typing import Mapping
from typing import Optional
from typing import Union
from urllib.parse import urljoin

from werkzeug.datastructures import Headers

from openapi_core.casting.schemas import schema_casters_factory
from openapi_core.casting.schemas.factories import SchemaCastersFactory
Expand Down Expand Up @@ -82,7 +78,7 @@ def _unmarshal(self, schema: Spec, value: Any) -> Any:
def _get_param_or_header_value(
self,
param_or_header: Spec,
location: Union[Headers, Dict[str, Any]],
location: Mapping[str, Any],
name: Optional[str] = None,
) -> Any:
try:
Expand Down
Loading