From 4942769fbe9ff2d53f92a1ea8ce65e29937acbe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20Bl=C3=B6m?= Date: Thu, 26 Jan 2017 18:37:16 -0800 Subject: [PATCH 01/26] Added Range request options for static files --- sanic/exceptions.py | 16 +++++++- sanic/response.py | 95 +++++++++++++++++++++++++++++++++++---------- sanic/sanic.py | 18 ++++----- sanic/static.py | 47 +++++++++++++++++----- 4 files changed, 137 insertions(+), 39 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 2596a97a..a98c9e14 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -104,6 +104,7 @@ INTERNAL_SERVER_ERROR_HTML = ''' class SanicException(Exception): def __init__(self, message, status_code=None): super().__init__(message) + if status_code is not None: self.status_code = status_code @@ -137,6 +138,17 @@ class PayloadTooLarge(SanicException): status_code = 413 +class ContentRangeError(SanicException): + status_code = 416 + + def __init__(self, message, content_range): + super().__init__(message) + self.headers = { + 'Content-Type': 'text/plain', + "Content-Range": "bytes */%s" % (content_range.total,) + } + + class Handler: handlers = None @@ -191,7 +203,9 @@ class Handler: if issubclass(type(exception), SanicException): return text( 'Error: {}'.format(exception), - status=getattr(exception, 'status_code', 500)) + status=getattr(exception, 'status_code', 500), + headers=getattr(exception, 'headers', dict()) + ) elif self.debug: html_output = self._render_traceback_html(exception, request) diff --git a/sanic/response.py b/sanic/response.py index c29a473e..dee0f5bc 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -97,21 +97,27 @@ class HTTPResponse: def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python - timeout_header = b'' if keep_alive and keep_alive_timeout: - timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout - + if 'Keep-Alive' not in self.headers: + self.headers['Keep-Alive'] = keep_alive_timeout + if 'Connection' not in self.headers: + if keep_alive: + self.headers['Connection'] = 'keep-alive' + else: + self.headers['Connection'] = 'close' + if 'Content-Length' not in self.headers: + self.headers['Content-Length'] = len(self.body) + if 'Content-Type' not in self.headers: + self.headers['Content-Type'] = self.content_type headers = b'' if self.headers: for name, value in self.headers.items(): try: - headers += ( - b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) + 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'))) - + 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 status = COMMON_STATUS_CODES.get(self.status) @@ -119,18 +125,11 @@ class HTTPResponse: status = ALL_STATUS_CODES.get(self.status) return (b'HTTP/%b %d %b\r\n' - b'Content-Type: %b\r\n' - b'Content-Length: %d\r\n' - b'Connection: %b\r\n' - b'%b%b\r\n' + b'%b\r\n' b'%b') % ( version.encode(), self.status, status, - self.content_type.encode(), - len(self.body), - b'keep-alive' if keep_alive else b'close', - timeout_header, headers, self.body ) @@ -142,13 +141,62 @@ class HTTPResponse: return self._cookies +class ContentRangeHandler: + """ + This class is for parsing the request header + """ + __slots__ = ('start', 'end', 'size', 'total', 'headers') + + def __init__(self, request, stats): + self.start = self.size = 0 + self.end = None + self.headers = dict() + self.total = stats.st_size + range_header = request.headers.get('Range') + if range_header: + self.start, self.end = ContentRangeHandler.parse_range(range_header) + if self.start is not None and self.end is not None: + self.size = self.end - self.start + elif self.end is not None: + self.size = self.end + elif self.start is not None: + self.size = self.total - self.start + else: + self.size = self.total + self.headers['Content-Range'] = "bytes %s-%s/%s" % ( + self.start, self.end, self.total) + else: + self.size = self.total + + def __bool__(self): + return self.size > 0 + + @staticmethod + def parse_range(range_header): + unit, _, value = tuple(map(str.strip, range_header.partition('='))) + if unit != 'bytes': + return None + start_b, _, end_b = tuple(map(str.strip, value.partition('-'))) + try: + start = int(start_b) if start_b.strip() else None + end = int(end_b) if end_b.strip() else None + except ValueError: + return None + if end is not None: + if start is None: + if end != 0: + start = -end + end = None + return start, end + + def json(body, status=200, headers=None, **kwargs): """ Returns response object with body in json format. :param body: Response data to be serialized. :param status: Response code. :param headers: Custom Headers. - :param \**kwargs: Remaining arguments that are passed to the json encoder. + :param kwargs: Remaining arguments that are passed to the json encoder. """ return HTTPResponse(json_dumps(body, **kwargs), headers=headers, status=status, content_type="application/json") @@ -176,17 +224,24 @@ def html(body, status=200, headers=None): content_type="text/html; charset=utf-8") -async def file(location, mime_type=None, headers=None): +async def file(location, mime_type=None, headers=None, _range=None): """ Returns response object with file data. :param location: Location of file on system. :param mime_type: Specific mime_type. :param headers: Custom Headers. + :param _range: """ filename = path.split(location)[-1] async with open_async(location, mode='rb') as _file: - out_stream = await _file.read() + if _range: + await _file.seek(_range.start) + out_stream = await _file.read(_range.size) + headers['Content-Range'] = 'bytes %s-%s/%s' % ( + _range.start, _range.end, _range.total) + else: + out_stream = await _file.read() mime_type = mime_type or guess_type(filename)[0] or 'text/plain' diff --git a/sanic/sanic.py b/sanic/sanic.py index cea09470..8b9dbc78 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -78,22 +78,22 @@ class Sanic: # Shorthand method decorators def get(self, uri, host=None): - return self.route(uri, methods=["GET"], host=host) + return self.route(uri, methods=frozenset({"GET"}), host=host) def post(self, uri, host=None): - return self.route(uri, methods=["POST"], host=host) + return self.route(uri, methods=frozenset({"POST"}), host=host) def put(self, uri, host=None): - return self.route(uri, methods=["PUT"], host=host) + return self.route(uri, methods=frozenset({"PUT"}), host=host) def head(self, uri, host=None): - return self.route(uri, methods=["HEAD"], host=host) + return self.route(uri, methods=frozenset({"HEAD"}), host=host) def options(self, uri, host=None): - return self.route(uri, methods=["OPTIONS"], host=host) + return self.route(uri, methods=frozenset({"OPTIONS"}), host=host) def patch(self, uri, host=None): - return self.route(uri, methods=["PATCH"], host=host) + return self.route(uri, methods=frozenset({"PATCH"}), host=host) def add_route(self, handler, uri, methods=None, host=None): """ @@ -117,7 +117,7 @@ class Sanic: """ Decorates a function to be registered as a handler for exceptions - :param \*exceptions: exceptions + :param exceptions: exceptions :return: decorated function """ @@ -152,13 +152,13 @@ class Sanic: # Static Files def static(self, uri, file_or_directory, pattern='.+', - use_modified_since=True): + use_modified_since=True, use_content_range=False): """ Registers a root to serve files from. The input can either be a file or a directory. See """ static_register(self, uri, file_or_directory, pattern, - use_modified_since) + use_modified_since, use_content_range) def blueprint(self, blueprint, **options): """ diff --git a/sanic/static.py b/sanic/static.py index 1d0bff0f..a3df5918 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -2,14 +2,16 @@ from aiofiles.os import stat from os import path from re import sub from time import strftime, gmtime +from mimetypes import guess_type from urllib.parse import unquote -from .exceptions import FileNotFound, InvalidUsage -from .response import file, HTTPResponse +from .exceptions import FileNotFound, InvalidUsage, ContentRangeError +from .response import file, HTTPResponse, ContentRangeHandler -def register(app, uri, file_or_directory, pattern, use_modified_since): - # TODO: Though sanic is not a file server, I feel like we should atleast +def register(app, uri, file_or_directory, pattern, + use_modified_since, use_content_range): + # TODO: Though sanic is not a file server, I feel like we should at least # make a good effort here. Modified-since is nice, but we could # also look into etags, expires, and caching """ @@ -23,8 +25,9 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): :param use_modified_since: If true, send file modified time, and return not modified if the browser's matches the server's + :param use_content_range: If true, process header for range requests + and sends the file part that is requested """ - # If we're not trying to match a file directly, # serve from the folder if not path.isfile(file_or_directory): @@ -50,6 +53,7 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): headers = {} # Check if the client has been sent this file before # and it has not been modified since + stats = None if use_modified_since: stats = await stat(file_path) modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT', @@ -57,11 +61,36 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): if request.headers.get('If-Modified-Since') == modified_since: return HTTPResponse(status=304) headers['Last-Modified'] = modified_since - - return await file(file_path, headers=headers) - except: + _range = None + if use_content_range: + if not stats: + stats = await stat(file_path) + headers['Accept-Ranges'] = 'bytes' + headers['Content-Length'] = str(stats.st_size) + if request.method != 'HEAD': + _range = ContentRangeHandler(request, stats) + # If the start byte is greater than the size + # of the entire file or if the end is + if _range.start >= _range.total or _range.end == 0: + raise ContentRangeError('Content-Range malformed', + _range) + if _range.start == 0 and _range.size == _range.total: + _range = None + else: + headers['Content-Length'] = str(_range.size) + for k, v in _range.headers.items(): + headers[k] = v + if request.method == 'HEAD': + return HTTPResponse( + headers=headers, + content_type=guess_type(file_path)[0] or 'text/plain') + else: + return await file(file_path, headers=headers, _range=_range) + except ContentRangeError: + raise + except Exception: raise FileNotFound('File not found', path=file_or_directory, relative_url=file_uri) - app.route(uri, methods=['GET'])(_handler) + app.route(uri, methods=['GET', 'HEAD'])(_handler) From abbb7cdaf07f7b4a75bc615a626be7611cc024c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20Bl=C3=B6m?= Date: Thu, 26 Jan 2017 18:37:51 -0800 Subject: [PATCH 02/26] PEP8 format changes --- tests/test_requests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index b2ee8e78..9450630a 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -112,7 +112,8 @@ def test_query_string(): async def handler(request): return text('OK') - request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")]) + request, response = sanic_endpoint_test( + app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")]) assert request.args.get('test1') == '1' assert request.args.get('test2') == 'false' @@ -150,7 +151,8 @@ def test_post_json(): payload = {'test': 'OK'} headers = {'content-type': 'application/json'} - request, response = sanic_endpoint_test(app, data=json_dumps(payload), headers=headers) + request, response = sanic_endpoint_test( + app, data=json_dumps(payload), headers=headers) assert request.json.get('test') == 'OK' assert response.text == 'OK' From 31ad850e37964332763fd98b2f3f071873ae82a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20Bl=C3=B6m?= Date: Thu, 26 Jan 2017 18:38:32 -0800 Subject: [PATCH 03/26] added Range request test cases --- tests/test_static.py | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_static.py b/tests/test_static.py index 82b0d1f9..5f9d9e09 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -60,3 +60,59 @@ def test_static_url_decode_file(static_file_directory): request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt') assert response.status == 200 assert response.body == decode_me_contents + + +def test_static_head_request(static_file_path, static_file_content): + app = Sanic('test_static') + app.static('/testing.file', static_file_path, use_content_range=True) + + request, response = sanic_endpoint_test( + app, uri='/testing.file', method='head') + assert response.status == 200 + assert 'Accept-Ranges' in response.headers + assert 'Content-Length' in response.headers + assert int(response.headers['Content-Length']) == len(static_file_content) + + +def test_static_content_range(static_file_path, static_file_content): + app = Sanic('test_static') + app.static('/testing.file', static_file_path, use_content_range=True) + + headers = { + 'Range': 'bytes=12-19' + } + request, response = sanic_endpoint_test( + app, uri='/testing.file', headers=headers) + assert response.status == 200 + assert 'Content-Length' in response.headers + assert 'Content-Range' in response.headers + assert int(response.headers['Content-Length']) == 19-12 + assert response.body == bytes(static_file_content)[12:19] + + +def test_static_content_range_empty(static_file_path, static_file_content): + app = Sanic('test_static') + app.static('/testing.file', static_file_path, use_content_range=True) + + request, response = sanic_endpoint_test(app, uri='/testing.file') + assert response.status == 200 + assert 'Content-Length' in response.headers + assert 'Content-Range' not in response.headers + assert int(response.headers['Content-Length']) == len(static_file_content) + assert response.body == bytes(static_file_content) + + +def test_static_content_range_error(static_file_path, static_file_content): + app = Sanic('test_static') + app.static('/testing.file', static_file_path, use_content_range=True) + + headers = { + 'Range': 'bytes=1-0' + } + request, response = sanic_endpoint_test( + app, uri='/testing.file', headers=headers) + assert response.status == 416 + assert 'Content-Length' in response.headers + assert 'Content-Range' in response.headers + assert response.headers['Content-Range'] == "bytes */%s" % ( + len(static_file_content),) From ee5e145e2d302770e5db06013ac4338de7930227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20Bl=C3=B6m?= Date: Fri, 27 Jan 2017 08:00:41 -0800 Subject: [PATCH 04/26] fixed line to long notice --- sanic/response.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index dee0f5bc..b8ea5ee8 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -152,9 +152,9 @@ class ContentRangeHandler: self.end = None self.headers = dict() self.total = stats.st_size - range_header = request.headers.get('Range') - if range_header: - self.start, self.end = ContentRangeHandler.parse_range(range_header) + _range = request.headers.get('Range') + if _range: + self.start, self.end = ContentRangeHandler.parse_range(_range) if self.start is not None and self.end is not None: self.size = self.end - self.start elif self.end is not None: From 8619e50845f4d1e48124ffde7d31760bda0a314c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20Bl=C3=B6m?= Date: Sat, 28 Jan 2017 11:18:52 -0800 Subject: [PATCH 05/26] Changed output to use a default_header dictionary and a ChainMap to unnecessary conditionals and simplified range parsing logic --- sanic/response.py | 82 +++++++++++++++++--------------------------- sanic/static.py | 2 +- tests/test_static.py | 2 +- 3 files changed, 34 insertions(+), 52 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index b8ea5ee8..287eec26 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,6 +1,7 @@ from aiofiles import open as open_async from mimetypes import guess_type from os import path +from collections import ChainMap from ujson import dumps as json_dumps @@ -97,26 +98,24 @@ class HTTPResponse: def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python - if keep_alive and keep_alive_timeout: - if 'Keep-Alive' not in self.headers: - self.headers['Keep-Alive'] = keep_alive_timeout - if 'Connection' not in self.headers: - if keep_alive: - self.headers['Connection'] = 'keep-alive' - else: - self.headers['Connection'] = 'close' - if 'Content-Length' not in self.headers: - self.headers['Content-Length'] = len(self.body) - if 'Content-Type' not in self.headers: - self.headers['Content-Type'] = self.content_type + default_header = dict() + if keep_alive: + if keep_alive_timeout: + default_header['Keep-Alive'] = keep_alive_timeout + default_header['Connection'] = 'keep-alive' + else: + default_header['Connection'] = 'close' + default_header['Content-Length'] = len(self.body) + default_header['Content-Type'] = self.content_type headers = b'' - if self.headers: - for name, value in self.headers.items(): - try: - headers += (b'%b: %b\r\n' % ( + for name, value in ChainMap(self.headers, default_header).items(): + try: + headers += ( + b'%b: %b\r\n' % ( name.encode(), value.encode('utf-8'))) - except AttributeError: - headers += (b'%b: %b\r\n' % ( + 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 @@ -148,46 +147,29 @@ class ContentRangeHandler: __slots__ = ('start', 'end', 'size', 'total', 'headers') def __init__(self, request, stats): - self.start = self.size = 0 + self.size = self.start = 0 self.end = None self.headers = dict() self.total = stats.st_size _range = request.headers.get('Range') - if _range: - self.start, self.end = ContentRangeHandler.parse_range(_range) - if self.start is not None and self.end is not None: - self.size = self.end - self.start - elif self.end is not None: - self.size = self.end - elif self.start is not None: - self.size = self.total - self.start - else: - self.size = self.total - self.headers['Content-Range'] = "bytes %s-%s/%s" % ( - self.start, self.end, self.total) - else: - self.size = self.total - - def __bool__(self): - return self.size > 0 - - @staticmethod - def parse_range(range_header): - unit, _, value = tuple(map(str.strip, range_header.partition('='))) + if _range is None: + return + unit, _, value = tuple(map(str.strip, _range.partition('='))) if unit != 'bytes': - return None + return start_b, _, end_b = tuple(map(str.strip, value.partition('-'))) try: - start = int(start_b) if start_b.strip() else None - end = int(end_b) if end_b.strip() else None + self.start = int(start_b) if start_b else 0 + self.end = int(end_b) if end_b else 0 except ValueError: - return None - if end is not None: - if start is None: - if end != 0: - start = -end - end = None - return start, end + self.start = self.end = 0 + return + self.size = self.end - self.start + self.headers['Content-Range'] = "bytes %s-%s/%s" % ( + self.start, self.end, self.total) + + def __bool__(self): + return self.size != 0 def json(body, status=200, headers=None, **kwargs): diff --git a/sanic/static.py b/sanic/static.py index a3df5918..e0255334 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -74,7 +74,7 @@ def register(app, uri, file_or_directory, pattern, if _range.start >= _range.total or _range.end == 0: raise ContentRangeError('Content-Range malformed', _range) - if _range.start == 0 and _range.size == _range.total: + if _range.start == 0 and _range.size == 0: _range = None else: headers['Content-Length'] = str(_range.size) diff --git a/tests/test_static.py b/tests/test_static.py index 5f9d9e09..601b0deb 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -74,7 +74,7 @@ def test_static_head_request(static_file_path, static_file_content): assert int(response.headers['Content-Length']) == len(static_file_content) -def test_static_content_range(static_file_path, static_file_content): +def test_static_content_range_correct(static_file_path, static_file_content): app = Sanic('test_static') app.static('/testing.file', static_file_path, use_content_range=True) From 753d2da6dbd0deb6f8cb2ceab7c93c8fba56acac Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 28 Jan 2017 15:26:44 -0800 Subject: [PATCH 06/26] fix async run --- sanic/server.py | 4 ++-- tests/test_loop_policy.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 tests/test_loop_policy.py diff --git a/sanic/server.py b/sanic/server.py index 48c3827e..be9bc31c 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -297,8 +297,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param protocol: Subclass of asyncio protocol class :return: Nothing """ - loop = async_loop.new_event_loop() - asyncio.set_event_loop(loop) + loop = asyncio.get_event_loop() + asyncio.set_event_loop_policy(async_loop.EventLoopPolicy()) if debug: loop.set_debug(debug) diff --git a/tests/test_loop_policy.py b/tests/test_loop_policy.py new file mode 100644 index 00000000..f764548c --- /dev/null +++ b/tests/test_loop_policy.py @@ -0,0 +1,27 @@ +from sanic import Sanic +import asyncio +from signal import signal, SIGINT +import uvloop + + +def test_loop_policy(): + app = Sanic('test_loop_policy') + + server = app.create_server(host="0.0.0.0", port=8000) + + loop = asyncio.get_event_loop() + task = asyncio.ensure_future(server) + signal(SIGINT, lambda s, f: loop.close()) + + # serve() sets the event loop policy to uvloop but + # doesn't get called until we run the server task + assert isinstance(asyncio.get_event_loop_policy(), + asyncio.unix_events._UnixDefaultEventLoopPolicy) + + try: + loop.run_until_complete(task) + except: + loop.stop() + + assert isinstance(asyncio.get_event_loop_policy(), + uvloop.EventLoopPolicy) From 10dbb9186d3ba0f23728f5974bc12262be67d7c3 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 28 Jan 2017 14:54:15 -0800 Subject: [PATCH 07/26] combine logic from create_server() and run() --- sanic/sanic.py | 159 +++++++++++++++++++------------------- sanic/server.py | 4 +- tests/test_loop_policy.py | 27 ------- 3 files changed, 81 insertions(+), 109 deletions(-) delete mode 100644 tests/test_loop_policy.py diff --git a/sanic/sanic.py b/sanic/sanic.py index ea2e8bef..e468ad7c 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -298,6 +298,84 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ + server_settings = \ + self._helper(host=host, port=port, debug=debug, + before_start=before_start, after_start=after_start, + before_stop=before_stop, after_stop=after_stop, + ssl=ssl, sock=sock, workers=workers, loop=loop, + protocol=protocol, backlog=backlog, + stop_event=stop_event, + register_sys_signals=register_sys_signals) + try: + if workers == 1: + serve(**server_settings) + else: + serve_multiple(server_settings, workers, stop_event) + + except Exception as e: + log.exception( + 'Experienced exception while trying to serve') + + log.info("Server Stopped") + + def stop(self): + """This kills the Sanic""" + get_event_loop().stop() + + async def create_server(self, host="127.0.0.1", port=8000, debug=False, + before_start=None, after_start=None, + before_stop=None, after_stop=None, ssl=None, + sock=None, loop=None, protocol=HttpProtocol, + backlog=100, stop_event=None): + """ + Asynchronous version of `run`. + """ + server_settings = \ + self._helper(host=host, port=port, debug=debug, + before_start=before_start, after_start=after_start, + before_stop=before_stop, after_stop=after_stop, + ssl=ssl, sock=sock, loop=loop, + protocol=protocol, backlog=backlog, + stop_event=stop_event) + + server_settings['run_async'] = True + + # Serve + proto = "http" + if ssl is not None: + proto = "https" + log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + + return await serve(**server_settings) + + def _helper(self, host="127.0.0.1", port=8000, debug=False, + before_start=None, 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: 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 + :param protocol: Subclass of asyncio protocol class + :return: Nothing + """ + self.error_handler.debug = debug self.debug = debug self.loop = loop @@ -357,83 +435,4 @@ class Sanic: if ssl is not None: proto = "https" log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) - - try: - if workers == 1: - serve(**server_settings) - else: - serve_multiple(server_settings, workers, stop_event) - - except Exception as e: - log.exception( - 'Experienced exception while trying to serve') - - log.info("Server Stopped") - - def stop(self): - """This kills the Sanic""" - get_event_loop().stop() - - async def create_server(self, host="127.0.0.1", port=8000, debug=False, - before_start=None, after_start=None, - before_stop=None, after_stop=None, ssl=None, - sock=None, loop=None, protocol=HttpProtocol, - backlog=100, stop_event=None): - """ - Asynchronous version of `run`. - """ - if loop is not None: - if self.debug: - warnings.simplefilter('default') - warnings.warn("Passing a loop will be deprecated in version" - " 0.4.0 https://github.com/channelcat/sanic/" - "pull/335 has more information.", - DeprecationWarning) - - loop = get_event_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, - 'backlog': backlog - } - - # -------------------------------------------- # - # Register start/stop events - # -------------------------------------------- # - - for event_name, settings_name, args, reverse in ( - ("before_server_start", "before_start", before_start, False), - ("after_server_start", "after_start", after_start, False), - ("before_server_stop", "before_stop", before_stop, True), - ("after_server_stop", "after_stop", after_stop, True)): - listeners = [] - for blueprint in self.blueprints.values(): - listeners += blueprint.listeners[event_name] - if args: - if callable(args): - args = [args] - listeners += args - if reverse: - listeners.reverse() - # Prepend sanic to the arguments when listeners are triggered - listeners = [partial(listener, self) for listener in listeners] - server_settings[settings_name] = listeners - - server_settings['run_async'] = True - - # Serve - proto = "http" - if ssl is not None: - proto = "https" - log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) - - return await serve(**server_settings) + return server_settings diff --git a/sanic/server.py b/sanic/server.py index be9bc31c..48c3827e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -297,8 +297,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param protocol: Subclass of asyncio protocol class :return: Nothing """ - loop = asyncio.get_event_loop() - asyncio.set_event_loop_policy(async_loop.EventLoopPolicy()) + loop = async_loop.new_event_loop() + asyncio.set_event_loop(loop) if debug: loop.set_debug(debug) diff --git a/tests/test_loop_policy.py b/tests/test_loop_policy.py deleted file mode 100644 index f764548c..00000000 --- a/tests/test_loop_policy.py +++ /dev/null @@ -1,27 +0,0 @@ -from sanic import Sanic -import asyncio -from signal import signal, SIGINT -import uvloop - - -def test_loop_policy(): - app = Sanic('test_loop_policy') - - server = app.create_server(host="0.0.0.0", port=8000) - - loop = asyncio.get_event_loop() - task = asyncio.ensure_future(server) - signal(SIGINT, lambda s, f: loop.close()) - - # serve() sets the event loop policy to uvloop but - # doesn't get called until we run the server task - assert isinstance(asyncio.get_event_loop_policy(), - asyncio.unix_events._UnixDefaultEventLoopPolicy) - - try: - loop.run_until_complete(task) - except: - loop.stop() - - assert isinstance(asyncio.get_event_loop_policy(), - uvloop.EventLoopPolicy) From 82d1d30a41ea61f23884bdc862dd6b090ff0bda4 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 29 Jan 2017 14:01:00 -0800 Subject: [PATCH 08/26] review updates --- sanic/sanic.py | 59 +++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index e468ad7c..a81a336a 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -298,14 +298,12 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ - server_settings = \ - self._helper(host=host, port=port, debug=debug, - before_start=before_start, after_start=after_start, - before_stop=before_stop, after_stop=after_stop, - ssl=ssl, sock=sock, workers=workers, loop=loop, - protocol=protocol, backlog=backlog, - stop_event=stop_event, - register_sys_signals=register_sys_signals) + server_settings = self._helper( + host=host, port=port, debug=debug, before_start=before_start, + after_start=after_start, before_stop=before_stop, + after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, + loop=loop, protocol=protocol, backlog=backlog, + stop_event=stop_event, register_sys_signals=register_sys_signals) try: if workers == 1: serve(**server_settings) @@ -330,15 +328,12 @@ class Sanic: """ Asynchronous version of `run`. """ - server_settings = \ - self._helper(host=host, port=port, debug=debug, - before_start=before_start, after_start=after_start, - before_stop=before_stop, after_stop=after_stop, - ssl=ssl, sock=sock, loop=loop, - protocol=protocol, backlog=backlog, - stop_event=stop_event) - - server_settings['run_async'] = True + server_settings = self._helper( + host=host, port=port, debug=debug, before_start=before_start, + after_start=after_start, before_stop=before_stop, + after_stop=after_stop, ssl=ssl, sock=sock, loop=loop, + protocol=protocol, backlog=backlog, stop_event=stop_event, + async_run=True) # Serve proto = "http" @@ -352,33 +347,14 @@ class Sanic: before_start=None, 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): + register_sys_signals=True, run_async=False): """ - 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: 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 - :param protocol: Subclass of asyncio protocol class - :return: Nothing + Helper function used by `run` and `create_server`. """ self.error_handler.debug = debug self.debug = debug - self.loop = loop + self.loop = loop = get_event_loop() if loop is not None: if self.debug: @@ -399,6 +375,7 @@ class Sanic: '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, 'backlog': backlog } @@ -430,9 +407,13 @@ class Sanic: log.setLevel(logging.DEBUG) log.debug(self.config.LOGO) + if run_async: + server_settings['run_async'] = True + # Serve proto = "http" if ssl is not None: proto = "https" log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + return server_settings From 52e485cce9ca830f442c71a6a5235cde7bb25ae4 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 16:46:16 -0800 Subject: [PATCH 09/26] Fix readthedocs includes --- docs/conf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index c929e9e7..21b9b9cf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,9 +22,7 @@ import sanic # -- General configuration ------------------------------------------------ -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages'] +extensions = [] templates_path = ['_templates'] From 629524af0428f2b4d9397675261bdf6663d59c34 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 17:39:55 -0800 Subject: [PATCH 10/26] Restructured blueprint class Blueprints currently queue functions to be called, which are simple, yet hard to inspect. These changes allow tools to be built that analyze blueprints more easily. --- sanic/blueprints.py | 132 +++++++++++++++++--------------------------- 1 file changed, 52 insertions(+), 80 deletions(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 4527fa67..faa7c541 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -1,59 +1,11 @@ -from collections import defaultdict +from collections import defaultdict, namedtuple -class BlueprintSetup: - """ - Creates a blueprint state like object. - """ - - def __init__(self, blueprint, app, options): - self.app = app - self.blueprint = blueprint - self.options = options - - url_prefix = self.options.get('url_prefix') - if url_prefix is None: - url_prefix = self.blueprint.url_prefix - - #: The prefix that should be used for all URLs defined on the - #: blueprint. - self.url_prefix = url_prefix - - 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 - - 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. - """ - self.app.exception(*args, **kwargs)(handler) - - def add_static(self, uri, file_or_directory, *args, **kwargs): - """ - Registers static files to sanic. - """ - if self.url_prefix: - uri = self.url_prefix + uri - - self.app.static(uri, file_or_directory, *args, **kwargs) - - def add_middleware(self, middleware, *args, **kwargs): - """ - Registers middleware to sanic. - """ - if args or kwargs: - self.app.middleware(*args, **kwargs)(middleware) - else: - self.app.middleware(middleware) +FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host']) +FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) +FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) +FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) +FutureStatic = namedtuple('Route', ['uri', 'file_or_directory', 'args', 'kwargs']) class Blueprint: @@ -65,30 +17,47 @@ class Blueprint: """ self.name = name self.url_prefix = url_prefix - self.deferred_functions = [] - self.listeners = defaultdict(list) self.host = host - def record(self, func): - """ - Registers a callback function that is invoked when the blueprint is - registered on the application. - """ - self.deferred_functions.append(func) - - def make_setup_state(self, app, options): - """ - Returns a new BlueprintSetup object - """ - return BlueprintSetup(self, app, options) + self.routes = [] + self.exceptions = [] + self.listeners = defaultdict(list) + self.middlewares = [] + self.statics = [] 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) + + url_prefix = options.get('url_prefix', self.url_prefix) + + # Routes + for future in self.routes: + # Prepend the blueprint URI prefix if available + uri = url_prefix + future.uri if url_prefix else future.uri + app.route( + uri=uri, + methods=future.methods, + host=future.host or self.host + )(future.handler) + + # Middleware + for future in self.middlewares: + if future.args or future.kwargs: + app.middleware(*future.args, **future.kwargs)(future.middleware) + else: + app.middleware(future.middleware) + + # Exceptions + for future in self.exceptions: + app.exception(*future.args, **future.kwargs)(future.handler) + + # Static Files + for future in self.statics: + # Prepend the blueprint URI prefix if available + uri = url_prefix + future.uri if url_prefix else future.uri + app.static(uri, future.file_or_directory, *future.args, **future.kwargs) def route(self, uri, methods=frozenset({'GET'}), host=None): """ @@ -97,7 +66,8 @@ class Blueprint: :param methods: List of acceptable HTTP methods. """ def decorator(handler): - self.record(lambda s: s.add_route(handler, uri, methods, host)) + route = FutureRoute(handler, uri, methods, host) + self.routes.append(route) return handler return decorator @@ -108,7 +78,8 @@ class Blueprint: :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, host)) + route = FutureRoute(handler, uri, methods, host) + self.routes.append(route) return handler def listener(self, event): @@ -125,10 +96,10 @@ class Blueprint: """ Creates a blueprint middleware from a decorated function. """ - def register_middleware(middleware): - self.record( - lambda s: s.add_middleware(middleware, *args, **kwargs)) - return middleware + def register_middleware(_middleware): + future_middleware = FutureMiddleware(_middleware, args, kwargs) + self.middlewares.append(future_middleware) + return _middleware # Detect which way this was called, @middleware or @middleware('AT') if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): @@ -143,7 +114,8 @@ class Blueprint: Creates a blueprint exception from a decorated function. """ def decorator(handler): - self.record(lambda s: s.add_exception(handler, *args, **kwargs)) + exception = FutureException(handler, args, kwargs) + self.exceptions.append(exception) return handler return decorator @@ -153,5 +125,5 @@ class Blueprint: :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)) + static = FutureStatic(uri, file_or_directory, args, kwargs) + self.statics.append(static) From 4c80cd185ffe7b8372949010a4327659f5b58bb8 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 17:44:46 -0800 Subject: [PATCH 11/26] Fix flake8 --- sanic/blueprints.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index faa7c541..3755e8b1 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -5,7 +5,8 @@ FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) -FutureStatic = namedtuple('Route', ['uri', 'file_or_directory', 'args', 'kwargs']) +FutureStatic = namedtuple('Route', + ['uri', 'file_or_directory', 'args', 'kwargs']) class Blueprint: @@ -45,7 +46,8 @@ class Blueprint: # Middleware for future in self.middlewares: if future.args or future.kwargs: - app.middleware(*future.args, **future.kwargs)(future.middleware) + app.middleware(*future.args, + **future.kwargs)(future.middleware) else: app.middleware(future.middleware) @@ -57,7 +59,8 @@ class Blueprint: for future in self.statics: # Prepend the blueprint URI prefix if available uri = url_prefix + future.uri if url_prefix else future.uri - app.static(uri, future.file_or_directory, *future.args, **future.kwargs) + app.static(uri, future.file_or_directory, + *future.args, **future.kwargs) def route(self, uri, methods=frozenset({'GET'}), host=None): """ From 0ef39f35aead3474acd78111ca202f5e60dc601d Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 23:20:38 -0800 Subject: [PATCH 12/26] Added route shorthands to blueprints --- sanic/blueprints.py | 22 +++++++++++++ tests/test_blueprints.py | 68 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 3755e8b1..c9a4b8ac 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -130,3 +130,25 @@ class Blueprint: """ static = FutureStatic(uri, file_or_directory, args, kwargs) self.statics.append(static) + + # 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 delete(self, uri, host=None): + return self.route(uri, methods=["DELETE"], host=host) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 75109e2c..d48c9ea9 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -228,3 +228,71 @@ def test_bp_static(): request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 assert response.body == current_file_contents + +def test_bp_shorthand(): + app = Sanic('test_shorhand_routes') + blueprint = Blueprint('test_shorhand_routes') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + def handler(request): + return text('OK') + + app.blueprint(blueprint) + + request, response = sanic_endpoint_test(app, uri='/get', method='get') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/get', method='post') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/put', method='put') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/put', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/post', method='post') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/post', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/head', method='head') + + request, response = sanic_endpoint_test(app, uri='/head', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/options', method='options') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/options', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/patch', method='patch') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/patch', method='get') + assert response.status == 405 + + request, response = sanic_endpoint_test(app, uri='/delete', method='delete') + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/delete', method='get') + assert response.status == 405 From b72d841619843df57df117003f43323f343e5d4d Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 29 Jan 2017 23:21:00 -0800 Subject: [PATCH 13/26] . --- tests/test_blueprints.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index d48c9ea9..aebb7429 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -233,24 +233,31 @@ def test_bp_shorthand(): app = Sanic('test_shorhand_routes') blueprint = Blueprint('test_shorhand_routes') + @blueprint.get('/get') def handler(request): return text('OK') + @blueprint.put('/put') def handler(request): return text('OK') + @blueprint.post('/post') def handler(request): return text('OK') + @blueprint.head('/head') def handler(request): return text('OK') + @blueprint.options('/options') def handler(request): return text('OK') + @blueprint.patch('/patch') def handler(request): return text('OK') + @blueprint.delete('/delete') def handler(request): return text('OK') @@ -275,6 +282,7 @@ def test_bp_shorthand(): assert response.status == 405 request, response = sanic_endpoint_test(app, uri='/head', method='head') + assert response.status == 200 request, response = sanic_endpoint_test(app, uri='/head', method='get') assert response.status == 405 From 82680bf43f4a1e5eb24b2534c29cbe8bf1cd27bb Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Mon, 30 Jan 2017 10:39:02 +0100 Subject: [PATCH 14/26] Fix docs/config.md: the MYAPP_SETTINGS is not exported If we don"t `export` the variable, it's not available in subcommand: MYAPP_SETTINGS=/path/to/config_file; python3 -c "import os; os.environ['MYAPP_SETTINGS']" Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.5/os.py", line 725, in __getitem__ raise KeyError(key) from None KeyError: 'MYAPP_SETTINGS' The ';' is the culprit here. --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 6dd67cf2..f5d56467 100644 --- a/docs/config.md +++ b/docs/config.md @@ -54,7 +54,7 @@ app.config.from_envvar('MYAPP_SETTINGS') Then you can run your application with the `MYAPP_SETTINGS` environment variable set: ``` -$ MYAPP_SETTINGS=/path/to/config_file; python3 myapp.py +$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py INFO: Goin' Fast @ http://0.0.0.0:8000 ``` From 1649f30808fe80a5c596cd233ccecdc1ca935347 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 30 Jan 2017 02:22:12 -0800 Subject: [PATCH 15/26] Updated password --- .travis.yml | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b31c4f3..71796003 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,14 @@ sudo: false language: python python: - - '3.5' - - '3.6' +- '3.5' +- '3.6' install: pip install tox-travis script: tox deploy: provider: pypi user: channelcat password: - secure: jH4+Di2/qcBwWVhI5/3NYd/JuDDgf5/cF85h+oQnAjgwP6me3th9RS0PHL2gjKJrmyRgwrW7a3eSAityo5sQSlBloQCNrtCE30rkDiwtgoIxDW72NR/nE8nUkS9Utgy87eS+3B4NrO7ag4GTqO5ET8SQ4/MCiQwyUQATLXj2s2eTpQvqJeZG6YgoeFAOYvlR580yznXoOwldWlkiymJiWSdR/01lthtWCi40sYC/QoU7psODJ/tPcsqgQtQKyUVsci7mKvp3Y8ImkoO/POM01jYNsS9qLh5pKTNCEYxtyzC77whenCNHn7WReVidd56g1ADosbNo4yY/1D3VAvwjUnkQ0SzdBQfT7IIzccEuC0j1NXKPN97OX0a6XzyUMYJ1XiU3juTJOPxdYBPbsDM3imQiwrOh1faIf0HCgNTN+Lxe5l8obCH7kffNcVUhs2zI0+2t4MS5tjb/OVuYD/TFn+bM33DqzLctTOK/pGn6xefzZcdzb191LPo99Lof+4fo6jNUpb0UmcBu5ZJzxh0lGe8FPIK3UAG/hrYDDgjx8s8RtUJjcEUQz0659XffYx7DLlgHO7cWyfjrHD3yrLzDbYr5mAS4FR+4D917V7UL+on4SsKHN00UuMGPguqSYo/xYyPLnJU5XK0du4MIpsNMB8TtrJOIewOOfD32+AisPQ8= + secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k= on: tags: true diff --git a/setup.py b/setup.py index 60606ad4..e7899e6f 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname( raise RuntimeError('Unable to determine version.') setup( - name='Sanic', + name='sanic', version=version, url='http://github.com/channelcat/sanic/', license='MIT', From cedf1d0b0023b07e0ed97bce6e1c6e11a50257f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20Bl=C3=B6m?= Date: Mon, 30 Jan 2017 09:13:43 -0800 Subject: [PATCH 16/26] Added new tests, new request logic, and handler file Added new tests for alternate uses for alternate range request types. Changed error handlnig for new request logic that simplifies the integration logic Moved the error handler and the content range handler to their own handler file to prevent circular imports. --- sanic/exceptions.py | 78 ++------------------------ sanic/handlers.py | 127 +++++++++++++++++++++++++++++++++++++++++++ sanic/response.py | 32 ----------- sanic/sanic.py | 4 +- sanic/static.py | 32 +++++------ tests/test_static.py | 39 ++++++++++++- 6 files changed, 188 insertions(+), 124 deletions(-) create mode 100644 sanic/handlers.py diff --git a/sanic/exceptions.py b/sanic/exceptions.py index a98c9e14..370882be 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -1,8 +1,3 @@ -from .response import text, html -from .log import log -from traceback import format_exc, extract_tb -import sys - TRACEBACK_STYLE = '''