Skip to content

Commit

Permalink
Merge branch 'dutradda-aiohttp_support'
Browse files Browse the repository at this point in the history
  • Loading branch information
hjacobs committed Apr 9, 2018
2 parents 9f20c5f + bbff57c commit 6cad83c
Show file tree
Hide file tree
Showing 27 changed files with 1,406 additions and 79 deletions.
13 changes: 13 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,17 @@ Flask with uWSGI`_ (this is common):
app = connexion.App(__name__, specification_dir='swagger/')
application = app.app # expose global WSGI application object
You can use the ``aiohttp`` framework as server backend as well:

.. code-block:: python
import connexion
app = connexion.AioHttpApp(__name__, specification_dir='swagger/')
app.run(port=8080)
**NOTE:** Also check aiohttp handler examples_.

Set up and run the installation code:

.. code-block:: bash
Expand All @@ -427,6 +438,8 @@ See the `uWSGI documentation`_ for more information.

.. _using Flask with uWSGI: http://flask.pocoo.org/docs/latest/deploying/uwsgi/
.. _uWSGI documentation: https://uwsgi-docs.readthedocs.org/
.. _examples: https://docs.aiohttp.org/en/stable/web.html#handler


Documentation
=============
Expand Down
28 changes: 21 additions & 7 deletions connexion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,39 @@
from .decorators.produces import NoContent # NOQA
from .resolver import Resolution, Resolver, RestyResolver # NOQA

try:
from .apis.flask_api import FlaskApi
from .apps.flask_app import FlaskApp
from flask import request # NOQA
except ImportError as e: # pragma: no cover
import sys
import sys


def not_installed_error(): # pragma: no cover
import six
import functools

def _required_lib(exec_info, *args, **kwargs):
six.reraise(*exec_info)

_flask_not_installed_error = functools.partial(_required_lib, sys.exc_info())
return functools.partial(_required_lib, sys.exc_info())


try:
from .apis.flask_api import FlaskApi
from .apps.flask_app import FlaskApp
from flask import request # NOQA
except ImportError: # pragma: no cover
_flask_not_installed_error = not_installed_error()
FlaskApi = _flask_not_installed_error
FlaskApp = _flask_not_installed_error

App = FlaskApp
Api = FlaskApi

if sys.version_info[0] >= 3: # pragma: no cover
try:
from .apis.aiohttp_api import AioHttpApi
from .apps.aiohttp_app import AioHttpApp
except ImportError: # pragma: no cover
_aiohttp_not_installed_error = not_installed_error()
AioHttpApi = _aiohttp_not_installed_error
AioHttpApp = _aiohttp_not_installed_error

# This version is replaced during release process.
__version__ = '2018.0.dev1'
26 changes: 20 additions & 6 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 @@ -297,14 +305,20 @@ def get_response(self, response, mimetype=None, request=None):

@classmethod
@abc.abstractmethod
def json_loads(self, data):
def get_connexion_response(cls, response):
"""
API specific JSON loader.
:param data:
:return:
This method converts the user framework response to a ConnexionResponse.
:param response: A response to cast.
"""

def json_loads(self, data):
return self.jsonifier.loads(data)

@classmethod
def _set_jsonifier(cls):
import json
cls.jsonifier = Jsonifier(json)


def canonical_base_path(base_path):
"""
Expand Down
266 changes: 266 additions & 0 deletions connexion/apis/aiohttp_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import asyncio
import logging
import re
from urllib.parse import parse_qs

import jinja2

import aiohttp_jinja2
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 Jsonifier, is_json_mimetype

try:
import ujson as json
from functools import partial
json.dumps = partial(json.dumps, escape_forward_slashes=True)

except ImportError: # pragma: no cover
import json

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


@web.middleware
@asyncio.coroutine
def oauth_problem_middleware(request, handler):
try:
response = yield from handler(request)
except OAuthProblem as oauth_error:
return web.Response(
status=oauth_error.code,
body=json.dumps(oauth_error.description).encode(),
content_type='application/problem+json'
)
return response


class AioHttpApi(AbstractAPI):
def __init__(self, *args, **kwargs):
self.subapp = web.Application(
debug=kwargs.get('debug', False),
middlewares=[oauth_problem_middleware]
)
AbstractAPI.__init__(self, *args, **kwargs)

aiohttp_jinja2.setup(
self.subapp,
loader=jinja2.FileSystemLoader(
str(self.options.openapi_console_ui_from_dir)
)
)
middlewares = self.options.as_dict().get('middlewares', [])
self.subapp.middlewares.extend(middlewares)

def _set_base_path(self, base_path):
AbstractAPI._set_base_path(self, base_path)
self._api_name = AioHttpApi.normalize_string(self.base_path)

@staticmethod
def normalize_string(string):
return re.sub(r'[^a-zA-Z0-9]', '_', string.strip('/'))

def add_swagger_json(self):
"""
Adds swagger json to {base_path}/swagger.json
"""
logger.debug('Adding swagger.json: %s/swagger.json', self.base_path)
self.subapp.router.add_route(
'GET',
'/swagger.json',
self._get_swagger_json
)

@asyncio.coroutine
def _get_swagger_json(self, req):
return web.Response(
status=200,
content_type='application/json',
body=self.jsonifier.dumps(self.specification)
)

def add_swagger_ui(self):
"""
Adds swagger ui to {base_path}/ui/
"""
console_ui_path = self.options.openapi_console_ui_path.strip().rstrip('/')
logger.debug('Adding swagger-ui: %s%s/',
self.base_path,
console_ui_path)

for path in (
console_ui_path,
console_ui_path + '/',
console_ui_path + '/index.html',
):
self.subapp.router.add_route(
'GET',
path,
self._get_swagger_ui_home
)

self.subapp.router.add_static(
console_ui_path + '/',
path=str(self.options.openapi_console_ui_from_dir),
name='swagger_ui_static'
)

@aiohttp_jinja2.template('index.html')
@asyncio.coroutine
def _get_swagger_ui_home(self, req):
return {'api_url': self.base_path}

def add_auth_on_not_found(self, security, security_definitions):
"""
Adds a 404 error handler to authenticate and only expose the 404 status if the security validation pass.
"""
logger.debug('Adding path not found authentication')
not_found_error = AuthErrorHandler(
self, _HttpNotFoundError(),
security=security,
security_definitions=security_definitions
)
endpoint_name = "{}_not_found".format(self._api_name)
self.subapp.router.add_route(
'*',
'/{not_found_path}',
not_found_error.function,
name=endpoint_name
)

def _add_operation_internal(self, method, path, operation):
method = method.upper()
operation_id = operation.operation_id or path

logger.debug('... Adding %s -> %s', method, operation_id,
extra=vars(operation))

handler = operation.function
endpoint_name = '{}_{}_{}'.format(
self._api_name,
AioHttpApi.normalize_string(path),
method.lower()
)
self.subapp.router.add_route(
method, path, handler, name=endpoint_name
)

if not path.endswith('/'):
self.subapp.router.add_route(
method, path + '/', handler, name=endpoint_name + '_'
)

@classmethod
@asyncio.coroutine
def get_request(cls, req):
"""Convert aiohttp request to connexion
:param req: instance of aiohttp.web.Request
:return: connexion request instance
:rtype: ConnexionRequest
"""
url = str(req.url)
logger.debug('Getting data and status code',
extra={'has_body': req.has_body, 'url': url})

query = {k: ','.join(v) for k, v in parse_qs(req.rel_url.query_string).items()}
headers = {k.decode(): v.decode() for k, v in req.raw_headers}
body = None
if req.can_read_body:
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
@asyncio.coroutine
def get_response(cls, response, mimetype=None, request=None):
"""Get response.
This method is used in the lifecycle decorators
:rtype: aiohttp.web.Response
"""
while asyncio.iscoroutine(response):
response = yield from response

url = str(request.url) if request else ''

logger.debug('Getting data and status code',
extra={
'data': response,
'url': url
})

if isinstance(response, ConnexionResponse):
response = cls._get_aiohttp_response_from_connexion(response, mimetype)

logger.debug('Got data and status code (%d)',
response.status, extra={'data': response.body, 'url': url})

return response

@classmethod
def get_connexion_response(cls, response):
return ConnexionResponse(
status_code=response.status,
mimetype=response.content_type,
content_type=response.content_type,
headers=response.headers,
body=response.body
)

@classmethod
def _get_aiohttp_response_from_connexion(cls, response, mimetype):
content_type = response.content_type if response.content_type else \
response.mimetype if response.mimetype else mimetype

body = cls._cast_body(response.body, content_type)

return web.Response(
status=response.status_code,
content_type=content_type,
headers=response.headers,
body=body
)

@classmethod
def _cast_body(cls, body, content_type):
if not isinstance(body, bytes):
if is_json_mimetype(content_type):
return json.dumps(body).encode()

elif isinstance(body, str):
return body.encode()

else:
return str(body).encode()
else:
return body

@classmethod
def _set_jsonifier(cls):
cls.jsonifier = Jsonifier(json)


class _HttpNotFoundError(HTTPNotFound):
def __init__(self):
self.name = 'Not Found'
self.description = (
'The requested URL was not found on the server. '
'If you entered the URL manually please check your spelling and '
'try again.'
)
self.code = type(self).status_code
self.empty_body = True

HTTPNotFound.__init__(self, reason=self.name)
Loading

0 comments on commit 6cad83c

Please sign in to comment.