Merge branch 'master' into convert_dict_to_set

This commit is contained in:
Eli Uriegas
2016-12-14 11:26:31 -06:00
committed by GitHub
23 changed files with 830 additions and 33 deletions

View File

@@ -1,6 +1,6 @@
from .sanic import Sanic
from .blueprints import Blueprint
__version__ = '0.1.7'
__version__ = '0.1.8'
__all__ = ['Sanic', 'Blueprint']

View File

@@ -91,6 +91,12 @@ class Blueprint:
return handler
return decorator
def add_route(self, handler, uri, methods=None):
"""
"""
self.record(lambda s: s.add_route(handler, uri, methods))
return handler
def listener(self, event):
"""
"""

View File

@@ -30,6 +30,14 @@ class FileNotFound(NotFound):
self.relative_url = relative_url
class RequestTimeout(SanicException):
status_code = 408
class PayloadTooLarge(SanicException):
status_code = 413
class Handler:
handlers = None

View File

@@ -4,6 +4,7 @@ from http.cookies import SimpleCookie
from httptools import parse_url
from urllib.parse import parse_qs
from ujson import loads as json_loads
from sanic.exceptions import InvalidUsage
from .log import log
@@ -67,7 +68,7 @@ class Request(dict):
try:
self.parsed_json = json_loads(self.body)
except Exception:
log.exception("failed when parsing body as json")
raise InvalidUsage("Failed when parsing body as json")
return self.parsed_json
@@ -89,7 +90,7 @@ class Request(dict):
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
except Exception:
log.exception("failed when parsing form")
log.exception("Failed when parsing form")
return self.parsed_form
@@ -114,9 +115,10 @@ class Request(dict):
@property
def cookies(self):
if self._cookies is None:
if 'Cookie' in self.headers:
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
if cookie is not None:
cookies = SimpleCookie()
cookies.load(self.headers['Cookie'])
cookies.load(cookie)
self._cookies = {name: cookie.value
for name, cookie in cookies.items()}
else:

View File

@@ -30,11 +30,17 @@ class Router:
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
def my_route(request, my_parameter):
do stuff...
or
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...])
def my_route_with_type(request, my_parameter):
do stuff...
Parameters will be passed as keyword arguments to the request handling
function provided Parameters can also have a type by appending :type to
the <parameter>. If no type is provided, a string is expected. A regular
expression can also be passed in as the type
function. Provided parameters can also have a type by appending :type to
the <parameter>. Given parameter must be able to be type-casted to this.
If no type is provided, a string is expected. A regular expression can
also be passed in as the type. The argument given to the function will
always be a string, independent of the type.
"""
routes_static = None
routes_dynamic = None

View File

@@ -60,6 +60,19 @@ class Sanic:
return response
def add_route(self, handler, uri, methods=None):
"""
A helper method to register class instance or
functions as a handler to the application url
routes.
:param handler: function or class instance
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: function or class instance
"""
self.route(uri=uri, methods=methods)(handler)
return handler
# Decorator
def exception(self, *exceptions):
"""
@@ -250,6 +263,7 @@ class Sanic:
'sock': sock,
'debug': debug,
'request_handler': self.handle_request,
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop

View File

@@ -4,7 +4,8 @@ from inspect import isawaitable
from multidict import CIMultiDict
from signal import SIGINT, SIGTERM
from time import time
import httptools
from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
try:
import uvloop as async_loop
@@ -13,6 +14,7 @@ except ImportError:
from .log import log
from .request import Request
from .exceptions import RequestTimeout, PayloadTooLarge
class Signal:
@@ -33,8 +35,9 @@ class HttpProtocol(asyncio.Protocol):
# connection management
'_total_request_size', '_timeout_handler', '_last_communication_time')
def __init__(self, *, loop, request_handler, signal=Signal(),
connections=set(), request_timeout=60,
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections={}, request_timeout=60,
request_max_size=None):
self.loop = loop
self.transport = None
@@ -45,11 +48,13 @@ class HttpProtocol(asyncio.Protocol):
self.signal = signal
self.connections = connections
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
self.request_max_size = request_max_size
self._total_request_size = 0
self._timeout_handler = None
self._last_request_time = None
self._request_handler_task = None
# -------------------------------------------- #
# Connection
@@ -75,7 +80,10 @@ class HttpProtocol(asyncio.Protocol):
self._timeout_handler = \
self.loop.call_later(time_left, self.connection_timeout)
else:
self.bail_out("Request timed out, connection closed")
if self._request_handler_task:
self._request_handler_task.cancel()
exception = RequestTimeout('Request Timeout')
self.write_error(exception)
# -------------------------------------------- #
# Parsing
@@ -86,20 +94,19 @@ class HttpProtocol(asyncio.Protocol):
# memory limits
self._total_request_size += len(data)
if self._total_request_size > self.request_max_size:
return self.bail_out(
"Request too large ({}), connection closed".format(
self._total_request_size))
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
# Create parser if this is the first time we're receiving data
if self.parser is None:
assert self.request is None
self.headers = []
self.parser = httptools.HttpRequestParser(self)
self.parser = HttpRequestParser(self)
# Parse request chunk or close connection
try:
self.parser.feed_data(data)
except httptools.parser.errors.HttpParserError as e:
except HttpParserError as e:
self.bail_out(
"Invalid request data, connection closed ({})".format(e))
@@ -108,8 +115,8 @@ class HttpProtocol(asyncio.Protocol):
def on_header(self, name, value):
if name == b'Content-Length' and int(value) > self.request_max_size:
return self.bail_out(
"Request body too large ({}), connection closed".format(value))
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
self.headers.append((name.decode(), value.decode('utf-8')))
@@ -132,7 +139,7 @@ class HttpProtocol(asyncio.Protocol):
self.request.body = body
def on_message_complete(self):
self.loop.create_task(
self._request_handler_task = self.loop.create_task(
self.request_handler(self.request, self.write_response))
# -------------------------------------------- #
@@ -156,6 +163,16 @@ class HttpProtocol(asyncio.Protocol):
self.bail_out(
"Writing response failed, connection closed {}".format(e))
def write_error(self, exception):
try:
response = self.error_handler.response(self.request, exception)
version = self.request.version if self.request else '1.1'
self.transport.write(response.output(version))
self.transport.close()
except Exception as e:
self.bail_out(
"Writing error failed, connection closed {}".format(e))
def bail_out(self, message):
log.debug(message)
self.transport.close()
@@ -165,6 +182,7 @@ class HttpProtocol(asyncio.Protocol):
self.request = None
self.url = None
self.headers = None
self._request_handler_task = None
self._total_request_size = 0
def close_if_idle(self):
@@ -204,8 +222,8 @@ def trigger_events(events, loop):
loop.run_until_complete(result)
def serve(host, port, request_handler, before_start=None, after_start=None,
before_stop=None, after_stop=None,
def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None,
debug=False, request_timeout=60, sock=None,
request_max_size=None, reuse_port=False, loop=None):
"""
@@ -235,14 +253,24 @@ def serve(host, port, request_handler, before_start=None, after_start=None,
connections = set()
signal = Signal()
server_coroutine = loop.create_server(lambda: HttpProtocol(
server = partial(
HttpProtocol,
loop=loop,
connections=connections,
signal=signal,
request_handler=request_handler,
error_handler=error_handler,
request_timeout=request_timeout,
request_max_size=request_max_size,
), host, port, reuse_port=reuse_port, sock=sock)
)
server_coroutine = loop.create_server(
server,
host,
port,
reuse_port=reuse_port,
sock=sock
)
# Instead of pulling time at the end of every request,
# pull it once per minute

View File

@@ -16,7 +16,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
loop=None, *request_args, **request_kwargs):
loop=None, debug=False, *request_args,
**request_kwargs):
results = []
exceptions = []
@@ -34,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
exceptions.append(e)
app.stop()
app.run(host=HOST, port=42101, after_start=_collect_response, loop=loop)
app.run(host=HOST, debug=debug, port=42101,
after_start=_collect_response, loop=loop)
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
@@ -45,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
return request, response
except:
raise ValueError(
"request and response object expected, got ({})".format(
"Request and response object expected, got ({})".format(
results))
else:
try:
return results[0]
except:
raise ValueError(
"request object expected, got ({})".format(results))
"Request object expected, got ({})".format(results))

39
sanic/views.py Normal file
View File

@@ -0,0 +1,39 @@
from .exceptions import InvalidUsage
class HTTPMethodView:
""" Simple class based implementation of view for the sanic.
You should implement methods (get, post, put, patch, delete) for the class
to every HTTP method you want to support.
For example:
class DummyView(View):
def get(self, request, *args, **kwargs):
return text('I am get method')
def put(self, request, *args, **kwargs):
return text('I am put method')
etc.
If someone tries to use a non-implemented method, there will be a
405 response.
If you need any url params just mention them in method definition:
class DummyView(View):
def get(self, request, my_param_here, *args, **kwargs):
return text('I am get method with %s' % my_param_here)
To add the view into the routing you could use
1) app.add_route(DummyView(), '/')
2) app.route('/')(DummyView())
"""
def __call__(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
if handler:
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)