Skip to content

Commit 17b7956

Browse files
authored
Merge pull request #177 from p1c2u/feature/flask-openapi-view
Flask OpenAPI view & decorator
2 parents ca63475 + 82f8280 commit 17b7956

20 files changed

+703
-192
lines changed

README.rst

+40
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,46 @@ or simply specify response factory for shortcuts
181181
Flask
182182
*****
183183

184+
Decorator
185+
=========
186+
187+
Flask views can be integrated by `FlaskOpenAPIViewDecorator` decorator.
188+
189+
.. code-block:: python
190+
191+
from openapi_core.contrib.flask.decorators import FlaskOpenAPIViewDecorator
192+
193+
openapi = FlaskOpenAPIViewDecorator.from_spec(spec)
194+
195+
@app.route('/home')
196+
@openapi
197+
def home():
198+
pass
199+
200+
If you want to decorate class based view you can use the decorators attribute:
201+
202+
.. code-block:: python
203+
204+
class MyView(View):
205+
decorators = [openapi]
206+
207+
View
208+
====
209+
210+
As an alternative to the decorator-based integration, Flask method based views can be integrated by inheritance from `FlaskOpenAPIView` class.
211+
212+
.. code-block:: python
213+
214+
from openapi_core.contrib.flask.views import FlaskOpenAPIView
215+
216+
class MyView(FlaskOpenAPIView):
217+
pass
218+
219+
app.add_url_rule('/home', view_func=MyView.as_view('home', spec))
220+
221+
Low level
222+
=========
223+
184224
You can use FlaskOpenAPIRequest a Flask/Werkzeug request factory:
185225

186226
.. code-block:: python
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""OpenAPI core contrib flask decorators module"""
2+
from openapi_core.contrib.flask.handlers import FlaskOpenAPIErrorsHandler
3+
from openapi_core.contrib.flask.providers import FlaskRequestProvider
4+
from openapi_core.contrib.flask.requests import FlaskOpenAPIRequestFactory
5+
from openapi_core.contrib.flask.responses import FlaskOpenAPIResponseFactory
6+
from openapi_core.validation.decorators import OpenAPIDecorator
7+
from openapi_core.validation.request.validators import RequestValidator
8+
from openapi_core.validation.response.validators import ResponseValidator
9+
10+
11+
class FlaskOpenAPIViewDecorator(OpenAPIDecorator):
12+
13+
def __init__(
14+
self,
15+
request_validator,
16+
response_validator,
17+
request_factory=FlaskOpenAPIRequestFactory,
18+
response_factory=FlaskOpenAPIResponseFactory,
19+
request_provider=FlaskRequestProvider,
20+
openapi_errors_handler=FlaskOpenAPIErrorsHandler,
21+
):
22+
super(FlaskOpenAPIViewDecorator, self).__init__(
23+
request_validator, response_validator,
24+
request_factory, response_factory,
25+
request_provider, openapi_errors_handler,
26+
)
27+
28+
@classmethod
29+
def from_spec(
30+
cls,
31+
spec,
32+
request_factory=FlaskOpenAPIRequestFactory,
33+
response_factory=FlaskOpenAPIResponseFactory,
34+
request_provider=FlaskRequestProvider,
35+
openapi_errors_handler=FlaskOpenAPIErrorsHandler,
36+
):
37+
request_validator = RequestValidator(spec)
38+
response_validator = ResponseValidator(spec)
39+
return cls(
40+
request_validator=request_validator,
41+
response_validator=response_validator,
42+
request_factory=request_factory,
43+
response_factory=response_factory,
44+
request_provider=request_provider,
45+
openapi_errors_handler=openapi_errors_handler,
46+
)
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""OpenAPI core contrib flask handlers module"""
2+
from flask.globals import current_app
3+
from flask.json import dumps
4+
5+
from openapi_core.schema.media_types.exceptions import InvalidContentType
6+
from openapi_core.schema.servers.exceptions import InvalidServer
7+
8+
9+
class FlaskOpenAPIErrorsHandler(object):
10+
11+
OPENAPI_ERROR_STATUS = {
12+
InvalidServer: 500,
13+
InvalidContentType: 415,
14+
}
15+
16+
@classmethod
17+
def handle(cls, errors):
18+
data_errors = [
19+
cls.format_openapi_error(err)
20+
for err in errors
21+
]
22+
data = {
23+
'errors': data_errors,
24+
}
25+
status = max(
26+
range(len(data_errors)),
27+
key=lambda idx: data_errors[idx]['status'],
28+
)
29+
return current_app.response_class(
30+
dumps(data),
31+
status=status,
32+
mimetype='application/json'
33+
)
34+
35+
@classmethod
36+
def format_openapi_error(cls, error):
37+
return {
38+
'title': str(error),
39+
'status': cls.OPENAPI_ERROR_STATUS.get(error.__class__, 400),
40+
'class': str(type(error)),
41+
}
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""OpenAPI core contrib flask providers module"""
2+
from flask.globals import request
3+
4+
5+
class FlaskRequestProvider(object):
6+
7+
@classmethod
8+
def provide(self, *args, **kwargs):
9+
return request

openapi_core/contrib/flask/views.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""OpenAPI core contrib flask views module"""
2+
from flask.views import MethodView
3+
4+
from openapi_core.contrib.flask.decorators import FlaskOpenAPIViewDecorator
5+
from openapi_core.contrib.flask.handlers import FlaskOpenAPIErrorsHandler
6+
from openapi_core.validation.request.validators import RequestValidator
7+
from openapi_core.validation.response.validators import ResponseValidator
8+
9+
10+
class FlaskOpenAPIView(MethodView):
11+
"""Brings OpenAPI specification validation and unmarshalling for views."""
12+
13+
openapi_errors_handler = FlaskOpenAPIErrorsHandler
14+
15+
def __init__(self, spec):
16+
super(FlaskOpenAPIView, self).__init__()
17+
self.request_validator = RequestValidator(spec)
18+
self.response_validator = ResponseValidator(spec)
19+
20+
def dispatch_request(self, *args, **kwargs):
21+
decorator = FlaskOpenAPIViewDecorator(
22+
request_validator=self.request_validator,
23+
response_validator=self.response_validator,
24+
openapi_errors_handler=self.openapi_errors_handler,
25+
)
26+
return decorator(super(FlaskOpenAPIView, self).dispatch_request)(
27+
*args, **kwargs)

openapi_core/schema/media_types/models.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
"""OpenAPI core media types models module"""
22
from collections import defaultdict
33

4-
from json import loads
5-
64
from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue
5+
from openapi_core.schema.media_types.util import json_loads
76
from openapi_core.schema.schemas.exceptions import (
87
CastError, ValidateError,
98
)
109
from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError
1110

1211

1312
MEDIA_TYPE_DESERIALIZERS = {
14-
'application/json': loads,
13+
'application/json': json_loads,
1514
}
1615

1716

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from json import loads
2+
3+
from six import binary_type
4+
5+
6+
def json_loads(value):
7+
# python 3.5 doesn't support binary input fix
8+
if isinstance(value, (binary_type, )):
9+
value = value.decode()
10+
return loads(value)

openapi_core/validation/decorators.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""OpenAPI core validation decorators module"""
2+
from functools import wraps
3+
4+
from openapi_core.validation.processors import OpenAPIProcessor
5+
6+
7+
class OpenAPIDecorator(OpenAPIProcessor):
8+
9+
def __init__(
10+
self,
11+
request_validator,
12+
response_validator,
13+
request_factory,
14+
response_factory,
15+
request_provider,
16+
openapi_errors_handler,
17+
):
18+
super(OpenAPIDecorator, self).__init__(
19+
request_validator, response_validator)
20+
self.request_factory = request_factory
21+
self.response_factory = response_factory
22+
self.request_provider = request_provider
23+
self.openapi_errors_handler = openapi_errors_handler
24+
25+
def __call__(self, view):
26+
@wraps(view)
27+
def decorated(*args, **kwargs):
28+
request = self._get_request(*args, **kwargs)
29+
openapi_request = self._get_openapi_request(request)
30+
errors = self.process_request(openapi_request)
31+
if errors:
32+
return self._handle_openapi_errors(errors)
33+
response = view(*args, **kwargs)
34+
openapi_response = self._get_openapi_response(response)
35+
errors = self.process_response(openapi_request, openapi_response)
36+
if errors:
37+
return self._handle_openapi_errors(errors)
38+
return response
39+
return decorated
40+
41+
def _get_request(self, *args, **kwargs):
42+
return self.request_provider.provide(*args, **kwargs)
43+
44+
def _handle_openapi_errors(self, errors):
45+
return self.openapi_errors_handler.handle(errors)
46+
47+
def _get_openapi_request(self, request):
48+
return self.request_factory.create(request)
49+
50+
def _get_openapi_response(self, response):
51+
return self.response_factory.create(response)

openapi_core/validation/processors.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""OpenAPI core validation processors module"""
2+
from openapi_core.schema.servers.exceptions import InvalidServer
3+
from openapi_core.schema.exceptions import OpenAPIMappingError
4+
5+
6+
class OpenAPIProcessor(object):
7+
8+
def __init__(self, request_validator, response_validator):
9+
self.request_validator = request_validator
10+
self.response_validator = response_validator
11+
12+
def process_request(self, request):
13+
request_result = self.request_validator.validate(request)
14+
try:
15+
request_result.raise_for_errors()
16+
# return instantly on server error
17+
except InvalidServer as exc:
18+
return [exc, ]
19+
except OpenAPIMappingError:
20+
return request_result.errors
21+
else:
22+
return
23+
24+
def process_response(self, request, response):
25+
response_result = self.response_validator.validate(request, response)
26+
try:
27+
response_result.raise_for_errors()
28+
except OpenAPIMappingError:
29+
return response_result.errors
30+
else:
31+
return

openapi_core/wrappers/tastypie.py

Whitespace-only changes.

tests/integration/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from os import path
22

3+
from openapi_spec_validator.schemas import read_yaml_file
34
import pytest
45
from six.moves.urllib import request
56
from yaml import safe_load
@@ -8,8 +9,7 @@
89
def spec_from_file(spec_file):
910
directory = path.abspath(path.dirname(__file__))
1011
path_full = path.join(directory, spec_file)
11-
with open(path_full) as fh:
12-
return safe_load(fh)
12+
return read_yaml_file(path_full)
1313

1414

1515
def spec_from_url(spec_url):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from flask.wrappers import Request, Response
2+
import pytest
3+
from werkzeug.routing import Map, Rule, Subdomain
4+
from werkzeug.test import create_environ
5+
6+
7+
@pytest.fixture
8+
def environ_factory():
9+
return create_environ
10+
11+
12+
@pytest.fixture
13+
def map():
14+
return Map([
15+
# Static URLs
16+
Rule('/', endpoint='static/index'),
17+
Rule('/about', endpoint='static/about'),
18+
Rule('/help', endpoint='static/help'),
19+
# Knowledge Base
20+
Subdomain('kb', [
21+
Rule('/', endpoint='kb/index'),
22+
Rule('/browse/', endpoint='kb/browse'),
23+
Rule('/browse/<int:id>/', endpoint='kb/browse'),
24+
Rule('/browse/<int:id>/<int:page>', endpoint='kb/browse')
25+
])
26+
], default_subdomain='www')
27+
28+
29+
@pytest.fixture
30+
def request_factory(map, environ_factory):
31+
server_name = 'localhost'
32+
33+
def create_request(method, path, subdomain=None, query_string=None):
34+
environ = environ_factory(query_string=query_string)
35+
req = Request(environ)
36+
urls = map.bind_to_environ(
37+
environ, server_name=server_name, subdomain=subdomain)
38+
req.url_rule, req.view_args = urls.match(
39+
path, method, return_rule=True)
40+
return req
41+
return create_request
42+
43+
44+
@pytest.fixture
45+
def response_factory():
46+
def create_response(
47+
data, status_code=200, content_type='application/json'):
48+
return Response(data, status=status_code, content_type=content_type)
49+
return create_response

0 commit comments

Comments
 (0)