From 9f2d73e2f152456df27941efc3d5fe2695e0af6b Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:10:25 +0200 Subject: [PATCH 01/30] class based views implementation for sanic --- docs/class_based_views.md | 44 +++++++++ docs/routing.md | 12 +++ requirements-dev.txt | 1 + sanic/blueprints.py | 6 ++ sanic/sanic.py | 11 +++ sanic/utils.py | 4 +- sanic/views.py | 33 +++++++ tests/test_routes.py | 184 +++++++++++++++++++++++++++++++++++++- tests/test_views.py | 155 ++++++++++++++++++++++++++++++++ 9 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 docs/class_based_views.md create mode 100644 sanic/views.py create mode 100644 tests/test_views.py diff --git a/docs/class_based_views.md b/docs/class_based_views.md new file mode 100644 index 00000000..b5f8ee02 --- /dev/null +++ b/docs/class_based_views.md @@ -0,0 +1,44 @@ +# Class based views + +Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone try to use not implemented method, there will be 405 response. + +## Examples +```python +from sanic import Sanic +from sanic.views import MethodView + +app = Sanic('some_name') + + +class SimpleView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def post(self, request, *args, **kwargs): + return text('I am post method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + + def patch(self, request, *args, **kwargs): + return text('I am patch method') + + def delete(self, request, *args, **kwargs): + return text('I am delete method') + +app.add_route(SimpleView(), '/') + +``` + +If you need any url params just mention them in method definition: + +```python +class NameView(MethodView): + + def get(self, request, name, *args, **kwargs): + return text('Hello {}'.format(name)) + +app.add_route(NameView(), '/') + +async def person_handler(request, name): + return text('Person - {}'.format(name)) +app.add_route(handler, '/person/') + ``` diff --git a/requirements-dev.txt b/requirements-dev.txt index 00feb17d..1c34d695 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ bottle kyoukai falcon tornado +aiofiles diff --git a/sanic/blueprints.py b/sanic/blueprints.py index bfef8557..92e376f1 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -91,6 +91,12 @@ class Blueprint: return handler return decorator + def add_route(self, handler, uri, methods=None): + """ + """ + self.record(lambda s: s.add_route(handler, uri, methods)) + return handler + def listener(self, event): """ """ diff --git a/sanic/sanic.py b/sanic/sanic.py index af284c00..7a9c35ad 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -60,6 +60,17 @@ class Sanic: return response + def add_route(self, handler, uri, methods=None): + """ + A helper method to register class instance or functions as a handler to the application url routes. + :param handler: function or class instance + :param uri: path of the URL + :param methods: list or tuple of methods allowed + :return: function or class instance + """ + self.route(uri=uri, methods=methods)(handler) + return handler + # Decorator def exception(self, *exceptions): """ diff --git a/sanic/utils.py b/sanic/utils.py index 0749464b..8190c1d0 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,7 +16,7 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, - loop=None, *request_args, **request_kwargs): + loop=None, debug=False, *request_args, **request_kwargs): results = [] exceptions = [] @@ -34,7 +34,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, port=42101, after_start=_collect_response, loop=loop) + app.run(host=HOST, debug=debug, port=42101, after_start=_collect_response, loop=loop) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/sanic/views.py b/sanic/views.py new file mode 100644 index 00000000..9cb04247 --- /dev/null +++ b/sanic/views.py @@ -0,0 +1,33 @@ +from .exceptions import InvalidUsage + + +class MethodView: + """ Simple class based implementation of view for the sanic. + You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. + For example: + class DummyView(View): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + etc. + If someone try use not implemented method, there will be 405 response + + If you need any url params just mention them in method definition like: + class DummyView(View): + + def get(self, request, my_param_here, *args, **kwargs): + return text('I am get method with %s' % my_param_here) + + To add the view into the routing you could use + 1) app.add_route(DummyView(), '/') + 2) app.route('/')(DummyView()) + """ + + def __call__(self, request, *args, **kwargs): + handler = getattr(self, request.method.lower(), None) + if handler: + return handler(request, *args, **kwargs) + raise InvalidUsage('Method {} not allowed for URL {}'.format(request.method, request.url), status_code=405) diff --git a/tests/test_routes.py b/tests/test_routes.py index 8b0fd9f6..38591e53 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -84,7 +84,7 @@ def test_dynamic_route_int(): def test_dynamic_route_number(): - app = Sanic('test_dynamic_route_int') + app = Sanic('test_dynamic_route_number') results = [] @@ -105,7 +105,7 @@ def test_dynamic_route_number(): def test_dynamic_route_regex(): - app = Sanic('test_dynamic_route_int') + app = Sanic('test_dynamic_route_regex') @app.route('/folder/') async def handler(request, folder_id): @@ -145,7 +145,7 @@ def test_dynamic_route_unhashable(): def test_route_duplicate(): - app = Sanic('test_dynamic_route') + app = Sanic('test_route_duplicate') with pytest.raises(RouteExists): @app.route('/test') @@ -178,3 +178,181 @@ def test_method_not_allowed(): request, response = sanic_endpoint_test(app, method='post', uri='/test') assert response.status == 405 + + +def test_static_add_route(): + app = Sanic('test_static_add_route') + + async def handler1(request): + return text('OK1') + + async def handler2(request): + return text('OK2') + + app.add_route(handler1, '/test') + app.add_route(handler2, '/test2') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.text == 'OK1' + + request, response = sanic_endpoint_test(app, uri='/test2') + assert response.text == 'OK2' + + +def test_dynamic_add_route(): + app = Sanic('test_dynamic_add_route') + + results = [] + + async def handler(request, name): + results.append(name) + return text('OK') + + app.add_route(handler, '/folder/') + request, response = sanic_endpoint_test(app, uri='/folder/test123') + + assert response.text == 'OK' + assert results[0] == 'test123' + + +def test_dynamic_add_route_string(): + app = Sanic('test_dynamic_add_route_string') + + results = [] + + async def handler(request, name): + results.append(name) + return text('OK') + + app.add_route(handler, '/folder/') + request, response = sanic_endpoint_test(app, uri='/folder/test123') + + assert response.text == 'OK' + assert results[0] == 'test123' + + request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico') + + assert response.text == 'OK' + assert results[1] == 'favicon.ico' + + +def test_dynamic_add_route_int(): + app = Sanic('test_dynamic_add_route_int') + + results = [] + + async def handler(request, folder_id): + results.append(folder_id) + return text('OK') + + app.add_route(handler, '/folder/') + + request, response = sanic_endpoint_test(app, uri='/folder/12345') + assert response.text == 'OK' + assert type(results[0]) is int + + request, response = sanic_endpoint_test(app, uri='/folder/asdf') + assert response.status == 404 + + +def test_dynamic_add_route_number(): + app = Sanic('test_dynamic_add_route_number') + + results = [] + + async def handler(request, weight): + results.append(weight) + return text('OK') + + app.add_route(handler, '/weight/') + + request, response = sanic_endpoint_test(app, uri='/weight/12345') + assert response.text == 'OK' + assert type(results[0]) is float + + request, response = sanic_endpoint_test(app, uri='/weight/1234.56') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/weight/1234-56') + assert response.status == 404 + + +def test_dynamic_add_route_regex(): + app = Sanic('test_dynamic_route_int') + + async def handler(request, folder_id): + return text('OK') + + app.add_route(handler, '/folder/') + + request, response = sanic_endpoint_test(app, uri='/folder/test') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test1') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/test-123') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/') + assert response.status == 200 + + +def test_dynamic_add_route_unhashable(): + app = Sanic('test_dynamic_add_route_unhashable') + + async def handler(request, unhashable): + return text('OK') + + app.add_route(handler, '/folder//end/') + + request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/nope/') + assert response.status == 404 + + +def test_add_route_duplicate(): + app = Sanic('test_add_route_duplicate') + + with pytest.raises(RouteExists): + async def handler1(request): + pass + + async def handler2(request): + pass + + app.add_route(handler1, '/test') + app.add_route(handler2, '/test') + + with pytest.raises(RouteExists): + async def handler1(request, dynamic): + pass + + async def handler2(request, dynamic): + pass + + app.add_route(handler1, '/test//') + app.add_route(handler2, '/test//') + + +def test_add_route_method_not_allowed(): + app = Sanic('test_add_route_method_not_allowed') + + async def handler(request): + return text('OK') + + app.add_route(handler, '/test', methods=['GET']) + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, method='post', uri='/test') + assert response.status == 405 diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 00000000..251b7a10 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,155 @@ +from sanic import Sanic +from sanic.response import text, HTTPResponse +from sanic.views import MethodView +from sanic.blueprints import Blueprint +from sanic.request import Request +from sanic.utils import sanic_endpoint_test + + +def test_methods(): + app = Sanic('test_methods') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def post(self, request, *args, **kwargs): + return text('I am post method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + + def patch(self, request, *args, **kwargs): + return text('I am patch method') + + def delete(self, request, *args, **kwargs): + return text('I am delete method') + + app.add_route(DummyView(), '/') + + request, response = sanic_endpoint_test(app, method="get") + assert response.text == 'I am get method' + request, response = sanic_endpoint_test(app, method="post") + assert response.text == 'I am post method' + request, response = sanic_endpoint_test(app, method="put") + assert response.text == 'I am put method' + request, response = sanic_endpoint_test(app, method="patch") + assert response.text == 'I am patch method' + request, response = sanic_endpoint_test(app, method="delete") + assert response.text == 'I am delete method' + + +def test_unexisting_methods(): + app = Sanic('test_unexisting_methods') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + app.add_route(DummyView(), '/') + request, response = sanic_endpoint_test(app, method="get") + assert response.text == 'I am get method' + request, response = sanic_endpoint_test(app, method="post") + assert response.text == 'Error: Method POST not allowed for URL /' + + +def test_argument_methods(): + app = Sanic('test_argument_methods') + + class DummyView(MethodView): + + def get(self, request, my_param_here, *args, **kwargs): + return text('I am get method with %s' % my_param_here) + + app.add_route(DummyView(), '/') + + request, response = sanic_endpoint_test(app, uri='/test123') + + assert response.text == 'I am get method with test123' + + +def test_with_bp(): + app = Sanic('test_with_bp') + bp = Blueprint('test_text') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + bp.add_route(DummyView(), '/') + + app.blueprint(bp) + request, response = sanic_endpoint_test(app) + + assert response.text == 'I am get method' + + +def test_with_bp_with_url_prefix(): + app = Sanic('test_with_bp_with_url_prefix') + bp = Blueprint('test_text', url_prefix='/test1') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + bp.add_route(DummyView(), '/') + + app.blueprint(bp) + request, response = sanic_endpoint_test(app, uri='/test1/') + + assert response.text == 'I am get method' + + +def test_with_middleware(): + app = Sanic('test_with_middleware') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + app.add_route(DummyView(), '/') + + results = [] + + @app.middleware + async def handler(request): + results.append(request) + + request, response = sanic_endpoint_test(app) + + assert response.text == 'I am get method' + assert type(results[0]) is Request + + +def test_with_middleware_response(): + app = Sanic('test_with_middleware_response') + + results = [] + + @app.middleware('request') + async def process_response(request): + results.append(request) + + @app.middleware('response') + async def process_response(request, response): + results.append(request) + results.append(response) + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + app.add_route(DummyView(), '/') + + request, response = sanic_endpoint_test(app) + + assert response.text == 'I am get method' + assert type(results[0]) is Request + assert type(results[1]) is Request + assert issubclass(type(results[2]), HTTPResponse) From fca0221d911dac1f53dd94b60ed51f06835472b8 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:14:37 +0200 Subject: [PATCH 02/30] update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b6d87de..2ece97d8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ app.run(host="0.0.0.0", port=8000) * [Middleware](docs/middleware.md) * [Exceptions](docs/exceptions.md) * [Blueprints](docs/blueprints.md) + * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) * [Deploying](docs/deploying.md) @@ -72,7 +73,7 @@ app.run(host="0.0.0.0", port=8000) ▄▄▄▄▄ ▀▀▀██████▄▄▄ _______________ ▄▄▄▄▄ █████████▄ / \ - ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | + ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | ▀▀█████▄▄ ▀██████▄██ | _________________/ ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ ▀▀▀▄ ▀▀███ ▀ ▄▄ From c3c7964e2e49fc5bac044551700365eeaf8ba006 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:29:25 +0200 Subject: [PATCH 03/30] pep8 fixes --- sanic/sanic.py | 4 +++- sanic/utils.py | 6 ++++-- sanic/views.py | 7 +++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 7a9c35ad..33e16af7 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -62,7 +62,9 @@ class Sanic: def add_route(self, handler, uri, methods=None): """ - A helper method to register class instance or functions as a handler to the application url routes. + 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 diff --git a/sanic/utils.py b/sanic/utils.py index 8190c1d0..5d896312 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,7 +16,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, - loop=None, debug=False, *request_args, **request_kwargs): + loop=None, debug=False, *request_args, + **request_kwargs): results = [] exceptions = [] @@ -34,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, debug=debug, port=42101, after_start=_collect_response, loop=loop) + app.run(host=HOST, debug=debug, port=42101, + after_start=_collect_response, loop=loop) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/sanic/views.py b/sanic/views.py index 9cb04247..2c4dcce2 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -3,7 +3,8 @@ from .exceptions import InvalidUsage class MethodView: """ Simple class based implementation of view for the sanic. - You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. + You should implement methods(get, post, put, patch, delete) for the class + to every HTTP method you want to support. For example: class DummyView(View): @@ -30,4 +31,6 @@ class MethodView: 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) + raise InvalidUsage( + 'Method {} not allowed for URL {}'.format( + request.method, request.url), status_code=405) From 13808bf282493fccf81458da129514d5e28e88ac Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Fri, 25 Nov 2016 14:53:18 -0500 Subject: [PATCH 04/30] Convert server lambda to partial Partials are faster then lambdas for repeated calls. --- sanic/server.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index b6233031..edc96968 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -235,14 +235,23 @@ def serve(host, port, request_handler, before_start=None, after_start=None, connections = {} signal = Signal() - server_coroutine = loop.create_server(lambda: HttpProtocol( + server = partial( + HttpProtocol, loop=loop, connections=connections, signal=signal, request_handler=request_handler, request_timeout=request_timeout, request_max_size=request_max_size, - ), host, port, reuse_port=reuse_port, sock=sock) + ) + + server_coroutine = loop.create_server( + server, + host, + port, + reuse_port=reuse_port, + sock=sock + ) # Instead of pulling time at the end of every request, # pull it once per minute From 0ca5c4eeff5e4eab6f986703acc7f3b7f3ef18c2 Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Fri, 25 Nov 2016 14:33:17 -0500 Subject: [PATCH 05/30] Use explicit import for httptools Explicit importing the parser and the exception to save a name lookup. --- sanic/server.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index b6233031..6301d18f 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -4,7 +4,8 @@ from inspect import isawaitable from multidict import CIMultiDict from signal import SIGINT, SIGTERM from time import time -import httptools +from httptools import HttpRequestParser +from httptools.parser.errors import HttpParserError try: import uvloop as async_loop @@ -94,12 +95,12 @@ class HttpProtocol(asyncio.Protocol): if self.parser is None: assert self.request is None self.headers = [] - self.parser = httptools.HttpRequestParser(self) + self.parser = HttpRequestParser(self) # Parse request chunk or close connection try: self.parser.feed_data(data) - except httptools.parser.errors.HttpParserError as e: + except HttpParserError as e: self.bail_out( "Invalid request data, connection closed ({})".format(e)) From c01cbb3a8c93e90fd22a3a58ec93804ee369a5dc Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 13:55:45 +0900 Subject: [PATCH 06/30] Change Request timeout process This add a request timeout exception. It cancels task, when request is timeout. --- examples/request_timeout.py | 21 ++++++++++++++++++ sanic/exceptions.py | 4 ++++ sanic/sanic.py | 1 + sanic/server.py | 28 ++++++++++++++++++------ tests/test_request_timeout.py | 40 +++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 examples/request_timeout.py create mode 100644 tests/test_request_timeout.py diff --git a/examples/request_timeout.py b/examples/request_timeout.py new file mode 100644 index 00000000..496864cd --- /dev/null +++ b/examples/request_timeout.py @@ -0,0 +1,21 @@ +from sanic import Sanic +import asyncio +from sanic.response import text +from sanic.config import Config +from sanic.exceptions import RequestTimeout + +Config.REQUEST_TIMEOUT = 1 +app = Sanic(__name__) + + +@app.route("/") +async def test(request): + await asyncio.sleep(3) + return text('Hello, world!') + + +@app.exception(RequestTimeout) +def timeout(request, exception): + return text('RequestTimeout from error_handler.') + +app.run(host="0.0.0.0", port=8000) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index e21aca63..bc052fbd 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -30,6 +30,10 @@ class FileNotFound(NotFound): self.relative_url = relative_url +class RequestTimeout(SanicException): + status_code = 408 + + class Handler: handlers = None diff --git a/sanic/sanic.py b/sanic/sanic.py index af284c00..128e3d28 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -250,6 +250,7 @@ class Sanic: 'sock': sock, 'debug': debug, 'request_handler': self.handle_request, + 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, 'loop': loop diff --git a/sanic/server.py b/sanic/server.py index b6233031..4b804353 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -13,6 +13,8 @@ except ImportError: from .log import log from .request import Request +from .response import HTTPResponse +from .exceptions import RequestTimeout class Signal: @@ -33,8 +35,8 @@ class HttpProtocol(asyncio.Protocol): # connection management '_total_request_size', '_timeout_handler', '_last_communication_time') - def __init__(self, *, loop, request_handler, signal=Signal(), - connections={}, request_timeout=60, + def __init__(self, *, loop, request_handler, error_handler, + signal=Signal(), connections={}, request_timeout=60, request_max_size=None): self.loop = loop self.transport = None @@ -45,11 +47,13 @@ class HttpProtocol(asyncio.Protocol): self.signal = signal self.connections = connections self.request_handler = request_handler + self.error_handler = error_handler self.request_timeout = request_timeout self.request_max_size = request_max_size self._total_request_size = 0 self._timeout_handler = None self._last_request_time = None + self._request_handler_task = None # -------------------------------------------- # # Connection @@ -75,7 +79,17 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = \ self.loop.call_later(time_left, self.connection_timeout) else: - self.bail_out("Request timed out, connection closed") + self._request_handler_task.cancel() + try: + response = self.error_handler.response( + self.request, RequestTimeout('Request Timeout')) + except Exception as e: + response = HTTPResponse( + 'Request Timeout', RequestTimeout.status_code) + self.transport.write( + response.output( + self.request.version, False, self.request_timeout)) + self.transport.close() # -------------------------------------------- # # Parsing @@ -132,7 +146,7 @@ class HttpProtocol(asyncio.Protocol): self.request.body = body def on_message_complete(self): - self.loop.create_task( + self._request_handler_task = self.loop.create_task( self.request_handler(self.request, self.write_response)) # -------------------------------------------- # @@ -165,6 +179,7 @@ class HttpProtocol(asyncio.Protocol): self.request = None self.url = None self.headers = None + self._request_handler_task = None self._total_request_size = 0 def close_if_idle(self): @@ -204,8 +219,8 @@ def trigger_events(events, loop): loop.run_until_complete(result) -def serve(host, port, request_handler, before_start=None, after_start=None, - before_stop=None, after_stop=None, +def serve(host, port, request_handler, error_handler, before_start=None, + after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, request_max_size=None, reuse_port=False, loop=None): """ @@ -240,6 +255,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, connections=connections, signal=signal, request_handler=request_handler, + error_handler=error_handler, request_timeout=request_timeout, request_max_size=request_max_size, ), host, port, reuse_port=reuse_port, sock=sock) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py new file mode 100644 index 00000000..8cf3a680 --- /dev/null +++ b/tests/test_request_timeout.py @@ -0,0 +1,40 @@ +from sanic import Sanic +import asyncio +from sanic.response import text +from sanic.exceptions import RequestTimeout +from sanic.utils import sanic_endpoint_test +from sanic.config import Config + +Config.REQUEST_TIMEOUT = 1 +request_timeout_app = Sanic('test_request_timeout') +request_timeout_default_app = Sanic('test_request_timeout_default') + + +@request_timeout_app.route('/1') +async def handler_1(request): + await asyncio.sleep(2) + return text('OK') + + +@request_timeout_app.exception(RequestTimeout) +def handler_exception(request, exception): + return text('Request Timeout from error_handler.', 408) + + +def test_server_error_request_timeout(): + request, response = sanic_endpoint_test(request_timeout_app, uri='/1') + assert response.status == 408 + assert response.text == 'Request Timeout from error_handler.' + + +@request_timeout_default_app.route('/1') +async def handler_2(request): + await asyncio.sleep(2) + return text('OK') + + +def test_default_server_error_request_timeout(): + request, response = sanic_endpoint_test( + request_timeout_default_app, uri='/1') + assert response.status == 408 + assert response.text == 'Error: Request Timeout' From 0bd61f6a57948c94cb9deff0e378b95fc8f0cb4f Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 14:14:30 +0900 Subject: [PATCH 07/30] Use write_response --- sanic/server.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 4b804353..339b8132 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -86,10 +86,7 @@ class HttpProtocol(asyncio.Protocol): except Exception as e: response = HTTPResponse( 'Request Timeout', RequestTimeout.status_code) - self.transport.write( - response.output( - self.request.version, False, self.request_timeout)) - self.transport.close() + self.write_response(response) # -------------------------------------------- # # Parsing From d8e480ab4889891906807696f36180086f00aa70 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 14:47:42 +0900 Subject: [PATCH 08/30] Change sleep time --- examples/request_timeout.py | 4 ++-- tests/test_request_timeout.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/request_timeout.py b/examples/request_timeout.py index 496864cd..ddae7688 100644 --- a/examples/request_timeout.py +++ b/examples/request_timeout.py @@ -8,7 +8,7 @@ Config.REQUEST_TIMEOUT = 1 app = Sanic(__name__) -@app.route("/") +@app.route('/') async def test(request): await asyncio.sleep(3) return text('Hello, world!') @@ -18,4 +18,4 @@ async def test(request): def timeout(request, exception): return text('RequestTimeout from error_handler.') -app.run(host="0.0.0.0", port=8000) +app.run(host='0.0.0.0', port=8000) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 8cf3a680..7b8cfb21 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -12,7 +12,7 @@ request_timeout_default_app = Sanic('test_request_timeout_default') @request_timeout_app.route('/1') async def handler_1(request): - await asyncio.sleep(2) + await asyncio.sleep(1) return text('OK') @@ -29,7 +29,7 @@ def test_server_error_request_timeout(): @request_timeout_default_app.route('/1') async def handler_2(request): - await asyncio.sleep(2) + await asyncio.sleep(1) return text('OK') From 9010a6573fea7f855b0597986248a2f3d79d1ba4 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 15:21:57 +0900 Subject: [PATCH 09/30] Add status code --- examples/request_timeout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/request_timeout.py b/examples/request_timeout.py index ddae7688..261f423a 100644 --- a/examples/request_timeout.py +++ b/examples/request_timeout.py @@ -16,6 +16,6 @@ async def test(request): @app.exception(RequestTimeout) def timeout(request, exception): - return text('RequestTimeout from error_handler.') + return text('RequestTimeout from error_handler.', 408) app.run(host='0.0.0.0', port=8000) From da4567eea5f84d208f7fbadb1bac7e310297a319 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 26 Nov 2016 08:44:46 +0200 Subject: [PATCH 10/30] changes in doc --- docs/class_based_views.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index b5f8ee02..c4ceeb0c 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -1,30 +1,30 @@ # Class based views -Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone try to use not implemented method, there will be 405 response. +Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response. ## Examples ```python from sanic import Sanic -from sanic.views import MethodView +from sanic.views import HTTPMethodView app = Sanic('some_name') -class SimpleView(MethodView): +class SimpleView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') - def post(self, request, *args, **kwargs): + def post(self, request): return text('I am post method') - def put(self, request, *args, **kwargs): + def put(self, request): return text('I am put method') - def patch(self, request, *args, **kwargs): + def patch(self, request): return text('I am patch method') - def delete(self, request, *args, **kwargs): + def delete(self, request): return text('I am delete method') app.add_route(SimpleView(), '/') @@ -34,9 +34,9 @@ app.add_route(SimpleView(), '/') If you need any url params just mention them in method definition: ```python -class NameView(MethodView): +class NameView(HTTPMethodView): - def get(self, request, name, *args, **kwargs): + def get(self, request, name): return text('Hello {}'.format(name)) app.add_route(NameView(), '/ Date: Sat, 26 Nov 2016 08:45:08 +0200 Subject: [PATCH 11/30] rename&remove redundant code --- sanic/views.py | 2 +- tests/test_views.py | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sanic/views.py b/sanic/views.py index 2c4dcce2..980a5f74 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -1,7 +1,7 @@ from .exceptions import InvalidUsage -class MethodView: +class HTTPMethodView: """ Simple class based implementation of view for the sanic. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. diff --git a/tests/test_views.py b/tests/test_views.py index 251b7a10..59acb847 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,6 +1,6 @@ from sanic import Sanic from sanic.response import text, HTTPResponse -from sanic.views import MethodView +from sanic.views import HTTPMethodView from sanic.blueprints import Blueprint from sanic.request import Request from sanic.utils import sanic_endpoint_test @@ -9,21 +9,21 @@ from sanic.utils import sanic_endpoint_test def test_methods(): app = Sanic('test_methods') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') - def post(self, request, *args, **kwargs): + def post(self, request): return text('I am post method') - def put(self, request, *args, **kwargs): + def put(self, request): return text('I am put method') - def patch(self, request, *args, **kwargs): + def patch(self, request): return text('I am patch method') - def delete(self, request, *args, **kwargs): + def delete(self, request): return text('I am delete method') app.add_route(DummyView(), '/') @@ -43,9 +43,9 @@ def test_methods(): def test_unexisting_methods(): app = Sanic('test_unexisting_methods') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') app.add_route(DummyView(), '/') @@ -58,9 +58,9 @@ def test_unexisting_methods(): def test_argument_methods(): app = Sanic('test_argument_methods') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, my_param_here, *args, **kwargs): + def get(self, request, my_param_here): return text('I am get method with %s' % my_param_here) app.add_route(DummyView(), '/') @@ -74,9 +74,9 @@ def test_with_bp(): app = Sanic('test_with_bp') bp = Blueprint('test_text') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') bp.add_route(DummyView(), '/') @@ -91,9 +91,9 @@ def test_with_bp_with_url_prefix(): app = Sanic('test_with_bp_with_url_prefix') bp = Blueprint('test_text', url_prefix='/test1') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') bp.add_route(DummyView(), '/') @@ -107,9 +107,9 @@ def test_with_bp_with_url_prefix(): def test_with_middleware(): app = Sanic('test_with_middleware') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') app.add_route(DummyView(), '/') @@ -140,9 +140,9 @@ def test_with_middleware_response(): results.append(request) results.append(response) - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') app.add_route(DummyView(), '/') From a5e6d6d2e8a9e3f879fba5cd0d8e175c774e5ceb Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 16:02:44 +0900 Subject: [PATCH 12/30] Use default error process --- sanic/server.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 339b8132..b68524f8 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -13,7 +13,6 @@ except ImportError: from .log import log from .request import Request -from .response import HTTPResponse from .exceptions import RequestTimeout @@ -80,12 +79,8 @@ class HttpProtocol(asyncio.Protocol): self.loop.call_later(time_left, self.connection_timeout) else: self._request_handler_task.cancel() - try: - response = self.error_handler.response( - self.request, RequestTimeout('Request Timeout')) - except Exception as e: - response = HTTPResponse( - 'Request Timeout', RequestTimeout.status_code) + response = self.error_handler.response( + self.request, RequestTimeout('Request Timeout')) self.write_response(response) # -------------------------------------------- # From ee89b6ad03839bbe526f7e84958f9720e9a30e3f Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 16:47:16 +0900 Subject: [PATCH 13/30] before process --- sanic/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index b68524f8..dd582325 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -78,7 +78,8 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = \ self.loop.call_later(time_left, self.connection_timeout) else: - self._request_handler_task.cancel() + if self._request_handler_task: + self._request_handler_task.cancel() response = self.error_handler.response( self.request, RequestTimeout('Request Timeout')) self.write_response(response) From d86ac5e3e03f3a1469e9f4e1bce9af1919fbaa54 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Sat, 26 Nov 2016 11:20:29 -0500 Subject: [PATCH 14/30] fix for cookie header capitalization bug --- sanic/request.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index 8023fd9c..676eaa51 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -114,6 +114,8 @@ class Request(dict): @property def cookies(self): if self._cookies is None: + if 'cookie' in self.headers: #HTTP2 cookie header + self.headers['Cookie'] = self.headers.pop('cookie') if 'Cookie' in self.headers: cookies = SimpleCookie() cookies.load(self.headers['Cookie']) From 0c215685f2e08ef4d5ff3b16d849194d81557133 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Sun, 27 Nov 2016 08:30:46 -0500 Subject: [PATCH 15/30] refactoring cookies --- sanic/request.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 676eaa51..e5da4ce3 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -114,11 +114,10 @@ class Request(dict): @property def cookies(self): if self._cookies is None: - if 'cookie' in self.headers: #HTTP2 cookie header - self.headers['Cookie'] = self.headers.pop('cookie') - if 'Cookie' in self.headers: + cookie = self.headers.get('Cookie') or self.headers.get('cookie') + if cookie is not None: cookies = SimpleCookie() - cookies.load(self.headers['Cookie']) + cookies.load(cookie) self._cookies = {name: cookie.value for name, cookie in cookies.items()} else: From 190b7a607610551d1071dae5ee682c45efb7c00a Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:00:39 -0500 Subject: [PATCH 16/30] improving comments and examples --- sanic/request.py | 4 ++-- sanic/router.py | 12 +++++++++--- sanic/utils.py | 4 ++-- sanic/views.py | 8 +++++--- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 8023fd9c..bc7fcabb 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -67,7 +67,7 @@ class Request(dict): try: self.parsed_json = json_loads(self.body) except Exception: - log.exception("failed when parsing body as json") + log.exception("Failed when parsing body as json") return self.parsed_json @@ -89,7 +89,7 @@ class Request(dict): self.parsed_form, self.parsed_files = ( parse_multipart_form(self.body, boundary)) except Exception: - log.exception("failed when parsing form") + log.exception("Failed when parsing form") return self.parsed_form diff --git a/sanic/router.py b/sanic/router.py index 8392dcd8..0a1faec5 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -30,11 +30,17 @@ class Router: @sanic.route('/my/url/', methods=['GET', 'POST', ...]) def my_route(request, my_parameter): do stuff... + or + @sanic.route('/my/url/:int', methods['GET', 'POST', ...]) + def my_route_with_type(request, my_parameter): + do stuff... Parameters will be passed as keyword arguments to the request handling - function provided Parameters can also have a type by appending :type to - the . If no type is provided, a string is expected. A regular - expression can also be passed in as the type + function. Provided parameters can also have a type by appending :type to + the . Given parameter must be able to be type-casted to this. + If no type is provided, a string is expected. A regular expression can + also be passed in as the type. The argument given to the function will + always be a string, independent of the type. """ routes_static = None routes_dynamic = None diff --git a/sanic/utils.py b/sanic/utils.py index 5d896312..88444b3c 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -47,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, return request, response except: raise ValueError( - "request and response object expected, got ({})".format( + "Request and response object expected, got ({})".format( results)) else: try: return results[0] except: raise ValueError( - "request object expected, got ({})".format(results)) + "Request object expected, got ({})".format(results)) diff --git a/sanic/views.py b/sanic/views.py index 980a5f74..440702bd 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -3,8 +3,9 @@ from .exceptions import InvalidUsage class HTTPMethodView: """ Simple class based implementation of view for the sanic. - You should implement methods(get, post, put, patch, delete) for the class + You should implement methods (get, post, put, patch, delete) for the class to every HTTP method you want to support. + For example: class DummyView(View): @@ -14,9 +15,10 @@ class HTTPMethodView: def put(self, request, *args, **kwargs): return text('I am put method') etc. - If someone try use not implemented method, there will be 405 response - If you need any url params just mention them in method definition like: + If someone tries to use a non-implemented method, there will be a 405 response. + + If you need any url params just mention them in method definition: class DummyView(View): def get(self, request, my_param_here, *args, **kwargs): From 209b7633025e63f1c6f163f3024139d50bd9dabd Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:05:47 -0500 Subject: [PATCH 17/30] fix typo --- sanic/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index 0a1faec5..4cc1f073 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -31,7 +31,7 @@ class Router: def my_route(request, my_parameter): do stuff... or - @sanic.route('/my/url/:int', methods['GET', 'POST', ...]) + @sanic.route('/my/url/:type', methods['GET', 'POST', ...]) def my_route_with_type(request, my_parameter): do stuff... From 70c56b7db33b558ff5fd5a95883fcb558b9621ba Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:22:07 -0500 Subject: [PATCH 18/30] fixing line length --- sanic/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/views.py b/sanic/views.py index 440702bd..9387bcf6 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -16,7 +16,8 @@ class HTTPMethodView: return text('I am put method') etc. - If someone tries to use a non-implemented method, there will be a 405 response. + 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): From 39f3a63cede12ee40e17073a95578d37a730c158 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 29 Nov 2016 15:59:03 -0600 Subject: [PATCH 19/30] Increment version to 0.1.8 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index d8a9e56e..6e7f8d23 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.7' +__version__ = '0.1.8' __all__ = ['Sanic', 'Blueprint'] From 9b466db5c9d1d717cfb136cacb741f59333445c3 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Sat, 3 Dec 2016 15:19:24 -0500 Subject: [PATCH 20/30] test for http2 lowercase header cookies --- tests/test_cookies.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 5b27c2e7..cf6a4259 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -25,6 +25,19 @@ def test_cookies(): assert response.text == 'Cookies are: working!' assert response_cookies['right_back'].value == 'at you' +def test_http2_cookies(): + app = Sanic('test_http2_cookies') + + @app.route('/') + async def handler(request): + response = text('Cookies are: {}'.format(request.cookies['test'])) + return response + + headers = {'cookie': 'test=working!'} + request, response = sanic_endpoint_test(app, headers=headers) + + assert response.text == 'Cookies are: working!' + def test_cookie_options(): app = Sanic('test_text') From 662e0c9965b71a1b18bf689b3ca972ce36ec2ec2 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sun, 4 Dec 2016 10:50:32 +0900 Subject: [PATCH 21/30] Change Payload Too Large process When Payload Too Large occurs, it uses error handler. --- sanic/exceptions.py | 4 +++ sanic/server.py | 26 ++++++++++------ tests/test_payload_too_large.py | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 tests/test_payload_too_large.py diff --git a/sanic/exceptions.py b/sanic/exceptions.py index bc052fbd..369a87a2 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -34,6 +34,10 @@ class RequestTimeout(SanicException): status_code = 408 +class PayloadTooLarge(SanicException): + status_code = 413 + + class Handler: handlers = None diff --git a/sanic/server.py b/sanic/server.py index a3074ecf..534436fa 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -14,7 +14,7 @@ except ImportError: from .log import log from .request import Request -from .exceptions import RequestTimeout +from .exceptions import RequestTimeout, PayloadTooLarge class Signal: @@ -81,9 +81,8 @@ class HttpProtocol(asyncio.Protocol): else: if self._request_handler_task: self._request_handler_task.cancel() - response = self.error_handler.response( - self.request, RequestTimeout('Request Timeout')) - self.write_response(response) + exception = RequestTimeout('Request Timeout') + self.write_error(exception) # -------------------------------------------- # # Parsing @@ -94,9 +93,8 @@ class HttpProtocol(asyncio.Protocol): # memory limits self._total_request_size += len(data) if self._total_request_size > self.request_max_size: - return self.bail_out( - "Request too large ({}), connection closed".format( - self._total_request_size)) + exception = PayloadTooLarge('Payload Too Large') + self.write_error(exception) # Create parser if this is the first time we're receiving data if self.parser is None: @@ -116,8 +114,8 @@ class HttpProtocol(asyncio.Protocol): def on_header(self, name, value): if name == b'Content-Length' and int(value) > self.request_max_size: - return self.bail_out( - "Request body too large ({}), connection closed".format(value)) + exception = PayloadTooLarge('Payload Too Large') + self.write_error(exception) self.headers.append((name.decode(), value.decode('utf-8'))) @@ -164,6 +162,16 @@ class HttpProtocol(asyncio.Protocol): self.bail_out( "Writing response failed, connection closed {}".format(e)) + def write_error(self, exception): + try: + response = self.error_handler.response(self.request, exception) + version = self.request.version if self.request else '1.1' + self.transport.write(response.output(version)) + self.transport.close() + except Exception as e: + self.bail_out( + "Writing error failed, connection closed {}".format(e)) + def bail_out(self, message): log.debug(message) self.transport.close() diff --git a/tests/test_payload_too_large.py b/tests/test_payload_too_large.py new file mode 100644 index 00000000..e8eec09e --- /dev/null +++ b/tests/test_payload_too_large.py @@ -0,0 +1,54 @@ +from sanic import Sanic +from sanic.response import text +from sanic.exceptions import PayloadTooLarge +from sanic.utils import sanic_endpoint_test + +data_received_app = Sanic('data_received') +data_received_app.config.REQUEST_MAX_SIZE = 1 +data_received_default_app = Sanic('data_received_default') +data_received_default_app.config.REQUEST_MAX_SIZE = 1 +on_header_default_app = Sanic('on_header') +on_header_default_app.config.REQUEST_MAX_SIZE = 500 + + +@data_received_app.route('/1') +async def handler1(request): + return text('OK') + + +@data_received_app.exception(PayloadTooLarge) +def handler_exception(request, exception): + return text('Payload Too Large from error_handler.', 413) + + +def test_payload_too_large_from_error_handler(): + response = sanic_endpoint_test( + data_received_app, uri='/1', gather_request=False) + assert response.status == 413 + assert response.text == 'Payload Too Large from error_handler.' + + +@data_received_default_app.route('/1') +async def handler2(request): + return text('OK') + + +def test_payload_too_large_at_data_received_default(): + response = sanic_endpoint_test( + data_received_default_app, uri='/1', gather_request=False) + assert response.status == 413 + assert response.text == 'Error: Payload Too Large' + + +@on_header_default_app.route('/1') +async def handler3(request): + return text('OK') + + +def test_payload_too_large_at_on_header_default(): + data = 'a' * 1000 + response = sanic_endpoint_test( + on_header_default_app, method='post', uri='/1', + gather_request=False, data=data) + assert response.status == 413 + assert response.text == 'Error: Payload Too Large' From fac4bca4f49c3dc34ff57851d59fba64790d1e31 Mon Sep 17 00:00:00 2001 From: 1a23456789 Date: Tue, 6 Dec 2016 10:44:08 +0900 Subject: [PATCH 22/30] Fix test_request_timeout.py This increases sleep time, Because sometimes timeout error does not occur. --- tests/test_request_timeout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 7b8cfb21..8cf3a680 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -12,7 +12,7 @@ request_timeout_default_app = Sanic('test_request_timeout_default') @request_timeout_app.route('/1') async def handler_1(request): - await asyncio.sleep(1) + await asyncio.sleep(2) return text('OK') @@ -29,7 +29,7 @@ def test_server_error_request_timeout(): @request_timeout_default_app.route('/1') async def handler_2(request): - await asyncio.sleep(1) + await asyncio.sleep(2) return text('OK') From 457507d8dc2772f59a144f713f01bd15f73183eb Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 7 Dec 2016 20:33:56 -0800 Subject: [PATCH 23/30] return 400 on invalid json post data --- sanic/request.py | 3 ++- tests/test_requests.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index d3c11cd0..62d89781 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -4,6 +4,7 @@ from http.cookies import SimpleCookie from httptools import parse_url from urllib.parse import parse_qs from ujson import loads as json_loads +from sanic.exceptions import InvalidUsage from .log import log @@ -67,7 +68,7 @@ class Request(dict): try: self.parsed_json = json_loads(self.body) except Exception: - log.exception("Failed when parsing body as json") + raise InvalidUsage("Failed when parsing body as json") return self.parsed_json diff --git a/tests/test_requests.py b/tests/test_requests.py index 756113b2..81895c8c 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -49,6 +49,19 @@ def test_json(): assert results.get('test') == True +def test_invalid_json(): + app = Sanic('test_json') + + @app.route('/') + async def handler(request): + return json(request.json()) + + data = "I am not json" + request, response = sanic_endpoint_test(app, data=data) + + assert response.status == 400 + + def test_query_string(): app = Sanic('test_query_string') From 154f8570f073c94bec55f1c2681162d6b1beec16 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 13:43:31 +0200 Subject: [PATCH 24/30] add sanic aiopg example with raw sql --- examples/sanic_aiopg_example.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/sanic_aiopg_example.py diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py new file mode 100644 index 00000000..7f4901e6 --- /dev/null +++ b/examples/sanic_aiopg_example.py @@ -0,0 +1,58 @@ +import os +import asyncio +import datetime + +import uvloop +import aiopg + +from sanic import Sanic +from sanic.response import json + +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + +database_name = os.environ['DATABASE_NAME'] +database_host = os.environ['DATABASE_HOST'] +database_user = os.environ['DATABASE_USER'] +database_password = os.environ['DATABASE_PASSWORD'] + +connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, database_password, database_host, database_name) +loop = asyncio.get_event_loop() + + +async def get_pool(): + return await aiopg.create_pool(connection) + +app = Sanic(name=__name__) +pool = loop.run_until_complete(get_pool()) + + +async def prepare_db(): + """ Let's create some table and add some data + + """ + async with pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute("""CREATE TABLE sanic_poll ( + id integer primary key, + question varchar(50), + pub_date timestamp + );""") + for i in range(0, 100): + await cur.execute("""INSERT INTO sanic_poll (id, question, pub_date) VALUES ({}, {}, now()) + """.format(i, i)) + + +@app.route("/") +async def handle(request): + async with pool.acquire() as conn: + async with conn.cursor() as cur: + result = [] + await cur.execute("SELECT question, pub_date FROM sanic_poll") + async for row in cur: + result.append({"question": row[0], "pub_date": row[1]}) + return json({"polls": result}) + + +if __name__ == '__main__': + loop.run_until_complete(prepare_db()) + app.run(host="0.0.0.0", port=8100, workers=3, loop=loop) From 721044b3786f4ac190256a3d0d870cf51077e2a8 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 14:04:24 +0200 Subject: [PATCH 25/30] improvements for aiopg example --- examples/sanic_aiopg_example.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index 7f4901e6..539917df 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -1,6 +1,5 @@ import os import asyncio -import datetime import uvloop import aiopg @@ -32,13 +31,14 @@ async def prepare_db(): """ async with pool.acquire() as conn: async with conn.cursor() as cur: - await cur.execute("""CREATE TABLE sanic_poll ( + await cur.execute('DROP TABLE IF EXISTS sanic_polls') + await cur.execute("""CREATE TABLE sanic_polls ( id integer primary key, question varchar(50), pub_date timestamp );""") for i in range(0, 100): - await cur.execute("""INSERT INTO sanic_poll (id, question, pub_date) VALUES ({}, {}, now()) + await cur.execute("""INSERT INTO sanic_polls (id, question, pub_date) VALUES ({}, {}, now()) """.format(i, i)) @@ -47,7 +47,7 @@ async def handle(request): async with pool.acquire() as conn: async with conn.cursor() as cur: result = [] - await cur.execute("SELECT question, pub_date FROM sanic_poll") + await cur.execute("SELECT question, pub_date FROM sanic_polls") async for row in cur: result.append({"question": row[0], "pub_date": row[1]}) return json({"polls": result}) From f9176bfdea547bcc3202316c3eb1fd563ace01e7 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 14:14:03 +0200 Subject: [PATCH 26/30] pep8&improvements --- examples/sanic_aiopg_example.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index 539917df..ff9ec65e 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -14,7 +14,10 @@ database_host = os.environ['DATABASE_HOST'] database_user = os.environ['DATABASE_USER'] database_password = os.environ['DATABASE_PASSWORD'] -connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, database_password, database_host, database_name) +connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, + database_password, + database_host, + database_name) loop = asyncio.get_event_loop() @@ -33,12 +36,13 @@ async def prepare_db(): async with conn.cursor() as cur: await cur.execute('DROP TABLE IF EXISTS sanic_polls') await cur.execute("""CREATE TABLE sanic_polls ( - id integer primary key, + id serial primary key, question varchar(50), pub_date timestamp );""") for i in range(0, 100): - await cur.execute("""INSERT INTO sanic_polls (id, question, pub_date) VALUES ({}, {}, now()) + await cur.execute("""INSERT INTO sanic_polls + (id, question, pub_date) VALUES ({}, {}, now()) """.format(i, i)) From b44e9baaecee1ec757409e9d1ce263f58e22fc86 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 14:21:02 +0200 Subject: [PATCH 27/30] aiopg with sqlalchemy example --- examples/sanic_aiopg_example.py | 5 +- examples/sanic_aiopg_sqlalchemy_example.py | 73 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 examples/sanic_aiopg_sqlalchemy_example.py diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index ff9ec65e..73ef6c64 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -1,3 +1,6 @@ +""" To run this example you need additional aiopg package + +""" import os import asyncio @@ -59,4 +62,4 @@ async def handle(request): if __name__ == '__main__': loop.run_until_complete(prepare_db()) - app.run(host="0.0.0.0", port=8100, workers=3, loop=loop) + app.run(host='0.0.0.0', port=8000, loop=loop) diff --git a/examples/sanic_aiopg_sqlalchemy_example.py b/examples/sanic_aiopg_sqlalchemy_example.py new file mode 100644 index 00000000..cb9f6c57 --- /dev/null +++ b/examples/sanic_aiopg_sqlalchemy_example.py @@ -0,0 +1,73 @@ +""" To run this example you need additional aiopg package + +""" +import os +import asyncio +import datetime + +import uvloop +from aiopg.sa import create_engine +import sqlalchemy as sa + +from sanic import Sanic +from sanic.response import json + +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + +database_name = os.environ['DATABASE_NAME'] +database_host = os.environ['DATABASE_HOST'] +database_user = os.environ['DATABASE_USER'] +database_password = os.environ['DATABASE_PASSWORD'] + +connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, + database_password, + database_host, + database_name) +loop = asyncio.get_event_loop() + + +metadata = sa.MetaData() + +polls = sa.Table('sanic_polls', metadata, + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('question', sa.String(50)), + sa.Column("pub_date", sa.DateTime)) + + +async def get_engine(): + return await create_engine(connection) + +app = Sanic(name=__name__) +engine = loop.run_until_complete(get_engine()) + + +async def prepare_db(): + """ Let's add some data + + """ + async with engine.acquire() as conn: + await conn.execute('DROP TABLE IF EXISTS sanic_polls') + await conn.execute("""CREATE TABLE sanic_polls ( + id serial primary key, + question varchar(50), + pub_date timestamp + );""") + for i in range(0, 100): + await conn.execute( + polls.insert().values(question=i, + pub_date=datetime.datetime.now()) + ) + + +@app.route("/") +async def handle(request): + async with engine.acquire() as conn: + result = [] + async for row in conn.execute(polls.select()): + result.append({"question": row.question, "pub_date": row.pub_date}) + return json({"polls": result}) + + +if __name__ == '__main__': + loop.run_until_complete(prepare_db()) + app.run(host='0.0.0.0', port=8000, loop=loop) From 6ef6d9a9051dca5d1896f3a03f2a8e13ce0bbc44 Mon Sep 17 00:00:00 2001 From: kamyar Date: Sun, 11 Dec 2016 16:34:22 +0200 Subject: [PATCH 28/30] url params docs typo fix add missing '>' in url params docs example --- docs/class_based_views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index c4ceeb0c..223304ae 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -39,6 +39,6 @@ class NameView(HTTPMethodView): def get(self, request, name): return text('Hello {}'.format(name)) -app.add_route(NameView(), '/') ``` From 93b45e9598cfa758559b1205ea399c6198bf3c73 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 12 Dec 2016 22:18:33 -0800 Subject: [PATCH 29/30] add jinja example --- examples/jinja_example.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 examples/jinja_example.py diff --git a/examples/jinja_example.py b/examples/jinja_example.py new file mode 100644 index 00000000..1f9bb1ba --- /dev/null +++ b/examples/jinja_example.py @@ -0,0 +1,18 @@ +## To use this example: +# curl -d '{"name": "John Doe"}' localhost:8000 + +from sanic import Sanic +from sanic.response import html +from jinja2 import Template + +template = Template('Hello {{ name }}!') + +app = Sanic(__name__) + +@app.route('/') +async def test(request): + data = request.json + return html(template.render(**data)) + + +app.run(host="0.0.0.0", port=8000) From 8957e4ec25fb2b6b1b20e36a78f5576b6a7736f1 Mon Sep 17 00:00:00 2001 From: Sam Agnew Date: Tue, 13 Dec 2016 12:35:46 -0500 Subject: [PATCH 30/30] Fix PEP8 in Hello World example --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ece97d8..669ed8d9 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,17 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E from sanic import Sanic from sanic.response import json + app = Sanic() + @app.route("/") async def test(request): return json({"hello": "world"}) -app.run(host="0.0.0.0", port=8000) +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000) + ``` ## Installation