Merge branch 'master' into add_register_sys_signals_flag

This commit is contained in:
Eli Uriegas
2017-01-21 10:25:57 -06:00
committed by GitHub
52 changed files with 2350 additions and 469 deletions

View File

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

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

@@ -3,6 +3,7 @@ from collections import defaultdict
class BlueprintSetup:
"""
Creates a blueprint state like object.
"""
def __init__(self, blueprint, app, options):
@@ -18,24 +19,27 @@ 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):
"""
Registers exceptions to sanic
Registers exceptions to sanic.
"""
self.app.exception(*args, **kwargs)(handler)
def add_static(self, uri, file_or_directory, *args, **kwargs):
"""
Registers static files to sanic
Registers static files to sanic.
"""
if self.url_prefix:
uri = self.url_prefix + uri
@@ -44,7 +48,7 @@ class BlueprintSetup:
def add_middleware(self, middleware, *args, **kwargs):
"""
Registers middleware to sanic
Registers middleware to sanic.
"""
if args or kwargs:
self.app.middleware(*args, **kwargs)(middleware)
@@ -53,7 +57,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 +67,7 @@ class Blueprint:
self.url_prefix = url_prefix
self.deferred_functions = []
self.listeners = defaultdict(list)
self.host = host
def record(self, func):
"""
@@ -73,32 +78,43 @@ class Blueprint:
def make_setup_state(self, app, options):
"""
Returns a new BlueprintSetup object
"""
return BlueprintSetup(self, app, options)
def register(self, app, options):
"""
Registers the blueprint to the sanic app.
"""
state = self.make_setup_state(app, options)
for deferred in self.deferred_functions:
deferred(state)
def route(self, uri, methods=None):
def route(self, uri, methods=frozenset({'GET'}), host=None):
"""
Creates a blueprint route from a decorated function.
:param uri: Endpoint at which the route will be accessible.
:param methods: List of acceptable HTTP methods.
"""
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):
"""
Creates a blueprint route from a function.
:param handler: Function to handle uri request.
:param uri: Endpoint at which the route will be accessible.
:param methods: List of acceptable HTTP methods.
"""
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):
"""
Create a listener from a decorated function.
:param event: Event to listen to.
"""
def decorator(listener):
self.listeners[event].append(listener)
@@ -107,6 +123,7 @@ class Blueprint:
def middleware(self, *args, **kwargs):
"""
Creates a blueprint middleware from a decorated function.
"""
def register_middleware(middleware):
self.record(
@@ -123,6 +140,7 @@ class Blueprint:
def exception(self, *args, **kwargs):
"""
Creates a blueprint exception from a decorated function.
"""
def decorator(handler):
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
@@ -131,6 +149,9 @@ class Blueprint:
def static(self, uri, file_or_directory, *args, **kwargs):
"""
Creates a blueprint static route from a decorated function.
:param uri: Endpoint at which the route will be accessible.
:param file_or_directory: Static asset.
"""
self.record(
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))

View File

@@ -1,5 +1,104 @@
from .response import text
from traceback import format_exc
from .response import text, html
from .log import log
from traceback import format_exc, extract_tb
import sys
TRACEBACK_STYLE = '''
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
}
p {
margin: 0;
}
.summary {
padding: 10px;
}
h1 {
margin-bottom: 0;
}
h3 {
margin-top: 10px;
}
h3 code {
font-size: 24px;
}
.frame-line > * {
padding: 5px 10px;
}
.frame-line {
margin-bottom: 5px;
}
.frame-code {
font-size: 16px;
padding-left: 30px;
}
.tb-wrapper {
border: 1px solid #f3f3f3;
}
.tb-header {
background-color: #f3f3f3;
padding: 5px 10px;
}
.frame-descriptor {
background-color: #e2eafb;
}
.frame-descriptor {
font-size: 14px;
}
</style>
'''
TRACEBACK_WRAPPER_HTML = '''
<html>
<head>
{style}
</head>
<body>
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
<p class="summary">
<b>{exc_name}: {exc_value}</b>
while handling uri <code>{uri}</code>
</p>
</div>
</body>
</html>
'''
TRACEBACK_LINE_HTML = '''
<div class="frame-line">
<p class="frame-descriptor">
File {0.filename}, line <i>{0.lineno}</i>,
in <code><b>{0.name}</b></code>
</p>
<p class="frame-code"><code>{0.line}</code></p>
</div>
'''
INTERNAL_SERVER_ERROR_HTML = '''
<h1>Internal Server Error</h1>
<p>
The server encountered an internal error and cannot complete
your request.
</p>
'''
class SanicException(Exception):
@@ -45,29 +144,61 @@ class Handler:
self.handlers = {}
self.sanic = sanic
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
frames = extract_tb(tb)
frame_html = []
for frame in frames:
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
return TRACEBACK_WRAPPER_HTML.format(
style=TRACEBACK_STYLE,
exc_name=exc_type.__name__,
exc_value=exc_value,
frame_html=''.join(frame_html),
uri=request.url)
def add(self, exception, handler):
self.handlers[exception] = handler
def response(self, request, exception):
"""
Fetches and executes an exception handler and returns a response object
:param request: Request
:param exception: Exception to handle
: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:
log.error(format_exc())
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):
log.error(format_exc())
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)
html_output = self._render_traceback_html(exception, request)
response_message = (
'Exception occurred while handling uri: "{}"\n{}'.format(
request.url, format_exc()))
log.error(response_message)
return html(html_output, status=500)
else:
return text(
"An error occurred while generating the request", status=500)
return html(INTERNAL_SERVER_ERROR_HTML, status=500)

View File

@@ -1,3 +1,3 @@
import logging
log = logging.getLogger(__name__)
log = logging.getLogger('sanic')

View File

@@ -21,16 +21,13 @@ class RequestParameters(dict):
value of the list and getlist returns the whole shebang
"""
def __init__(self, *args, **kwargs):
self.super = super()
self.super.__init__(*args, **kwargs)
def get(self, name, default=None):
values = self.super.get(name)
return values[0] if values else default
"""Return the first value, either the default or actual"""
return super().get(name, [default])[0]
def getlist(self, name, default=None):
return self.super.get(name, default)
"""Return the entire list"""
return super().get(name, default)
class Request(dict):
@@ -38,18 +35,20 @@ class Request(dict):
Properties of an HTTP request such as URL, headers, etc.
"""
__slots__ = (
'url', 'headers', 'version', 'method', '_cookies',
'url', 'headers', 'version', 'method', '_cookies', 'transport',
'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip',
)
def __init__(self, url_bytes, headers, version, method):
def __init__(self, url_bytes, headers, version, method, transport):
# TODO: Content-Encoding detection
url_parsed = parse_url(url_bytes)
self.url = url_parsed.path.decode('utf-8')
self.headers = headers
self.version = version
self.method = method
self.transport = transport
self.query_string = None
if url_parsed.query:
self.query_string = url_parsed.query.decode('utf-8')
@@ -64,7 +63,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 +71,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:
@@ -125,6 +135,12 @@ class Request(dict):
self._cookies = {}
return self._cookies
@property
def ip(self):
if not hasattr(self, '_ip'):
self._ip = self.transport.get_extra_info('peername')
return self._ip
File = namedtuple('File', ['type', 'body', 'name'])
@@ -132,6 +148,7 @@ File = namedtuple('File', ['type', 'body', 'name'])
def parse_multipart_form(body, boundary):
"""
Parses a request body and returns fields and files
:param body: Bytes request body
:param boundary: Bytes multipart boundary
:return: fields (RequestParameters), files (RequestParameters)

View File

@@ -83,10 +83,10 @@ class HTTPResponse:
if body is not None:
try:
# Try to encode it regularly
self.body = body.encode('utf-8')
self.body = body.encode()
except AttributeError:
# Convert it to a str if you can't
self.body = str(body).encode('utf-8')
self.body = str(body).encode()
else:
self.body = body_bytes
@@ -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
@@ -139,21 +143,45 @@ class HTTPResponse:
def json(body, status=200, headers=None):
"""
Returns response object with body in json format.
:param body: Response data to be serialized.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(json_dumps(body), headers=headers, status=status,
content_type="application/json")
def text(body, status=200, headers=None):
"""
Returns response object with body in text format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers,
content_type="text/plain; charset=utf-8")
def html(body, status=200, headers=None):
"""
Returns response object with body in html format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None):
"""
Returns response object with file data.
:param location: Location of file on system.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
"""
filename = path.split(location)[-1]
async with open_async(location, mode='rb') as _file:
@@ -165,3 +193,26 @@ async def file(location, mime_type=None, headers=None):
headers=headers,
content_type=mime_type,
body_bytes=out_stream)
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
"""
Aborts execution and causes a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type:
the content type (string) of the response
:returns: the redirecting Response
"""
headers = headers or {}
# According to RFC 7231, a relative URI is now permitted.
headers['Location'] = to
return HTTPResponse(
status=status,
headers=headers,
content_type=content_type)

View File

@@ -3,6 +3,7 @@ from collections import defaultdict, namedtuple
from functools import lru_cache
from .config import Config
from .exceptions import NotFound, InvalidUsage
from .views import CompositionView
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
@@ -23,16 +24,28 @@ 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):
.. code-block:: python
@sanic.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):
.. code-block:: python
@sanic.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
@@ -51,19 +64,36 @@ 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
:param methods: Array of accepted method names.
If none are provided, any method is allowed
If none are provided, any method is allowed
:param handler: Request handler function.
When executed, it should provide a response object.
When executed, it should provide a response object.
:return: Nothing
"""
if uri in self.routes_all:
raise RouteExists("Route already registered: {}".format(uri))
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:
if isinstance(host, list):
host = set(host)
self.hosts.add(host)
if isinstance(host, str):
uri = host + uri
else:
for h in host:
self.add(uri, methods, handler, h)
return
# Dict for faster lookups of if method allowed
if methods:
@@ -97,9 +127,35 @@ class Router:
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
pattern = re.compile(r'^{}$'.format(pattern_string))
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters)
def merge_route(route, methods, handler):
# merge to the existing route when possible.
if not route.methods or not methods:
# method-unspecified routes are not mergeable.
raise RouteExists(
"Route already registered: {}".format(uri))
elif route.methods.intersection(methods):
# already existing method is not overloadable.
duplicated = methods.intersection(route.methods)
raise RouteExists(
"Route already registered: {} [{}]".format(
uri, ','.join(list(duplicated))))
if isinstance(route.handler, CompositionView):
view = route.handler
else:
view = CompositionView()
view.add(route.methods, route.handler)
view.add(methods, handler)
route = route._replace(
handler=view, methods=methods.union(route.methods))
return route
route = self.routes_all.get(uri)
if route:
route = merge_route(route, methods, handler)
else:
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters)
self.routes_all[uri] = route
if properties['unhashable']:
@@ -109,17 +165,41 @@ 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
error
: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=Config.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.
@@ -127,6 +207,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,19 +12,25 @@ 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:
def __init__(self, name=None, router=None,
error_handler=None, logger=None):
if logger is None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s: %(levelname)s: %(message)s"
)
error_handler=None):
# Only set up a default log handler if the
# end-user application didn't set anything up.
if not logging.root.handlers and log.level == logging.NOTSET:
formatter = logging.Formatter(
"%(asctime)s: %(levelname)s: %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)
log.addHandler(handler)
log.setLevel(logging.INFO)
if name is None:
frame_records = stack()[1]
name = getmodulename(frame_records[1])
@@ -39,6 +44,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,9 +55,10 @@ class Sanic:
# -------------------------------------------------------------------- #
# Decorator
def route(self, uri, methods=None):
def route(self, uri, methods=frozenset({'GET'}), host=None):
"""
Decorates a function to be registered as a route
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: decorated function
@@ -62,29 +70,54 @@ 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):
# Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=["GET"], host=host)
def post(self, uri, host=None):
return self.route(uri, methods=["POST"], host=host)
def put(self, uri, host=None):
return self.route(uri, methods=["PUT"], host=host)
def head(self, uri, host=None):
return self.route(uri, methods=["HEAD"], host=host)
def options(self, uri, host=None):
return self.route(uri, methods=["OPTIONS"], host=host)
def patch(self, uri, host=None):
return self.route(uri, methods=["PATCH"], host=host)
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
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)
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):
"""
Decorates a function to be registered as a handler for exceptions
:param *exceptions: exceptions
:param \*exceptions: exceptions
:return: decorated function
"""
@@ -130,6 +163,7 @@ class Sanic:
def blueprint(self, blueprint, **options):
"""
Registers a blueprint on the application.
:param blueprint: Blueprint object
:param options: option dictionary with blueprint defaults
:return: Nothing
@@ -147,7 +181,8 @@ class Sanic:
def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0
log.warning("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method instead")
"version 1.0. Please use the blueprint method instead",
DeprecationWarning)
return self.blueprint(*args, **kwargs)
# -------------------------------------------------------------------- #
@@ -162,9 +197,10 @@ class Sanic:
Takes a request from the HTTP Server and returns a response object to
be sent back The HTTP Server only expects a response object, so
exception handling must be done here
:param request: HTTP Request object
:param response_callback: Response function to be called with the
response as the only argument
response as the only argument
:return: Nothing
"""
try:
@@ -229,7 +265,7 @@ class Sanic:
e, format_exc()))
else:
response = HTTPResponse(
"An error occured while handling an error")
"An error occurred while handling an error")
response_callback(response)
@@ -238,26 +274,30 @@ 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, register_sys_signals=True):
after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None, register_sys_signals=True):
"""
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
accepting connections
:param after_start: Function to be executed after the server starts
accepting connections
:param before_stop: Function to be executed when a stop signal is
received before it is respected
:param after_stop: Function to be executed when all requests are
complete
:param before_start: Functions to be executed before the server starts
accepting connections
:param after_start: Functions to be executed after the server starts
accepting connections
:param before_stop: Functions to be executed when a stop signal is
received before it is respected
:param after_stop: Functions to be executed when all requests are
complete
:param ssl: SSLContext for SSL encryption of worker(s)
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
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,16 +305,19 @@ class Sanic:
self.loop = loop
server_settings = {
'protocol': protocol,
'host': host,
'port': port,
'sock': sock,
'ssl': ssl,
'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,
'register_sys_signals': register_sys_signals
'register_sys_signals': register_sys_signals,
'backlog': backlog
}
# -------------------------------------------- #
@@ -291,7 +334,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:
@@ -305,7 +348,11 @@ class Sanic:
log.debug(self.config.LOGO)
# Serve
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
if ssl is None:
proto = "http"
else:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
try:
if workers == 1:
@@ -313,7 +360,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(
@@ -325,13 +372,17 @@ 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.
:param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch
:param stop_event: if provided, is used as a stop signal
@@ -340,26 +391,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

@@ -1,7 +1,7 @@
import asyncio
import traceback
from functools import partial
from inspect import isawaitable
from multidict import CIMultiDict
from signal import SIGINT, SIGTERM
from time import time
from httptools import HttpRequestParser
@@ -18,11 +18,30 @@ from .request import Request
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
current_time = None
class Signal:
stopped = False
current_time = None
class CIDict(dict):
"""
Case Insensitive dict where all keys are converted to lowercase
This does not maintain the inputted case when calling items() or keys()
in favor of speed, since headers are case insensitive
"""
def get(self, key, default=None):
return super().get(key.casefold(), default)
def __getitem__(self, key):
return super().__getitem__(key.casefold())
def __setitem__(self, key, value):
return super().__setitem__(key.casefold(), value)
def __contains__(self, key):
return super().__contains__(key.casefold())
class HttpProtocol(asyncio.Protocol):
@@ -70,15 +89,14 @@ class HttpProtocol(asyncio.Protocol):
def connection_lost(self, exc):
self.connections.discard(self)
self._timeout_handler.cancel()
self.cleanup()
def connection_timeout(self):
# Check if
time_elapsed = current_time - self._last_request_time
if time_elapsed < self.request_timeout:
time_left = self.request_timeout - time_elapsed
self._timeout_handler = \
self.loop.call_later(time_left, self.connection_timeout)
self._timeout_handler = (
self.loop.call_later(time_left, self.connection_timeout))
else:
if self._request_handler_task:
self._request_handler_task.cancel()
@@ -118,18 +136,15 @@ class HttpProtocol(asyncio.Protocol):
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
self.headers.append((name.decode(), value.decode('utf-8')))
self.headers.append((name.decode().casefold(), value.decode()))
def on_headers_complete(self):
remote_addr = self.transport.get_extra_info('peername')
if remote_addr:
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
self.request = Request(
url_bytes=self.url,
headers=CIMultiDict(self.headers),
headers=CIDict(self.headers),
version=self.parser.get_http_version(),
method=self.parser.get_method().decode()
method=self.parser.get_method().decode(),
transport=self.transport
)
def on_body(self, body):
@@ -148,35 +163,54 @@ class HttpProtocol(asyncio.Protocol):
def write_response(self, response):
try:
keep_alive = self.parser.should_keep_alive() \
and not self.signal.stopped
keep_alive = (
self.parser.should_keep_alive() and not self.signal.stopped)
self.transport.write(
response.output(
self.request.version, keep_alive, self.request_timeout))
except RuntimeError:
log.error(
'Connection lost before response written @ {}'.format(
self.request.ip))
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(e))
finally:
if not keep_alive:
self.transport.close()
else:
# Record that we received data
self._last_request_time = current_time
self.cleanup()
except Exception as e:
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 RuntimeError:
log.error(
'Connection lost before error written @ {}'.format(
self.request.ip))
except Exception as e:
self.bail_out(
"Writing error failed, connection closed {}".format(e))
"Writing error failed, connection closed {}".format(e),
from_error=True)
finally:
self.transport.close()
def bail_out(self, message):
exception = ServerError(message)
self.write_error(exception)
log.error(message)
def bail_out(self, message, from_error=False):
if from_error and self.transport.is_closing():
log.error(
("Transport closed @ {} and exception "
"experienced during error handling").format(
self.transport.get_extra_info('peername')))
log.debug(
'Exception:\n{}'.format(traceback.format_exc()))
else:
exception = ServerError(message)
self.write_error(exception)
log.error(message)
def cleanup(self):
self.parser = None
@@ -201,6 +235,7 @@ def update_current_time(loop):
"""
Caches the current time, since it is needed
at the end of every keep-alive request to update the request timeout time
:param loop:
:return:
"""
@@ -224,25 +259,35 @@ 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, ssl=None, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True):
"""
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`
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`
received before it is respected. Takes single
argument `loop`
:param after_stop: Function to be executed when a stop signal is
received after it is respected. Takes single
argument `loop`
:param debug: Enables debug output (slows server)
:param request_timeout: time in seconds
:param ssl: SSLContext
: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()
@@ -256,7 +301,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,8 +315,10 @@ def serve(host, port, request_handler, error_handler, before_start=None,
server,
host,
port,
ssl=ssl,
reuse_port=reuse_port,
sock=sock
sock=sock,
backlog=backlog
)
# Instead of pulling time at the end of every request,

View File

@@ -15,12 +15,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
"""
Registers a static directory handler with Sanic by adding a route to the
router and registering a handler.
:param app: Sanic
:param file_or_directory: File or directory path to serve from
:param uri: URL to serve from
:param pattern: regular expression used to match files in the URL
:param use_modified_since: If true, send file modified time, and return
not modified if the browser's matches the server's
not modified if the browser's matches the
server's
"""
# If we're not trying to match a file directly,

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,33 +7,96 @@ class HTTPMethodView:
to every HTTP method you want to support.
For example:
class DummyView(View):
.. code-block:: python
class DummyView(HTTPMethodView):
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):
.. code-block:: python
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
class CompositionView:
""" Simple method-function mapped view for the sanic.
You can add handler functions to methods (get, post, put, patch, delete)
for every HTTP method you want to support.
For example:
view = CompositionView()
view.add(['GET'], lambda request: text('I am get method'))
view.add(['POST', 'PUT'], lambda request: text('I am post/put method'))
etc.
If someone tries to use a non-implemented method, there will be a
405 response.
"""
def __init__(self):
self.handlers = {}
def add(self, methods, handler):
for method in methods:
if method in self.handlers:
raise KeyError(
'Method {} already is registered.'.format(method))
self.handlers[method] = handler
def __call__(self, request, *args, **kwargs):
handler = self.handlers.get(request.method.upper(), None)
if handler is None:
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
return handler(request, *args, **kwargs)