Skip to content

Commit d7d1fac

Browse files
authored
Merge pull request #667 from python-openapi/feature/skip-response-validation-option
Skip response validation option
2 parents 1b688bb + a863e8f commit d7d1fac

File tree

14 files changed

+419
-169
lines changed

14 files changed

+419
-169
lines changed

docs/integrations.rst

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ Django can be integrated by middleware. Add ``DjangoOpenAPIMiddleware`` to your
6363
6464
OPENAPI_SPEC = Spec.from_dict(spec_dict)
6565
66+
You can skip response validation process: by setting ``OPENAPI_RESPONSE_CLS`` to ``None``
67+
68+
.. code-block:: python
69+
:emphasize-lines: 10
70+
71+
# settings.py
72+
from openapi_core import Spec
73+
74+
MIDDLEWARE = [
75+
# ...
76+
'openapi_core.contrib.django.middlewares.DjangoOpenAPIMiddleware',
77+
]
78+
79+
OPENAPI_SPEC = Spec.from_dict(spec_dict)
80+
OPENAPI_RESPONSE_CLS = None
81+
6682
After that you have access to unmarshal result object with all validated request data from Django view through request object.
6783

6884
.. code-block:: python
@@ -146,6 +162,23 @@ Additional customization parameters can be passed to the middleware.
146162
middleware=[openapi_middleware],
147163
)
148164
165+
You can skip response validation process: by setting ``response_cls`` to ``None``
166+
167+
.. code-block:: python
168+
:emphasize-lines: 5
169+
170+
from openapi_core.contrib.falcon.middlewares import FalconOpenAPIMiddleware
171+
172+
openapi_middleware = FalconOpenAPIMiddleware.from_spec(
173+
spec,
174+
response_cls=None,
175+
)
176+
177+
app = falcon.App(
178+
# ...
179+
middleware=[openapi_middleware],
180+
)
181+
149182
After that you will have access to validation result object with all validated request data from Falcon view through request context.
150183

151184
.. code-block:: python
@@ -221,6 +254,18 @@ Additional customization parameters can be passed to the decorator.
221254
extra_format_validators=extra_format_validators,
222255
)
223256
257+
You can skip response validation process: by setting ``response_cls`` to ``None``
258+
259+
.. code-block:: python
260+
:emphasize-lines: 5
261+
262+
from openapi_core.contrib.flask.decorators import FlaskOpenAPIViewDecorator
263+
264+
openapi = FlaskOpenAPIViewDecorator.from_spec(
265+
spec,
266+
response_cls=None,
267+
)
268+
224269
If you want to decorate class based view you can use the decorators attribute:
225270

226271
.. code-block:: python

openapi_core/contrib/django/middlewares.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919

2020
class DjangoOpenAPIMiddleware:
21-
request_class = DjangoOpenAPIRequest
22-
response_class = DjangoOpenAPIResponse
21+
request_cls = DjangoOpenAPIRequest
22+
response_cls = DjangoOpenAPIResponse
2323
errors_handler = DjangoOpenAPIErrorsHandler()
2424

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

31+
if hasattr(settings, "OPENAPI_RESPONSE_CLS"):
32+
self.response_cls = settings.OPENAPI_RESPONSE_CLS
33+
3134
self.processor = UnmarshallingProcessor(settings.OPENAPI_SPEC)
3235

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

45+
if self.response_cls is None:
46+
return response
4247
openapi_response = self._get_openapi_response(response)
4348
resp_result = self.processor.process_response(
4449
openapi_request, openapi_response
@@ -64,9 +69,10 @@ def _handle_response_errors(
6469
def _get_openapi_request(
6570
self, request: HttpRequest
6671
) -> DjangoOpenAPIRequest:
67-
return self.request_class(request)
72+
return self.request_cls(request)
6873

6974
def _get_openapi_response(
7075
self, response: HttpResponse
7176
) -> DjangoOpenAPIResponse:
72-
return self.response_class(response)
77+
assert self.response_cls is not None
78+
return self.response_cls(response)

openapi_core/contrib/falcon/middlewares.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@
2020

2121

2222
class FalconOpenAPIMiddleware(UnmarshallingProcessor):
23-
request_class = FalconOpenAPIRequest
24-
response_class = FalconOpenAPIResponse
23+
request_cls = FalconOpenAPIRequest
24+
response_cls = FalconOpenAPIResponse
2525
errors_handler = FalconOpenAPIErrorsHandler()
2626

2727
def __init__(
2828
self,
2929
spec: Spec,
3030
request_unmarshaller_cls: Optional[RequestUnmarshallerType] = None,
3131
response_unmarshaller_cls: Optional[ResponseUnmarshallerType] = None,
32-
request_class: Type[FalconOpenAPIRequest] = FalconOpenAPIRequest,
33-
response_class: Type[FalconOpenAPIResponse] = FalconOpenAPIResponse,
32+
request_cls: Type[FalconOpenAPIRequest] = FalconOpenAPIRequest,
33+
response_cls: Type[FalconOpenAPIResponse] = FalconOpenAPIResponse,
3434
errors_handler: Optional[FalconOpenAPIErrorsHandler] = None,
3535
**unmarshaller_kwargs: Any,
3636
):
@@ -40,8 +40,8 @@ def __init__(
4040
response_unmarshaller_cls=response_unmarshaller_cls,
4141
**unmarshaller_kwargs,
4242
)
43-
self.request_class = request_class or self.request_class
44-
self.response_class = response_class or self.response_class
43+
self.request_cls = request_cls or self.request_cls
44+
self.response_cls = response_cls or self.response_cls
4545
self.errors_handler = errors_handler or self.errors_handler
4646

4747
@classmethod
@@ -50,17 +50,17 @@ def from_spec(
5050
spec: Spec,
5151
request_unmarshaller_cls: Optional[RequestUnmarshallerType] = None,
5252
response_unmarshaller_cls: Optional[ResponseUnmarshallerType] = None,
53-
request_class: Type[FalconOpenAPIRequest] = FalconOpenAPIRequest,
54-
response_class: Type[FalconOpenAPIResponse] = FalconOpenAPIResponse,
53+
request_cls: Type[FalconOpenAPIRequest] = FalconOpenAPIRequest,
54+
response_cls: Type[FalconOpenAPIResponse] = FalconOpenAPIResponse,
5555
errors_handler: Optional[FalconOpenAPIErrorsHandler] = None,
5656
**unmarshaller_kwargs: Any,
5757
) -> "FalconOpenAPIMiddleware":
5858
return cls(
5959
spec,
6060
request_unmarshaller_cls=request_unmarshaller_cls,
6161
response_unmarshaller_cls=response_unmarshaller_cls,
62-
request_class=request_class,
63-
response_class=response_class,
62+
request_cls=request_cls,
63+
response_cls=response_cls,
6464
errors_handler=errors_handler,
6565
**unmarshaller_kwargs,
6666
)
@@ -74,6 +74,8 @@ def process_request(self, req: Request, resp: Response) -> None: # type: ignore
7474
def process_response( # type: ignore
7575
self, req: Request, resp: Response, resource: Any, req_succeeded: bool
7676
) -> None:
77+
if self.response_cls is None:
78+
return resp
7779
openapi_req = self._get_openapi_request(req)
7880
openapi_resp = self._get_openapi_response(resp)
7981
resp.context.openapi = super().process_response(
@@ -101,9 +103,10 @@ def _handle_response_errors(
101103
return self.errors_handler.handle(req, resp, response_result.errors)
102104

103105
def _get_openapi_request(self, request: Request) -> FalconOpenAPIRequest:
104-
return self.request_class(request)
106+
return self.request_cls(request)
105107

106108
def _get_openapi_response(
107109
self, response: Response
108110
) -> FalconOpenAPIResponse:
109-
return self.response_class(response)
111+
assert self.response_cls is not None
112+
return self.response_cls(response)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from openapi_core.contrib.flask.decorators import FlaskOpenAPIViewDecorator
12
from openapi_core.contrib.flask.requests import FlaskOpenAPIRequest
23
from openapi_core.contrib.flask.responses import FlaskOpenAPIResponse
34

45
__all__ = [
6+
"FlaskOpenAPIViewDecorator",
57
"FlaskOpenAPIRequest",
68
"FlaskOpenAPIResponse",
79
]

openapi_core/contrib/flask/decorators.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ def __init__(
3030
spec: Spec,
3131
request_unmarshaller_cls: Optional[RequestUnmarshallerType] = None,
3232
response_unmarshaller_cls: Optional[ResponseUnmarshallerType] = None,
33-
request_class: Type[FlaskOpenAPIRequest] = FlaskOpenAPIRequest,
34-
response_class: Type[FlaskOpenAPIResponse] = FlaskOpenAPIResponse,
33+
request_cls: Type[FlaskOpenAPIRequest] = FlaskOpenAPIRequest,
34+
response_cls: Optional[
35+
Type[FlaskOpenAPIResponse]
36+
] = FlaskOpenAPIResponse,
3537
request_provider: Type[FlaskRequestProvider] = FlaskRequestProvider,
3638
openapi_errors_handler: Type[
3739
FlaskOpenAPIErrorsHandler
@@ -44,8 +46,8 @@ def __init__(
4446
response_unmarshaller_cls=response_unmarshaller_cls,
4547
**unmarshaller_kwargs,
4648
)
47-
self.request_class = request_class
48-
self.response_class = response_class
49+
self.request_cls = request_cls
50+
self.response_cls = response_cls
4951
self.request_provider = request_provider
5052
self.openapi_errors_handler = openapi_errors_handler
5153

@@ -60,6 +62,8 @@ def decorated(*args: Any, **kwargs: Any) -> Response:
6062
response = self._handle_request_view(
6163
request_result, view, *args, **kwargs
6264
)
65+
if self.response_cls is None:
66+
return response
6367
openapi_response = self._get_openapi_response(response)
6468
response_result = self.process_response(
6569
openapi_request, openapi_response
@@ -96,21 +100,22 @@ def _get_request(self) -> Request:
96100
return request
97101

98102
def _get_openapi_request(self, request: Request) -> FlaskOpenAPIRequest:
99-
return self.request_class(request)
103+
return self.request_cls(request)
100104

101105
def _get_openapi_response(
102106
self, response: Response
103107
) -> FlaskOpenAPIResponse:
104-
return self.response_class(response)
108+
assert self.response_cls is not None
109+
return self.response_cls(response)
105110

106111
@classmethod
107112
def from_spec(
108113
cls,
109114
spec: Spec,
110115
request_unmarshaller_cls: Optional[RequestUnmarshallerType] = None,
111116
response_unmarshaller_cls: Optional[ResponseUnmarshallerType] = None,
112-
request_class: Type[FlaskOpenAPIRequest] = FlaskOpenAPIRequest,
113-
response_class: Type[FlaskOpenAPIResponse] = FlaskOpenAPIResponse,
117+
request_cls: Type[FlaskOpenAPIRequest] = FlaskOpenAPIRequest,
118+
response_cls: Type[FlaskOpenAPIResponse] = FlaskOpenAPIResponse,
114119
request_provider: Type[FlaskRequestProvider] = FlaskRequestProvider,
115120
openapi_errors_handler: Type[
116121
FlaskOpenAPIErrorsHandler
@@ -121,8 +126,8 @@ def from_spec(
121126
spec,
122127
request_unmarshaller_cls=request_unmarshaller_cls,
123128
response_unmarshaller_cls=response_unmarshaller_cls,
124-
request_class=request_class,
125-
response_class=response_class,
129+
request_cls=request_cls,
130+
response_cls=response_cls,
126131
request_provider=request_provider,
127132
openapi_errors_handler=openapi_errors_handler,
128133
**unmarshaller_kwargs,

tests/integration/contrib/django/data/v3.0/djangoproject/tags/__init__.py

Whitespace-only changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.http import HttpResponse
2+
from rest_framework.views import APIView
3+
4+
5+
class TagListView(APIView):
6+
def get(self, request):
7+
assert request.openapi
8+
assert not request.openapi.errors
9+
return HttpResponse("success")
10+
11+
@staticmethod
12+
def get_extra_actions():
13+
return []

tests/integration/contrib/django/data/v3.0/djangoproject/urls.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
from django.contrib import admin
1717
from django.urls import include
1818
from django.urls import path
19-
from djangoproject.pets import views
19+
from djangoproject.pets.views import PetDetailView
20+
from djangoproject.pets.views import PetListView
21+
from djangoproject.tags.views import TagListView
2022

2123
urlpatterns = [
2224
path("admin/", admin.site.urls),
@@ -26,12 +28,17 @@
2628
),
2729
path(
2830
"v1/pets",
29-
views.PetListView.as_view(),
31+
PetListView.as_view(),
3032
name="pet_list_view",
3133
),
3234
path(
3335
"v1/pets/<int:petId>",
34-
views.PetDetailView.as_view(),
36+
PetDetailView.as_view(),
3537
name="pet_detail_view",
3638
),
39+
path(
40+
"v1/tags",
41+
TagListView.as_view(),
42+
name="tag_list_view",
43+
),
3744
]

tests/integration/contrib/django/test_django_project.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from unittest import mock
66

77
import pytest
8+
from django.test.utils import override_settings
89

910

1011
class BaseTestDjangoProject:
@@ -372,3 +373,25 @@ def test_post_valid(self, api_client):
372373

373374
assert response.status_code == 201
374375
assert not response.content
376+
377+
378+
class TestDRFTagListView(BaseTestDRF):
379+
def test_get_response_invalid(self, client):
380+
headers = {
381+
"HTTP_AUTHORIZATION": "Basic testuser",
382+
"HTTP_HOST": "petstore.swagger.io",
383+
}
384+
response = client.get("/v1/tags", **headers)
385+
386+
assert response.status_code == 415
387+
388+
def test_get_skip_response_validation(self, client):
389+
headers = {
390+
"HTTP_AUTHORIZATION": "Basic testuser",
391+
"HTTP_HOST": "petstore.swagger.io",
392+
}
393+
with override_settings(OPENAPI_RESPONSE_CLS=None):
394+
response = client.get("/v1/tags", **headers)
395+
396+
assert response.status_code == 200
397+
assert response.content == b"success"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pytest
2+
from flask import Flask
3+
4+
5+
@pytest.fixture(scope="session")
6+
def spec(factory):
7+
specfile = "contrib/flask/data/v3.0/flask_factory.yaml"
8+
return factory.spec_from_file(specfile)
9+
10+
11+
@pytest.fixture
12+
def app(app_factory):
13+
return app_factory()
14+
15+
16+
@pytest.fixture
17+
def client(client_factory, app):
18+
return client_factory(app)
19+
20+
21+
@pytest.fixture(scope="session")
22+
def client_factory():
23+
def create(app):
24+
return app.test_client()
25+
26+
return create
27+
28+
29+
@pytest.fixture(scope="session")
30+
def app_factory():
31+
def create(root_path=None):
32+
app = Flask("__main__", root_path=root_path)
33+
app.config["DEBUG"] = True
34+
app.config["TESTING"] = True
35+
return app
36+
37+
return create

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy