-
-
Notifications
You must be signed in to change notification settings - Fork 773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added aiohttp support #530
Changes from all commits
5ce5145
f24ca44
51449d4
8328507
09daab6
d1565cf
34b4f94
1408dbe
fdaaa28
2841697
cce83b5
8f4ff03
afbd478
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I meant here is that everywhere else in the code the version check looks like |
||
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' |
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 \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think such complex conditions should be written as a regular multiline |
||
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we could add something like
NOTE: Also check aiohttp handler examples