Skip to content

Commit

Permalink
Changes for aiohttp support:
Browse files Browse the repository at this point in the history
 - Created a metaclass for AbstractAPI (it is used to set the jsonifier for the class);
 - Created a new class method AbstractAPI._set_jsonifier;
 - Changed the code to use the new jsonifier interface;
 - Create a new module called coroutines_wrapper to put the wrapper functions with the 'yield from' statement. It is used to enable frameworks with coroutine handlers;
 - Did the AioHttpApi.get_request coroutine and add req.read() to get the request body;
 - Moved the flask jsonifier to utils and did it a generic jsonifier;
 - Created a function called 'has_coroutine' on utils module;
 - Added aiohttp_jinja2 to requirements-aiohttp;
 - Added a new python3 coreragerc file to skip only python2 lines;
 - Fixed the set of validation_response on test_aiohttp_simple_api.py;
 - Added the test to check the aiohttp body request;
 - Fixed the response for 'aiohttp_bytes_response' and 'aiohttp_non_str_non_json_response' paths on aiohttp/swagger_simple.yml file.
  • Loading branch information
dutradda committed Feb 5, 2018
1 parent cce83b5 commit 8f4ff03
Show file tree
Hide file tree
Showing 16 changed files with 174 additions and 107 deletions.
22 changes: 14 additions & 8 deletions connexion/apis/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ..operation import Operation
from ..options import ConnexionOptions
from ..resolver import Resolver
from ..utils import Jsonifier

MODULE_PATH = pathlib.Path(__file__).absolute().parent.parent
SWAGGER_UI_PATH = MODULE_PATH / 'vendor' / 'swagger-ui'
Expand All @@ -24,7 +25,14 @@
logger = logging.getLogger('connexion.apis.abstract')


@six.add_metaclass(abc.ABCMeta)
class AbstractAPIMeta(abc.ABCMeta):

def __init__(cls, name, bases, attrs):
abc.ABCMeta.__init__(cls, name, bases, attrs)
cls._set_jsonifier()


@six.add_metaclass(AbstractAPIMeta)
class AbstractAPI(object):
"""
Defines an abstract interface for a Swagger API
Expand Down Expand Up @@ -303,15 +311,13 @@ def get_connexion_response(cls, response):
:param response: A response to cast.
"""

@classmethod
@abc.abstractmethod
def json_loads(self, data):
"""
API specific JSON loader.
return self.jsonifier.loads(data)

:param data:
:return:
"""
@classmethod
def _set_jsonifier(cls):
import json
cls.jsonifier = Jsonifier(json)


def canonical_base_path(base_path):
Expand Down
21 changes: 9 additions & 12 deletions connexion/apis/aiohttp_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
import jinja2

import aiohttp_jinja2
from aiohttp.web_exceptions import HTTPNotFound
from aiohttp import web
from aiohttp.web_exceptions import HTTPNotFound
from connexion.apis.abstract import AbstractAPI
from connexion.exceptions import OAuthProblem
from connexion.handlers import AuthErrorHandler
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
from connexion.utils import is_json_mimetype

from connexion.utils import Jsonifier, is_json_mimetype

try:
import ujson as json
Expand Down Expand Up @@ -81,7 +80,7 @@ def _get_swagger_json(self, req):
return web.Response(
status=200,
content_type='application/json',
body=json.dumps(self.specification)
body=self.jsonifier.dumps(self.specification)
)

def add_swagger_ui(self):
Expand Down Expand Up @@ -156,6 +155,7 @@ def _add_operation_internal(self, method, path, operation):
)

@classmethod
@asyncio.coroutine
def get_request(cls, req):
"""Convert aiohttp request to connexion
Expand All @@ -171,16 +171,15 @@ def get_request(cls, req):
headers = {k.decode(): v.decode() for k, v in req.raw_headers}
body = None
if req.can_read_body:
# FIXME: the body is awaitable
# body = yield from req.read()
body = req.content
body = yield from req.read()

return ConnexionRequest(url=url,
method=req.method.lower(),
path_params=dict(req.match_info),
query=query,
headers=headers,
body=body,
json_getter=lambda: cls.jsonifier.loads(body),
files={})

@classmethod
Expand Down Expand Up @@ -248,11 +247,9 @@ def _cast_body(cls, body, content_type):
else:
return body

def json_loads(self, data):
"""
Use json as JSON loader
"""
return json.loads(data)
@classmethod
def _set_jsonifier(cls):
cls.jsonifier = Jsonifier(json)


class _HttpNotFoundError(HTTPNotFound):
Expand Down
43 changes: 11 additions & 32 deletions connexion/apis/flask_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
from connexion.decorators.produces import NoContent
from connexion.handlers import AuthErrorHandler
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
from connexion.utils import is_json_mimetype
from connexion.utils import Jsonifier, is_json_mimetype

logger = logging.getLogger('connexion.apis.flask_api')


class FlaskApi(AbstractAPI):

def _set_base_path(self, base_path):
super(FlaskApi, self)._set_base_path(base_path)
self._set_blueprint()
Expand All @@ -25,12 +26,6 @@ def _set_blueprint(self):
self.blueprint = flask.Blueprint(endpoint, __name__, url_prefix=self.base_path,
template_folder=str(self.options.openapi_console_ui_from_dir))

def json_loads(self, data):
"""
Use Flask specific JSON loader
"""
return Jsonifier.loads(data)

def add_swagger_json(self):
"""
Adds swagger json to {base_path}/swagger.json
Expand Down Expand Up @@ -173,7 +168,7 @@ def _build_flask_response(cls, mimetype=None, content_type=None,
def _jsonify_data(cls, data, mimetype):
if (isinstance(mimetype, six.string_types) and is_json_mimetype(mimetype)) \
or not (isinstance(data, six.binary_type) or isinstance(data, six.text_type)):
return Jsonifier.dumps(data)
return cls.jsonifier.dumps(data)

return data

Expand Down Expand Up @@ -229,7 +224,7 @@ def get_request(cls, *args, **params):
form=flask_request.form,
query=flask_request.args,
body=flask_request.get_data(),
json=flask_request.get_json(silent=True),
json_getter=lambda: flask_request.get_json(silent=True),
files=flask_request.files,
path_params=params,
context=FlaskRequestContextProxy()
Expand All @@ -242,6 +237,13 @@ def get_request(cls, *args, **params):
})
return request

@classmethod
def _set_jsonifier(cls):
"""
Use Flask specific JSON loader
"""
cls.jsonifier = Jsonifier(flask.json)


class FlaskRequestContextProxy(object):
""""Proxy assignments from `ConnexionRequest.context`
Expand All @@ -262,29 +264,6 @@ def items(self):
return self.values.items()


class Jsonifier(object):
@staticmethod
def dumps(data):
""" Central point where JSON serialization happens inside
Connexion.
"""
return "{}\n".format(flask.json.dumps(data, indent=2))

@staticmethod
def loads(data):
""" Central point where JSON serialization happens inside
Connexion.
"""
if isinstance(data, six.binary_type):
data = data.decode()

try:
return flask.json.loads(data)
except Exception as error:
if isinstance(data, six.string_types):
return data


class InternalHandlers(object):
"""
Flask handlers for internally registered endpoints.
Expand Down
53 changes: 53 additions & 0 deletions connexion/decorators/coroutine_wrappers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import asyncio
import functools


def get_request_life_cycle_wrapper(function, api, mimetype):
"""
It is a wrapper used on `EndOfRequestLifecycleDecorator` class.
This function is located in an extra module because python2.7 don't
support the 'yield from' syntax. This function is used to await
the coroutines to connexion does the proper validation of parameters
and responses.
:rtype asyncio.coroutine
"""
@functools.wraps(function)
def wrapper(*args, **kwargs):
connexion_request = api.get_request(*args, **kwargs)
while asyncio.iscoroutine(connexion_request):
connexion_request = yield from connexion_request

connexion_response = function(connexion_request)
while asyncio.iscoroutine(connexion_response):
connexion_response = yield from connexion_response

framework_response = api.get_response(connexion_response, mimetype,
connexion_request)
while asyncio.iscoroutine(framework_response):
framework_response = yield from framework_response

return framework_response

return asyncio.coroutine(wrapper)


def get_response_validator_wrapper(function, _wrapper):
"""
It is a wrapper used on `ResponseValidator` class.
This function is located in an extra module because python2.7 don't
support the 'yield from' syntax. This function is used to await
the coroutines to connexion does the proper validation of parameters
and responses.
:rtype asyncio.coroutine
"""
@functools.wraps(function)
def wrapper(request):
response = function(request)
while asyncio.iscoroutine(response):
response = yield from response

return _wrapper(request, response)

return asyncio.coroutine(wrapper)
17 changes: 12 additions & 5 deletions connexion/decorators/decorator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import functools
import logging

from ..utils import has_coroutine

logger = logging.getLogger('connexion.decorators.decorator')


Expand Down Expand Up @@ -60,10 +62,15 @@ def __call__(self, function):
:type function: types.FunctionType
:rtype: types.FunctionType
"""
@functools.wraps(function)
def wrapper(*args, **kwargs):
request = self.api.get_request(*args, **kwargs)
response = function(request)
return self.api.get_response(response, self.mimetype, request)
if has_coroutine(function, self.api): # pragma: 2.7 no cover
from .coroutine_wrappers import get_request_life_cycle_wrapper
wrapper = get_request_life_cycle_wrapper(function, self.api, self.mimetype)

else: # pragma: 3 no cover
@functools.wraps(function)
def wrapper(*args, **kwargs):
request = self.api.get_request(*args, **kwargs)
response = function(request)
return self.api.get_response(response, self.mimetype, request)

return wrapper
17 changes: 5 additions & 12 deletions connexion/decorators/response.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# Decorators to change the return type of endpoints
import functools
import logging
import sys

from jsonschema import ValidationError

from ..exceptions import (NonConformingResponseBody,
NonConformingResponseHeaders)
from ..problem import problem
from ..utils import all_json
from ..utils import all_json, has_coroutine
from .decorator import BaseDecorator
from .validation import ResponseBodyValidator

Expand Down Expand Up @@ -95,17 +94,11 @@ def _wrapper(request, response):

return response

is_coroutine = False
if has_coroutine(function): # pragma: 2.7 no cover
from .coroutine_wrappers import get_response_validator_wrapper
wrapper = get_response_validator_wrapper(function, _wrapper)

if sys.version_info[0] >= 3: # pragma: 2.7 no cover
import asyncio
is_coroutine = asyncio.iscoroutinefunction(function)

if is_coroutine: # pragma: 2.7 no cover
from .response_coroutine import get_wrapper
wrapper = get_wrapper(function, _wrapper)

else:
else: # pragma: 3 no cover
@functools.wraps(function)
def wrapper(request):
response = function(request)
Expand Down
11 changes: 0 additions & 11 deletions connexion/decorators/response_coroutine.py

This file was deleted.

8 changes: 6 additions & 2 deletions connexion/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def __init__(self,
headers=None,
form=None,
body=None,
json=None,
json_getter=None,
files=None,
context=None):
self.url = url
Expand All @@ -18,10 +18,14 @@ def __init__(self,
self.headers = headers or {}
self.form = form or {}
self.body = body
self.json = json
self.json_getter = json_getter
self.files = files
self.context = context or {}

@property
def json(self):
return self.json_getter()


class ConnexionResponse(object):
def __init__(self,
Expand Down
Loading

0 comments on commit 8f4ff03

Please sign in to comment.