Skip to content
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

Skip response validation option #667

Merged
merged 1 commit into from
Sep 15, 2023
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
45 changes: 45 additions & 0 deletions docs/integrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ Django can be integrated by middleware. Add ``DjangoOpenAPIMiddleware`` to your

OPENAPI_SPEC = Spec.from_dict(spec_dict)

You can skip response validation process: by setting ``OPENAPI_RESPONSE_CLS`` to ``None``

.. code-block:: python
:emphasize-lines: 10

# settings.py
from openapi_core import Spec

MIDDLEWARE = [
# ...
'openapi_core.contrib.django.middlewares.DjangoOpenAPIMiddleware',
]

OPENAPI_SPEC = Spec.from_dict(spec_dict)
OPENAPI_RESPONSE_CLS = None

After that you have access to unmarshal result object with all validated request data from Django view through request object.

.. code-block:: python
Expand Down Expand Up @@ -146,6 +162,23 @@ Additional customization parameters can be passed to the middleware.
middleware=[openapi_middleware],
)

You can skip response validation process: by setting ``response_cls`` to ``None``

.. code-block:: python
:emphasize-lines: 5

from openapi_core.contrib.falcon.middlewares import FalconOpenAPIMiddleware

openapi_middleware = FalconOpenAPIMiddleware.from_spec(
spec,
response_cls=None,
)

app = falcon.App(
# ...
middleware=[openapi_middleware],
)

After that you will have access to validation result object with all validated request data from Falcon view through request context.

.. code-block:: python
Expand Down Expand Up @@ -221,6 +254,18 @@ Additional customization parameters can be passed to the decorator.
extra_format_validators=extra_format_validators,
)

You can skip response validation process: by setting ``response_cls`` to ``None``

.. code-block:: python
:emphasize-lines: 5

from openapi_core.contrib.flask.decorators import FlaskOpenAPIViewDecorator

openapi = FlaskOpenAPIViewDecorator.from_spec(
spec,
response_cls=None,
)

If you want to decorate class based view you can use the decorators attribute:

.. code-block:: python
Expand Down
14 changes: 10 additions & 4 deletions openapi_core/contrib/django/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@


class DjangoOpenAPIMiddleware:
request_class = DjangoOpenAPIRequest
response_class = DjangoOpenAPIResponse
request_cls = DjangoOpenAPIRequest
response_cls = DjangoOpenAPIResponse
errors_handler = DjangoOpenAPIErrorsHandler()

def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
Expand All @@ -28,6 +28,9 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
if not hasattr(settings, "OPENAPI_SPEC"):
raise ImproperlyConfigured("OPENAPI_SPEC not defined in settings")

if hasattr(settings, "OPENAPI_RESPONSE_CLS"):
self.response_cls = settings.OPENAPI_RESPONSE_CLS

self.processor = UnmarshallingProcessor(settings.OPENAPI_SPEC)

def __call__(self, request: HttpRequest) -> HttpResponse:
Expand All @@ -39,6 +42,8 @@ def __call__(self, request: HttpRequest) -> HttpResponse:
request.openapi = req_result
response = self.get_response(request)

if self.response_cls is None:
return response
openapi_response = self._get_openapi_response(response)
resp_result = self.processor.process_response(
openapi_request, openapi_response
Expand All @@ -64,9 +69,10 @@ def _handle_response_errors(
def _get_openapi_request(
self, request: HttpRequest
) -> DjangoOpenAPIRequest:
return self.request_class(request)
return self.request_cls(request)

def _get_openapi_response(
self, response: HttpResponse
) -> DjangoOpenAPIResponse:
return self.response_class(response)
assert self.response_cls is not None
return self.response_cls(response)
27 changes: 15 additions & 12 deletions openapi_core/contrib/falcon/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@


class FalconOpenAPIMiddleware(UnmarshallingProcessor):
request_class = FalconOpenAPIRequest
response_class = FalconOpenAPIResponse
request_cls = FalconOpenAPIRequest
response_cls = FalconOpenAPIResponse
errors_handler = FalconOpenAPIErrorsHandler()

def __init__(
self,
spec: Spec,
request_unmarshaller_cls: Optional[RequestUnmarshallerType] = None,
response_unmarshaller_cls: Optional[ResponseUnmarshallerType] = None,
request_class: Type[FalconOpenAPIRequest] = FalconOpenAPIRequest,
response_class: Type[FalconOpenAPIResponse] = FalconOpenAPIResponse,
request_cls: Type[FalconOpenAPIRequest] = FalconOpenAPIRequest,
response_cls: Type[FalconOpenAPIResponse] = FalconOpenAPIResponse,
errors_handler: Optional[FalconOpenAPIErrorsHandler] = None,
**unmarshaller_kwargs: Any,
):
Expand All @@ -40,8 +40,8 @@ def __init__(
response_unmarshaller_cls=response_unmarshaller_cls,
**unmarshaller_kwargs,
)
self.request_class = request_class or self.request_class
self.response_class = response_class or self.response_class
self.request_cls = request_cls or self.request_cls
self.response_cls = response_cls or self.response_cls
self.errors_handler = errors_handler or self.errors_handler

@classmethod
Expand All @@ -50,17 +50,17 @@ def from_spec(
spec: Spec,
request_unmarshaller_cls: Optional[RequestUnmarshallerType] = None,
response_unmarshaller_cls: Optional[ResponseUnmarshallerType] = None,
request_class: Type[FalconOpenAPIRequest] = FalconOpenAPIRequest,
response_class: Type[FalconOpenAPIResponse] = FalconOpenAPIResponse,
request_cls: Type[FalconOpenAPIRequest] = FalconOpenAPIRequest,
response_cls: Type[FalconOpenAPIResponse] = FalconOpenAPIResponse,
errors_handler: Optional[FalconOpenAPIErrorsHandler] = None,
**unmarshaller_kwargs: Any,
) -> "FalconOpenAPIMiddleware":
return cls(
spec,
request_unmarshaller_cls=request_unmarshaller_cls,
response_unmarshaller_cls=response_unmarshaller_cls,
request_class=request_class,
response_class=response_class,
request_cls=request_cls,
response_cls=response_cls,
errors_handler=errors_handler,
**unmarshaller_kwargs,
)
Expand All @@ -74,6 +74,8 @@ def process_request(self, req: Request, resp: Response) -> None: # type: ignore
def process_response( # type: ignore
self, req: Request, resp: Response, resource: Any, req_succeeded: bool
) -> None:
if self.response_cls is None:
return resp
openapi_req = self._get_openapi_request(req)
openapi_resp = self._get_openapi_response(resp)
resp.context.openapi = super().process_response(
Expand Down Expand Up @@ -101,9 +103,10 @@ def _handle_response_errors(
return self.errors_handler.handle(req, resp, response_result.errors)

def _get_openapi_request(self, request: Request) -> FalconOpenAPIRequest:
return self.request_class(request)
return self.request_cls(request)

def _get_openapi_response(
self, response: Response
) -> FalconOpenAPIResponse:
return self.response_class(response)
assert self.response_cls is not None
return self.response_cls(response)
2 changes: 2 additions & 0 deletions openapi_core/contrib/flask/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from openapi_core.contrib.flask.decorators import FlaskOpenAPIViewDecorator
from openapi_core.contrib.flask.requests import FlaskOpenAPIRequest
from openapi_core.contrib.flask.responses import FlaskOpenAPIResponse

__all__ = [
"FlaskOpenAPIViewDecorator",
"FlaskOpenAPIRequest",
"FlaskOpenAPIResponse",
]
25 changes: 15 additions & 10 deletions openapi_core/contrib/flask/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ def __init__(
spec: Spec,
request_unmarshaller_cls: Optional[RequestUnmarshallerType] = None,
response_unmarshaller_cls: Optional[ResponseUnmarshallerType] = None,
request_class: Type[FlaskOpenAPIRequest] = FlaskOpenAPIRequest,
response_class: Type[FlaskOpenAPIResponse] = FlaskOpenAPIResponse,
request_cls: Type[FlaskOpenAPIRequest] = FlaskOpenAPIRequest,
response_cls: Optional[
Type[FlaskOpenAPIResponse]
] = FlaskOpenAPIResponse,
request_provider: Type[FlaskRequestProvider] = FlaskRequestProvider,
openapi_errors_handler: Type[
FlaskOpenAPIErrorsHandler
Expand All @@ -44,8 +46,8 @@ def __init__(
response_unmarshaller_cls=response_unmarshaller_cls,
**unmarshaller_kwargs,
)
self.request_class = request_class
self.response_class = response_class
self.request_cls = request_cls
self.response_cls = response_cls
self.request_provider = request_provider
self.openapi_errors_handler = openapi_errors_handler

Expand All @@ -60,6 +62,8 @@ def decorated(*args: Any, **kwargs: Any) -> Response:
response = self._handle_request_view(
request_result, view, *args, **kwargs
)
if self.response_cls is None:
return response
openapi_response = self._get_openapi_response(response)
response_result = self.process_response(
openapi_request, openapi_response
Expand Down Expand Up @@ -96,21 +100,22 @@ def _get_request(self) -> Request:
return request

def _get_openapi_request(self, request: Request) -> FlaskOpenAPIRequest:
return self.request_class(request)
return self.request_cls(request)

def _get_openapi_response(
self, response: Response
) -> FlaskOpenAPIResponse:
return self.response_class(response)
assert self.response_cls is not None
return self.response_cls(response)

@classmethod
def from_spec(
cls,
spec: Spec,
request_unmarshaller_cls: Optional[RequestUnmarshallerType] = None,
response_unmarshaller_cls: Optional[ResponseUnmarshallerType] = None,
request_class: Type[FlaskOpenAPIRequest] = FlaskOpenAPIRequest,
response_class: Type[FlaskOpenAPIResponse] = FlaskOpenAPIResponse,
request_cls: Type[FlaskOpenAPIRequest] = FlaskOpenAPIRequest,
response_cls: Type[FlaskOpenAPIResponse] = FlaskOpenAPIResponse,
request_provider: Type[FlaskRequestProvider] = FlaskRequestProvider,
openapi_errors_handler: Type[
FlaskOpenAPIErrorsHandler
Expand All @@ -121,8 +126,8 @@ def from_spec(
spec,
request_unmarshaller_cls=request_unmarshaller_cls,
response_unmarshaller_cls=response_unmarshaller_cls,
request_class=request_class,
response_class=response_class,
request_cls=request_cls,
response_cls=response_cls,
request_provider=request_provider,
openapi_errors_handler=openapi_errors_handler,
**unmarshaller_kwargs,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.http import HttpResponse
from rest_framework.views import APIView


class TagListView(APIView):
def get(self, request):
assert request.openapi
assert not request.openapi.errors
return HttpResponse("success")

@staticmethod
def get_extra_actions():
return []
13 changes: 10 additions & 3 deletions tests/integration/contrib/django/data/v3.0/djangoproject/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
from django.contrib import admin
from django.urls import include
from django.urls import path
from djangoproject.pets import views
from djangoproject.pets.views import PetDetailView
from djangoproject.pets.views import PetListView
from djangoproject.tags.views import TagListView

urlpatterns = [
path("admin/", admin.site.urls),
Expand All @@ -26,12 +28,17 @@
),
path(
"v1/pets",
views.PetListView.as_view(),
PetListView.as_view(),
name="pet_list_view",
),
path(
"v1/pets/<int:petId>",
views.PetDetailView.as_view(),
PetDetailView.as_view(),
name="pet_detail_view",
),
path(
"v1/tags",
TagListView.as_view(),
name="tag_list_view",
),
]
23 changes: 23 additions & 0 deletions tests/integration/contrib/django/test_django_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from unittest import mock

import pytest
from django.test.utils import override_settings


class BaseTestDjangoProject:
Expand Down Expand Up @@ -372,3 +373,25 @@ def test_post_valid(self, api_client):

assert response.status_code == 201
assert not response.content


class TestDRFTagListView(BaseTestDRF):
def test_get_response_invalid(self, client):
headers = {
"HTTP_AUTHORIZATION": "Basic testuser",
"HTTP_HOST": "petstore.swagger.io",
}
response = client.get("/v1/tags", **headers)

assert response.status_code == 415

def test_get_skip_response_validation(self, client):
headers = {
"HTTP_AUTHORIZATION": "Basic testuser",
"HTTP_HOST": "petstore.swagger.io",
}
with override_settings(OPENAPI_RESPONSE_CLS=None):
response = client.get("/v1/tags", **headers)

assert response.status_code == 200
assert response.content == b"success"
37 changes: 37 additions & 0 deletions tests/integration/contrib/flask/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pytest
from flask import Flask


@pytest.fixture(scope="session")
def spec(factory):
specfile = "contrib/flask/data/v3.0/flask_factory.yaml"
return factory.spec_from_file(specfile)


@pytest.fixture
def app(app_factory):
return app_factory()


@pytest.fixture
def client(client_factory, app):
return client_factory(app)


@pytest.fixture(scope="session")
def client_factory():
def create(app):
return app.test_client()

return create


@pytest.fixture(scope="session")
def app_factory():
def create(root_path=None):
app = Flask("__main__", root_path=root_path)
app.config["DEBUG"] = True
app.config["TESTING"] = True
return app

return create
Loading