Skip to content

Commit 0d0fa52

Browse files
committed
Flask OpenAPI view
1 parent ca63475 commit 0d0fa52

File tree

16 files changed

+554
-189
lines changed

16 files changed

+554
-189
lines changed
Lines changed: 46 additions & 0 deletions
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+
)
Lines changed: 41 additions & 0 deletions
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+
}
Lines changed: 9 additions & 0 deletions
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
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.validation.request.validators import RequestValidator
6+
from openapi_core.validation.response.validators import ResponseValidator
7+
8+
9+
class FlaskOpenAPIView(MethodView):
10+
"""Brings OpenAPI specification validation and unmarshalling for views."""
11+
12+
def __init__(self, request_validator, response_validator):
13+
super(MethodView, self).__init__()
14+
self.request_validator = request_validator
15+
self.response_validator = response_validator
16+
17+
def dispatch_request(self, *args, **kwargs):
18+
decorator = FlaskOpenAPIViewDecorator(
19+
request_validator=self.request_validator,
20+
response_validator=self.response_validator,
21+
openapi_errors_handler=self.handle_openapi_errors,
22+
)
23+
return decorator(super(FlaskOpenAPIView, self).dispatch_request)(
24+
*args, **kwargs)
25+
26+
def handle_openapi_errors(self, errors):
27+
"""Handles OpenAPI request/response errors.
28+
29+
Should return response object::
30+
31+
class MyView(FlaskOpenAPIView):
32+
33+
def handle_openapi_errors(self, errors):
34+
return jsonify({'errors': errors})
35+
"""
36+
raise NotImplementedError
37+
38+
@classmethod
39+
def from_spec(cls, spec):
40+
request_validator = RequestValidator(spec)
41+
response_validator = ResponseValidator(spec)
42+
return cls(request_validator, response_validator)
Lines changed: 51 additions & 0 deletions
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)
Lines changed: 31 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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):
Lines changed: 49 additions & 0 deletions
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
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Basic OpenAPI specification used with test_flask.TestFlaskOpenAPIIValidation
4+
version: "0.1"
5+
servers:
6+
- url: 'http://localhost'
7+
paths:
8+
'/browse/{id}/':
9+
parameters:
10+
- name: id
11+
in: path
12+
required: true
13+
description: the ID of the resource to retrieve
14+
schema:
15+
type: integer
16+
get:
17+
responses:
18+
200:
19+
description: Return the resource.
20+
content:
21+
application/json:
22+
schema:
23+
type: object
24+
required:
25+
- data
26+
properties:
27+
data:
28+
type: string
29+
default:
30+
description: Return errors.
31+
content:
32+
application/json:
33+
schema:
34+
type: object
35+
required:
36+
- errors
37+
properties:
38+
errors:
39+
type: array
40+
items:
41+
type: object
42+
properties:
43+
title:
44+
type: string
45+
code:
46+
type: string
47+
message:
48+
type: string

0 commit comments

Comments
 (0)