Skip to content

Commit

Permalink
First cut at sanic port.
Browse files Browse the repository at this point in the history
After two days it seems to be at least a little bit working!
  • Loading branch information
ashleysommer committed Nov 1, 2017
1 parent a2ed420 commit 9777004
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 117 deletions.
3 changes: 3 additions & 0 deletions .bowerrc
@@ -0,0 +1,3 @@
{
"directory": "./sanic_restplus/static/bower/"
}
5 changes: 4 additions & 1 deletion .gitignore
Expand Up @@ -49,6 +49,9 @@ histograms/
.project
.pydevproject

#idea
.idea

# Rope
.ropeproject

Expand All @@ -60,5 +63,5 @@ histograms/
doc/_build/

# Specifics
flask_restplus/static
sanic_restplus/static
node_modules
112 changes: 78 additions & 34 deletions sanic_restplus/api.py
Expand Up @@ -12,25 +12,30 @@
from functools import wraps, partial
from types import MethodType

from flask import url_for, request, current_app
from flask import make_response as original_flask_make_response
from flask.helpers import _endpoint_from_view_func
from flask.signals import got_request_exception
# from flask import url_for, request, current_app
# from flask import make_response as original_flask_make_response
# from flask.helpers import _endpoint_from_view_func
# from flask.signals import got_request_exception

from sanic.router import RouteExists
from sanic.response import HTTPResponse
from sanic import exceptions

from jsonschema import RefResolver

from werkzeug import cached_property
from werkzeug.datastructures import Headers
from werkzeug.exceptions import HTTPException, MethodNotAllowed, NotFound, NotAcceptable, InternalServerError
from werkzeug.wrappers import BaseResponse
# from werkzeug import cached_property
# from werkzeug.datastructures import Headers
# from werkzeug.exceptions import HTTPException, MethodNotAllowed, NotFound, NotAcceptable, InternalServerError
# from werkzeug.http import HTTP_STATUS_CODES
# from werkzeug.wrappers import BaseResponse

from . import apidoc
from .mask import ParseError, MaskError
from .namespace import Namespace
from .postman import PostmanCollectionV1
from .resource import Resource
from .swagger import Swagger
from .utils import default_id, camel_to_dash, unpack
from .utils import default_id, camel_to_dash, unpack, best_match_accept_mimetype
from .representations import output_json
from ._http import HTTPStatus

Expand Down Expand Up @@ -193,8 +198,9 @@ def _init_app(self, app):
self._register_specs(self.blueprint or app)
self._register_doc(self.blueprint or app)

app.handle_exception = partial(self.error_router, app.handle_exception)
app.handle_user_exception = partial(self.error_router, app.handle_user_exception)
#TODO Sanic fix exception handling
#app.handle_exception = partial(self.error_router, app.handle_exception)
#app.handle_user_exception = partial(self.error_router, app.handle_user_exception)

if len(self.resources) > 0:
for resource, urls, kwargs in self.resources:
Expand Down Expand Up @@ -224,9 +230,11 @@ def _complete_url(self, url_part, registration_prefix):
return ''.join(part for part in parts if part)

def _register_apidoc(self, app):
if not hasattr(app, 'extensions'):
app.extensions = {}
conf = app.extensions.setdefault('restplus', {})
if not conf.get('apidoc_registered', False):
app.register_blueprint(apidoc.apidoc)
app.blueprint(apidoc.apidoc)
conf['apidoc_registered'] = True

def _register_specs(self, app_or_blueprint):
Expand All @@ -242,10 +250,20 @@ def _register_specs(self, app_or_blueprint):
self.endpoints.add(endpoint)

def _register_doc(self, app_or_blueprint):
root_path = self.prefix or '/'
if self._add_specs and self._doc:
# Register documentation before root if enabled
app_or_blueprint.add_url_rule(self._doc, 'doc', self.render_doc)
app_or_blueprint.add_url_rule(self.prefix or '/', 'root', self.render_root)
# app_or_blueprint.add_url_rule(self._doc, 'doc', self.render_doc)
app_or_blueprint.add_route(named_route_fn('doc', self.render_doc), self._doc)

if self._doc != root_path:
try:# app_or_blueprint.add_url_rule(self.prefix or '/', 'root', self.render_root)
app_or_blueprint.add_route(named_route_fn('root', self.render_root), root_path)

except RouteExists:
pass




def register_resource(self, namespace, resource, *urls, **kwargs):
endpoint = kwargs.pop('endpoint', None)
Expand Down Expand Up @@ -276,9 +294,10 @@ def _register_view(self, app, resource, *urls, **kwargs):

resource.mediatypes = self.mediatypes_method() # Hacky
resource.endpoint = endpoint
resource_func = self.output(resource.as_view(endpoint, self, *resource_class_args,
resource_func = self.output(resource.as_view(self, *resource_class_args,
**resource_class_kwargs))

# hacky, we want to change the __name__ of this func to `endpoint` so it can be found with url_for.
resource_func.__name__ = endpoint
for decorator in self.decorators:
resource_func = decorator(resource_func)

Expand All @@ -302,7 +321,7 @@ def _register_view(self, app, resource, *urls, **kwargs):
# If we've got no Blueprint, just build a url with no prefix
rule = self._complete_url(url, '')
# Add the url to the application or blueprint
app.add_url_rule(rule, view_func=resource_func, **kwargs)
app.add_route(resource_func, rule)

def output(self, resource):
'''
Expand All @@ -312,15 +331,15 @@ def output(self, resource):
:param resource: The resource as a flask view function
'''
@wraps(resource)
def wrapper(*args, **kwargs):
resp = resource(*args, **kwargs)
if isinstance(resp, BaseResponse):
def wrapper(request, *args, **kwargs):
resp = resource(request, *args, **kwargs)
if isinstance(resp, HTTPResponse):
return resp
data, code, headers = unpack(resp)
return self.make_response(data, code, headers=headers)
return self.make_response(request, data, code, headers=headers)
return wrapper

def make_response(self, data, *args, **kwargs):
def make_response(self, request, data, *args, **kwargs):
'''
Looks up the representation transformer for the requested media
type, invoking the transformer to create a response object. This
Expand All @@ -331,14 +350,14 @@ def make_response(self, data, *args, **kwargs):
:param data: Python object containing response data to be transformed
'''
default_mediatype = kwargs.pop('fallback_mediatype', None) or self.default_mediatype
mediatype = request.accept_mimetypes.best_match(
mediatype = best_match_accept_mimetype(request,
self.representations,
default=default_mediatype,
)
if mediatype is None:
raise NotAcceptable()
raise exceptions.SanicException("Not Acceptable", 406)
if mediatype in self.representations:
resp = self.representations[mediatype](data, *args, **kwargs)
resp = self.representations[mediatype](request, data, *args, **kwargs)
resp.headers['Content-Type'] = mediatype
return resp
elif mediatype == 'text/plain':
Expand All @@ -349,20 +368,20 @@ def make_response(self, data, *args, **kwargs):
raise InternalServerError()

def documentation(self, func):
'''A decorator to specify a view funtion for the documentation'''
'''A decorator to specify a view function for the documentation'''
self._doc_view = func
return func

def render_root(self):
def render_root(self, request):
self.abort(HTTPStatus.NOT_FOUND)

def render_doc(self):
def render_doc(self, request):
'''Override this method to customize the documentation page'''
if self._doc_view:
return self._doc_view()
elif not self._doc:
self.abort(HTTPStatus.NOT_FOUND)
return apidoc.ui_for(self)
return apidoc.ui_for(request, self)

def default_endpoint(self, resource, namespace):
'''
Expand Down Expand Up @@ -444,7 +463,7 @@ def specs_url(self):
:rtype: str
'''
return url_for(self.endpoint('specs'), _external=True)
return self.app.url_for(self.endpoint('specs'), _external=True)

@property
def base_url(self):
Expand All @@ -453,7 +472,11 @@ def base_url(self):
:rtype: str
'''
return url_for(self.endpoint('root'), _external=True)
root_path = self.prefix or '/'
if self._doc == root_path:
return self.app.url_for(self.endpoint('doc'), _external=True)
return self.app.url_for(self.endpoint('root'), _external=True)


@property
def base_path(self):
Expand All @@ -462,9 +485,13 @@ def base_path(self):
:rtype: str
'''
return url_for(self.endpoint('root'))
root_path = self.prefix or '/'
if self._doc == root_path:
return self.app.url_for(self.endpoint('doc'))
return self.app.url_for(self.endpoint('root'))

@cached_property
#@cached_property
@property
def __schema__(self):
'''
The Swagger specifications/schema for this API
Expand Down Expand Up @@ -784,6 +811,23 @@ def get(self):
def mediatypes(self):
return ['application/json']

class named_route_fn(object):
__slots__ = ['__name', 'fn']

def __init__(self, name, fn):
self.__name = name
self.fn = fn

@property
def __name__(self):
return self.__name

@__name__.setter
def __name__(self, val):
self.__name = val

def __call__(self, *args, **kwargs):
return self.fn(*args, **kwargs)

def mask_parse_error_handler(error):
'''When a mask can't be parsed'''
Expand Down
47 changes: 36 additions & 11 deletions sanic_restplus/apidoc.py
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from flask import url_for, Blueprint, render_template

#from flask import url_for, Blueprint, render_template
from sanic import Blueprint
from sanic_jinja2 import SanicJinja2
from jinja2 import PackageLoader

class Apidoc(Blueprint):
'''
Expand All @@ -11,26 +13,49 @@ class Apidoc(Blueprint):
'''
def __init__(self, *args, **kwargs):
self.registered = False
self.app = None
super(Apidoc, self).__init__(*args, **kwargs)

def register(self, *args, **kwargs):
app = args[0]
self.app = app
super(Apidoc, self).register(*args, **kwargs)
self.registered = True

@property
def config(self):
if self.app:
return self.app.config
return {}

def url_for(self, *args, **kwargs):
return self.app.url_for(*args, **kwargs)


apidoc = Apidoc('restplus_doc', None)

apidoc = Apidoc('restplus_doc', __name__,
template_folder='templates',
static_folder='static',
static_url_path='/swaggerui',
)
loader = PackageLoader(__name__, 'templates')
j2 = SanicJinja2(apidoc, loader)


@apidoc.add_app_template_global
apidoc.static('/swaggerui', './sanic_restplus/static')

def swagger_static(filename):
return url_for('restplus_doc.static', filename=filename)
return url_for('restplus_doc.static', filename=filename

def config():
return apidoc.config

j2.add_env('swagger_static', swagger_static)
j2.add_env('config', swagger_static)

# @apidoc.add_app_template_global
# def swagger_static(filename):
# return url_for('restplus_doc.static',
# filename='bower/swagger-ui/dist/{0}'.format(filename))


def ui_for(api):
def ui_for(request, api):
'''Render a SwaggerUI for a given API'''
return render_template('swagger-ui.html', title=api.title,
return j2.render('swagger-ui.html', request, title=api.title,
specs_url=api.specs_url)
14 changes: 2 additions & 12 deletions sanic_restplus/errors.py
@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import flask

from werkzeug.exceptions import HTTPException
from sanic import exceptions

from ._http import HTTPStatus

Expand All @@ -27,15 +25,7 @@ def abort(code=HTTPStatus.INTERNAL_SERVER_ERROR, message=None, **kwargs):
:param kwargs: Any additional data to pass to the error payload
:raise HTTPException:
'''
try:
flask.abort(code)
except HTTPException as e:
if message:
kwargs['message'] = str(message)
if kwargs:
e.data = kwargs
raise

raise exceptions.SanicException(message=message, status_code=code)

class RestError(Exception):
'''Base class for all Flask-Restplus Errors'''
Expand Down
11 changes: 5 additions & 6 deletions sanic_restplus/representations.py
Expand Up @@ -6,12 +6,11 @@
except ImportError:
from json import dumps

from flask import make_response, current_app
from sanic.response import text


def output_json(data, code, headers=None):
def output_json(request, data, code, headers=None):
'''Makes a Flask response with a JSON encoded body'''

current_app = request.app
settings = current_app.config.get('RESTPLUS_JSON', {})

# If we're in debug mode, and the indent is not set, we set it to a
Expand All @@ -24,6 +23,6 @@ def output_json(data, code, headers=None):
# see https://github.com/mitsuhiko/flask/pull/1262
dumped = dumps(data, **settings) + "\n"

resp = make_response(dumped, code)
resp.headers.extend(headers or {})
resp = text(dumped, code, content_type='application/json')
resp.headers.update(headers or {})
return resp

0 comments on commit 9777004

Please sign in to comment.