Merge branch 'master' into improved_config

This commit is contained in:
Tim Mundt
2017-01-13 12:34:56 +01:00
29 changed files with 870 additions and 145 deletions

View File

@@ -20,7 +20,7 @@ if __name__ == "__main__":
module = import_module(module_name)
app = getattr(module, app_name, None)
if type(app) is not Sanic:
if not isinstance(app, Sanic):
raise ValueError("Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?"
.format(type(app).__name__, args.module))

View File

@@ -18,14 +18,17 @@ class BlueprintSetup:
#: blueprint.
self.url_prefix = url_prefix
def add_route(self, handler, uri, methods):
def add_route(self, handler, uri, methods, host=None):
"""
A helper method to register a handler to the application url routes.
"""
if self.url_prefix:
uri = self.url_prefix + uri
self.app.route(uri=uri, methods=methods)(handler)
if host is None:
host = self.blueprint.host
self.app.route(uri=uri, methods=methods, host=host)(handler)
def add_exception(self, handler, *args, **kwargs):
"""
@@ -53,7 +56,7 @@ class BlueprintSetup:
class Blueprint:
def __init__(self, name, url_prefix=None):
def __init__(self, name, url_prefix=None, host=None):
"""
Creates a new blueprint
:param name: Unique name of the blueprint
@@ -63,6 +66,7 @@ class Blueprint:
self.url_prefix = url_prefix
self.deferred_functions = []
self.listeners = defaultdict(list)
self.host = host
def record(self, func):
"""
@@ -83,18 +87,18 @@ class Blueprint:
for deferred in self.deferred_functions:
deferred(state)
def route(self, uri, methods=None):
def route(self, uri, methods=None, host=None):
"""
"""
def decorator(handler):
self.record(lambda s: s.add_route(handler, uri, methods))
self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler
return decorator
def add_route(self, handler, uri, methods=None):
def add_route(self, handler, uri, methods=None, host=None):
"""
"""
self.record(lambda s: s.add_route(handler, uri, methods))
self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler
def listener(self, event):

View File

@@ -1,4 +1,5 @@
from .response import text
from .log import log
from traceback import format_exc
@@ -56,18 +57,31 @@ class Handler:
:return: Response object
"""
handler = self.handlers.get(type(exception), self.default)
response = handler(request=request, exception=exception)
try:
response = handler(request=request, exception=exception)
except:
if self.sanic.debug:
response_message = (
'Exception raised in exception handler "{}" '
'for uri: "{}"\n{}').format(
handler.__name__, request.url, format_exc())
log.error(response_message)
return text(response_message, 500)
else:
return text('An error occurred while handling an error', 500)
return response
def default(self, request, exception):
if issubclass(type(exception), SanicException):
return text(
"Error: {}".format(exception),
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500))
elif self.sanic.debug:
return text(
"Error: {}\nException: {}".format(
exception, format_exc()), status=500)
response_message = (
'Exception occurred while handling uri: "{}"\n{}'.format(
request.url, format_exc()))
log.error(response_message)
return text(response_message, status=500)
else:
return text(
"An error occurred while generating the request", status=500)
'An error occurred while generating the response', status=500)

View File

@@ -25,6 +25,9 @@ class RequestParameters(dict):
self.super = super()
self.super.__init__(*args, **kwargs)
def __getitem__(self, name):
return self.get(name)
def get(self, name, default=None):
values = self.super.get(name)
return values[0] if values else default
@@ -64,7 +67,7 @@ class Request(dict):
@property
def json(self):
if not self.parsed_json:
if self.parsed_json is None:
try:
self.parsed_json = json_loads(self.body)
except Exception:
@@ -72,6 +75,17 @@ class Request(dict):
return self.parsed_json
@property
def token(self):
"""
Attempts to return the auth header token.
:return: token related to request
"""
auth_header = self.headers.get('Authorization')
if auth_header is not None:
return auth_header.split()[1]
return auth_header
@property
def form(self):
if self.parsed_form is None:

View File

@@ -103,10 +103,14 @@ class HTTPResponse:
headers = b''
if self.headers:
headers = b''.join(
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
for name, value in self.headers.items()
)
for name, value in self.headers.items():
try:
headers += (
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
except AttributeError:
headers += (
b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
# Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all

View File

@@ -24,16 +24,20 @@ class RouteExists(Exception):
pass
class RouteDoesNotExist(Exception):
pass
class Router:
"""
Router supports basic routing with parameters and method checks
Usage:
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
def my_route(request, my_parameter):
@app.route('/my_url/<my_param>', methods=['GET', 'POST', ...])
def my_route(request, my_param):
do stuff...
or
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...])
def my_route_with_type(request, my_parameter):
@app.route('/my_url/<my_param:my_type>', methods=['GET', 'POST', ...])
def my_route_with_type(request, my_param: my_type):
do stuff...
Parameters will be passed as keyword arguments to the request handling
@@ -52,8 +56,9 @@ class Router:
self.routes_static = {}
self.routes_dynamic = defaultdict(list)
self.routes_always_check = []
self.hosts = None
def add(self, uri, methods, handler):
def add(self, uri, methods, handler, host=None):
"""
Adds a handler to the route list
:param uri: Path to match
@@ -63,6 +68,17 @@ class Router:
When executed, it should provide a response object.
:return: Nothing
"""
if host is not None:
# we want to track if there are any
# vhosts on the Router instance so that we can
# default to the behavior without vhosts
if self.hosts is None:
self.hosts = set(host)
else:
self.hosts.add(host)
uri = host + uri
if uri in self.routes_all:
raise RouteExists("Route already registered: {}".format(uri))
@@ -110,6 +126,25 @@ class Router:
else:
self.routes_static[uri] = route
def remove(self, uri, clean_cache=True, host=None):
if host is not None:
uri = host + uri
try:
route = self.routes_all.pop(uri)
except KeyError:
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
if route in self.routes_always_check:
self.routes_always_check.remove(route)
elif url_hash(uri) in self.routes_dynamic \
and route in self.routes_dynamic[url_hash(uri)]:
self.routes_dynamic[url_hash(uri)].remove(route)
else:
self.routes_static.pop(uri)
if clean_cache:
self._get.cache_clear()
def get(self, request):
"""
Gets a request handler based on the URL of the request, or raises an
@@ -117,10 +152,14 @@ class Router:
:param request: Request object
:return: handler, arguments, keyword arguments
"""
return self._get(request.url, request.method)
if self.hosts is None:
return self._get(request.url, request.method, '')
else:
return self._get(request.url, request.method,
request.headers.get("Host", ''))
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method):
def _get(self, url, method, host):
"""
Gets a request handler based on the URL of the request, or raises an
error. Internal method for caching.
@@ -128,6 +167,7 @@ class Router:
:param method: Request method
:return: handler, arguments, keyword arguments
"""
url = host + url
# Check against known static routes
route = self.routes_static.get(url)
if route:

View File

@@ -4,7 +4,6 @@ from functools import partial
from inspect import isawaitable, stack, getmodulename
from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT
from time import sleep
from traceback import format_exc
import logging
@@ -13,9 +12,11 @@ from .exceptions import Handler
from .log import log
from .response import HTTPResponse
from .router import Router
from .server import serve
from .server import serve, HttpProtocol
from .static import register as static_register
from .exceptions import ServerError
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from os import set_inheritable
class Sanic:
@@ -39,6 +40,8 @@ class Sanic:
self._blueprint_order = []
self.loop = None
self.debug = None
self.sock = None
self.processes = None
# Register alternative method names
self.go_fast = self.run
@@ -48,7 +51,7 @@ class Sanic:
# -------------------------------------------------------------------- #
# Decorator
def route(self, uri, methods=None):
def route(self, uri, methods=None, host=None):
"""
Decorates a function to be registered as a route
:param uri: path of the URL
@@ -62,12 +65,13 @@ class Sanic:
uri = '/' + uri
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler)
self.router.add(uri=uri, methods=methods, handler=handler,
host=host)
return handler
return response
def add_route(self, handler, uri, methods=None):
def add_route(self, handler, uri, methods=None, host=None):
"""
A helper method to register class instance or
functions as a handler to the application url
@@ -77,9 +81,12 @@ class Sanic:
:param methods: list or tuple of methods allowed
:return: function or class instance
"""
self.route(uri=uri, methods=methods)(handler)
self.route(uri=uri, methods=methods, host=host)(handler)
return handler
def remove_route(self, uri, clean_cache=True, host=None):
self.router.remove(uri, clean_cache, host)
# Decorator
def exception(self, *exceptions):
"""
@@ -239,25 +246,27 @@ class Sanic:
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, sock=None,
workers=1, loop=None):
workers=1, loop=None, protocol=HttpProtocol, backlog=100,
stop_event=None):
"""
Runs the HTTP Server and listens until keyboard interrupt or term
signal. On termination, drains connections before closing.
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param before_start: Function to be executed before the server starts
:param before_start: Functions to be executed before the server starts
accepting connections
:param after_start: Function to be executed after the server starts
:param after_start: Functions to be executed after the server starts
accepting connections
:param before_stop: Function to be executed when a stop signal is
:param before_stop: Functions to be executed when a stop signal is
received before it is respected
:param after_stop: Function to be executed when all requests are
:param after_stop: Functions to be executed when all requests are
complete
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
self.error_handler.debug = True
@@ -265,6 +274,7 @@ class Sanic:
self.loop = loop
server_settings = {
'protocol': protocol,
'host': host,
'port': port,
'sock': sock,
@@ -273,7 +283,8 @@ class Sanic:
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop
'loop': loop,
'backlog': backlog
}
# -------------------------------------------- #
@@ -290,7 +301,7 @@ class Sanic:
for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name]
if args:
if type(args) is not list:
if callable(args):
args = [args]
listeners += args
if reverse:
@@ -312,7 +323,7 @@ class Sanic:
else:
log.info('Spinning up {} workers...'.format(workers))
self.serve_multiple(server_settings, workers)
self.serve_multiple(server_settings, workers, stop_event)
except Exception as e:
log.exception(
@@ -324,10 +335,13 @@ class Sanic:
"""
This kills the Sanic
"""
if self.processes is not None:
for process in self.processes:
process.terminate()
self.sock.close()
get_event_loop().stop()
@staticmethod
def serve_multiple(server_settings, workers, stop_event=None):
def serve_multiple(self, server_settings, workers, stop_event=None):
"""
Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete.
@@ -339,26 +353,28 @@ class Sanic:
server_settings['reuse_port'] = True
# Create a stop event to be triggered by a signal
if not stop_event:
if stop_event is None:
stop_event = Event()
signal(SIGINT, lambda s, f: stop_event.set())
signal(SIGTERM, lambda s, f: stop_event.set())
processes = []
self.sock = socket()
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.sock.bind((server_settings['host'], server_settings['port']))
set_inheritable(self.sock.fileno(), True)
server_settings['sock'] = self.sock
server_settings['host'] = None
server_settings['port'] = None
self.processes = []
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start()
processes.append(process)
self.processes.append(process)
# Infinitely wait for the stop event
try:
while not stop_event.is_set():
sleep(0.3)
except:
pass
log.info('Spinning down workers...')
for process in processes:
process.terminate()
for process in processes:
for process in self.processes:
process.join()
# the above processes will block this until they're stopped
self.stop()

View File

@@ -224,24 +224,30 @@ def trigger_events(events, loop):
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):
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, protocol=HttpProtocol, backlog=100):
"""
Starts asynchronous HTTP Server on an individual process.
:param host: Address to host on
:param port: Port to host on
:param request_handler: Sanic request handler with middleware
:param error_handler: Sanic error handler with middleware
:param before_start: Function to be executed before the server starts
listening. Takes single argument `loop`
:param after_start: Function to be executed after the server starts
listening. Takes single argument `loop`
:param before_stop: Function to be executed when a stop signal is
received before it is respected. Takes single argumenet `loop`
:param after_stop: Function to be executed when a stop signal is
received after it is respected. Takes single argumenet `loop`
:param debug: Enables debug output (slows server)
:param request_timeout: time in seconds
:param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit
:param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
loop = loop or async_loop.new_event_loop()
@@ -255,7 +261,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
connections = set()
signal = Signal()
server = partial(
HttpProtocol,
protocol,
loop=loop,
connections=connections,
signal=signal,
@@ -270,7 +276,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
host,
port,
reuse_port=reuse_port,
sock=sock
sock=sock,
backlog=backlog
)
# Instead of pulling time at the end of every request,

View File

@@ -16,15 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
loop=None, debug=False, *request_args,
**request_kwargs):
loop=None, debug=False, server_kwargs={},
*request_args, **request_kwargs):
results = []
exceptions = []
if gather_request:
@app.middleware
def _collect_request(request):
results.append(request)
app.request_middleware.appendleft(_collect_request)
async def _collect_response(sanic, loop):
try:
@@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
exceptions.append(e)
app.stop()
app.run(host=HOST, debug=debug, port=42101,
after_start=_collect_response, loop=loop)
app.run(host=HOST, debug=debug, port=PORT,
after_start=_collect_response, loop=loop, **server_kwargs)
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))

View File

@@ -7,7 +7,7 @@ class HTTPMethodView:
to every HTTP method you want to support.
For example:
class DummyView(View):
class DummyView(HTTPMethodView):
def get(self, request, *args, **kwargs):
return text('I am get method')
@@ -20,20 +20,44 @@ class HTTPMethodView:
405 response.
If you need any url params just mention them in method definition:
class DummyView(View):
class DummyView(HTTPMethodView):
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())
1) app.add_route(DummyView.as_view(), '/')
2) app.route('/')(DummyView.as_view())
To add any decorator you could set it into decorators variable
"""
def __call__(self, request, *args, **kwargs):
decorators = []
def dispatch_request(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)
@classmethod
def as_view(cls, *class_args, **class_kwargs):
""" Converts the class into an actual view function that can be used
with the routing system.
"""
def view(*args, **kwargs):
self = view.view_class(*class_args, **class_kwargs)
return self.dispatch_request(*args, **kwargs)
if cls.decorators:
view.__module__ = cls.__module__
for decorator in cls.decorators:
view = decorator(view)
view.view_class = cls
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
return view