Merge branch 'master' into add_register_sys_signals_flag
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from .sanic import Sanic
|
||||
from .blueprints import Blueprint
|
||||
|
||||
__version__ = '0.1.9'
|
||||
__version__ = '0.2.0'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log = logging.getLogger('sanic')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
109
sanic/router.py
109
sanic/router.py
@@ -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:
|
||||
|
||||
147
sanic/sanic.py
147
sanic/sanic.py
@@ -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()
|
||||
|
||||
107
sanic/server.py
107
sanic/server.py
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user