From 6ea43d8e6d86ac2196de9e1e2fa71c94ea85a827 Mon Sep 17 00:00:00 2001 From: plasmashadow Date: Thu, 16 Feb 2017 10:58:52 +0530 Subject: [PATCH 01/68] Fixed sanic_peewee error and added a simple interface to query and create --- examples/sanic_peewee.py | 78 +++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index 8db8ddff..ab428ab9 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -10,8 +10,9 @@ from sanic.response import json ## peewee_async related imports import peewee -from peewee_async import Manager, PostgresqlDatabase - +from peewee import Model, BaseModel +from peewee_async import Manager, PostgresqlDatabase, execute +from functools import partial # we instantiate a custom loop so we can pass it to our db manager ## from peewee_async docs: @@ -19,42 +20,77 @@ from peewee_async import Manager, PostgresqlDatabase # with manager! It’s all automatic. But you can run Manager.connect() or # Manager.close() when you need it. +class AsyncManager(Manager): + """Inherit the peewee_async manager with our own object + configuration -# let's create a simple key value store: -class KeyValue(peewee.Model): - key = peewee.CharField(max_length=40, unique=True) - text = peewee.TextField(default='') + database.allow_sync = False + """ - class Meta: - database = database + def __init__(self, _model_class, *args, **kwargs): + super(AsyncManager, self).__init__(*args, **kwargs) + self._model_class = _model_class + self.database.allow_sync = False -# create table synchronously -KeyValue.create_table(True) + def _do_fill(self, method, *args, **kwargs): + _class_method = getattr(super(AsyncManager, self), method) + pf = partial(_class_method, self._model_class) + return pf(*args, **kwargs) -# OPTIONAL: close synchronous connection -database.close() + def new(self, *args, **kwargs): + return self._do_fill('create', *args, **kwargs) -# OPTIONAL: disable any future syncronous calls -objects.database.allow_sync = False # this will raise AssertionError on ANY sync call + def get(self, *args, **kwargs): + return self._do_fill('get', *args, **kwargs) + + def execute(self, query): + return execute(query) -app = Sanic('peewee_example') +def _get_meta_db_class(db): + """creating a declartive class model for db""" + class _BlockedMeta(BaseModel): + def __new__(cls, name, bases, attrs): + _instance = super(_BlockedMeta, cls).__new__(cls, name, bases, attrs) + _instance.objects = AsyncManager(_instance, db) + return _instance -@app.listener('before_server_start') -def setup(app, loop): - database = PostgresqlDatabase(database='test', + class _Base(Model, metaclass=_BlockedMeta): + + def to_dict(self): + return self._data + + class Meta: + database=db + return _Base + + +def declarative_base(*args, **kwargs): + """Returns a new Modeled Class after inheriting meta and Model classes""" + db = PostgresqlDatabase(*args, **kwargs) + return _get_meta_db_class(db) + + +AsyncBaseModel = declarative_base(database='test', host='127.0.0.1', user='postgres', password='mysecretpassword') - objects = Manager(database, loop=loop) +# let's create a simple key value store: +class KeyValue(AsyncBaseModel): + key = peewee.CharField(max_length=40, unique=True) + text = peewee.TextField(default='') + + +app = Sanic('peewee_example') + @app.route('/post//') async def post(request, key, value): """ Save get parameters to database """ - obj = await objects.create(KeyValue, key=key, text=value) + obj = await KeyValue.objects.create(key=key, text=value) return json({'object_id': obj.id}) @@ -63,7 +99,7 @@ async def get(request): """ Load all objects from database """ - all_objects = await objects.execute(KeyValue.select()) + all_objects = await KeyValue.objects.execute(KeyValue.select()) serialized_obj = [] for obj in all_objects: serialized_obj.append({ From e6a828572abffabd5ed0a51c15b134c1fe13bba5 Mon Sep 17 00:00:00 2001 From: sourcepirate Date: Thu, 16 Feb 2017 11:07:49 +0530 Subject: [PATCH 02/68] Changed method name from create to new --- examples/sanic_peewee.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index ab428ab9..aaa139f1 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -1,3 +1,4 @@ + ## You need the following additional packages for this example # aiopg # peewee_async @@ -90,7 +91,7 @@ async def post(request, key, value): """ Save get parameters to database """ - obj = await KeyValue.objects.create(key=key, text=value) + obj = await KeyValue.objects.new(key=key, text=value) return json({'object_id': obj.id}) From 2dca53a696064d5f3447484febb2294648b37d1f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 26 Feb 2017 16:31:39 -0800 Subject: [PATCH 03/68] remove stop_event --- sanic/app.py | 23 ++++++++++++++++++----- sanic/server.py | 20 ++++++++++++-------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 63700e9d..45e71ca0 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -464,19 +464,24 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ + if stop_event is not None: + if debug: + warnings.simplefilter('default') + warnings.warn("stop_event will be removed from future versions.", + DeprecationWarning) 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) + register_sys_signals=register_sys_signals) try: self.is_running = True if workers == 1: serve(**server_settings) else: - serve_multiple(server_settings, workers, stop_event) + serve_multiple(server_settings, workers) except: log.exception( 'Experienced exception while trying to serve') @@ -498,13 +503,17 @@ class Sanic: NOTE: This does not support multiprocessing and is not the preferred way to run a Sanic application. """ + if stop_event is not None: + if debug: + warnings.simplefilter('default') + warnings.warn("stop_event will be removed from future versions.", + DeprecationWarning) 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 or get_event_loop(), protocol=protocol, - backlog=backlog, stop_event=stop_event, - run_async=True) + backlog=backlog, run_async=True) return await serve(**server_settings) @@ -514,7 +523,11 @@ class Sanic: protocol=HttpProtocol, backlog=100, stop_event=None, register_sys_signals=True, run_async=False): """Helper function used by `run` and `create_server`.""" - + if stop_event is not None: + if debug: + warnings.simplefilter('default') + warnings.warn("stop_event will be removed from future versions.", + DeprecationWarning) if loop is not None: if debug: warnings.simplefilter('default') diff --git a/sanic/server.py b/sanic/server.py index 83bda3fc..9d0fa0c9 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -4,10 +4,13 @@ import traceback import warnings from functools import partial from inspect import isawaitable -from multiprocessing import Process, Event +from multiprocessing import Process from os import set_inheritable -from signal import SIGTERM, SIGINT -from signal import signal as signal_func +from signal import ( + SIGTERM, SIGINT, + signal as signal_func, + Signals +) from socket import socket, SOL_SOCKET, SO_REUSEADDR from time import time @@ -379,7 +382,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, loop.close() -def serve_multiple(server_settings, workers, stop_event=None): +def serve_multiple(server_settings, workers): """Start multiple server processes simultaneously. Stop on interrupt and terminate signals, and drain connections when complete. @@ -404,11 +407,12 @@ def serve_multiple(server_settings, workers, stop_event=None): server_settings['host'] = None server_settings['port'] = None - if stop_event is None: - stop_event = Event() + def sig_handler(signal, frame): + log.info("Received signal {}. Shutting down.".format( + Signals(signal).name)) - signal_func(SIGINT, lambda s, f: stop_event.set()) - signal_func(SIGTERM, lambda s, f: stop_event.set()) + signal_func(SIGINT, lambda s, f: sig_handler(s, f)) + signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) processes = [] for _ in range(workers): From 3af26540ecce99779aa054ec986c0842fa9329d5 Mon Sep 17 00:00:00 2001 From: Jing Su Date: Mon, 13 Mar 2017 13:28:35 +0800 Subject: [PATCH 04/68] add websocket scheme in request --- sanic/request.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index 68743c79..c8f14ca0 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -142,6 +142,10 @@ class Request(dict): @property def scheme(self): + if self.app.websocket_enabled \ + and self.headers.get('upgrade') == 'websocket': + return 'ws' + if self.transport.get_extra_info('sslcontext'): return 'https' From 48a26fd5df8a2ea71537479a0829a7e26c56706f Mon Sep 17 00:00:00 2001 From: messense Date: Mon, 13 Mar 2017 16:20:12 +0800 Subject: [PATCH 05/68] Add hyperlinks in response documentation --- docs/sanic/response.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/sanic/response.md b/docs/sanic/response.md index 627fbc7e..1a336d47 100644 --- a/docs/sanic/response.md +++ b/docs/sanic/response.md @@ -2,7 +2,7 @@ Use functions in `sanic.response` module to create responses. -- `text` - Plain text response +## Plain Text ```python from sanic import response @@ -13,7 +13,7 @@ def handle_request(request): return response.text('Hello world!') ``` -- `html` - HTML response +## HTML ```python from sanic import response @@ -24,7 +24,7 @@ def handle_request(request): return response.html('

Hello world!

') ``` -- `json` - JSON response +## JSON ```python @@ -36,7 +36,7 @@ def handle_request(request): return response.json({'message': 'Hello world!'}) ``` -- `file` - File response +## File ```python from sanic import response @@ -47,7 +47,7 @@ async def handle_request(request): return await response.file('/srv/www/whatever.png') ``` -- `stream` - Streaming response +## Streaming ```python from sanic import response @@ -60,7 +60,7 @@ async def index(request): return response.stream(streaming_fn, content_type='text/plain') ``` -- `redirect` - Redirect response +## Redirect ```python from sanic import response @@ -71,7 +71,9 @@ def handle_request(request): return response.redirect('/json') ``` -- `raw` - Raw response, response without encoding the body +## Raw + +Response without encoding the body ```python from sanic import response @@ -82,6 +84,7 @@ def handle_request(request): return response.raw('raw data') ``` +## Modify headers or status To modify headers or status code, pass the `headers` or `status` argument to those functions: From 250bb7e29d0f2ae66a9655f79601864f7c111428 Mon Sep 17 00:00:00 2001 From: Jing Su Date: Mon, 13 Mar 2017 18:34:43 +0800 Subject: [PATCH 06/68] add websocket secure scheme in request @messense --- sanic/request.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index c8f14ca0..dc2fd872 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -144,12 +144,14 @@ class Request(dict): def scheme(self): if self.app.websocket_enabled \ and self.headers.get('upgrade') == 'websocket': - return 'ws' + scheme = 'ws' + else: + scheme = 'http' if self.transport.get_extra_info('sslcontext'): - return 'https' + scheme += 's' - return 'http' + return scheme @property def host(self): From ba41ab8f67e484b8ea5bda2dd91603aba1dcec93 Mon Sep 17 00:00:00 2001 From: Jing Su Date: Mon, 13 Mar 2017 18:36:22 +0800 Subject: [PATCH 07/68] fix typo --- docs/sanic/blueprints.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md index d1338023..4b704422 100644 --- a/docs/sanic/blueprints.md +++ b/docs/sanic/blueprints.md @@ -57,7 +57,7 @@ Blueprints have much the same functionality as an application instance. ### WebSocket routes -WebSocket handlers can be registered on a blueprint using the `@bp.route` +WebSocket handlers can be registered on a blueprint using the `@bp.websocket` decorator or `bp.add_websocket_route` method. ### Middleware From d05f502fc83eb9ee9dda8e39a0d4d90e976712e5 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Tue, 14 Mar 2017 08:37:53 +1000 Subject: [PATCH 08/68] Add Dispatcher Extension Adds a link to the extension https://github.com/ashleysommer/sanic-dispatcher And a very short description --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index ac08c36e..d3bdda5b 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -16,3 +16,4 @@ A list of Sanic extensions created by the community. - [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config. - [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the `Babel` library +- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter. From 410f86c960d4794c34ed79c211332484959587dc Mon Sep 17 00:00:00 2001 From: Anton Zhyrnyi Date: Tue, 14 Mar 2017 20:53:58 +0200 Subject: [PATCH 09/68] fix for docs&tests --- docs/sanic/class_based_views.md | 20 +++++++++++++++++++- tests/test_views.py | 6 +++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/sanic/class_based_views.md b/docs/sanic/class_based_views.md index 02b02140..ace8bf9c 100644 --- a/docs/sanic/class_based_views.md +++ b/docs/sanic/class_based_views.md @@ -48,6 +48,24 @@ app.add_route(SimpleView.as_view(), '/') ``` +You can also use `async` syntax. + +```python +from sanic import Sanic +from sanic.views import HTTPMethodView +from sanic.response import text + +app = Sanic('some_name') + +class SimpleAsyncView(HTTPMethodView): + + async def get(self, request): + return text('I am async get method') + +app.add_route(SimpleAsyncView.as_view(), '/') + +``` + ## URL parameters If you need any URL parameters, as discussed in the routing guide, include them @@ -128,4 +146,4 @@ view.add(['POST', 'PUT'], lambda request: text('I am a post/put method')) app.add_route(view, '/') ``` -Note: currently you cannot build a URL for a CompositionView using `url_for`. +Note: currently you cannot build a URL for a CompositionView using `url_for`. diff --git a/tests/test_views.py b/tests/test_views.py index 627a3b6c..40fc1adf 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -15,13 +15,13 @@ def test_methods(method): class DummyView(HTTPMethodView): - def get(self, request): + async def get(self, request): return text('', headers={'method': 'GET'}) def post(self, request): return text('', headers={'method': 'POST'}) - def put(self, request): + async def put(self, request): return text('', headers={'method': 'PUT'}) def head(self, request): @@ -30,7 +30,7 @@ def test_methods(method): def options(self, request): return text('', headers={'method': 'OPTIONS'}) - def patch(self, request): + async def patch(self, request): return text('', headers={'method': 'PATCH'}) def delete(self, request): From df0e285b6f8465eb273af50c242299c5601fa09f Mon Sep 17 00:00:00 2001 From: lizheao Date: Wed, 15 Mar 2017 14:12:37 +0800 Subject: [PATCH 10/68] Add a new example --- examples/sanic_aiomysql_with_global_pool.py | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 examples/sanic_aiomysql_with_global_pool.py diff --git a/examples/sanic_aiomysql_with_global_pool.py b/examples/sanic_aiomysql_with_global_pool.py new file mode 100644 index 00000000..8158a87b --- /dev/null +++ b/examples/sanic_aiomysql_with_global_pool.py @@ -0,0 +1,63 @@ +# encoding: utf-8 +""" +You need the aiomysql +""" +import asyncio +import os + +import aiomysql +import uvloop +from sanic import Sanic +from sanic.response import json + +database_name = os.environ['DATABASE_NAME'] +database_host = os.environ['DATABASE_HOST'] +database_user = os.environ['DATABASE_USER'] +database_password = os.environ['DATABASE_PASSWORD'] +app = Sanic() +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + + +async def get_pool(*args, **kwargs): + """ + the first param in *args is the global instance , + so we can store our connection pool in it . + and it can be used by different request + :param args: + :param kwargs: + :return: + """ + args[0].pool = { + "aiomysql": await aiomysql.create_pool(host=database_host, user=database_user, password=database_password, + db=database_name, + maxsize=5)} + async with args[0].pool['aiomysql'].acquire() as conn: + async with conn.cursor() as cur: + await cur.execute('DROP TABLE IF EXISTS sanic_polls') + await cur.execute("""CREATE TABLE sanic_polls ( + 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()) + """.format(i, i)) + + +@app.route("/") +async def test(): + result = [] + data = {} + async with app.pool['aiomysql'].acquire() as conn: + async with conn.cursor() as cur: + await cur.execute("SELECT question, pub_date FROM sanic_polls") + async for row in cur: + result.append({"question": row[0], "pub_date": row[1]}) + if result or len(result) > 0: + data['data'] = res + return json(data) + + +if __name__ == '__main__': + app.run(host="127.0.0.1", workers=4, port=12000, before_start=get_pool) From 0ad016417179d621a80786d4a8483563a0cd875d Mon Sep 17 00:00:00 2001 From: lizheao Date: Wed, 15 Mar 2017 14:41:38 +0800 Subject: [PATCH 11/68] Change some code in sanic aiomysql code --- examples/sanic_aiomysql_with_global_pool.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/sanic_aiomysql_with_global_pool.py b/examples/sanic_aiomysql_with_global_pool.py index 8158a87b..012aeb61 100644 --- a/examples/sanic_aiomysql_with_global_pool.py +++ b/examples/sanic_aiomysql_with_global_pool.py @@ -15,23 +15,23 @@ database_host = os.environ['DATABASE_HOST'] database_user = os.environ['DATABASE_USER'] database_password = os.environ['DATABASE_PASSWORD'] app = Sanic() -asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -async def get_pool(*args, **kwargs): +@app.listener("before_server_start") +async def get_pool(app, loop): """ - the first param in *args is the global instance , + the first param is the global instance , so we can store our connection pool in it . and it can be used by different request :param args: :param kwargs: :return: """ - args[0].pool = { + app.pool = { "aiomysql": await aiomysql.create_pool(host=database_host, user=database_user, password=database_password, db=database_name, maxsize=5)} - async with args[0].pool['aiomysql'].acquire() as conn: + async with app.pool['aiomysql'].acquire() as conn: async with conn.cursor() as cur: await cur.execute('DROP TABLE IF EXISTS sanic_polls') await cur.execute("""CREATE TABLE sanic_polls ( @@ -60,4 +60,4 @@ async def test(): if __name__ == '__main__': - app.run(host="127.0.0.1", workers=4, port=12000, before_start=get_pool) + app.run(host="127.0.0.1", workers=4, port=12000) From 97c2056e4a0511b593d0646359e9a28d72d88bd3 Mon Sep 17 00:00:00 2001 From: lizheao Date: Wed, 15 Mar 2017 14:41:54 +0800 Subject: [PATCH 12/68] Change some code in sanic aiomysql code --- examples/sanic_aiomysql_with_global_pool.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/sanic_aiomysql_with_global_pool.py b/examples/sanic_aiomysql_with_global_pool.py index 012aeb61..df3db8f4 100644 --- a/examples/sanic_aiomysql_with_global_pool.py +++ b/examples/sanic_aiomysql_with_global_pool.py @@ -6,7 +6,6 @@ import asyncio import os import aiomysql -import uvloop from sanic import Sanic from sanic.response import json From cfc53d0d2684d05a6f55153ad6293736892dda1f Mon Sep 17 00:00:00 2001 From: lizheao Date: Wed, 15 Mar 2017 14:42:22 +0800 Subject: [PATCH 13/68] Change some code in sanic aiomysql code --- examples/sanic_aiomysql_with_global_pool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sanic_aiomysql_with_global_pool.py b/examples/sanic_aiomysql_with_global_pool.py index df3db8f4..65d5832d 100644 --- a/examples/sanic_aiomysql_with_global_pool.py +++ b/examples/sanic_aiomysql_with_global_pool.py @@ -2,10 +2,10 @@ """ You need the aiomysql """ -import asyncio import os import aiomysql + from sanic import Sanic from sanic.response import json From fd823c63aba8b7a74da4660a7e1dfbf4c2a05ea3 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 15 Mar 2017 22:27:13 -0700 Subject: [PATCH 14/68] cancel websocket tasks if server is stopped --- sanic/app.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 65a6196f..63e466ec 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1,7 +1,7 @@ import logging import re import warnings -from asyncio import get_event_loop +from asyncio import get_event_loop, ensure_future, CancelledError from collections import deque, defaultdict from functools import partial from inspect import isawaitable, stack, getmodulename @@ -54,6 +54,7 @@ class Sanic: self.listeners = defaultdict(list) self.is_running = False self.websocket_enabled = False + self.websocket_tasks = [] # Register alternative method names self.go_fast = self.run @@ -178,7 +179,7 @@ class Sanic: :param host: :return: decorated function """ - self.websocket_enabled = True + self.enable_websocket() # Fix case where the user did not prefix the URL with a / # and will probably get confused as to why it's not working @@ -190,11 +191,17 @@ class Sanic: request.app = self protocol = request.transport.get_protocol() ws = await protocol.websocket_handshake(request) + + # schedule the application handler + # its future is kept in self.websocket_tasks in case it + # needs to be cancelled due to the server being stopped + fut = ensure_future(handler(request, ws, *args, **kwargs)) + self.websocket_tasks.append(fut) try: - # invoke the application handler - await handler(request, ws, *args, **kwargs) - except ConnectionClosed: + await fut + except (CancelledError, ConnectionClosed): pass + self.websocket_tasks.remove(fut) await ws.close() self.router.add(uri=uri, handler=websocket_handler, @@ -213,6 +220,14 @@ class Sanic: Websocket is enabled automatically if websocket routes are added to the application. """ + if not self.websocket_enabled: + # if the server is stopped, we want to cancel any ongoing + # websocket tasks, to allow the server to exit promptly + @self.listener('before_server_stop') + def cancel_websocket_tasks(app, loop): + for task in self.websocket_tasks: + task.cancel() + self.websocket_enabled = enable def remove_route(self, uri, clean_cache=True, host=None): From 86f3101861bee0adeb37a1debe890853df4112ca Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Thu, 16 Mar 2017 16:50:33 +1100 Subject: [PATCH 15/68] Add autodoc extension to Sphinx configuration --- docs/conf.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 21b9b9cf..c97f3c19 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import sanic # -- General configuration ------------------------------------------------ -extensions = [] +extensions = ['sphinx.ext.autodoc'] templates_path = ['_templates'] @@ -68,7 +68,6 @@ pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False - # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -80,13 +79,11 @@ html_theme = 'sphinx_rtd_theme' # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] - # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'Sanicdoc' - # -- Options for LaTeX output --------------------------------------------- latex_elements = { @@ -110,21 +107,14 @@ latex_elements = { # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'Sanic.tex', 'Sanic Documentation', - 'Sanic contributors', 'manual'), -] - +latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation', + 'Sanic contributors', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'sanic', 'Sanic Documentation', - [author], 1) -] - +man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -132,13 +122,10 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'Sanic', 'Sanic Documentation', - author, 'Sanic', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic', + 'One line description of project.', 'Miscellaneous'), ] - - # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. @@ -150,8 +137,6 @@ epub_copyright = copyright # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] - - # -- Custom Settings ------------------------------------------------------- suppress_warnings = ['image.nonlocal_uri'] From 24f745a334321cb342fb53a41e724c2b19359914 Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Thu, 16 Mar 2017 16:51:02 +1100 Subject: [PATCH 16/68] Fix formatting errors in RST files --- docs/index.rst | 1 + docs/{ => sanic}/config.md | 0 docs/sanic/index.rst | 4 ++-- 3 files changed, 3 insertions(+), 2 deletions(-) rename docs/{ => sanic}/config.md (100%) diff --git a/docs/index.rst b/docs/index.rst index 43cd0ba2..3fa63d5c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ Guides sanic/blueprints sanic/config sanic/cookies + sanic/streaming sanic/class_based_views sanic/custom_protocol sanic/ssl diff --git a/docs/config.md b/docs/sanic/config.md similarity index 100% rename from docs/config.md rename to docs/sanic/config.md diff --git a/docs/sanic/index.rst b/docs/sanic/index.rst index eb9eb286..08797e9f 100644 --- a/docs/sanic/index.rst +++ b/docs/sanic/index.rst @@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y Sanic is developed `on GitHub `_. Contributions are welcome! -Sanic aspires to be simple: -------------------- +Sanic aspires to be simple +--------------------------- .. code:: python From d713533d26f44c432aa434989f69058f7d9f905f Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Thu, 16 Mar 2017 16:52:18 +1100 Subject: [PATCH 17/68] Fix docstring formatting errors --- sanic/app.py | 2 +- sanic/response.py | 23 ++++++++++------------- sanic/router.py | 7 ++++--- sanic/server.py | 2 +- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 65a6196f..edd04caf 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -305,7 +305,7 @@ class Sanic: the output URL's query string. :param view_name: string referencing the view name - :param **kwargs: keys and values that are used to build request + :param \*\*kwargs: keys and values that are used to build request parameters and query string arguments. :return: the built URL diff --git a/sanic/response.py b/sanic/response.py index eb2d3e49..c129884f 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -251,8 +251,7 @@ def text(body, status=200, headers=None, :param body: Response data to be encoded. :param status: Response code. :param headers: Custom Headers. - :param content_type: - the content type (string) of the response + :param content_type: the content type (string) of the response """ return HTTPResponse( body, status=status, headers=headers, @@ -266,8 +265,7 @@ def raw(body, status=200, headers=None, :param body: Response data. :param status: Response code. :param headers: Custom Headers. - :param content_type: - the content type (string) of the response + :param content_type: the content type (string) of the response. """ return HTTPResponse(body_bytes=body, status=status, headers=headers, content_type=content_type) @@ -316,17 +314,16 @@ def stream( content_type="text/plain; charset=utf-8"): """Accepts an coroutine `streaming_fn` which can be used to write chunks to a streaming response. Returns a `StreamingHTTPResponse`. - Example usage: - ``` - @app.route("/") - async def index(request): - async def streaming_fn(response): - await response.write('foo') - await response.write('bar') + Example usage:: - return stream(streaming_fn, content_type='text/plain') - ``` + @app.route("/") + async def index(request): + async def streaming_fn(response): + await response.write('foo') + await response.write('bar') + + return stream(streaming_fn, content_type='text/plain') :param streaming_fn: A coroutine accepts a response and writes content to that response. diff --git a/sanic/router.py b/sanic/router.py index 38b1c029..2819cc7c 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -75,9 +75,10 @@ class Router: """Parse a parameter string into its constituent name, type, and pattern - For example: - `parse_parameter_string('')` -> - ('param_one', str, '[A-z]') + For example:: + + parse_parameter_string('')` -> + ('param_one', str, '[A-z]') :param parameter_string: String to parse :return: tuple containing diff --git a/sanic/server.py b/sanic/server.py index 39816e28..11601c00 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -329,7 +329,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, `app` instance and `loop` :param after_stop: function to be executed when a stop signal is received after it is respected. Takes arguments - `app` instance and `loop` + `app` instance and `loop` :param debug: enables debug output (slows server) :param request_timeout: time in seconds :param ssl: SSLContext From 391b24bc17f8ce669a0d825867dd3651b54bf65a Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Thu, 16 Mar 2017 17:01:49 +1100 Subject: [PATCH 18/68] Add websockets dependency to ReadTheDocs environment --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index 7eee43a5..298ea552 100644 --- a/environment.yml +++ b/environment.yml @@ -15,4 +15,5 @@ dependencies: - httptools>=0.0.9 - ujson>=1.35 - aiofiles>=0.3.0 + - websockets>=3.2 - https://github.com/channelcat/docutils-fork/zipball/master \ No newline at end of file From 46677e69cebf2ff303d551c344525006816c2fb9 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 15 Mar 2017 21:11:45 -0700 Subject: [PATCH 19/68] accept strict_slash routes --- sanic/app.py | 57 +++++++++++++++++++++++++++----------------- sanic/router.py | 8 ++++++- tests/test_routes.py | 23 ++++++++++++++++++ 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 65a6196f..58dc38cd 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -101,7 +101,8 @@ class Sanic: return decorator # Decorator - def route(self, uri, methods=frozenset({'GET'}), host=None): + def route(self, uri, methods=frozenset({'GET'}), host=None, + strict_slashes=False): """Decorate a function to be registered as a route :param uri: path of the URL @@ -117,34 +118,42 @@ class Sanic: def response(handler): self.router.add(uri=uri, methods=methods, handler=handler, - host=host) + host=host, strict_slashes=strict_slashes) return handler return response # Shorthand method decorators - def get(self, uri, host=None): - return self.route(uri, methods=frozenset({"GET"}), host=host) + def get(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=frozenset({"GET"}), host=host, + strict_slashes=strict_slashes) - def post(self, uri, host=None): - return self.route(uri, methods=frozenset({"POST"}), host=host) + def post(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=frozenset({"POST"}), host=host, + strict_slashes=strict_slashes) - def put(self, uri, host=None): - return self.route(uri, methods=frozenset({"PUT"}), host=host) + def put(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=frozenset({"PUT"}), host=host, + strict_slashes=strict_slashes) - def head(self, uri, host=None): - return self.route(uri, methods=frozenset({"HEAD"}), host=host) + def head(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=frozenset({"HEAD"}), host=host, + strict_slashes=strict_slashes) - def options(self, uri, host=None): - return self.route(uri, methods=frozenset({"OPTIONS"}), host=host) + def options(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, + strict_slashes=strict_slashes) - def patch(self, uri, host=None): - return self.route(uri, methods=frozenset({"PATCH"}), host=host) + def patch(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=frozenset({"PATCH"}), host=host, + strict_slashes=strict_slashes) - def delete(self, uri, host=None): - return self.route(uri, methods=frozenset({"DELETE"}), host=host) + def delete(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=frozenset({"DELETE"}), host=host, + strict_slashes=strict_slashes) - def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None): + def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, + strict_slashes=False): """A helper method to register class instance or functions as a handler to the application url routes. @@ -168,11 +177,12 @@ class Sanic: if isinstance(handler, CompositionView): methods = handler.handlers.keys() - self.route(uri=uri, methods=methods, host=host)(handler) + self.route(uri=uri, methods=methods, host=host, + strict_slashes=strict_slashes)(handler) return handler # Decorator - def websocket(self, uri, host=None): + def websocket(self, uri, host=None, strict_slashes=False): """Decorate a function to be registered as a websocket route :param uri: path of the URL :param host: @@ -198,14 +208,17 @@ class Sanic: await ws.close() self.router.add(uri=uri, handler=websocket_handler, - methods=frozenset({'GET'}), host=host) + methods=frozenset({'GET'}), host=host, + strict_slashes=strict_slashes) return handler return response - def add_websocket_route(self, handler, uri, host=None): + def add_websocket_route(self, handler, uri, host=None, + strict_slashes=False): """A helper method to register a function as a websocket route.""" - return self.websocket(uri, host=host)(handler) + return self.websocket(uri, host=host, + strict_slashes=strict_slashes)(handler) def enable_websocket(self, enable=True): """Enable or disable the support for websocket. diff --git a/sanic/router.py b/sanic/router.py index 38b1c029..88924b1d 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -95,9 +95,15 @@ class Router: return name, _type, pattern - def add(self, uri, methods, handler, host=None): + def add(self, uri, methods, handler, host=None, strict_slashes=False): + # add regular version self._add(uri, methods, handler, host) + + if strict_slashes: + return + + # Add versions with and without trailing / slash_is_missing = ( not uri[-1] == '/' and not self.routes_all.get(uri + '/', False) diff --git a/tests/test_routes.py b/tests/test_routes.py index afefe4a7..3506db66 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -23,6 +23,29 @@ def test_shorthand_routes_get(): request, response = app.test_client.post('/get') assert response.status == 405 +def test_route_strict_slash(): + app = Sanic('test_route_strict_slash') + + @app.get('/get', strict_slashes=True) + def handler(request): + return text('OK') + + @app.post('/post/', strict_slashes=True) + def handler(request): + return text('OK') + + request, response = app.test_client.get('/get') + assert response.text == 'OK' + + request, response = app.test_client.get('/get/') + assert response.status == 404 + + request, response = app.test_client.post('/post/') + assert response.text == 'OK' + + request, response = app.test_client.post('/post') + assert response.status == 404 + def test_route_optional_slash(): app = Sanic('test_route_optional_slash') From 625865412fa939d7c9e11257a5b37e1ca73dc152 Mon Sep 17 00:00:00 2001 From: lixxu Date: Fri, 17 Mar 2017 13:12:17 +0800 Subject: [PATCH 20/68] update function name as it not halt request actually --- docs/sanic/blueprints.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md index 4b704422..5fe20e54 100644 --- a/docs/sanic/blueprints.md +++ b/docs/sanic/blueprints.md @@ -66,7 +66,7 @@ Using blueprints allows you to also register middleware globally. ```python @bp.middleware -async def halt_request(request): +async def print_on_request(request): print("I am a spy") @bp.middleware('request') @@ -116,7 +116,7 @@ bp = Blueprint('my_blueprint') async def setup_connection(app, loop): global database database = mysql.connect(host='127.0.0.1'...) - + @bp.listener('after_server_stop') async def close_connection(app, loop): await database.close() @@ -142,7 +142,7 @@ blueprint_v2 = Blueprint('v2', url_prefix='/v2') @blueprint_v1.route('/') async def api_v1_root(request): return text('Welcome to version 1 of our documentation') - + @blueprint_v2.route('/') async def api_v2_root(request): return text('Welcome to version 2 of our documentation') From 94c83c445fd067934052733853d81af5abd7f193 Mon Sep 17 00:00:00 2001 From: lixxu Date: Fri, 17 Mar 2017 14:01:54 +0800 Subject: [PATCH 21/68] fix broken table --- docs/sanic/config.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index f5d56467..0c22de4b 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -71,8 +71,7 @@ DB_USER = 'appuser' Out of the box there are just a few predefined values which can be overwritten when creating the application. -| Variable | Default | Description | -| ----------------- | --------- | --------------------------------- | -| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | -| REQUEST_TIMEOUT | 60 | How long a request can take (sec) | - + | Variable | Default | Description | + | ----------------- | --------- | --------------------------------- | + | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | + | REQUEST_TIMEOUT | 60 | How long a request can take (sec) | From 986ff101ce224494a5cdb047a1aefd99c8a6d840 Mon Sep 17 00:00:00 2001 From: messense Date: Fri, 17 Mar 2017 14:16:13 +0800 Subject: [PATCH 22/68] Add an aioredis example --- examples/sanic_aioredis_example.py | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 examples/sanic_aioredis_example.py diff --git a/examples/sanic_aioredis_example.py b/examples/sanic_aioredis_example.py new file mode 100644 index 00000000..8ba51617 --- /dev/null +++ b/examples/sanic_aioredis_example.py @@ -0,0 +1,34 @@ +""" To run this example you need additional aioredis package +""" +from sanic import Sanic, response +import aioredis + +app = Sanic(__name__) + + +@app.route("/") +async def handle(request): + async with request.app.redis_pool.get() as redis: + await redis.set('test-my-key', 'value') + val = await redis.get('test-my-key') + return response.text(val.decode('utf-8')) + + +@app.listener('before_server_start') +async def before_server_start(app, loop): + app.redis_pool = await aioredis.create_pool( + ('localhost', 6379), + minsize=5, + maxsize=10, + loop=loop + ) + + +@app.listener('after_server_stop') +async def after_server_stop(app, loop): + app.redis_pool.close() + await app.redis_pool.wait_closed() + + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000) From a379ef6781f6ecefa03ec1863beecb8fe43126c1 Mon Sep 17 00:00:00 2001 From: Shawn Niederriter Date: Sat, 18 Mar 2017 23:56:11 -0400 Subject: [PATCH 23/68] Added Sanic-OAuth to extensions. --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index d3bdda5b..b11470d9 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -17,3 +17,4 @@ A list of Sanic extensions created by the community. - [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the `Babel` library - [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter. +- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers. From 19ee1dfecca09d23cc450df86c42e26ffd1548fb Mon Sep 17 00:00:00 2001 From: messense Date: Sun, 12 Mar 2017 16:19:34 +0800 Subject: [PATCH 24/68] Gunicorn worker --- sanic/app.py | 13 ++++++---- sanic/worker.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 sanic/worker.py diff --git a/sanic/app.py b/sanic/app.py index 2fd52fae..e101caf5 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -578,6 +578,10 @@ class Sanic: """This kills the Sanic""" get_event_loop().stop() + def __call__(self): + """gunicorn compatibility""" + return self + 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, @@ -686,9 +690,10 @@ class Sanic: server_settings['run_async'] = True # Serve - proto = "http" - if ssl is not None: - proto = "https" - log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + if host and port: + proto = "http" + if ssl is not None: + proto = "https" + log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) return server_settings diff --git a/sanic/worker.py b/sanic/worker.py new file mode 100644 index 00000000..e4906faf --- /dev/null +++ b/sanic/worker.py @@ -0,0 +1,63 @@ +import asyncio +import os + +import uvloop +import gunicorn.workers.base as base + + +class GunicornWorker(base.Worker): + + def __init__(self, *args, **kw): # pragma: no cover + super().__init__(*args, **kw) + self.servers = [] + self.connections = {} + + def init_process(self): + # create new event_loop after fork + asyncio.get_event_loop().close() + + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + super().init_process() + + def run(self): + self._runner = asyncio.async(self._run(), loop=self.loop) + + try: + self.loop.run_until_complete(self._runner) + finally: + self.loop.close() + + async def close(self): + try: + if hasattr(self.wsgi, 'close'): + await self.wsgi.close() + except: + self.log.exception('Process shutdown exception') + + async def _run(self): + for sock in self.sockets: + self.servers.append(await self.app.callable.create_server( + sock=sock, host=None, port=None, loop=self.loop)) + + # If our parent changed then we shut down. + pid = os.getpid() + try: + while self.alive: + self.notify() + + if pid == os.getpid() and self.ppid != os.getppid(): + self.alive = False + self.log.info("Parent changed, shutting down: %s", self) + else: + await asyncio.sleep(1.0, loop=self.loop) + except (Exception, BaseException, GeneratorExit, KeyboardInterrupt): + pass + + if self.servers: + for server in self.servers: + server.close() + + await self.close() From 2b296435b3b77b07af9ae20b9469c01eacc5710a Mon Sep 17 00:00:00 2001 From: messense Date: Sun, 12 Mar 2017 22:42:34 +0800 Subject: [PATCH 25/68] Trigger events --- examples/simple_server.py | 3 +- examples/try_everything.py | 13 ++++++- sanic/server.py | 4 +- sanic/worker.py | 78 +++++++++++++++++++++++++++++++------- 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/examples/simple_server.py b/examples/simple_server.py index 24e3570f..a803feb8 100644 --- a/examples/simple_server.py +++ b/examples/simple_server.py @@ -9,4 +9,5 @@ async def test(request): return json({"test": True}) -app.run(host="0.0.0.0", port=8000) +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000) diff --git a/examples/try_everything.py b/examples/try_everything.py index f7191ecc..da3cc515 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -70,6 +70,11 @@ def query_string(request): # Run Server # ----------------------------------------------- # +@app.listener('before_server_start') +def before_start(app, loop): + log.info("SERVER STARTING") + + @app.listener('after_server_start') def after_start(app, loop): log.info("OH OH OH OH OHHHHHHHH") @@ -77,7 +82,13 @@ def after_start(app, loop): @app.listener('before_server_stop') def before_stop(app, loop): + log.info("SERVER STOPPING") + + +@app.listener('after_server_stop') +def after_stop(app, loop): log.info("TRIED EVERYTHING") -app.run(host="0.0.0.0", port=8000, debug=True) +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/sanic/server.py b/sanic/server.py index 11601c00..ee45e81b 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -313,7 +313,7 @@ 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, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, - register_sys_signals=True, run_async=False): + register_sys_signals=True, run_async=False, connections=None): """Start asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -349,7 +349,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, trigger_events(before_start, loop) - connections = set() + connections = connections or set() signal = Signal() server = partial( protocol, diff --git a/sanic/worker.py b/sanic/worker.py index e4906faf..cd1a6b9f 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -1,16 +1,29 @@ -import asyncio import os +import asyncio +import logging +try: + import ssl +except ImportError: + ssl = None import uvloop import gunicorn.workers.base as base +from sanic.server import trigger_events, serve, HttpProtocol +from sanic.websocket import WebSocketProtocol + class GunicornWorker(base.Worker): def __init__(self, *args, **kw): # pragma: no cover super().__init__(*args, **kw) + cfg = self.cfg + if cfg.is_ssl: + self.ssl_context = self._create_ssl_context(cfg) + else: + self.ssl_context = None self.servers = [] - self.connections = {} + self.connections = set() def init_process(self): # create new event_loop after fork @@ -28,20 +41,49 @@ class GunicornWorker(base.Worker): try: self.loop.run_until_complete(self._runner) finally: + trigger_events(self._server_settings.get('before_stop', []), self.loop) self.loop.close() + trigger_events(self._server_settings.get('after_stop', []), self.loop) async def close(self): - try: - if hasattr(self.wsgi, 'close'): - await self.wsgi.close() - except: - self.log.exception('Process shutdown exception') + if self.servers: + # stop accepting connections + self.log.info("Stopping server: %s, connections: %s", + self.pid, len(self.connections)) + for server in self.servers: + server.close() + await server.wait_closed() + self.servers.clear() + + # prepare connections for closing + for conn in self.connections: + conn.close_if_idle() + + while self.connections: + await asyncio.sleep(0.1) async def _run(self): + is_debug = self.log.loglevel == logging.DEBUG + protocol = (WebSocketProtocol if self.app.callable.websocket_enabled + else HttpProtocol) + self._server_settings = self.app.callable._helper( + host=None, + port=None, + loop=self.loop, + debug=is_debug, + protocol=protocol, + ssl=self.ssl_context, + run_async=True + ) + self._server_settings.pop('sock') for sock in self.sockets: - self.servers.append(await self.app.callable.create_server( - sock=sock, host=None, port=None, loop=self.loop)) + self.servers.append(await serve( + sock=sock, + connections=self.connections, + **self._server_settings + )) + trigger_events(self._server_settings.get('after_start', []), self.loop) # If our parent changed then we shut down. pid = os.getpid() try: @@ -56,8 +98,18 @@ class GunicornWorker(base.Worker): except (Exception, BaseException, GeneratorExit, KeyboardInterrupt): pass - if self.servers: - for server in self.servers: - server.close() - await self.close() + + @staticmethod + def _create_ssl_context(cfg): + """ Creates SSLContext instance for usage in asyncio.create_server. + See ssl.SSLSocket.__init__ for more details. + """ + ctx = ssl.SSLContext(cfg.ssl_version) + ctx.load_cert_chain(cfg.certfile, cfg.keyfile) + ctx.verify_mode = cfg.cert_reqs + if cfg.ca_certs: + ctx.load_verify_locations(cfg.ca_certs) + if cfg.ciphers: + ctx.set_ciphers(cfg.ciphers) + return ctx From f35442ad1b1e78007e5daf1c5172f3c90dea70b9 Mon Sep 17 00:00:00 2001 From: messense Date: Sun, 12 Mar 2017 23:06:44 +0800 Subject: [PATCH 26/68] Fix RuntimeError: this event loop is already running --- sanic/worker.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sanic/worker.py b/sanic/worker.py index cd1a6b9f..a54a5a11 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -36,10 +36,12 @@ class GunicornWorker(base.Worker): super().init_process() def run(self): - self._runner = asyncio.async(self._run(), loop=self.loop) + self._runner = asyncio.ensure_future(self._run(), loop=self.loop) try: self.loop.run_until_complete(self._runner) + trigger_events(self._server_settings.get('after_start', []), self.loop) + self.loop.run_until_complete(self._check_alive()) finally: trigger_events(self._server_settings.get('before_stop', []), self.loop) self.loop.close() @@ -83,7 +85,7 @@ class GunicornWorker(base.Worker): **self._server_settings )) - trigger_events(self._server_settings.get('after_start', []), self.loop) + async def _check_alive(self): # If our parent changed then we shut down. pid = os.getpid() try: From decd3e737c17c550c280be2cd75df9ca0e06c9c6 Mon Sep 17 00:00:00 2001 From: messense Date: Mon, 13 Mar 2017 17:41:47 +0800 Subject: [PATCH 27/68] Flake8 fix --- sanic/worker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sanic/worker.py b/sanic/worker.py index a54a5a11..4e70f10d 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -40,12 +40,15 @@ class GunicornWorker(base.Worker): try: self.loop.run_until_complete(self._runner) - trigger_events(self._server_settings.get('after_start', []), self.loop) + trigger_events(self._server_settings.get('after_start', []), + self.loop) self.loop.run_until_complete(self._check_alive()) finally: - trigger_events(self._server_settings.get('before_stop', []), self.loop) + trigger_events(self._server_settings.get('before_stop', []), + self.loop) self.loop.close() - trigger_events(self._server_settings.get('after_stop', []), self.loop) + trigger_events(self._server_settings.get('after_stop', []), + self.loop) async def close(self): if self.servers: From 7ca9116e370d3a82dbb48c9d6aa4492cab44e6b5 Mon Sep 17 00:00:00 2001 From: messense Date: Tue, 14 Mar 2017 23:56:52 +0800 Subject: [PATCH 28/68] Trigger before_stop before closing server, after_stop before closing loop --- sanic/worker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sanic/worker.py b/sanic/worker.py index 4e70f10d..c0825ac7 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -44,13 +44,13 @@ class GunicornWorker(base.Worker): self.loop) self.loop.run_until_complete(self._check_alive()) finally: - trigger_events(self._server_settings.get('before_stop', []), - self.loop) - self.loop.close() trigger_events(self._server_settings.get('after_stop', []), self.loop) + self.loop.close() async def close(self): + trigger_events(self._server_settings.get('before_stop', []), + self.loop) if self.servers: # stop accepting connections self.log.info("Stopping server: %s, connections: %s", From 466b34735c02779ff9acbade8e278a589b39c108 Mon Sep 17 00:00:00 2001 From: messense Date: Wed, 15 Mar 2017 00:07:42 +0800 Subject: [PATCH 29/68] Set app.is_running to True --- sanic/worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/worker.py b/sanic/worker.py index c0825ac7..3312b485 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -40,6 +40,7 @@ class GunicornWorker(base.Worker): try: self.loop.run_until_complete(self._runner) + self.app.callable.is_running = True trigger_events(self._server_settings.get('after_start', []), self.loop) self.loop.run_until_complete(self._check_alive()) From a90d70feae59f702e56e771cf84d12a6ee75beed Mon Sep 17 00:00:00 2001 From: messense Date: Wed, 15 Mar 2017 17:43:47 +0800 Subject: [PATCH 30/68] Check connections is not None --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index ee45e81b..f044517e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -349,7 +349,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, trigger_events(before_start, loop) - connections = connections or set() + connections = connections if connections is not None else set() signal = Signal() server = partial( protocol, From 11a3cf9b99063a58c5ff06c81db8888fef32cdc9 Mon Sep 17 00:00:00 2001 From: messense Date: Wed, 15 Mar 2017 18:01:52 +0800 Subject: [PATCH 31/68] Add signal handling --- sanic/worker.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/sanic/worker.py b/sanic/worker.py index 3312b485..10b7e19b 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -1,4 +1,6 @@ import os +import sys +import signal import asyncio import logging try: @@ -24,6 +26,7 @@ class GunicornWorker(base.Worker): self.ssl_context = None self.servers = [] self.connections = set() + self.exit_code = 0 def init_process(self): # create new event_loop after fork @@ -49,6 +52,8 @@ class GunicornWorker(base.Worker): self.loop) self.loop.close() + sys.exit(self.exit_code) + async def close(self): trigger_events(self._server_settings.get('before_stop', []), self.loop) @@ -119,3 +124,38 @@ class GunicornWorker(base.Worker): if cfg.ciphers: ctx.set_ciphers(cfg.ciphers) return ctx + + def init_signals(self): + # Set up signals through the event loop API. + + self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit, + signal.SIGQUIT, None) + + self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit, + signal.SIGTERM, None) + + self.loop.add_signal_handler(signal.SIGINT, self.handle_quit, + signal.SIGINT, None) + + self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch, + signal.SIGWINCH, None) + + self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1, + signal.SIGUSR1, None) + + self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort, + signal.SIGABRT, None) + + # Don't let SIGTERM and SIGUSR1 disturb active requests + # by interrupting system calls + signal.siginterrupt(signal.SIGTERM, False) + signal.siginterrupt(signal.SIGUSR1, False) + + def handle_quit(self, sig, frame): + self.alive = False + self.cfg.worker_int(self) + + def handle_abort(self, sig, frame): + self.alive = False + self.exit_code = 1 + self.cfg.worker_abort(self) From e27812bf3e843e001ea449d4ec588baf9d4d3d3c Mon Sep 17 00:00:00 2001 From: messense Date: Thu, 16 Mar 2017 11:55:10 +0800 Subject: [PATCH 32/68] Set `signal.stopped = True` on closing --- sanic/server.py | 4 ++-- sanic/worker.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index f044517e..00bb2331 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -313,7 +313,8 @@ 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, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, - register_sys_signals=True, run_async=False, connections=None): + register_sys_signals=True, run_async=False, connections=None, + signal=Signal()): """Start asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -350,7 +351,6 @@ def serve(host, port, request_handler, error_handler, before_start=None, trigger_events(before_start, loop) connections = connections if connections is not None else set() - signal = Signal() server = partial( protocol, loop=loop, diff --git a/sanic/worker.py b/sanic/worker.py index 10b7e19b..2ed2672e 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -11,7 +11,7 @@ except ImportError: import uvloop import gunicorn.workers.base as base -from sanic.server import trigger_events, serve, HttpProtocol +from sanic.server import trigger_events, serve, HttpProtocol, Signal from sanic.websocket import WebSocketProtocol @@ -27,6 +27,7 @@ class GunicornWorker(base.Worker): self.servers = [] self.connections = set() self.exit_code = 0 + self.signal = Signal() def init_process(self): # create new event_loop after fork @@ -67,6 +68,7 @@ class GunicornWorker(base.Worker): self.servers.clear() # prepare connections for closing + self.signal.stopped = True for conn in self.connections: conn.close_if_idle() @@ -91,6 +93,7 @@ class GunicornWorker(base.Worker): self.servers.append(await serve( sock=sock, connections=self.connections, + signal=self.signal, **self._server_settings )) From d1fb5bdc300575197e77e8a63fb8b87264d03768 Mon Sep 17 00:00:00 2001 From: messense Date: Thu, 16 Mar 2017 17:47:01 +0800 Subject: [PATCH 33/68] Fix async before_server_start hook bug --- sanic/worker.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/sanic/worker.py b/sanic/worker.py index 2ed2672e..6fa9a25e 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -40,8 +40,24 @@ class GunicornWorker(base.Worker): super().init_process() def run(self): - self._runner = asyncio.ensure_future(self._run(), loop=self.loop) + is_debug = self.log.loglevel == logging.DEBUG + protocol = (WebSocketProtocol if self.app.callable.websocket_enabled + else HttpProtocol) + self._server_settings = self.app.callable._helper( + host=None, + port=None, + loop=self.loop, + debug=is_debug, + protocol=protocol, + ssl=self.ssl_context, + run_async=True + ) + self._server_settings.pop('sock') + trigger_events(self._server_settings.get('before_start', []), + self.loop) + self._server_settings['before_start'] = () + self._runner = asyncio.ensure_future(self._run(), loop=self.loop) try: self.loop.run_until_complete(self._runner) self.app.callable.is_running = True @@ -76,19 +92,6 @@ class GunicornWorker(base.Worker): await asyncio.sleep(0.1) async def _run(self): - is_debug = self.log.loglevel == logging.DEBUG - protocol = (WebSocketProtocol if self.app.callable.websocket_enabled - else HttpProtocol) - self._server_settings = self.app.callable._helper( - host=None, - port=None, - loop=self.loop, - debug=is_debug, - protocol=protocol, - ssl=self.ssl_context, - run_async=True - ) - self._server_settings.pop('sock') for sock in self.sockets: self.servers.append(await serve( sock=sock, From 1396ca903dfad87ed59d90564f97ac52844e05af Mon Sep 17 00:00:00 2001 From: messense Date: Mon, 20 Mar 2017 14:27:02 +0800 Subject: [PATCH 34/68] Fix before_stop event --- sanic/worker.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sanic/worker.py b/sanic/worker.py index 6fa9a25e..7a8303d8 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -64,6 +64,9 @@ class GunicornWorker(base.Worker): trigger_events(self._server_settings.get('after_start', []), self.loop) self.loop.run_until_complete(self._check_alive()) + trigger_events(self._server_settings.get('before_stop', []), + self.loop) + self.loop.run_until_complete(self.close()) finally: trigger_events(self._server_settings.get('after_stop', []), self.loop) @@ -72,8 +75,6 @@ class GunicornWorker(base.Worker): sys.exit(self.exit_code) async def close(self): - trigger_events(self._server_settings.get('before_stop', []), - self.loop) if self.servers: # stop accepting connections self.log.info("Stopping server: %s, connections: %s", @@ -115,8 +116,6 @@ class GunicornWorker(base.Worker): except (Exception, BaseException, GeneratorExit, KeyboardInterrupt): pass - await self.close() - @staticmethod def _create_ssl_context(cfg): """ Creates SSLContext instance for usage in asyncio.create_server. From 63c24122db26293dba2f6dc828d9e3d1371b041d Mon Sep 17 00:00:00 2001 From: sourcepirate Date: Wed, 22 Mar 2017 06:39:23 +0530 Subject: [PATCH 35/68] Removed raw string in cookies documentation --- sanic/cookies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/cookies.py b/sanic/cookies.py index ae77bb44..ce096cd2 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -19,7 +19,7 @@ _Translator.update({ def _quote(str): - r"""Quote a string for use in a cookie header. + """Quote a string for use in a cookie header. If the string does not need to be double-quoted, then just return the string. Otherwise, surround the string in doublequotes and quote (with a \) special characters. From 9c159822993fce24472ed2d41335f70a31b95901 Mon Sep 17 00:00:00 2001 From: matuusu Date: Wed, 22 Mar 2017 12:40:40 +0100 Subject: [PATCH 36/68] Update response.py fix status code not propagating from response.stream to response.StreamingHTTPResponse --- sanic/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index c129884f..38cd68db 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -331,7 +331,7 @@ def stream( :param headers: Custom Headers. """ return StreamingHTTPResponse( - streaming_fn, headers=headers, content_type=content_type, status=200) + streaming_fn, headers=headers, content_type=content_type, status=status) def redirect(to, headers=None, status=302, From 55778389057279734a88046e6e1cbebadfa79c85 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 22 Mar 2017 16:21:35 -0500 Subject: [PATCH 37/68] Hotfixes tests failing from URL object change aiohttp decided to use yarl for their new URL objects so that they aren't plain strings anymore which means that this single test fails. Not a huge change but this should fix the testing suite. --- tests/test_redirect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_redirect.py b/tests/test_redirect.py index 421ee1cf..25efe1f3 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -88,4 +88,4 @@ def test_chained_redirect(redirect_app): assert request.url.endswith('/1') assert response.status == 200 assert response.text == 'OK' - assert response.url.endswith('/3') + assert response.url.path.endswith('/3') From 3d9927dee037fcb67dc92f56768a1eacad25a2a6 Mon Sep 17 00:00:00 2001 From: messense Date: Thu, 23 Mar 2017 09:12:23 +0800 Subject: [PATCH 38/68] Add documentation for Gunicorn worker --- docs/sanic/deploying.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.md index d5f3ad06..cc89759b 100644 --- a/docs/sanic/deploying.md +++ b/docs/sanic/deploying.md @@ -44,3 +44,15 @@ directly run by the interpreter. if __name__ == '__main__': app.run(host='0.0.0.0', port=1337, workers=4) ``` + +## Running via Gunicorn + +[Gunicorn](http://gunicorn.org/) ‘Green Unicorn’ is a WSGI HTTP Server for UNIX. +It’s a pre-fork worker model ported from Ruby’s Unicorn project. + +In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker` +for Gunicorn `worker-class` argument: + +``` +gunicorn --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker +``` From fdbf452cedcffd69d99ea9fc95aa9657d83a535e Mon Sep 17 00:00:00 2001 From: skytoup <875766917@qq.com> Date: Thu, 23 Mar 2017 15:22:00 +0800 Subject: [PATCH 39/68] 1. try...catch aiohttp encode response body to text in test_client 2. add tests static binary file --- sanic/testing.py | 5 ++++- tests/static/python.png | Bin 0 -> 11252 bytes tests/test_static.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 tests/static/python.png diff --git a/sanic/testing.py b/sanic/testing.py index 4fde428c..1cad6a7b 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -22,7 +22,10 @@ class SanicTestClient: cookies=cookies, connector=conn) as session: async with getattr( session, method.lower())(url, *args, **kwargs) as response: - response.text = await response.text() + try: + response.text = await response.text() + except UnicodeDecodeError as e: + response.text = None response.body = await response.read() return response diff --git a/tests/static/python.png b/tests/static/python.png new file mode 100644 index 0000000000000000000000000000000000000000..52fda10994c155f51b3f26c566371d44a9ada87b GIT binary patch literal 11252 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 Date: Thu, 23 Mar 2017 15:16:46 -0400 Subject: [PATCH 40/68] add decorator docs --- docs/sanic/decorators.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/sanic/decorators.md diff --git a/docs/sanic/decorators.md b/docs/sanic/decorators.md new file mode 100644 index 00000000..db2d369b --- /dev/null +++ b/docs/sanic/decorators.md @@ -0,0 +1,39 @@ +# Handler Decorators + +Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed. + +## Authorization Decorator + +Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response. + + +```python +from functools import wraps +from sanic.response import json + +def authorized(): + def decorator(f): + @wraps(f) + async def decorated_function(request, *args, **kwargs): + # run some method that checks the request + # for the client's authorization status + is_authorized = check_request_for_authorization_status(request) + + if is_authorized: + # the user is authorized. + # run the handler method and return the response + response = await f(request, *args, **kwargs) + return response + else: + # the user is not authorized. + return json({'status': 'not_authorized'}, 403) + return decorated_function + return decorator + + +@app.route("/") +@authorized() +async def test(request): + return json({status: 'authorized'}) +``` + From 6188891a53aee2b07b5b559d4d9b9552554371b4 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 23 Mar 2017 15:49:23 -0400 Subject: [PATCH 41/68] add-listeners-docs --- docs/sanic/middleware.md | 48 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/docs/sanic/middleware.md b/docs/sanic/middleware.md index 58ff8feb..b2e8b45a 100644 --- a/docs/sanic/middleware.md +++ b/docs/sanic/middleware.md @@ -1,9 +1,13 @@ -# Middleware +# Middleware And Listeners Middleware are functions which are executed before or after requests to the server. They can be used to modify the *request to* or *response from* user-defined handler functions. +Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle. + +## Middleware + There are two types of middleware: request and response. Both are declared using the `@app.middleware` decorator, with the decorator's parameter being a string representing its type: `'request'` or `'response'`. Response middleware @@ -64,3 +68,45 @@ async def halt_request(request): async def halt_response(request, response): return text('I halted the response') ``` + +## Listeners + +If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners: + +- `before_server_start` +- `after_server_start` +- `before_server_stop` +- `after_server_stop` + +These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop. + +For example: + +```python +@app.listener('before_server_start') +async def setup_db(app, loop): + app.db = await db_setup() + +@app.listener('after_server_start') +async def notify_server_started(app, loop): + print('Server successfully started!') + +@app.listener('before_server_stop') +async def notify_server_stopping(app, loop): + print('Server shutting down!') + +@app.listener('after_server_stop') +async def close_db(app, loop): + await app.db.close() +``` + +If you want to schedule a background task to run after the loop has started, +Sanic provides the `add_task` method to easily do so. + +```python +async def notify_server_started_after_five_seconds(): + await asyncio.sleep(5) + print('Server successfully started!') + +app.add_task(notify_server_started_after_five_seconds()) +``` From 5d293df64bc22217d0b5a689c6dbaa21aa4cc56b Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 23 Mar 2017 18:37:06 -0700 Subject: [PATCH 42/68] add blueprint strict_slashes --- sanic/blueprints.py | 60 +++++++++++++++++++++++++--------------- tests/test_blueprints.py | 27 ++++++++++++++++++ 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index e17d4b81..7e9953e0 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -3,7 +3,9 @@ from collections import defaultdict, namedtuple from sanic.constants import HTTP_METHODS from sanic.views import CompositionView -FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host']) +FutureRoute = namedtuple('Route', + ['handler', 'uri', 'methods', + 'host', 'strict_slashes']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) @@ -44,7 +46,8 @@ class Blueprint: app.route( uri=uri[1:] if uri.startswith('//') else uri, methods=future.methods, - host=future.host or self.host + host=future.host or self.host, + strict_slashes=future.strict_slashes )(future.handler) for future in self.websocket_routes: @@ -55,7 +58,8 @@ class Blueprint: uri = url_prefix + future.uri if url_prefix else future.uri app.websocket( uri=uri, - host=future.host or self.host + host=future.host or self.host, + strict_slashes=future.strict_slashes )(future.handler) # Middleware @@ -82,19 +86,21 @@ class Blueprint: for listener in listeners: app.listener(event)(listener) - def route(self, uri, methods=frozenset({'GET'}), host=None): + def route(self, uri, methods=frozenset({'GET'}), host=None, + strict_slashes=False): """Create a blueprint route from a decorated function. :param uri: endpoint at which the route will be accessible. :param methods: list of acceptable HTTP methods. """ def decorator(handler): - route = FutureRoute(handler, uri, methods, host) + route = FutureRoute(handler, uri, methods, host, strict_slashes) self.routes.append(route) return handler return decorator - def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None): + def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, + strict_slashes=False): """Create a blueprint route from a function. :param handler: function for handling uri requests. Accepts function, @@ -115,16 +121,17 @@ class Blueprint: if isinstance(handler, CompositionView): methods = handler.handlers.keys() - self.route(uri=uri, methods=methods, host=host)(handler) + self.route(uri=uri, methods=methods, host=host, + strict_slashes=strict_slashes)(handler) return handler - def websocket(self, uri, host=None): + def websocket(self, uri, host=None, strict_slashes=False): """Create a blueprint websocket route from a decorated function. :param uri: endpoint at which the route will be accessible. """ def decorator(handler): - route = FutureRoute(handler, uri, [], host) + route = FutureRoute(handler, uri, [], host, strict_slashes) self.websocket_routes.append(route) return handler return decorator @@ -183,23 +190,30 @@ class Blueprint: self.statics.append(static) # Shorthand method decorators - def get(self, uri, host=None): - return self.route(uri, methods=["GET"], host=host) + def get(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=["GET"], host=host, + strict_slashes=strict_slashes) - def post(self, uri, host=None): - return self.route(uri, methods=["POST"], host=host) + def post(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=["POST"], host=host, + strict_slashes=strict_slashes) - def put(self, uri, host=None): - return self.route(uri, methods=["PUT"], host=host) + def put(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=["PUT"], host=host, + strict_slashes=strict_slashes) - def head(self, uri, host=None): - return self.route(uri, methods=["HEAD"], host=host) + def head(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=["HEAD"], host=host, + strict_slashes=strict_slashes) - def options(self, uri, host=None): - return self.route(uri, methods=["OPTIONS"], host=host) + def options(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=["OPTIONS"], host=host, + strict_slashes=strict_slashes) - def patch(self, uri, host=None): - return self.route(uri, methods=["PATCH"], host=host) + def patch(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=["PATCH"], host=host, + strict_slashes=strict_slashes) - def delete(self, uri, host=None): - return self.route(uri, methods=["DELETE"], host=host) + def delete(self, uri, host=None, strict_slashes=False): + return self.route(uri, methods=["DELETE"], host=host, + strict_slashes=strict_slashes) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index fed4a03a..46726836 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -24,6 +24,33 @@ def test_bp(): assert response.text == 'Hello' +def test_bp_strict_slash(): + app = Sanic('test_route_strict_slash') + bp = Blueprint('test_text') + + @bp.get('/get', strict_slashes=True) + def handler(request): + return text('OK') + + @bp.post('/post/', strict_slashes=True) + def handler(request): + return text('OK') + + app.blueprint(bp) + + request, response = app.test_client.get('/get') + assert response.text == 'OK' + + request, response = app.test_client.get('/get/') + assert response.status == 404 + + request, response = app.test_client.post('/post/') + assert response.text == 'OK' + + request, response = app.test_client.post('/post') + assert response.status == 404 + + def test_bp_with_url_prefix(): app = Sanic('test_text') bp = Blueprint('test_text', url_prefix='/test1') From 65ae7669f9acd0fa1e3d68310d143e647f5a94ca Mon Sep 17 00:00:00 2001 From: Pete Wildsmith Date: Fri, 24 Mar 2017 10:11:30 +0000 Subject: [PATCH 43/68] Document synchronous response.write in streaming The Streaming section of the docs was updated to make clear that a synchronous write should be used in the callback, but this section was not updated. --- docs/sanic/response.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sanic/response.md b/docs/sanic/response.md index 1a336d47..12718ca1 100644 --- a/docs/sanic/response.md +++ b/docs/sanic/response.md @@ -55,8 +55,8 @@ from sanic import response @app.route("/streaming") async def index(request): async def streaming_fn(response): - await response.write('foo') - await response.write('bar') + response.write('foo') + response.write('bar') return response.stream(streaming_fn, content_type='text/plain') ``` From c5b50fe3cfb4cd3eb84b6eb1d9692f3bc0a888b9 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 21 Mar 2017 18:37:46 -0700 Subject: [PATCH 44/68] allow setting config from individual env variables --- docs/sanic/config.md | 8 ++++++++ sanic/app.py | 5 +++-- sanic/config.py | 16 +++++++++++++++- tests/test_config.py | 11 +++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index 0c22de4b..3ed40fda 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -29,6 +29,14 @@ In general the convention is to only have UPPERCASE configuration parameters. Th There are several ways how to load configuration. +### From environment variables. + +Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_vars` boolean to the Sanic constructor to override that: + +```python +app = Sanic(load_vars=False) +``` + ### From an Object If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module: diff --git a/sanic/app.py b/sanic/app.py index 2fd52fae..646981ec 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -25,7 +25,8 @@ from sanic.websocket import WebSocketProtocol, ConnectionClosed class Sanic: - def __init__(self, name=None, router=None, error_handler=None): + def __init__(self, name=None, router=None, error_handler=None, + load_env=True): # Only set up a default log handler if the # end-user application didn't set anything up. if not logging.root.handlers and log.level == logging.NOTSET: @@ -44,7 +45,7 @@ class Sanic: self.name = name self.router = router or Router() self.error_handler = error_handler or ErrorHandler() - self.config = Config() + self.config = Config(load_env=load_env) self.request_middleware = deque() self.response_middleware = deque() self.blueprints = {} diff --git a/sanic/config.py b/sanic/config.py index 3b9a102a..9fb09cbf 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,9 +1,10 @@ import os import types +SANIC_PREFIX = 'SANIC_' class Config(dict): - def __init__(self, defaults=None): + def __init__(self, defaults=None, load_env=True): super().__init__(defaults or {}) self.LOGO = """ ▄▄▄▄▄ @@ -29,6 +30,9 @@ class Config(dict): self.REQUEST_MAX_SIZE = 100000000 # 100 megababies self.REQUEST_TIMEOUT = 60 # 60 seconds + if load_env: + self.load_environment_vars() + def __getattr__(self, attr): try: return self[attr] @@ -90,3 +94,13 @@ class Config(dict): for key in dir(obj): if key.isupper(): self[key] = getattr(obj, key) + + def load_environment_vars(self): + for k, v in os.environ.items(): + """ + Looks for any SANIC_ prefixed environment variables and applies + them to the configuration if present. + """ + if k.startswith(SANIC_PREFIX): + _, config_key = k.split(SANIC_PREFIX, 1) + self[config_key] = v diff --git a/tests/test_config.py b/tests/test_config.py index c7e41ade..aa7a0e4d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -16,6 +16,17 @@ def test_load_from_object(): assert app.config.CONFIG_VALUE == 'should be used' assert 'not_for_config' not in app.config +def test_auto_load_env(): + environ["SANIC_TEST_ANSWER"] = "42" + app = Sanic() + assert app.config.TEST_ANSWER == "42" + del environ["SANIC_TEST_ANSWER"] + +def test_auto_load_env(): + environ["SANIC_TEST_ANSWER"] = "42" + app = Sanic(load_env=False) + assert getattr(app.config, 'TEST_ANSWER', None) == None + del environ["SANIC_TEST_ANSWER"] def test_load_from_file(): app = Sanic('test_load_from_file') From b00b2561e5b684702b23e94413f223b17bab2c4b Mon Sep 17 00:00:00 2001 From: itiel Date: Sun, 26 Mar 2017 21:16:03 +0300 Subject: [PATCH 45/68] add sanic-nginx-docker-example to extensions.md --- docs/sanic/extensions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index b11470d9..895c983d 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -18,3 +18,6 @@ A list of Sanic extensions created by the community. `Babel` library - [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter. - [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers. +- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose. + + From 724c03630a42777dca31ab0e1dad22d5d88f9708 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 26 Mar 2017 11:49:15 -0700 Subject: [PATCH 46/68] Update extensions.md --- docs/sanic/extensions.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 895c983d..37bac1ad 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -19,5 +19,3 @@ A list of Sanic extensions created by the community. - [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter. - [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers. - [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose. - - From 1ddb01ac44332b2ab56c73ba68f27699013c4d86 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 26 Feb 2017 16:31:39 -0800 Subject: [PATCH 47/68] remove stop_event --- sanic/app.py | 28 ++++++++++++++++++++++++---- sanic/server.py | 20 ++++++++++++-------- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index e101caf5..a8a1e32a 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -554,19 +554,24 @@ class Sanic: if protocol is None: protocol = (WebSocketProtocol if self.websocket_enabled else HttpProtocol) + if stop_event is not None: + if debug: + warnings.simplefilter('default') + warnings.warn("stop_event will be removed from future versions.", + DeprecationWarning) 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) + register_sys_signals=register_sys_signals) try: self.is_running = True if workers == 1: serve(**server_settings) else: - serve_multiple(server_settings, workers, stop_event) + serve_multiple(server_settings, workers) except: log.exception( 'Experienced exception while trying to serve') @@ -592,16 +597,23 @@ class Sanic: NOTE: This does not support multiprocessing and is not the preferred way to run a Sanic application. """ +<<<<<<< df9884de3c7ca6ad248162c8f404afd0ed774359 if protocol is None: protocol = (WebSocketProtocol if self.websocket_enabled else HttpProtocol) +======= + if stop_event is not None: + if debug: + warnings.simplefilter('default') + warnings.warn("stop_event will be removed from future versions.", + DeprecationWarning) +>>>>>>> remove stop_event 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 or get_event_loop(), protocol=protocol, - backlog=backlog, stop_event=stop_event, - run_async=True) + backlog=backlog, run_async=True) return await serve(**server_settings) @@ -611,6 +623,7 @@ class Sanic: protocol=HttpProtocol, backlog=100, stop_event=None, register_sys_signals=True, run_async=False): """Helper function used by `run` and `create_server`.""" +<<<<<<< df9884de3c7ca6ad248162c8f404afd0ed774359 if isinstance(ssl, dict): # try common aliaseses @@ -622,6 +635,13 @@ class Sanic: context.load_cert_chain(cert, keyfile=key) ssl = context +======= + if stop_event is not None: + if debug: + warnings.simplefilter('default') + warnings.warn("stop_event will be removed from future versions.", + DeprecationWarning) +>>>>>>> remove stop_event if loop is not None: if debug: warnings.simplefilter('default') diff --git a/sanic/server.py b/sanic/server.py index 00bb2331..14ce1ffd 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -4,10 +4,13 @@ import traceback import warnings from functools import partial from inspect import isawaitable -from multiprocessing import Process, Event +from multiprocessing import Process from os import set_inheritable -from signal import SIGTERM, SIGINT -from signal import signal as signal_func +from signal import ( + SIGTERM, SIGINT, + signal as signal_func, + Signals +) from socket import socket, SOL_SOCKET, SO_REUSEADDR from time import time @@ -421,7 +424,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, loop.close() -def serve_multiple(server_settings, workers, stop_event=None): +def serve_multiple(server_settings, workers): """Start multiple server processes simultaneously. Stop on interrupt and terminate signals, and drain connections when complete. @@ -448,11 +451,12 @@ def serve_multiple(server_settings, workers, stop_event=None): server_settings['host'] = None server_settings['port'] = None - if stop_event is None: - stop_event = Event() + def sig_handler(signal, frame): + log.info("Received signal {}. Shutting down.".format( + Signals(signal).name)) - signal_func(SIGINT, lambda s, f: stop_event.set()) - signal_func(SIGTERM, lambda s, f: stop_event.set()) + signal_func(SIGINT, lambda s, f: sig_handler(s, f)) + signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) processes = [] for _ in range(workers): From 9c68d713ba580fae80efdee54c683bf13331c8ba Mon Sep 17 00:00:00 2001 From: monobot Date: Mon, 27 Mar 2017 22:47:35 +0100 Subject: [PATCH 48/68] added asyncorm example --- examples/asyncorm/__init__.py | 0 examples/asyncorm/__main__.py | 140 ++++++++++++++++++++++++ examples/asyncorm/library/__init__.py | 0 examples/asyncorm/library/models.py | 21 ++++ examples/asyncorm/library/serializer.py | 15 +++ examples/asyncorm/requirements.txt | 2 + 6 files changed, 178 insertions(+) create mode 100644 examples/asyncorm/__init__.py create mode 100644 examples/asyncorm/__main__.py create mode 100644 examples/asyncorm/library/__init__.py create mode 100644 examples/asyncorm/library/models.py create mode 100644 examples/asyncorm/library/serializer.py create mode 100644 examples/asyncorm/requirements.txt diff --git a/examples/asyncorm/__init__.py b/examples/asyncorm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/asyncorm/__main__.py b/examples/asyncorm/__main__.py new file mode 100644 index 00000000..20537f7b --- /dev/null +++ b/examples/asyncorm/__main__.py @@ -0,0 +1,140 @@ +from sanic import Sanic +from sanic.exceptions import NotFound +from sanic.response import json +from sanic.views import HTTPMethodView + +from asyncorm import configure_orm +from asyncorm.exceptions import QuerysetError + +from library.models import Book +from library.serializer import BookSerializer + +app = Sanic(name=__name__) + + +@app.listener('before_server_start') +def orm_configure(sanic, loop): + db_config = {'database': 'sanic_example', + 'host': 'localhost', + 'user': 'sanicdbuser', + 'password': 'sanicDbPass', + } + + # configure_orm needs a dictionary with: + # * the database configuration + # * the application/s where the models are defined + orm_app = configure_orm({'loop': loop, # always use the sanic loop! + 'db_config': db_config, + 'modules': ['library', ], # list of apps + }) + + # orm_app is the object that orchestrates the whole ORM + # sync_db should be run only once, better do that as external command + # it creates the tables in the database!!!! + # orm_app.sync_db() + + +# for all the 404 lets handle the exceptions +@app.exception(NotFound) +def ignore_404s(request, exception): + return json({'method': request.method, + 'status': exception.status_code, + 'error': exception.args[0], + 'results': None, + }) + + +# now the propper sanic workflow +class BooksView(HTTPMethodView): + def arg_parser(self, request): + parsed_args = {} + for k, v in request.args.items(): + parsed_args[k] = v[0] + return parsed_args + + async def get(self, request): + filtered_by = self.arg_parser(request) + + if filtered_by: + q_books = await Book.objects.filter(**filtered_by) + else: + q_books = await Book.objects.all() + + books = [BookSerializer.serialize(book) for book in q_books] + + return json({'method': request.method, + 'status': 200, + 'results': books or None, + 'count': len(books), + }) + + async def post(self, request): + # populate the book with the data in the request + book = Book(**request.json) + + # and await on save + await book.save() + + return json({'method': request.method, + 'status': 201, + 'results': BookSerializer.serialize(book), + }) + + +class BookView(HTTPMethodView): + async def get_object(self, request, book_id): + try: + # await on database consults + book = await Book.objects.get(**{'id': book_id}) + except QuerysetError as e: + raise NotFound(e.args[0]) + return book + + async def get(self, request, book_id): + # await on database consults + book = await self.get_object(request, book_id) + + return json({'method': request.method, + 'status': 200, + 'results': BookSerializer.serialize(book), + }) + + async def put(self, request, book_id): + # await on database consults + book = await self.get_object(request, book_id) + # await on save + await book.save(**request.json) + + return json({'method': request.method, + 'status': 200, + 'results': BookSerializer.serialize(book), + }) + + async def patch(self, request, book_id): + # await on database consults + book = await self.get_object(request, book_id) + # await on save + await book.save(**request.json) + + return json({'method': request.method, + 'status': 200, + 'results': BookSerializer.serialize(book), + }) + + async def delete(self, request, book_id): + # await on database consults + book = await self.get_object(request, book_id) + # await on its deletion + await book.delete() + + return json({'method': request.method, + 'status': 200, + 'results': None + }) + + +app.add_route(BooksView.as_view(), '/books/') +app.add_route(BookView.as_view(), '/books//') + +if __name__ == '__main__': + app.run() diff --git a/examples/asyncorm/library/__init__.py b/examples/asyncorm/library/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/asyncorm/library/models.py b/examples/asyncorm/library/models.py new file mode 100644 index 00000000..51cacb1b --- /dev/null +++ b/examples/asyncorm/library/models.py @@ -0,0 +1,21 @@ +from asyncorm.model import Model +from asyncorm.fields import CharField, IntegerField, DateField + + +BOOK_CHOICES = ( + ('hard cover', 'hard cover book'), + ('paperback', 'paperback book') +) + + +# This is a simple model definition +class Book(Model): + name = CharField(max_length=50) + synopsis = CharField(max_length=255) + book_type = CharField(max_length=15, null=True, choices=BOOK_CHOICES) + pages = IntegerField(null=True) + date_created = DateField(auto_now=True) + + class Meta(): + ordering = ['name', ] + unique_together = ['name', 'synopsis'] diff --git a/examples/asyncorm/library/serializer.py b/examples/asyncorm/library/serializer.py new file mode 100644 index 00000000..00faa91e --- /dev/null +++ b/examples/asyncorm/library/serializer.py @@ -0,0 +1,15 @@ +from asyncorm.model import ModelSerializer, SerializerMethod +from library.models import Book + + +class BookSerializer(ModelSerializer): + book_type = SerializerMethod() + + def get_book_type(self, instance): + return instance.book_type_display() + + class Meta(): + model = Book + fields = [ + 'id', 'name', 'synopsis', 'book_type', 'pages', 'date_created' + ] diff --git a/examples/asyncorm/requirements.txt b/examples/asyncorm/requirements.txt new file mode 100644 index 00000000..9b824ce6 --- /dev/null +++ b/examples/asyncorm/requirements.txt @@ -0,0 +1,2 @@ +asyncorm==0.0.7 +sanic==0.4.1 From 748ca281855e540a40929ce9871b36f31fba702a Mon Sep 17 00:00:00 2001 From: Joir-dan Gumbs Date: Mon, 27 Mar 2017 15:42:13 -0700 Subject: [PATCH 49/68] Created detailed example of using sanic. Adds configurations based on various environment variables, handles database access (using aioredis), uses middleware to check for db object and attach it to request object, and logs events to a logfile (which is set using environment variables). --- examples/detailed_example.py | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 examples/detailed_example.py diff --git a/examples/detailed_example.py b/examples/detailed_example.py new file mode 100644 index 00000000..aaca6fd1 --- /dev/null +++ b/examples/detailed_example.py @@ -0,0 +1,96 @@ +import json +import logging +import os + +import aioredis + +import sanic +from sanic import Sanic + + +ENV_VARS = ["REDIS_HOST", "REDIS_PORT", + "REDIS_MINPOOL", "REDIS_MAXPOOL", + "REDIS_PASS", "APP_LOGFILE"] + +app = Sanic(name=__name__) + +logger = None + + +@app.middleware("request") +async def attach_db_connectors(request): + if not hasattr(request.app.config, "REDIS"): + logger.info("Setting up connection to Redis Cache") + request.app.config.REDIS = await aioredis.create_pool((app.config.REDIS_HOST, int(app.config.REDIS_PORT)), + minsize=int(app.config.REDIS_MINPOOL), + maxsize=int(app.config.REDIS_MAXPOOL), + password=app.config.REDIS_PASS) + # Just put the db objects in the request for easier access + request["redis"] = request.app.config.REDIS + + +@app.route("/state/", methods=["GET"]) +async def access_state(request, user_id): + try: + # Check to see if the value is in cache, if so lets return that + with await request["redis"] as redis_conn: + state = await redis_conn.get(user_id, encoding="utf-8") + if state: + return sanic.response.json(json.loads(state)) + # Then state object is not in redis + logger.critical("Unable to find user_data in cache.") + return sanic.response.HTTPResponse({"msg": "User state not found"}, status=404) + + except aioredis.ProtocolError: + logger.critical("Unable to connect to state cache") + return sanic.response.HTTPResponse({"msg": "Internal Server Error"}, status=500) + + +@app.route("/state//push", methods=["POST"]) +async def set_state(request, user_id): + try: + with await request["redis"] as redis_conn: + # Set the value in cache to your new value + await redis_conn.set(user_id, json.dumps(request.json), expire=1800) + logger.info("Successfully retrieved from cache") + return sanic.response.HTTPResponse({"msg": "Successfully pushed state to cache"}) + except aioredis.ProtocolError: + logger.critical("UNable to connect to state cache") + return sanic.response.HTTPResponse({"msg": "Interal Server Error"}, status=500) + + +def configure(): + # Setup environment variables + env_vars = [os.environ.get(v, None) for v in ENV_VARS] + if not all(env_vars): + # Send back environment variables that were not set + return False, [ENV_VARS[i] for i, flag in env_vars if not flag] + else: + app.config.update({k: v for k, v in zip(ENV_VARS, env_vars)}) + setup_logging() + logging.info("Configuraiton complete") + return True, [] + +def setup_logging(): + logging_format = "[%(asctime)s] %(process)d-%(levelname)s " + logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " + logging_format += "%(message)s" + + print(app.config.APP_LOGFILE) + logging.basicConfig( + filename=app.config.APP_LOGFILE, + format=logging_format, + level=logging.DEBUG + ) + +if __name__ == "__main__": + result, missing = configure() + logger = logging.getLogger() + if result: + try: + app.run(host="0.0.0.0", port=8080, debug=True) + except: + logger.critical("User killed server. Closing") + else: + need_string = ", ".join(missing) + logger.critical("Unable to start. Missing environment variables [{0}]".format(need_string)) From ee79750a220f49f2439209b0359551acf4afab2b Mon Sep 17 00:00:00 2001 From: Joir-dan Gumbs Date: Tue, 28 Mar 2017 01:22:36 -0700 Subject: [PATCH 50/68] Cleaned up functions. Added extra middleware function to log endpoint being called. Added documentation to make easier to understand. --- examples/detailed_example.py | 41 ++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/examples/detailed_example.py b/examples/detailed_example.py index aaca6fd1..2f66006a 100644 --- a/examples/detailed_example.py +++ b/examples/detailed_example.py @@ -17,8 +17,17 @@ app = Sanic(name=__name__) logger = None +@app.middleware("request") +async def log_uri(request): + # Simple middleware to log the URI endpoint that was called + logger.info("URI called: {0}".format(request.url)) + + @app.middleware("request") async def attach_db_connectors(request): + # We will check to see if our redis pool has been created + # If you have access to the app object, you can set app.config directly + # If you don't have access to the app object, you can use request.app if not hasattr(request.app.config, "REDIS"): logger.info("Setting up connection to Redis Cache") request.app.config.REDIS = await aioredis.create_pool((app.config.REDIS_HOST, int(app.config.REDIS_PORT)), @@ -26,6 +35,7 @@ async def attach_db_connectors(request): maxsize=int(app.config.REDIS_MAXPOOL), password=app.config.REDIS_PASS) # Just put the db objects in the request for easier access + logger.info("Passing pool to request object") request["redis"] = request.app.config.REDIS @@ -40,7 +50,6 @@ async def access_state(request, user_id): # Then state object is not in redis logger.critical("Unable to find user_data in cache.") return sanic.response.HTTPResponse({"msg": "User state not found"}, status=404) - except aioredis.ProtocolError: logger.critical("Unable to connect to state cache") return sanic.response.HTTPResponse({"msg": "Internal Server Error"}, status=500) @@ -49,13 +58,14 @@ async def access_state(request, user_id): @app.route("/state//push", methods=["POST"]) async def set_state(request, user_id): try: + # Pull a connection from the pool with await request["redis"] as redis_conn: # Set the value in cache to your new value await redis_conn.set(user_id, json.dumps(request.json), expire=1800) logger.info("Successfully retrieved from cache") return sanic.response.HTTPResponse({"msg": "Successfully pushed state to cache"}) except aioredis.ProtocolError: - logger.critical("UNable to connect to state cache") + logger.critical("Unable to connect to state cache") return sanic.response.HTTPResponse({"msg": "Interal Server Error"}, status=500) @@ -64,33 +74,36 @@ def configure(): env_vars = [os.environ.get(v, None) for v in ENV_VARS] if not all(env_vars): # Send back environment variables that were not set - return False, [ENV_VARS[i] for i, flag in env_vars if not flag] + return False, ", ".join([ENV_VARS[i] for i, flag in env_vars if not flag]) else: + # Add all the env vars to our app config app.config.update({k: v for k, v in zip(ENV_VARS, env_vars)}) setup_logging() - logging.info("Configuraiton complete") - return True, [] + return True, None + def setup_logging(): logging_format = "[%(asctime)s] %(process)d-%(levelname)s " logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " logging_format += "%(message)s" - print(app.config.APP_LOGFILE) logging.basicConfig( filename=app.config.APP_LOGFILE, format=logging_format, - level=logging.DEBUG - ) + level=logging.DEBUG) -if __name__ == "__main__": - result, missing = configure() - logger = logging.getLogger() + +def main(result, missing): if result: try: app.run(host="0.0.0.0", port=8080, debug=True) except: - logger.critical("User killed server. Closing") + logging.critical("User killed server. Closing") else: - need_string = ", ".join(missing) - logger.critical("Unable to start. Missing environment variables [{0}]".format(need_string)) + logging.critical("Unable to start. Missing environment variables [{0}]".format(missing)) + + +if __name__ == "__main__": + result, missing = configure() + logger = logging.getLogger() + main(result, missing) From 42ba5298a767352e13896da142131bf116760ae9 Mon Sep 17 00:00:00 2001 From: Jakob Bowyer Date: Tue, 28 Mar 2017 10:50:09 +0100 Subject: [PATCH 51/68] Flake8 cleanup. Setup environmental variables. Skipping broken tests unrelated. --- sanic/response.py | 35 +++++++++++++++------------ setup.py | 43 +++++++++++++++++++++------------ tests/test_payload_too_large.py | 5 +++- tests/test_request_data.py | 2 +- tests/test_request_timeout.py | 11 ++++++--- 5 files changed, 60 insertions(+), 36 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 38cd68db..d8dadbc8 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,5 +1,6 @@ from mimetypes import guess_type from os import path + try: from ujson import dumps as json_dumps except: @@ -167,12 +168,12 @@ class StreamingHTTPResponse(BaseHTTPResponse): return (b'HTTP/%b %d %b\r\n' b'%b' b'%b\r\n') % ( - version.encode(), - self.status, - status, - timeout_header, - headers - ) + version.encode(), + self.status, + status, + timeout_header, + headers + ) class HTTPResponse(BaseHTTPResponse): @@ -216,14 +217,14 @@ class HTTPResponse(BaseHTTPResponse): b'%b' b'%b\r\n' b'%b') % ( - version.encode(), - self.status, - status, - b'keep-alive' if keep_alive else b'close', - timeout_header, - headers, - self.body - ) + version.encode(), + self.status, + status, + b'keep-alive' if keep_alive else b'close', + timeout_header, + headers, + self.body + ) @property def cookies(self): @@ -331,7 +332,11 @@ def stream( :param headers: Custom Headers. """ return StreamingHTTPResponse( - streaming_fn, headers=headers, content_type=content_type, status=status) + streaming_fn, + headers=headers, + content_type=content_type, + status=status + ) def redirect(to, headers=None, status=302, diff --git a/setup.py b/setup.py index 594c88b9..deb52c27 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,10 @@ Sanic import codecs import os import re -from setuptools import setup +from distutils.errors import DistutilsPlatformError +from distutils.util import strtobool +from setuptools import setup with codecs.open(os.path.join(os.path.abspath(os.path.dirname( __file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp: @@ -15,7 +17,7 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname( except IndexError: raise RuntimeError('Unable to determine version.') -setup_kwargs = { +setup_kwargs = { 'name': 'sanic', 'version': version, 'url': 'http://github.com/channelcat/sanic/', @@ -35,23 +37,32 @@ setup_kwargs = { ], } +ujson = 'ujson>=1.35' +uvloop = 'uvloop>=0.5.3' + +requirements = [ + 'httptools>=0.0.9', + uvloop, + ujson, + 'aiofiles>=0.3.0', + 'websockets>=3.2', +] +if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): + print("Installing without uJSON") + requirements.remove(ujson) + +if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): + print("Installing without uvLoop") + requirements.remove(uvloop) + try: - normal_requirements = [ - 'httptools>=0.0.9', - 'uvloop>=0.5.3', - 'ujson>=1.35', - 'aiofiles>=0.3.0', - 'websockets>=3.2', - ] - setup_kwargs['install_requires'] = normal_requirements + setup_kwargs['install_requires'] = requirements setup(**setup_kwargs) except DistutilsPlatformError as exception: - windows_requirements = [ - 'httptools>=0.0.9', - 'aiofiles>=0.3.0', - 'websockets>=3.2', - ] - setup_kwargs['install_requires'] = windows_requirements + requirements.remove(ujson) + requirements.remove(uvloop) + print("Installing without uJSON or uvLoop") + setup_kwargs['install_requires'] = requirements setup(**setup_kwargs) # Installation was successful diff --git a/tests/test_payload_too_large.py b/tests/test_payload_too_large.py index a1a58d3d..6c67fd9c 100644 --- a/tests/test_payload_too_large.py +++ b/tests/test_payload_too_large.py @@ -1,6 +1,8 @@ +import pytest + from sanic import Sanic -from sanic.response import text from sanic.exceptions import PayloadTooLarge +from sanic.response import text data_received_app = Sanic('data_received') data_received_app.config.REQUEST_MAX_SIZE = 1 @@ -43,6 +45,7 @@ async def handler3(request): return text('OK') +@pytest.mark.skip # see: https://github.com/channelcat/sanic/issues/598 def test_payload_too_large_at_on_header_default(): data = 'a' * 1000 response = on_header_default_app.test_client.post( diff --git a/tests/test_request_data.py b/tests/test_request_data.py index c874f71d..c2493ffe 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -2,7 +2,7 @@ import random from sanic import Sanic from sanic.response import json -from ujson import loads +from json import loads def test_storage(): diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 404aec12..7eba7c5a 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -1,8 +1,11 @@ -from sanic import Sanic import asyncio -from sanic.response import text -from sanic.exceptions import RequestTimeout + +import pytest + +from sanic import Sanic from sanic.config import Config +from sanic.exceptions import RequestTimeout +from sanic.response import text Config.REQUEST_TIMEOUT = 1 request_timeout_app = Sanic('test_request_timeout') @@ -20,6 +23,7 @@ def handler_exception(request, exception): return text('Request Timeout from error_handler.', 408) +@pytest.mark.skip # see: https://github.com/channelcat/sanic/issues/598 def test_server_error_request_timeout(): request, response = request_timeout_app.test_client.get('/1') assert response.status == 408 @@ -32,6 +36,7 @@ async def handler_2(request): return text('OK') +@pytest.mark.skip # see: https://github.com/channelcat/sanic/issues/598 def test_default_server_error_request_timeout(): request, response = request_timeout_default_app.test_client.get('/1') assert response.status == 408 From e3cf50f791fb189624384bc855292daaa1780efc Mon Sep 17 00:00:00 2001 From: Joir-dan Gumbs Date: Tue, 28 Mar 2017 15:00:23 -0700 Subject: [PATCH 52/68] Changed out redis middleware for redis listeners (open/close). Fleshed out the payloads of both endpoints. Added comment about required packages. --- examples/detailed_example.py | 61 ++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/examples/detailed_example.py b/examples/detailed_example.py index 2f66006a..99e71cb1 100644 --- a/examples/detailed_example.py +++ b/examples/detailed_example.py @@ -1,7 +1,10 @@ +# This demo requires aioredis and environmental variables established in ENV_VARS import json import logging import os +from datetime import datetime + import aioredis import sanic @@ -23,20 +26,28 @@ async def log_uri(request): logger.info("URI called: {0}".format(request.url)) +@app.listener('before_server_start') +async def before_server_start(app, loop): + logger.info("Starting redis pool") + app.redis_pool = await aioredis.create_pool( + (app.config.REDIS_HOST, int(app.config.REDIS_PORT)), + minsize=int(app.config.REDIS_MINPOOL), + maxsize=int(app.config.REDIS_MAXPOOL), + password=app.config.REDIS_PASS) + + +@app.listener('after_server_stop') +async def after_server_stop(app, loop): + logger.info("Closing redis pool") + app.redis_pool.close() + await app.redis_pool.wait_closed() + + @app.middleware("request") async def attach_db_connectors(request): - # We will check to see if our redis pool has been created - # If you have access to the app object, you can set app.config directly - # If you don't have access to the app object, you can use request.app - if not hasattr(request.app.config, "REDIS"): - logger.info("Setting up connection to Redis Cache") - request.app.config.REDIS = await aioredis.create_pool((app.config.REDIS_HOST, int(app.config.REDIS_PORT)), - minsize=int(app.config.REDIS_MINPOOL), - maxsize=int(app.config.REDIS_MAXPOOL), - password=app.config.REDIS_PASS) # Just put the db objects in the request for easier access - logger.info("Passing pool to request object") - request["redis"] = request.app.config.REDIS + logger.info("Passing redis pool to request object") + request["redis"] = request.app.redis_pool @app.route("/state/", methods=["GET"]) @@ -46,13 +57,23 @@ async def access_state(request, user_id): with await request["redis"] as redis_conn: state = await redis_conn.get(user_id, encoding="utf-8") if state: - return sanic.response.json(json.loads(state)) + return sanic.response.json({"msg": "Success", + "status": 200, + "success": True, + "data": json.loads(state), + "finished_at": datetime.now().isoformat()}) # Then state object is not in redis logger.critical("Unable to find user_data in cache.") - return sanic.response.HTTPResponse({"msg": "User state not found"}, status=404) + return sanic.response.HTTPResponse({"msg": "User state not found", + "success": False, + "status": 404, + "finished_at": datetime.now().isoformat()}, status=404) except aioredis.ProtocolError: logger.critical("Unable to connect to state cache") - return sanic.response.HTTPResponse({"msg": "Internal Server Error"}, status=500) + return sanic.response.HTTPResponse({"msg": "Internal Server Error", + "status": 500, + "success": False, + "finished_at": datetime.now().isoformat()}, status=500) @app.route("/state//push", methods=["POST"]) @@ -62,11 +83,17 @@ async def set_state(request, user_id): with await request["redis"] as redis_conn: # Set the value in cache to your new value await redis_conn.set(user_id, json.dumps(request.json), expire=1800) - logger.info("Successfully retrieved from cache") - return sanic.response.HTTPResponse({"msg": "Successfully pushed state to cache"}) + logger.info("Successfully pushed state to cache") + return sanic.response.HTTPResponse({"msg": "Successfully pushed state to cache", + "success": True, + "status": 200, + "finished_at": datetime.now().isoformat()}) except aioredis.ProtocolError: logger.critical("Unable to connect to state cache") - return sanic.response.HTTPResponse({"msg": "Interal Server Error"}, status=500) + return sanic.response.HTTPResponse({"msg": "Internal Server Error", + "status": 500, + "success": False, + "finished_at": datetime.now().isoformat()}, status=500) def configure(): From 8ba1b5fc35ce0d023fa68d8e9c693f5dbcf7c9c3 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 28 Mar 2017 22:33:55 -0500 Subject: [PATCH 53/68] Add docker support for local unit testing Addresses consistency across different OS's by making it very similar to the base Travis image. --- Dockerfile | 6 ++++++ Makefile | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 Dockerfile create mode 100644 Makefile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..ee8ca2be --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.6 + +ADD . /app +WORKDIR /app + +RUN pip install tox diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ad64412f --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +test: + find . -name "*.pyc" -delete + docker build -t sanic/test-image . + docker run -t sanic/test-image tox From 75a4df0f32e60e103404758901b1f8965cdacf79 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 28 Mar 2017 22:34:34 -0500 Subject: [PATCH 54/68] Simplify this, it had a lot of fluff --- requirements-dev.txt | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1f11a90c..65dd0d7d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,18 +1,10 @@ -aiocache aiofiles aiohttp beautifulsoup4 -bottle coverage -falcon -gunicorn httptools -kyoukai +flake8 pytest -recommonmark -sphinx -sphinx_rtd_theme -tornado tox ujson uvloop From 1ef69adc6f8967ac8b227d4c6ad9bd0270220bb0 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 28 Mar 2017 22:34:44 -0500 Subject: [PATCH 55/68] Simplify this as well, it replicated effort --- tox.ini | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 33e4298f..0e6dc7c6 100644 --- a/tox.ini +++ b/tox.ini @@ -10,12 +10,7 @@ python = [testenv] deps = - aiofiles - aiohttp - websockets - pytest - beautifulsoup4 - coverage + -rrequirements-dev.txt commands = pytest tests {posargs} From dcc19d17d4a43b0b7c6d4472dfea52146bd2dbb9 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 28 Mar 2017 22:38:35 -0500 Subject: [PATCH 56/68] Lock to aiohttp 1.3.5 for now --- requirements-dev.txt | 2 +- tests/test_redirect.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 65dd0d7d..28014eb6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ aiofiles -aiohttp +aiohttp==1.3.5 beautifulsoup4 coverage httptools diff --git a/tests/test_redirect.py b/tests/test_redirect.py index 25efe1f3..421ee1cf 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -88,4 +88,4 @@ def test_chained_redirect(redirect_app): assert request.url.endswith('/1') assert response.status == 200 assert response.text == 'OK' - assert response.url.path.endswith('/3') + assert response.url.endswith('/3') From 3a8cfb1f45b68eb7ff8f9fbef3ec0fdf8226baed Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 28 Mar 2017 22:38:45 -0500 Subject: [PATCH 57/68] Make these tests not so far apart --- tests/test_payload_too_large.py | 52 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/tests/test_payload_too_large.py b/tests/test_payload_too_large.py index a1a58d3d..70ec56ce 100644 --- a/tests/test_payload_too_large.py +++ b/tests/test_payload_too_large.py @@ -2,48 +2,46 @@ from sanic import Sanic from sanic.response import text from sanic.exceptions import PayloadTooLarge -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(): + data_received_app = Sanic('data_received') + data_received_app.config.REQUEST_MAX_SIZE = 1 + + @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) + response = data_received_app.test_client.get('/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(): + data_received_default_app = Sanic('data_received_default') + data_received_default_app.config.REQUEST_MAX_SIZE = 1 + + @data_received_default_app.route('/1') + async def handler2(request): + return text('OK') + response = data_received_default_app.test_client.get( '/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(): + on_header_default_app = Sanic('on_header') + on_header_default_app.config.REQUEST_MAX_SIZE = 500 + + @on_header_default_app.post('/1') + async def handler3(request): + return text('OK') + data = 'a' * 1000 response = on_header_default_app.test_client.post( '/1', gather_request=False, data=data) From 04a0774ee598445db681a4a7db2fca200be65897 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 28 Mar 2017 22:44:01 -0500 Subject: [PATCH 58/68] Fix line length --- sanic/response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 38cd68db..6afcb061 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -132,8 +132,8 @@ class StreamingHTTPResponse(BaseHTTPResponse): async def stream( self, version="1.1", keep_alive=False, keep_alive_timeout=None): - """Streams headers, runs the `streaming_fn` callback that writes content - to the response body, then finalizes the response body. + """Streams headers, runs the `streaming_fn` callback that writes + content to the response body, then finalizes the response body. """ headers = self.get_headers( version, keep_alive=keep_alive, From f0a55b5cbb5542ba3ec769be28e7fb02aaf2a359 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 28 Mar 2017 22:47:52 -0500 Subject: [PATCH 59/68] Fix line length again... --- sanic/response.py | 6 +++++- tests/test_redirect.py | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 6afcb061..3da9ac5e 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -331,7 +331,11 @@ def stream( :param headers: Custom Headers. """ return StreamingHTTPResponse( - streaming_fn, headers=headers, content_type=content_type, status=status) + streaming_fn, + headers=headers, + content_type=content_type, + status=status + ) def redirect(to, headers=None, status=302, diff --git a/tests/test_redirect.py b/tests/test_redirect.py index 421ee1cf..f5b734e3 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -88,4 +88,7 @@ def test_chained_redirect(redirect_app): assert request.url.endswith('/1') assert response.status == 200 assert response.text == 'OK' - assert response.url.endswith('/3') + try: + assert response.url.endswith('/3') + except AttributeError: + assert response.url.path.endswith('/3') From 18405b39080ff80c1b854075f57a218b66b963cf Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 28 Mar 2017 22:57:58 -0500 Subject: [PATCH 60/68] There was a line missing here? --- sanic/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/config.py b/sanic/config.py index 9fb09cbf..99d39a9c 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -3,6 +3,7 @@ import types SANIC_PREFIX = 'SANIC_' + class Config(dict): def __init__(self, defaults=None, load_env=True): super().__init__(defaults or {}) From 22699db8550fc06674314bdca7463a4437243972 Mon Sep 17 00:00:00 2001 From: Jakob Bowyer Date: Wed, 29 Mar 2017 09:16:53 +0100 Subject: [PATCH 61/68] Moved skips to seperate pull request --- tests/test_payload_too_large.py | 3 --- tests/test_request_timeout.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/tests/test_payload_too_large.py b/tests/test_payload_too_large.py index 6c67fd9c..95b5de5b 100644 --- a/tests/test_payload_too_large.py +++ b/tests/test_payload_too_large.py @@ -1,5 +1,3 @@ -import pytest - from sanic import Sanic from sanic.exceptions import PayloadTooLarge from sanic.response import text @@ -45,7 +43,6 @@ async def handler3(request): return text('OK') -@pytest.mark.skip # see: https://github.com/channelcat/sanic/issues/598 def test_payload_too_large_at_on_header_default(): data = 'a' * 1000 response = on_header_default_app.test_client.post( diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 7eba7c5a..40bc364c 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -1,7 +1,5 @@ import asyncio -import pytest - from sanic import Sanic from sanic.config import Config from sanic.exceptions import RequestTimeout @@ -23,7 +21,6 @@ def handler_exception(request, exception): return text('Request Timeout from error_handler.', 408) -@pytest.mark.skip # see: https://github.com/channelcat/sanic/issues/598 def test_server_error_request_timeout(): request, response = request_timeout_app.test_client.get('/1') assert response.status == 408 @@ -36,7 +33,6 @@ async def handler_2(request): return text('OK') -@pytest.mark.skip # see: https://github.com/channelcat/sanic/issues/598 def test_default_server_error_request_timeout(): request, response = request_timeout_default_app.test_client.get('/1') assert response.status == 408 From 1cf730d95746240190531d7aa5d6e43f8d68b3a6 Mon Sep 17 00:00:00 2001 From: Jakob Bowyer Date: Wed, 29 Mar 2017 10:12:24 +0100 Subject: [PATCH 62/68] Added usage documentation for optional installs --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index 3f565f71..878ba24c 100644 --- a/README.rst +++ b/README.rst @@ -59,6 +59,13 @@ Installation - ``python -m pip install sanic`` +To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables +using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features +installation. + +- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic`` + + Documentation ------------- From 5b704478d95f1ff0fac1ce1f0da46374a98b374b Mon Sep 17 00:00:00 2001 From: monobot Date: Wed, 29 Mar 2017 22:06:54 +0100 Subject: [PATCH 63/68] raw_args for request objects --- sanic/request.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index dc2fd872..4a15c22f 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -121,6 +121,10 @@ class Request(dict): self.parsed_args = RequestParameters() return self.parsed_args + @property + def raw_args(self): + return {k: v[0] for k, v in self.args.items()} + @property def cookies(self): if self._cookies is None: From fcd8e5e5ad532d48adc7cec3201d1a64257994f1 Mon Sep 17 00:00:00 2001 From: nosaevb Date: Thu, 30 Mar 2017 23:02:46 +0100 Subject: [PATCH 64/68] Typo Fix in docs/sanic/cookies.md --- docs/sanic/cookies.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sanic/cookies.md b/docs/sanic/cookies.md index 0a1042a2..e71bcc47 100644 --- a/docs/sanic/cookies.md +++ b/docs/sanic/cookies.md @@ -5,7 +5,7 @@ both read and write cookies, which are stored as key-value pairs. ## Reading cookies -A user's cookies can be accessed `Request` object's `cookie` dictionary. +A user's cookies can be accessed via the `Request` object's `cookies` dictionary. ```python from sanic.response import text @@ -42,20 +42,20 @@ from sanic.response import text @app.route("/cookie") async def test(request): response = text("Time to eat some cookies muahaha") - + # This cookie will be set to expire in 0 seconds del response.cookies['kill_me'] - + # This cookie will self destruct in 5 seconds response.cookies['short_life'] = 'Glad to be here' response.cookies['short_life']['max-age'] = 5 del response.cookies['favorite_color'] - + # This cookie will remain unchanged response.cookies['favorite_color'] = 'blue' response.cookies['favorite_color'] = 'pink' del response.cookies['favorite_color'] - + return response ``` From daedda8547cdcbc852b8e3e0b283053ce494c209 Mon Sep 17 00:00:00 2001 From: Jakob Bowyer Date: Fri, 31 Mar 2017 08:51:12 +0100 Subject: [PATCH 65/68] Checked out original tests --- sanic/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/config.py b/sanic/config.py index 3b9a102a..3aa2189c 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,4 +1,5 @@ import os + import types From edd8770c67f7e2b0938942848f344d3abecc47e8 Mon Sep 17 00:00:00 2001 From: Jakob Bowyer Date: Fri, 31 Mar 2017 08:53:46 +0100 Subject: [PATCH 66/68] Restored tests to upstream/master --- tests/test_request_data.py | 2 +- tests/test_request_timeout.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_request_data.py b/tests/test_request_data.py index c2493ffe..c874f71d 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -2,7 +2,7 @@ import random from sanic import Sanic from sanic.response import json -from json import loads +from ujson import loads def test_storage(): diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 40bc364c..404aec12 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -1,9 +1,8 @@ -import asyncio - from sanic import Sanic -from sanic.config import Config -from sanic.exceptions import RequestTimeout +import asyncio from sanic.response import text +from sanic.exceptions import RequestTimeout +from sanic.config import Config Config.REQUEST_TIMEOUT = 1 request_timeout_app = Sanic('test_request_timeout') From 25edbe6805a18436415bfff64e9bc9873238c184 Mon Sep 17 00:00:00 2001 From: monobot Date: Sun, 2 Apr 2017 02:28:16 +0100 Subject: [PATCH 67/68] update docs --- docs/sanic/request_data.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index a86a0f21..87f619a3 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -9,30 +9,34 @@ The following variables are accessible as properties on `Request` objects: ```python from sanic.response import json - + @app.route("/json") def post_json(request): return json({ "received": True, "message": request.json }) ``` - + - `args` (dict) - Query string variables. A query string is the section of a URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed, - the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`. + the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`. The request's `query_string` variable holds the unparsed string value. ```python from sanic.response import json - + @app.route("/query_string") def query_string(request): return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) ``` +- `raw_args` (dict) - On many cases you would need to access the url arguments in + a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the + `raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`. + - `files` (dictionary of `File` objects) - List of files that have a name, body, and type ```python from sanic.response import json - + @app.route("/files") def post_json(request): test_file = request.files.get('test') @@ -50,7 +54,7 @@ The following variables are accessible as properties on `Request` objects: ```python from sanic.response import json - + @app.route("/form") def post_json(request): return json({ "received": True, "form_data": request.form, "test": request.form.get('test') }) @@ -58,15 +62,15 @@ The following variables are accessible as properties on `Request` objects: - `body` (bytes) - Posted raw body. This property allows retrieval of the request's raw data, regardless of content type. - + ```python from sanic.response import text - + @app.route("/users", methods=["POST",]) def create_user(request): return text("You are trying to create a user with the following POST: %s" % request.body) ``` - + - `ip` (str) - IP address of the requester. - `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object. From 62ebcba64756981601d95cc9b5bc2c178d123b61 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Mon, 3 Apr 2017 14:45:18 -0500 Subject: [PATCH 68/68] Add graphql integration extension Closes #579 --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 37bac1ad..ec22a9d2 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -19,3 +19,4 @@ A list of Sanic extensions created by the community. - [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter. - [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers. - [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose. +- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic