From 0024edbbb9699431a67c3b0e481ddf9c6a4cc31a Mon Sep 17 00:00:00 2001 From: messense Date: Fri, 26 May 2017 11:11:26 +0800 Subject: [PATCH 01/45] Add websocket max_size and max_queue configuration --- sanic/app.py | 4 +++- sanic/config.py | 4 +++- sanic/server.py | 8 ++++++-- sanic/websocket.py | 10 ++++++++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 776fcbec..f6b1e84e 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -696,7 +696,9 @@ class Sanic: 'loop': loop, 'register_sys_signals': register_sys_signals, 'backlog': backlog, - 'has_log': has_log + 'has_log': has_log, + 'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE, + 'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE } # -------------------------------------------- # diff --git a/sanic/config.py b/sanic/config.py index 8ffdb6c4..e3563bc1 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -122,9 +122,11 @@ class Config(dict): ▌ ▐ ▀▀▄▄▄▀ ▀▀▄▄▀ """ - self.REQUEST_MAX_SIZE = 100000000 # 100 megababies + self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes self.REQUEST_TIMEOUT = 60 # 60 seconds self.KEEP_ALIVE = keep_alive + self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes + self.WEBSOCKET_MAX_QUEUE = 32 if load_env: self.load_environment_vars() diff --git a/sanic/server.py b/sanic/server.py index 36b2a7a2..f3106226 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -74,7 +74,8 @@ class HttpProtocol(asyncio.Protocol): def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections=set(), request_timeout=60, request_max_size=None, request_class=None, has_log=True, - keep_alive=True, is_request_stream=False, router=None): + keep_alive=True, is_request_stream=False, router=None, + **kwargs): self.loop = loop self.transport = None self.request = None @@ -387,7 +388,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, signal=Signal(), request_class=None, has_log=True, keep_alive=True, - is_request_stream=False, router=None): + is_request_stream=False, router=None, websocket_max_size=None, + websocket_max_queue=None): """Start asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -442,6 +444,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, keep_alive=keep_alive, is_request_stream=is_request_stream, router=router, + websocket_max_size=websocket_max_size, + websocket_max_queue=websocket_max_queue ) server_coroutine = loop.create_server( diff --git a/sanic/websocket.py b/sanic/websocket.py index a712eda8..94320a5e 100644 --- a/sanic/websocket.py +++ b/sanic/websocket.py @@ -6,9 +6,12 @@ from websockets import ConnectionClosed # noqa class WebSocketProtocol(HttpProtocol): - def __init__(self, *args, **kwargs): + def __init__(self, *args, websocket_max_size=None, + websocket_max_queue=None, **kwargs): super().__init__(*args, **kwargs) self.websocket = None + self.websocket_max_size = websocket_max_size + self.websocket_max_queue = websocket_max_queue def connection_timeout(self): # timeouts make no sense for websocket routes @@ -62,6 +65,9 @@ class WebSocketProtocol(HttpProtocol): request.transport.write(rv) # hook up the websocket protocol - self.websocket = WebSocketCommonProtocol() + self.websocket = WebSocketCommonProtocol( + max_size=self.websocket_max_size, + max_queue=self.websocket_max_queue + ) self.websocket.connection_made(request.transport) return self.websocket From a5249d1f5d1d10278e32012e05bccded6343deb0 Mon Sep 17 00:00:00 2001 From: Tadas Talaikis Date: Sat, 27 May 2017 11:06:45 +0300 Subject: [PATCH 02/45] aiomysql has DictCursor --- examples/sanic_aiomysql_with_global_pool.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/sanic_aiomysql_with_global_pool.py b/examples/sanic_aiomysql_with_global_pool.py index 65d5832d..aa6c680c 100644 --- a/examples/sanic_aiomysql_with_global_pool.py +++ b/examples/sanic_aiomysql_with_global_pool.py @@ -46,15 +46,13 @@ async def get_pool(app, loop): @app.route("/") async def test(): - result = [] data = {} async with app.pool['aiomysql'].acquire() as conn: - async with conn.cursor() as cur: + async with conn.cursor(aiomysql.DictCursor) 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]}) + result = await cur.fetchall() if result or len(result) > 0: - data['data'] = res + data['data'] = result return json(data) From 5bb640ca1706a42a012109dc3d811925d7453217 Mon Sep 17 00:00:00 2001 From: Anton Kochnev Date: Sun, 28 May 2017 14:37:41 +0800 Subject: [PATCH 03/45] Update jinja_example.py Added python version check for enabling async mode. --- examples/jinja_example/jinja_example.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/jinja_example/jinja_example.py b/examples/jinja_example/jinja_example.py index 430940c5..a866489a 100644 --- a/examples/jinja_example/jinja_example.py +++ b/examples/jinja_example/jinja_example.py @@ -5,13 +5,19 @@ from sanic import Sanic from sanic import response from jinja2 import Environment, PackageLoader, select_autoescape +import sys +# Enabling async template execution which allows you to take advantage +# of newer Python features requires Python 3.6 or later. +enable_async = sys.version_info >= (3, 6) + + app = Sanic(__name__) # Load the template environment with async support template_env = Environment( loader=PackageLoader('jinja_example', 'templates'), autoescape=select_autoescape(['html', 'xml']), - enable_async=True + enable_async=enable_async ) # Load the template from file From 9a2755576382ccc9097a894c6ea7506464d78172 Mon Sep 17 00:00:00 2001 From: monobot Date: Mon, 29 May 2017 00:01:56 +0100 Subject: [PATCH 04/45] update asyncorm version example to 0.2.0 --- examples/asyncorm/__main__.py | 16 +++++++++++++--- examples/asyncorm/library/models.py | 21 ++++++++++++--------- examples/asyncorm/library/serializer.py | 2 +- examples/asyncorm/requirements.txt | 3 ++- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/examples/asyncorm/__main__.py b/examples/asyncorm/__main__.py index 6e4d4ff2..f7fca250 100644 --- a/examples/asyncorm/__main__.py +++ b/examples/asyncorm/__main__.py @@ -16,8 +16,8 @@ app = Sanic(name=__name__) def orm_configure(sanic, loop): db_config = {'database': 'sanic_example', 'host': 'localhost', - 'user': 'sanicdbuser', - 'password': 'sanicDbPass', + 'user': 'ormdbuser', + 'password': 'ormDbPass', } # configure_orm needs a dictionary with: @@ -44,6 +44,15 @@ def ignore_404s(request, exception): }) +@app.exception(URLBuildError) +def ignore_urlbuilderrors(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): @@ -80,6 +89,7 @@ class BooksView(HTTPMethodView): 'results': BookSerializer.serialize(book), }) + class BookView(HTTPMethodView): async def get_object(self, request, book_id): try: @@ -136,4 +146,4 @@ app.add_route(BooksView.as_view(), '/books/') app.add_route(BookView.as_view(), '/books//') if __name__ == '__main__': - app.run() + app.run(port=9000, debug=True) diff --git a/examples/asyncorm/library/models.py b/examples/asyncorm/library/models.py index 51cacb1b..b1a0749f 100644 --- a/examples/asyncorm/library/models.py +++ b/examples/asyncorm/library/models.py @@ -1,5 +1,4 @@ -from asyncorm.model import Model -from asyncorm.fields import CharField, IntegerField, DateField +from asyncorm import models BOOK_CHOICES = ( @@ -9,13 +8,17 @@ BOOK_CHOICES = ( # 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 Book(models.Model): + name = models.CharField(max_length=50) + synopsis = models.CharField(max_length=255) + book_type = models.CharField( + max_length=15, + null=True, + choices=BOOK_CHOICES + ) + pages = models.IntegerField(null=True) + date_created = models.DateField(auto_now=True) class Meta(): - ordering = ['name', ] + ordering = ['-name', ] unique_together = ['name', 'synopsis'] diff --git a/examples/asyncorm/library/serializer.py b/examples/asyncorm/library/serializer.py index 00faa91e..a31d5897 100644 --- a/examples/asyncorm/library/serializer.py +++ b/examples/asyncorm/library/serializer.py @@ -1,4 +1,4 @@ -from asyncorm.model import ModelSerializer, SerializerMethod +from asyncorm.serializers import ModelSerializer, SerializerMethod from library.models import Book diff --git a/examples/asyncorm/requirements.txt b/examples/asyncorm/requirements.txt index b4aa9cae..e9f21a88 100644 --- a/examples/asyncorm/requirements.txt +++ b/examples/asyncorm/requirements.txt @@ -1,2 +1,3 @@ -asyncorm==0.0.9 +asyncorm>=0.0.9 sanic==0.5.4 + From 4b5320a8f0e3f9baece2b202ae41e4ea182cb94e Mon Sep 17 00:00:00 2001 From: Jeremy Zimmerman Date: Thu, 1 Jun 2017 11:53:05 -0700 Subject: [PATCH 05/45] Clean up of examples. Removes non-core examples, optimizes and restyles remaining to strictly follow PEP 8 styling guidelines. Non-Core examples will be moved to Wiki. --- examples/aiohttp_example.py | 26 --- examples/asyncorm/__init__.py | 0 examples/asyncorm/__main__.py | 149 ------------------ examples/asyncorm/library/__init__.py | 0 examples/asyncorm/library/models.py | 24 --- examples/asyncorm/library/serializer.py | 15 -- examples/asyncorm/requirements.txt | 3 - examples/blueprints.py | 2 +- examples/cache_example.py | 73 --------- examples/dask_distributed.py | 41 ----- examples/detailed_example.py | 136 ---------------- examples/exception_monitoring.py | 8 +- examples/jinja_example/jinja_example.py | 34 ---- examples/jinja_example/requirements.txt | 8 - .../templates/example_template.html | 10 -- examples/limit_concurrency.py | 5 +- examples/modify_header_example.py | 4 +- examples/override_logging.py | 2 + examples/plotly_example/plotlyjs_example.py | 25 --- examples/plotly_example/requirements.txt | 2 - examples/redirect_example.py | 3 +- examples/request_stream/client.py | 10 -- examples/request_stream/server.py | 65 -------- examples/request_timeout.py | 2 +- examples/run_async.py | 2 +- examples/sanic_aiomysql_with_global_pool.py | 60 ------- examples/sanic_aiopeewee.py | 61 ------- examples/sanic_aiopg_example.py | 65 -------- examples/sanic_aiopg_sqlalchemy_example.py | 67 -------- examples/sanic_aioredis_example.py | 34 ---- examples/sanic_asyncpg_example.py | 51 ------ examples/sanic_motor.py | 41 ----- examples/sanic_peewee.py | 116 -------------- examples/try_everything.py | 14 +- examples/unix_socket.py | 2 +- examples/url_for_example.py | 4 +- examples/vhosts.py | 4 + examples/websocket.py | 3 +- 38 files changed, 36 insertions(+), 1135 deletions(-) delete mode 100644 examples/aiohttp_example.py delete mode 100644 examples/asyncorm/__init__.py delete mode 100644 examples/asyncorm/__main__.py delete mode 100644 examples/asyncorm/library/__init__.py delete mode 100644 examples/asyncorm/library/models.py delete mode 100644 examples/asyncorm/library/serializer.py delete mode 100644 examples/asyncorm/requirements.txt delete mode 100644 examples/cache_example.py delete mode 100644 examples/dask_distributed.py delete mode 100644 examples/detailed_example.py delete mode 100644 examples/jinja_example/jinja_example.py delete mode 100644 examples/jinja_example/requirements.txt delete mode 100644 examples/jinja_example/templates/example_template.html delete mode 100644 examples/plotly_example/plotlyjs_example.py delete mode 100644 examples/plotly_example/requirements.txt delete mode 100644 examples/request_stream/client.py delete mode 100644 examples/request_stream/server.py delete mode 100644 examples/sanic_aiomysql_with_global_pool.py delete mode 100644 examples/sanic_aiopeewee.py delete mode 100644 examples/sanic_aiopg_example.py delete mode 100644 examples/sanic_aiopg_sqlalchemy_example.py delete mode 100644 examples/sanic_aioredis_example.py delete mode 100644 examples/sanic_asyncpg_example.py delete mode 100644 examples/sanic_motor.py delete mode 100644 examples/sanic_peewee.py diff --git a/examples/aiohttp_example.py b/examples/aiohttp_example.py deleted file mode 100644 index 853ea959..00000000 --- a/examples/aiohttp_example.py +++ /dev/null @@ -1,26 +0,0 @@ -from sanic import Sanic -from sanic import response - -import aiohttp - -app = Sanic(__name__) - -async def fetch(session, url): - """ - Use session object to perform 'get' request on url - """ - async with session.get(url) as result: - return await result.json() - - -@app.route('/') -async def handle_request(request): - url = "https://api.github.com/repos/channelcat/sanic" - - async with aiohttp.ClientSession() as session: - result = await fetch(session, url) - return response.json(result) - - -if __name__ == '__main__': - app.run(host="0.0.0.0", port=8000, workers=2) diff --git a/examples/asyncorm/__init__.py b/examples/asyncorm/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/asyncorm/__main__.py b/examples/asyncorm/__main__.py deleted file mode 100644 index f7fca250..00000000 --- a/examples/asyncorm/__main__.py +++ /dev/null @@ -1,149 +0,0 @@ -from sanic import Sanic -from sanic.exceptions import NotFound, URLBuildError -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': 'ormdbuser', - 'password': 'ormDbPass', - } - - # 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, - }) - - -@app.exception(URLBuildError) -def ignore_urlbuilderrors(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): - - async def get(self, request): - filtered_by = request.raw_args - - if filtered_by: - try: - q_books = Book.objects.filter(**filtered_by) - except AttributeError as e: - raise URLBuildError(e.args[0]) - else: - q_books = Book.objects.all() - - books = [] - async for book in q_books: - books.append(BookSerializer.serialize(book)) - - 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(port=9000, debug=True) diff --git a/examples/asyncorm/library/__init__.py b/examples/asyncorm/library/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/asyncorm/library/models.py b/examples/asyncorm/library/models.py deleted file mode 100644 index b1a0749f..00000000 --- a/examples/asyncorm/library/models.py +++ /dev/null @@ -1,24 +0,0 @@ -from asyncorm import models - - -BOOK_CHOICES = ( - ('hard cover', 'hard cover book'), - ('paperback', 'paperback book') -) - - -# This is a simple model definition -class Book(models.Model): - name = models.CharField(max_length=50) - synopsis = models.CharField(max_length=255) - book_type = models.CharField( - max_length=15, - null=True, - choices=BOOK_CHOICES - ) - pages = models.IntegerField(null=True) - date_created = models.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 deleted file mode 100644 index a31d5897..00000000 --- a/examples/asyncorm/library/serializer.py +++ /dev/null @@ -1,15 +0,0 @@ -from asyncorm.serializers 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 deleted file mode 100644 index e9f21a88..00000000 --- a/examples/asyncorm/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -asyncorm>=0.0.9 -sanic==0.5.4 - diff --git a/examples/blueprints.py b/examples/blueprints.py index c8c9b43d..03154049 100644 --- a/examples/blueprints.py +++ b/examples/blueprints.py @@ -1,6 +1,6 @@ from sanic import Sanic from sanic import Blueprint -from sanic.response import json, text +from sanic.response import json app = Sanic(__name__) diff --git a/examples/cache_example.py b/examples/cache_example.py deleted file mode 100644 index 80b8edd8..00000000 --- a/examples/cache_example.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Example of caching using aiocache package. To run it you will need to install -aiocache with `pip install aiocache` plus a Redis instance running -in localhost:6379 - -Running this example you will see that the first call lasts 3 seconds and -the rest are instant because the value is retrieved from Redis. - -If you want more info about the package check -https://github.com/argaen/aiocache -""" - -import asyncio -import uuid - -from sanic import Sanic -from sanic.response import json -from sanic.log import log - -from aiocache import caches, cached - - -app = Sanic(__name__) - - -config = { - "default": { - "cache": "aiocache.RedisCache", - "endpoint": "127.0.0.1", - "timeout": 2, - "namespace": "sanic", - "serializer": { - "class": "aiocache.serializers.JsonSerializer" - } - } -} - - -@app.listener('before_server_start') -def init_cache(sanic, loop): - caches.set_config(config) - - -# You can use alias or pass explicit args instead -@cached(key="my_custom_key", ttl=30, alias="default") -async def expensive_call(): - log.info("Expensive has been called") - await asyncio.sleep(3) - # You are storing the whole dict under "my_custom_key" - return {"test": str(uuid.uuid4())} - - -async def get_cache_value(): - # This lazy loads a singleton so it will return the same instance every - # time. If you want to create a new instance, you can use - # `caches.create("default")` - cache = caches.get("default") - return await cache.get("my_custom_key") - - -@app.route("/") -async def test(request): - log.info("Received GET /") - return json(await expensive_call()) - - -@app.route("/retrieve") -async def test(request): - log.info("Received GET /retrieve") - return json(await get_cache_value()) - - -app.run(host="0.0.0.0", port=8000) diff --git a/examples/dask_distributed.py b/examples/dask_distributed.py deleted file mode 100644 index ef3fe423..00000000 --- a/examples/dask_distributed.py +++ /dev/null @@ -1,41 +0,0 @@ -from sanic import Sanic -from sanic import response - -from tornado.platform.asyncio import BaseAsyncIOLoop, to_asyncio_future -from distributed import LocalCluster, Client - - -app = Sanic(__name__) - - -def square(x): - return x**2 - - -@app.listener('after_server_start') -async def setup(app, loop): - # configure tornado use asyncio's loop - ioloop = BaseAsyncIOLoop(loop) - - # init distributed client - app.client = Client('tcp://localhost:8786', loop=ioloop, start=False) - await to_asyncio_future(app.client._start()) - - -@app.listener('before_server_stop') -async def stop(app, loop): - await to_asyncio_future(app.client._shutdown()) - - -@app.route('/') -async def test(request, value): - future = app.client.submit(square, value) - result = await to_asyncio_future(future._result()) - return response.text(f'The square of {value} is {result}') - - -if __name__ == '__main__': - # Distributed cluster should run somewhere else - with LocalCluster(scheduler_port=8786, nanny=False, n_workers=2, - threads_per_worker=1) as cluster: - app.run(host="0.0.0.0", port=8000) diff --git a/examples/detailed_example.py b/examples/detailed_example.py deleted file mode 100644 index 99e71cb1..00000000 --- a/examples/detailed_example.py +++ /dev/null @@ -1,136 +0,0 @@ -# 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 -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 log_uri(request): - # Simple middleware to log the URI endpoint that was called - 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): - # Just put the db objects in the request for easier access - logger.info("Passing redis pool to request object") - request["redis"] = request.app.redis_pool - - -@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({"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", - "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, - "success": False, - "finished_at": datetime.now().isoformat()}, status=500) - - -@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 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": "Internal Server Error", - "status": 500, - "success": False, - "finished_at": datetime.now().isoformat()}, 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, ", ".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() - 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" - - logging.basicConfig( - filename=app.config.APP_LOGFILE, - format=logging_format, - level=logging.DEBUG) - - -def main(result, missing): - if result: - try: - app.run(host="0.0.0.0", port=8080, debug=True) - except: - logging.critical("User killed server. Closing") - else: - logging.critical("Unable to start. Missing environment variables [{0}]".format(missing)) - - -if __name__ == "__main__": - result, missing = configure() - logger = logging.getLogger() - main(result, missing) diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py index 76d16d90..1cb18e27 100644 --- a/examples/exception_monitoring.py +++ b/examples/exception_monitoring.py @@ -37,7 +37,6 @@ server's error_handler to an instance of our CustomHandler """ from sanic import Sanic -from sanic import response app = Sanic(__name__) @@ -49,8 +48,7 @@ app.error_handler = handler async def test(request): # Here, something occurs which causes an unexpected exception # This exception will flow to our custom handler. - 1 / 0 - return response.json({"test": True}) + raise SanicException('You Broke It!') - -app.run(host="0.0.0.0", port=8000, debug=True) \ No newline at end of file +if __name__ == '__main__': + app.run(host="127.0.0.1", port=8000, debug=True) diff --git a/examples/jinja_example/jinja_example.py b/examples/jinja_example/jinja_example.py deleted file mode 100644 index a866489a..00000000 --- a/examples/jinja_example/jinja_example.py +++ /dev/null @@ -1,34 +0,0 @@ -# Render templates in a Flask like way from a "template" directory in -# the project - -from sanic import Sanic -from sanic import response -from jinja2 import Environment, PackageLoader, select_autoescape - -import sys -# Enabling async template execution which allows you to take advantage -# of newer Python features requires Python 3.6 or later. -enable_async = sys.version_info >= (3, 6) - - -app = Sanic(__name__) - -# Load the template environment with async support -template_env = Environment( - loader=PackageLoader('jinja_example', 'templates'), - autoescape=select_autoescape(['html', 'xml']), - enable_async=enable_async -) - -# Load the template from file -template = template_env.get_template("example_template.html") - - -@app.route('/') -async def test(request): - rendered_template = await template.render_async( - knights='that say nih; asynchronously') - return response.html(rendered_template) - - -app.run(host="0.0.0.0", port=8080, debug=True) diff --git a/examples/jinja_example/requirements.txt b/examples/jinja_example/requirements.txt deleted file mode 100644 index c7cd5408..00000000 --- a/examples/jinja_example/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -aiofiles==0.3.1 -httptools==0.0.9 -Jinja2==2.9.6 -MarkupSafe==1.0 -sanic==0.5.2 -ujson==1.35 -uvloop==0.8.0 -websockets==3.3 diff --git a/examples/jinja_example/templates/example_template.html b/examples/jinja_example/templates/example_template.html deleted file mode 100644 index b914ad64..00000000 --- a/examples/jinja_example/templates/example_template.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - My Webpage - - -

Hello World

-

knights - {{ knights }}

- - diff --git a/examples/limit_concurrency.py b/examples/limit_concurrency.py index 8d38a813..f6b4b01a 100644 --- a/examples/limit_concurrency.py +++ b/examples/limit_concurrency.py @@ -8,11 +8,12 @@ app = Sanic(__name__) sem = None + @app.listener('before_server_start') def init(sanic, loop): global sem - CONCURRENCY_PER_WORKER = 4 - sem = asyncio.Semaphore(CONCURRENCY_PER_WORKER, loop=loop) + concurrency_per_worker = 4 + sem = asyncio.Semaphore(concurrency_per_worker, loop=loop) async def bounded_fetch(session, url): """ diff --git a/examples/modify_header_example.py b/examples/modify_header_example.py index bb5efe8e..f13e5f00 100644 --- a/examples/modify_header_example.py +++ b/examples/modify_header_example.py @@ -7,6 +7,7 @@ from sanic import response app = Sanic(__name__) + @app.route('/') def handle_request(request): return response.json( @@ -14,7 +15,8 @@ def handle_request(request): headers={'X-Served-By': 'sanic'}, status=200 ) - + + @app.route('/unauthorized') def handle_request(request): return response.json( diff --git a/examples/override_logging.py b/examples/override_logging.py index e4d529e8..99d26997 100644 --- a/examples/override_logging.py +++ b/examples/override_logging.py @@ -14,6 +14,8 @@ log = logging.getLogger() # Set logger to override default basicConfig sanic = Sanic() + + @sanic.route("/") def test(request): log.info("received request; responding with 'hey'") diff --git a/examples/plotly_example/plotlyjs_example.py b/examples/plotly_example/plotlyjs_example.py deleted file mode 100644 index d32491a6..00000000 --- a/examples/plotly_example/plotlyjs_example.py +++ /dev/null @@ -1,25 +0,0 @@ -from sanic import Sanic -from sanic.response import html -import plotly -import plotly.graph_objs as go - -app = Sanic(__name__) - - -@app.route('/') -async def index(request): - trace1 = go.Scatter( - x=[0, 1, 2, 3, 4, 5], - y=[1.5, 1, 1.3, 0.7, 0.8, 0.9] - ) - trace2 = go.Bar( - x=[0, 1, 2, 3, 4, 5], - y=[1, 0.5, 0.7, -1.2, 0.3, 0.4] - ) - - data = [trace1, trace2] - return html(plotly.offline.plot(data, auto_open=False, output_type='div')) - - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=8000, debug=True) diff --git a/examples/plotly_example/requirements.txt b/examples/plotly_example/requirements.txt deleted file mode 100644 index 8e1e92c2..00000000 --- a/examples/plotly_example/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -plotly>=2.0.7 -sanic>=0.5.0 \ No newline at end of file diff --git a/examples/redirect_example.py b/examples/redirect_example.py index acc7d1ff..f73ad178 100644 --- a/examples/redirect_example.py +++ b/examples/redirect_example.py @@ -7,7 +7,8 @@ app = Sanic(__name__) @app.route('/') def handle_request(request): return response.redirect('/redirect') - + + @app.route('/redirect') async def test(request): return response.json({"Redirected": True}) diff --git a/examples/request_stream/client.py b/examples/request_stream/client.py deleted file mode 100644 index 30fcff06..00000000 --- a/examples/request_stream/client.py +++ /dev/null @@ -1,10 +0,0 @@ -import requests - -# Warning: This is a heavy process. - -data = "" -for i in range(1, 250000): - data += str(i) - -r = requests.post('http://127.0.0.1:8000/stream', data=data) -print(r.text) diff --git a/examples/request_stream/server.py b/examples/request_stream/server.py deleted file mode 100644 index 37acfd5d..00000000 --- a/examples/request_stream/server.py +++ /dev/null @@ -1,65 +0,0 @@ -from sanic import Sanic -from sanic.views import CompositionView -from sanic.views import HTTPMethodView -from sanic.views import stream as stream_decorator -from sanic.blueprints import Blueprint -from sanic.response import stream, text - -bp = Blueprint('blueprint_request_stream') -app = Sanic('request_stream') - - -class SimpleView(HTTPMethodView): - - @stream_decorator - async def post(self, request): - result = '' - while True: - body = await request.stream.get() - if body is None: - break - result += body.decode('utf-8') - return text(result) - - -@app.post('/stream', stream=True) -async def handler(request): - async def streaming(response): - while True: - body = await request.stream.get() - if body is None: - break - body = body.decode('utf-8').replace('1', 'A') - response.write(body) - return stream(streaming) - - -@bp.put('/bp_stream', stream=True) -async def bp_handler(request): - result = '' - while True: - body = await request.stream.get() - if body is None: - break - result += body.decode('utf-8').replace('1', 'A') - return text(result) - - -async def post_handler(request): - result = '' - while True: - body = await request.stream.get() - if body is None: - break - result += body.decode('utf-8') - return text(result) - -app.blueprint(bp) -app.add_route(SimpleView.as_view(), '/method_view') -view = CompositionView() -view.add(['POST'], post_handler, stream=True) -app.add_route(view, '/composition_view') - - -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8000) diff --git a/examples/request_timeout.py b/examples/request_timeout.py index fb2822ee..0c2c489c 100644 --- a/examples/request_timeout.py +++ b/examples/request_timeout.py @@ -18,4 +18,4 @@ async def test(request): def timeout(request, exception): return response.text('RequestTimeout from error_handler.', 408) -app.run(host='0.0.0.0', port=8000) \ No newline at end of file +app.run(host='0.0.0.0', port=8000) diff --git a/examples/run_async.py b/examples/run_async.py index b6f6d76d..3d8ab55a 100644 --- a/examples/run_async.py +++ b/examples/run_async.py @@ -1,12 +1,12 @@ from sanic import Sanic from sanic import response -from multiprocessing import Event from signal import signal, SIGINT import asyncio import uvloop app = Sanic(__name__) + @app.route("/") async def test(request): return response.json({"answer": "42"}) diff --git a/examples/sanic_aiomysql_with_global_pool.py b/examples/sanic_aiomysql_with_global_pool.py deleted file mode 100644 index aa6c680c..00000000 --- a/examples/sanic_aiomysql_with_global_pool.py +++ /dev/null @@ -1,60 +0,0 @@ -# encoding: utf-8 -""" -You need the aiomysql -""" -import os - -import aiomysql - -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() - - -@app.listener("before_server_start") -async def get_pool(app, loop): - """ - 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: - """ - app.pool = { - "aiomysql": await aiomysql.create_pool(host=database_host, user=database_user, password=database_password, - db=database_name, - maxsize=5)} - 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 ( - 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(): - data = {} - async with app.pool['aiomysql'].acquire() as conn: - async with conn.cursor(aiomysql.DictCursor) as cur: - await cur.execute("SELECT question, pub_date FROM sanic_polls") - result = await cur.fetchall() - if result or len(result) > 0: - data['data'] = result - return json(data) - - -if __name__ == '__main__': - app.run(host="127.0.0.1", workers=4, port=12000) diff --git a/examples/sanic_aiopeewee.py b/examples/sanic_aiopeewee.py deleted file mode 100644 index 53146862..00000000 --- a/examples/sanic_aiopeewee.py +++ /dev/null @@ -1,61 +0,0 @@ -from sanic import Sanic -from sanic.response import json - -from aiopeewee import AioModel, AioMySQLDatabase, model_to_dict -from peewee import CharField, TextField, DateTimeField -from peewee import ForeignKeyField, PrimaryKeyField - - -db = AioMySQLDatabase('test', user='root', password='', - host='127.0.0.1', port=3306) - - -class User(AioModel): - username = CharField() - - class Meta: - database = db - - -class Blog(AioModel): - user = ForeignKeyField(User) - title = CharField(max_length=25) - content = TextField(default='') - pub_date = DateTimeField(null=True) - pk = PrimaryKeyField() - - class Meta: - database = db - - -app = Sanic(__name__) - - -@app.listener('before_server_start') -async def setup(app, loop): - # create connection pool - await db.connect(loop) - # create table if not exists - await db.create_tables([User, Blog], safe=True) - - -@app.listener('before_server_stop') -async def stop(app, loop): - # close connection pool - await db.close() - - -@app.post('/users') -async def add_user(request): - user = await User.create(**request.json) - return json(await model_to_dict(user)) - - -@app.get('/users/count') -async def user_count(request): - count = await User.select().count() - return json({'count': count}) - - -if __name__ == '__main__': - app.run(host="0.0.0.0", port=8000) diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py deleted file mode 100644 index 16bde35f..00000000 --- a/examples/sanic_aiopg_example.py +++ /dev/null @@ -1,65 +0,0 @@ -""" To run this example you need additional aiopg package - -""" -import os -import asyncio - -import uvloop -import aiopg - -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'] - -connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, - database_password, - database_host, - database_name) - - -async def get_pool(): - return await aiopg.create_pool(connection) - -app = Sanic(name=__name__) - -@app.listener('before_server_start') -async def prepare_db(app, loop): - """ - Let's create some table and add some data - """ - async with aiopg.create_pool(connection) as pool: - async with pool.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 handle(request): - result = [] - async def test_select(): - async with aiopg.create_pool(connection) as pool: - async with pool.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]}) - res = await test_select() - return json({'polls': result}) - -if __name__ == '__main__': - app.run(host='0.0.0.0', - port=8000, - debug=True) diff --git a/examples/sanic_aiopg_sqlalchemy_example.py b/examples/sanic_aiopg_sqlalchemy_example.py deleted file mode 100644 index 34bda28a..00000000 --- a/examples/sanic_aiopg_sqlalchemy_example.py +++ /dev/null @@ -1,67 +0,0 @@ -""" To run this example you need additional aiopg package - -""" -import os -import asyncio -import datetime - -import uvloop -from aiopg.sa import create_engine -import sqlalchemy as sa - -from sanic import Sanic -from sanic.response import json - -database_name = os.environ['DATABASE_NAME'] -database_host = os.environ['DATABASE_HOST'] -database_user = os.environ['DATABASE_USER'] -database_password = os.environ['DATABASE_PASSWORD'] - -connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, - database_password, - database_host, - database_name) - -metadata = sa.MetaData() - -polls = sa.Table('sanic_polls', metadata, - sa.Column('id', sa.Integer, primary_key=True), - sa.Column('question', sa.String(50)), - sa.Column("pub_date", sa.DateTime)) - - -app = Sanic(name=__name__) - -@app.listener('before_server_start') -async def prepare_db(app, loop): - """ Let's add some data - - """ - async with create_engine(connection) as engine: - async with engine.acquire() as conn: - await conn.execute('DROP TABLE IF EXISTS sanic_polls') - await conn.execute("""CREATE TABLE sanic_polls ( - id serial primary key, - question varchar(50), - pub_date timestamp - );""") - for i in range(0, 100): - await conn.execute( - polls.insert().values(question=i, - pub_date=datetime.datetime.now()) - ) - - -@app.route("/") -async def handle(request): - async with create_engine(connection) as engine: - async with engine.acquire() as conn: - result = [] - async for row in conn.execute(polls.select()): - result.append({"question": row.question, - "pub_date": row.pub_date}) - return json({"polls": result}) - - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=8000) diff --git a/examples/sanic_aioredis_example.py b/examples/sanic_aioredis_example.py deleted file mode 100644 index 8ba51617..00000000 --- a/examples/sanic_aioredis_example.py +++ /dev/null @@ -1,34 +0,0 @@ -""" 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) diff --git a/examples/sanic_asyncpg_example.py b/examples/sanic_asyncpg_example.py deleted file mode 100644 index 9e2b6c65..00000000 --- a/examples/sanic_asyncpg_example.py +++ /dev/null @@ -1,51 +0,0 @@ -import os -import asyncio - -import uvloop -from asyncpg import connect, create_pool - -from sanic import Sanic -from sanic.response import json - -DB_CONFIG = { - 'host': '', - 'user': '', - 'password': '', - 'port': '', - 'database': '' -} - - -def jsonify(records): - """ - Parse asyncpg record response into JSON format - """ - return [dict(r.items()) for r in records] - - -app = Sanic(__name__) - - -@app.listener('before_server_start') -async def register_db(app, loop): - app.pool = await create_pool(**DB_CONFIG, loop=loop, max_size=100) - async with app.pool.acquire() as connection: - await connection.execute('DROP TABLE IF EXISTS sanic_post') - await connection.execute("""CREATE TABLE sanic_post ( - id serial primary key, - content varchar(50), - post_date timestamp - );""") - for i in range(0, 1000): - await connection.execute(f"""INSERT INTO sanic_post - (id, content, post_date) VALUES ({i}, {i}, now())""") - - -@app.get('/') -async def root_get(request): - async with app.pool.acquire() as connection: - results = await connection.fetch('SELECT * FROM sanic_post') - return json({'posts': jsonify(results)}) - -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080) diff --git a/examples/sanic_motor.py b/examples/sanic_motor.py deleted file mode 100644 index c7d2b60f..00000000 --- a/examples/sanic_motor.py +++ /dev/null @@ -1,41 +0,0 @@ -""" sanic motor (async driver for mongodb) example -Required packages: -pymongo==3.4.0 -motor==1.1 -sanic==0.2.0 -""" -from sanic import Sanic -from sanic import response - - -app = Sanic('motor_mongodb') - - -def get_db(): - from motor.motor_asyncio import AsyncIOMotorClient - mongo_uri = "mongodb://127.0.0.1:27017/test" - client = AsyncIOMotorClient(mongo_uri) - return client['test'] - - -@app.route('/objects', methods=['GET']) -async def get(request): - db = get_db() - docs = await db.test_col.find().to_list(length=100) - for doc in docs: - doc['id'] = str(doc['_id']) - del doc['_id'] - return response.json(docs) - - -@app.route('/post', methods=['POST']) -async def new(request): - doc = request.json - print(doc) - db = get_db() - object_id = await db.test_col.save(doc) - return response.json({'object_id': str(object_id)}) - - -if __name__ == "__main__": - app.run(host='0.0.0.0', port=8000, debug=True) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py deleted file mode 100644 index aaa139f1..00000000 --- a/examples/sanic_peewee.py +++ /dev/null @@ -1,116 +0,0 @@ - -## You need the following additional packages for this example -# aiopg -# peewee_async -# peewee - - -## sanic imports -from sanic import Sanic -from sanic.response import json - -## peewee_async related imports -import peewee -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: -# Also there’s no need to connect and re-connect before executing async queries -# 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 - - database.allow_sync = False - """ - - def __init__(self, _model_class, *args, **kwargs): - super(AsyncManager, self).__init__(*args, **kwargs) - self._model_class = _model_class - self.database.allow_sync = False - - 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) - - def new(self, *args, **kwargs): - return self._do_fill('create', *args, **kwargs) - - def get(self, *args, **kwargs): - return self._do_fill('get', *args, **kwargs) - - def execute(self, query): - return execute(query) - - -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 - - 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') - -# 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 KeyValue.objects.new(key=key, text=value) - return json({'object_id': obj.id}) - - -@app.route('/get') -async def get(request): - """ - Load all objects from database - """ - all_objects = await KeyValue.objects.execute(KeyValue.select()) - serialized_obj = [] - for obj in all_objects: - serialized_obj.append({ - 'id': obj.id, - 'key': obj.key, - 'value': obj.text} - ) - - return json({'objects': serialized_obj}) - - -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 f380d925..2bc1c7b3 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -19,24 +19,27 @@ def test_sync(request): @app.route("/dynamic//") -def test_params(request, name, id): - return response.text("yeehaww {} {}".format(name, id)) +def test_params(request, name, i): + return response.text("yeehaww {} {}".format(name, i)) @app.route("/exception") def exception(request): raise ServerError("It's dead jim") + @app.route("/await") async def test_await(request): import asyncio await asyncio.sleep(5) return response.text("I'm feeling sleepy") + @app.route("/file") async def test_file(request): return await response.file(os.path.abspath("setup.py")) + @app.route("/file_stream") async def test_file_stream(request): return await response.file_stream(os.path.abspath("setup.py"), @@ -46,9 +49,11 @@ async def test_file_stream(request): # Exceptions # ----------------------------------------------- # + @app.exception(ServerError) async def test(request, exception): - return response.json({"exception": "{}".format(exception), "status": exception.status_code}, status=exception.status_code) + return response.json({"exception": "{}".format(exception), "status": exception.status_code}, + status=exception.status_code) # ----------------------------------------------- # @@ -67,7 +72,8 @@ def post_json(request): @app.route("/query_string") def query_string(request): - return response.json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string}) + return response.json({"parsed": True, "args": request.args, "url": request.url, + "query_string": request.query_string}) # ----------------------------------------------- # diff --git a/examples/unix_socket.py b/examples/unix_socket.py index 070074fe..08e89445 100644 --- a/examples/unix_socket.py +++ b/examples/unix_socket.py @@ -1,11 +1,11 @@ from sanic import Sanic from sanic import response import socket -import sys import os app = Sanic(__name__) + @app.route("/test") async def test(request): return response.text("OK") diff --git a/examples/url_for_example.py b/examples/url_for_example.py index c26debf4..cb895b0c 100644 --- a/examples/url_for_example.py +++ b/examples/url_for_example.py @@ -3,6 +3,7 @@ from sanic import response app = Sanic(__name__) + @app.route('/') async def index(request): # generate a URL for the endpoint `post_handler` @@ -10,9 +11,10 @@ async def index(request): # the URL is `/posts/5`, redirect to it return response.redirect(url) + @app.route('/posts/') async def post_handler(request, post_id): return response.text('Post - {}'.format(post_id)) if __name__ == '__main__': - app.run(host="0.0.0.0", port=8000, debug=True) \ No newline at end of file + app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/examples/vhosts.py b/examples/vhosts.py index 91b2b647..a6f946bc 100644 --- a/examples/vhosts.py +++ b/examples/vhosts.py @@ -11,20 +11,24 @@ from sanic.blueprints import Blueprint app = Sanic() bp = Blueprint("bp", host="bp.example.com") + @app.route('/', host=["example.com", "somethingelse.com", "therestofyourdomains.com"]) async def hello(request): return response.text("Some defaults") + @app.route('/', host="sub.example.com") async def hello(request): return response.text("42") + @bp.route("/question") async def hello(request): return response.text("What is the meaning of life?") + @bp.route("/answer") async def hello(request): return response.text("42") diff --git a/examples/websocket.py b/examples/websocket.py index 16cc4015..9cba083c 100644 --- a/examples/websocket.py +++ b/examples/websocket.py @@ -20,4 +20,5 @@ async def feed(request, ws): if __name__ == '__main__': - app.run() + app.run(host="0.0.0.0", port=8000, debug=True) + From f47e571d927b252e9f9383dbad3c33746321dd5a Mon Sep 17 00:00:00 2001 From: Tue Topholm Date: Thu, 1 Jun 2017 22:53:56 +0200 Subject: [PATCH 06/45] Added content_type to be set for son response --- sanic/response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 9a304bb7..645bcaa2 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -233,7 +233,7 @@ class HTTPResponse(BaseHTTPResponse): return self._cookies -def json(body, status=200, headers=None, **kwargs): +def json(body, status=200, headers=None, content_type="application/json", **kwargs): """ Returns response object with body in json format. :param body: Response data to be serialized. @@ -242,7 +242,7 @@ def json(body, status=200, headers=None, **kwargs): :param kwargs: Remaining arguments that are passed to the json encoder. """ return HTTPResponse(json_dumps(body, **kwargs), headers=headers, - status=status, content_type="application/json") + status=status, content_type=content_type) def text(body, status=200, headers=None, From beee7b68bf61f59bbc88f5869289abfca79d5342 Mon Sep 17 00:00:00 2001 From: Jeremy Zimmerman Date: Thu, 1 Jun 2017 14:01:13 -0700 Subject: [PATCH 07/45] reverted back to default 0.0.0.0 host --- examples/exception_monitoring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py index 1cb18e27..02a13e7d 100644 --- a/examples/exception_monitoring.py +++ b/examples/exception_monitoring.py @@ -51,4 +51,4 @@ async def test(request): raise SanicException('You Broke It!') if __name__ == '__main__': - app.run(host="127.0.0.1", port=8000, debug=True) + app.run(host="0.0.0.0", port=8000, debug=True) From c102e761464e0989d2a39faf8898932c09e4d0c8 Mon Sep 17 00:00:00 2001 From: Tue Topholm Date: Thu, 1 Jun 2017 23:01:27 +0200 Subject: [PATCH 08/45] Fixed line width --- sanic/response.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 645bcaa2..0f4d1356 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -233,7 +233,8 @@ class HTTPResponse(BaseHTTPResponse): return self._cookies -def json(body, status=200, headers=None, content_type="application/json", **kwargs): +def json(body, status=200, headers=None, + content_type="application/json", **kwargs): """ Returns response object with body in json format. :param body: Response data to be serialized. From 3d97fd8d2a17ae74aaceea07515a48436cbc7cf0 Mon Sep 17 00:00:00 2001 From: Tue Topholm Date: Thu, 1 Jun 2017 23:09:37 +0200 Subject: [PATCH 09/45] Removed whitespace --- sanic/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 0f4d1356..ea233d9a 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -233,7 +233,7 @@ class HTTPResponse(BaseHTTPResponse): return self._cookies -def json(body, status=200, headers=None, +def json(body, status=200, headers=None, content_type="application/json", **kwargs): """ Returns response object with body in json format. From 349c108ebce367876f84130d476d6fb142209c7f Mon Sep 17 00:00:00 2001 From: Jeremy Zimmerman Date: Thu, 1 Jun 2017 16:52:56 -0700 Subject: [PATCH 10/45] re-added request_stream example. --- examples/request_stream/client.py | 10 +++++ examples/request_stream/server.py | 65 +++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 examples/request_stream/client.py create mode 100644 examples/request_stream/server.py diff --git a/examples/request_stream/client.py b/examples/request_stream/client.py new file mode 100644 index 00000000..a59c4c23 --- /dev/null +++ b/examples/request_stream/client.py @@ -0,0 +1,10 @@ +import requests + +# Warning: This is a heavy process. + +data = "" +for i in range(1, 250000): + data += str(i) + +r = requests.post('http://0.0.0.0:8000/stream', data=data) +print(r.text) diff --git a/examples/request_stream/server.py b/examples/request_stream/server.py new file mode 100644 index 00000000..e53a224c --- /dev/null +++ b/examples/request_stream/server.py @@ -0,0 +1,65 @@ +from sanic import Sanic +from sanic.views import CompositionView +from sanic.views import HTTPMethodView +from sanic.views import stream as stream_decorator +from sanic.blueprints import Blueprint +from sanic.response import stream, text + +bp = Blueprint('blueprint_request_stream') +app = Sanic('request_stream') + + +class SimpleView(HTTPMethodView): + + @stream_decorator + async def post(self, request): + result = '' + while True: + body = await request.stream.get() + if body is None: + break + result += body.decode('utf-8') + return text(result) + + +@app.post('/stream', stream=True) +async def handler(request): + async def streaming(response): + while True: + body = await request.stream.get() + if body is None: + break + body = body.decode('utf-8').replace('1', 'A') + response.write(body) + return stream(streaming) + + +@bp.put('/bp_stream', stream=True) +async def bp_handler(request): + result = '' + while True: + body = await request.stream.get() + if body is None: + break + result += body.decode('utf-8').replace('1', 'A') + return text(result) + + +async def post_handler(request): + result = '' + while True: + body = await request.stream.get() + if body is None: + break + result += body.decode('utf-8') + return text(result) + +app.blueprint(bp) +app.add_route(SimpleView.as_view(), '/method_view') +view = CompositionView() +view.add(['POST'], post_handler, stream=True) +app.add_route(view, '/composition_view') + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8000) From 199fa50a9d1840e5b0d612972383b43c1616f941 Mon Sep 17 00:00:00 2001 From: Miroslav Batchkarov Date: Mon, 5 Jun 2017 16:24:23 +0100 Subject: [PATCH 11/45] also store json result in local request --- sanic/testing.py | 1 + tests/test_response.py | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/sanic/testing.py b/sanic/testing.py index 787106a6..6be79cd0 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -26,6 +26,7 @@ class SanicTestClient: session, method.lower())(url, *args, **kwargs) as response: try: response.text = await response.text() + response.json = await response.json() except UnicodeDecodeError as e: response.text = None response.body = await response.read() diff --git a/tests/test_response.py b/tests/test_response.py index ba87f68d..fb213b56 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -9,10 +9,12 @@ import pytest from random import choice from sanic import Sanic -from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream +from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json from sanic.testing import HOST, PORT from unittest.mock import MagicMock +JSON_DATA = {'ok': True} + def test_response_body_not_a_string(): @@ -34,6 +36,24 @@ async def sample_streaming_fn(response): response.write('bar') +@pytest.fixture +def json_app(): + app = Sanic('json') + + @app.route("/") + async def test(request): + return json(JSON_DATA) + + return app + + +def test_json_response(json_app): + from sanic.response import json_dumps + request, response = json_app.test_client.get('/') + assert response.status == 200 + assert response.text == json_dumps(JSON_DATA) + assert response.json == JSON_DATA + @pytest.fixture def streaming_app(): app = Sanic('streaming') From 735b8665f1d97285af49fa228e6a2d648c2a4afa Mon Sep 17 00:00:00 2001 From: Matthew Snyder Date: Mon, 5 Jun 2017 10:42:17 -0700 Subject: [PATCH 12/45] Small logging docs fixes --- docs/sanic/logging.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sanic/logging.md b/docs/sanic/logging.md index fa1dc171..eb807388 100644 --- a/docs/sanic/logging.md +++ b/docs/sanic/logging.md @@ -1,11 +1,11 @@ # Logging -Sanic allows you to do different types of logging (access log, error log) on the requests based on the [python3 logging API](https://docs.python.org/3/howto/logging.html). You should have some basic knowledge on python3 logging if you want do create a new configuration. +Sanic allows you to do different types of logging (access log, error log) on the requests based on the [python3 logging API](https://docs.python.org/3/howto/logging.html). You should have some basic knowledge on python3 logging if you want to create a new configuration. -### Quck Start +### Quick Start -A simple example using default setting would be like this: +A simple example using default settings would be like this: ```python from sanic import Sanic From 3f22b644b6d4bfef961065854f0c419481277eba Mon Sep 17 00:00:00 2001 From: Miroslav Batchkarov Date: Wed, 7 Jun 2017 09:57:07 +0100 Subject: [PATCH 13/45] wrap call to json in try-except to make tests pass --- sanic/testing.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sanic/testing.py b/sanic/testing.py index 6be79cd0..09554e21 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -1,4 +1,5 @@ import traceback +from json import JSONDecodeError from sanic.log import log @@ -26,9 +27,14 @@ class SanicTestClient: session, method.lower())(url, *args, **kwargs) as response: try: response.text = await response.text() - response.json = await response.json() except UnicodeDecodeError as e: response.text = None + + try: + response.json = await response.json() + except (JSONDecodeError, UnicodeDecodeError): + response.json = None + response.body = await response.read() return response From ddd7145153d5a52e74f27a4d7f0750f72d84a17d Mon Sep 17 00:00:00 2001 From: Miroslav Batchkarov Date: Wed, 7 Jun 2017 10:03:27 +0100 Subject: [PATCH 14/45] check json is None if body is not JSON --- tests/test_blueprints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index d9f5cd61..9ab387be 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -41,6 +41,7 @@ def test_bp_strict_slash(): request, response = app.test_client.get('/get') assert response.text == 'OK' + assert response.json == None request, response = app.test_client.get('/get/') assert response.status == 404 From 566a6369a5ad7dbd9bf00dd96bdb0297a4b6df57 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 7 Jun 2017 20:27:54 -0700 Subject: [PATCH 15/45] add sanic-transmute --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 88552e13..da49da89 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -22,3 +22,4 @@ A list of Sanic extensions created by the community. - [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic - [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic - [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation. +- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically. From aac99c45c02247ac546852769c96d23e7232dabb Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 7 Jun 2017 20:46:48 -0700 Subject: [PATCH 16/45] add content_type property in request --- sanic/request.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index f3de36f8..a5d204a8 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -174,6 +174,10 @@ class Request(dict): # so pull it from the headers return self.headers.get('Host', '') + @property + def content_type(self): + return self.headers.get('Content-Type', DEFAULT_HTTP_CONTENT_TYPE) + @property def path(self): return self._parsed_url.path.decode('utf-8') From 81889fd7a39b2fdab2a8362b7223773fea233668 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 7 Jun 2017 20:48:07 -0700 Subject: [PATCH 17/45] add unit tests --- tests/test_requests.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index 06e2b7ae..b4acb2dd 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -8,7 +8,7 @@ import pytest from sanic import Sanic from sanic.exceptions import ServerError from sanic.response import json, text - +from sanic.request import DEFAULT_HTTP_CONTENT_TYPE from sanic.testing import HOST, PORT @@ -192,6 +192,23 @@ def test_token(): assert request.token is None +def test_content_type(): + app = Sanic('test_content_type') + + @app.route('/') + async def handler(request): + return text('OK') + + request, response = app.test_client.get('/') + assert request.content_type == DEFAULT_HTTP_CONTENT_TYPE + + headers = { + 'content-type': 'application/json', + } + request, response = app.test_client.get('/', headers=headers) + assert request.content_type == 'application/json' + + # ------------------------------------------------------------ # # POST # ------------------------------------------------------------ # From 3802f8ff65facf9f890358ff2d112b4ead06006a Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Thu, 8 Jun 2017 17:25:22 -0700 Subject: [PATCH 18/45] unit tests --- tests/test_requests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index b4acb2dd..d61d4d55 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -197,16 +197,18 @@ def test_content_type(): @app.route('/') async def handler(request): - return text('OK') + return text(request.content_type) request, response = app.test_client.get('/') assert request.content_type == DEFAULT_HTTP_CONTENT_TYPE + assert response.text == DEFAULT_HTTP_CONTENT_TYPE headers = { 'content-type': 'application/json', } request, response = app.test_client.get('/', headers=headers) assert request.content_type == 'application/json' + assert response.text == 'application/json' # ------------------------------------------------------------ # From 29b4a2a08cbbd5c39c05049fa62bbcc6b11d6b10 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 9 Jun 2017 14:46:12 +0900 Subject: [PATCH 19/45] Gunicorn worker hints app weather it is being terminated For now, `Sanic.is_running` is set when the worker is started but not unset when it is about to stopped. Setting the flag for quit signal will not affect working requests, but the `Sanic.is_running` flag still can be used to support graceful termination. --- sanic/worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/worker.py b/sanic/worker.py index d15fda41..1d3e384b 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -159,6 +159,7 @@ class GunicornWorker(base.Worker): def handle_quit(self, sig, frame): self.alive = False + self.app.callable.is_running = False self.cfg.worker_int(self) def handle_abort(self, sig, frame): From 4942af27dc51fdba4fbcf2e5c3190022e351feb1 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Fri, 9 Jun 2017 08:33:34 -0700 Subject: [PATCH 20/45] handle NotFound --- sanic/router.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index 2581e178..f379b095 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -351,7 +351,10 @@ class Router: :param request: Request object :return: bool """ - handler = self.get(request)[0] + try: + handler = self.get(request)[0] + except NotFound: + return False if (hasattr(handler, 'view_class') and hasattr(handler.view_class, request.method.lower())): handler = getattr(handler.view_class, request.method.lower()) From 236daf48ffde46694f7aeeeabc5681e6a238bab6 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Fri, 9 Jun 2017 08:42:48 -0700 Subject: [PATCH 21/45] add unit tests --- tests/test_request_stream.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index 0ceccf60..3a81a7c8 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -163,6 +163,30 @@ def test_request_stream_app(): assert response.text == data +def test_request_stream_handle_exception(): + '''for handling exceptions properly''' + + app = Sanic('test_request_stream_exception') + + @app.post('/post/', stream=True) + async def post(request, id): + assert isinstance(request.stream, asyncio.Queue) + + async def streaming(response): + while True: + body = await request.stream.get() + if body is None: + break + response.write(body.decode('utf-8')) + return stream(streaming) + + + # 404 + request, response = app.test_client.post('/in_valid_post', data=data) + assert response.status == 404 + assert response.text == 'Error: Requested URL /in_valid_post not found' + + def test_request_stream_blueprint(): '''for self.is_request_stream = True''' From 24b946e850ee05d8b9c354ed4bfeb8bc658b5339 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Fri, 9 Jun 2017 08:43:23 -0700 Subject: [PATCH 22/45] make flake8 happy --- tests/test_request_stream.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index 3a81a7c8..fdfffb77 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -180,7 +180,6 @@ def test_request_stream_handle_exception(): response.write(body.decode('utf-8')) return stream(streaming) - # 404 request, response = app.test_client.post('/in_valid_post', data=data) assert response.status == 404 From cf30ed745cd127cf3bd509344353555401e85c98 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sat, 10 Jun 2017 09:42:48 -0700 Subject: [PATCH 23/45] also should handle InvalidUsage exception --- sanic/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index f379b095..691f1388 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -353,7 +353,7 @@ class Router: """ try: handler = self.get(request)[0] - except NotFound: + except (NotFound, InvalidUsage): return False if (hasattr(handler, 'view_class') and hasattr(handler.view_class, request.method.lower())): From 6a80bdafa6cab506164ca7a7fc89b66c4bea43b4 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sat, 10 Jun 2017 09:48:30 -0700 Subject: [PATCH 24/45] add unit tests --- tests/test_request_stream.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index fdfffb77..4ca4e44e 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -185,6 +185,11 @@ def test_request_stream_handle_exception(): assert response.status == 404 assert response.text == 'Error: Requested URL /in_valid_post not found' + # 405 + request, response = app.test_client.get('/post/random_id', data=data) + assert response.status == 405 + assert response.text == 'Error: Method GET not allowed for URL /post/random_id' + def test_request_stream_blueprint(): '''for self.is_request_stream = True''' From acaafabc23ab0e726bba15fbeeeb9efa9c7392d6 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sat, 10 Jun 2017 09:57:32 -0700 Subject: [PATCH 25/45] retry build From 47e761bbe2f4dd162ff459bd2695c4558b6754e0 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 11 Jun 2017 08:49:35 -0700 Subject: [PATCH 26/45] add coverage report --- tox.ini | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 3092e875..de1dc2d5 100644 --- a/tox.ini +++ b/tox.ini @@ -9,16 +9,14 @@ setenv = deps = coverage pytest + pytest-cov pytest-sugar aiohttp==1.3.5 chardet<=2.3.0 beautifulsoup4 gunicorn commands = - pytest tests {posargs} - coverage erase - coverage run -m sanic.app - coverage report + pytest tests --cov sanic --cov-report term-missing {posargs} [testenv:flake8] deps = From ce2df8030c2430b71f8276107009bb3823590ccb Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 11 Jun 2017 09:06:48 -0700 Subject: [PATCH 27/45] quick fix for test_gunicorn_worker test --- tests/test_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_worker.py b/tests/test_worker.py index 73838f68..2c1a0123 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -11,7 +11,7 @@ import pytest def gunicorn_worker(): command = 'gunicorn --bind 127.0.0.1:1337 --worker-class sanic.worker.GunicornWorker examples.simple_server:app' worker = subprocess.Popen(shlex.split(command)) - time.sleep(1) + time.sleep(3) yield worker.kill() From 599fbcee6e1e427829a9f03de7f7aad2ee67ac22 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 11 Jun 2017 23:20:04 -0700 Subject: [PATCH 28/45] fix #752 --- sanic/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index f6b1e84e..01af3526 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -33,7 +33,9 @@ class Sanic: logging.config.dictConfig(log_config) # 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: + if not (logging.root.handlers and + log.level == logging.NOTSET and + log_config): formatter = logging.Formatter( "%(asctime)s: %(levelname)s: %(message)s") handler = logging.StreamHandler() From ba1b34e37582240543c583e64e1710437144493e Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Mon, 12 Jun 2017 10:29:38 -0700 Subject: [PATCH 29/45] Add docs requirements Closes channelcat/sanic#787 --- requirements-docs.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 requirements-docs.txt diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 00000000..efa74079 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,3 @@ +sphinx +sphinx_rtd_theme +recommonmark From 2dfb0610638efdee0e6df2eee3c02e63faf1a56d Mon Sep 17 00:00:00 2001 From: Eran Kampf Date: Thu, 15 Jun 2017 10:39:00 -0700 Subject: [PATCH 30/45] Prevent `run` from overriding logging config set in constructor When creating the `Sanic` instance I provide it with a customized `log_config`. Calling `run` overrides these settings unless I provide it *again* with the same `log_config`. This is confusing and error prone. `run` shouldnt override configurations set in the `Sanic` constructor... --- sanic/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index 01af3526..ee4f5d7f 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -545,7 +545,7 @@ class Sanic: def run(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, protocol=None, backlog=100, stop_event=None, register_sys_signals=True, - log_config=LOGGING): + log_config=None): """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. From 77cf0b678aee9594f1b158ff41f41d99c8d87ed1 Mon Sep 17 00:00:00 2001 From: Eran Kampf Date: Thu, 15 Jun 2017 11:21:08 -0700 Subject: [PATCH 31/45] Fix has_log value --- sanic/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index ee4f5d7f..ff680d9c 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -567,6 +567,7 @@ class Sanic: host, port = host or "127.0.0.1", port or 8000 if log_config: + self.log_config = log_config logging.config.dictConfig(log_config) if protocol is None: protocol = (WebSocketProtocol if self.websocket_enabled @@ -580,7 +581,7 @@ class Sanic: host=host, port=port, debug=debug, ssl=ssl, sock=sock, workers=workers, protocol=protocol, backlog=backlog, register_sys_signals=register_sys_signals, - has_log=log_config is not None) + has_log=self.log_config is not None) try: self.is_running = True From 20138ee85f12633aac64f78139b1c764913fca39 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sat, 17 Jun 2017 09:47:58 -0700 Subject: [PATCH 32/45] add match_info to request --- sanic/request.py | 5 +++++ tests/test_requests.py | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index a5d204a8..3cc9c10b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -178,6 +178,11 @@ class Request(dict): def content_type(self): return self.headers.get('Content-Type', DEFAULT_HTTP_CONTENT_TYPE) + @property + def match_info(self): + """return matched info after resolving route""" + return self.app.router.get(self)[2] + @property def path(self): return self._parsed_url.path.decode('utf-8') diff --git a/tests/test_requests.py b/tests/test_requests.py index d61d4d55..2351a3b0 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -211,6 +211,19 @@ def test_content_type(): assert response.text == 'application/json' +def test_match_info(): + app = Sanic('test_match_info') + + @app.route('/api/v1/user//') + async def handler(request, user_id): + return json(request.match_info) + + request, response = app.test_client.get('/api/v1/user/sanic_user/') + + assert request.match_info == {"user_id": "sanic_user"} + assert json_loads(response.text) == {"user_id": "sanic_user"} + + # ------------------------------------------------------------ # # POST # ------------------------------------------------------------ # From aac0d58417feb826d83a3213dd732edb0708fc49 Mon Sep 17 00:00:00 2001 From: Jeremy Zimmerman Date: Mon, 19 Jun 2017 15:18:32 -0700 Subject: [PATCH 33/45] Delete extensions.md non-core content moved to wiki --- docs/sanic/extensions.md | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 docs/sanic/extensions.md diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md deleted file mode 100644 index 88552e13..00000000 --- a/docs/sanic/extensions.md +++ /dev/null @@ -1,24 +0,0 @@ -# Extensions - -A list of Sanic extensions created by the community. - -- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. - Allows using redis, memcache or an in memory store. -- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. -- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress. -- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template. -- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. -- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support. -- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper. -- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models. -- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request -- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic. -- [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. -- [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 -- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic -- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation. From 9fac37588c4f15446397638c72efc79bfcf0d636 Mon Sep 17 00:00:00 2001 From: Nikola Kolevski Date: Tue, 20 Jun 2017 13:15:30 +0200 Subject: [PATCH 34/45] Allow textual responses when using test_client and aiohttp 2 --- sanic/testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/testing.py b/sanic/testing.py index 09554e21..075185b7 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -32,7 +32,7 @@ class SanicTestClient: try: response.json = await response.json() - except (JSONDecodeError, UnicodeDecodeError): + except (JSONDecodeError, UnicodeDecodeError, aiohttp.ClientResponseError): response.json = None response.body = await response.read() From d865c5e2b64f57416123ec3f2e1d8d4515e927ea Mon Sep 17 00:00:00 2001 From: Nikola Kolevski Date: Tue, 20 Jun 2017 13:22:28 +0200 Subject: [PATCH 35/45] Conform to pep8 --- sanic/testing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/testing.py b/sanic/testing.py index 075185b7..de26d025 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -32,7 +32,9 @@ class SanicTestClient: try: response.json = await response.json() - except (JSONDecodeError, UnicodeDecodeError, aiohttp.ClientResponseError): + except (JSONDecodeError, + UnicodeDecodeError, + aiohttp.ClientResponseError): response.json = None response.body = await response.read() From 3d1dd1c6ac63ac0004f58d19691a12fac69edb44 Mon Sep 17 00:00:00 2001 From: Jeremy Zimmerman Date: Tue, 20 Jun 2017 14:49:12 -0700 Subject: [PATCH 36/45] re-add extensions.md to fix merge conflict. --- docs/sanic/extensions.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/sanic/extensions.md diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md new file mode 100644 index 00000000..88552e13 --- /dev/null +++ b/docs/sanic/extensions.md @@ -0,0 +1,24 @@ +# Extensions + +A list of Sanic extensions created by the community. + +- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. + Allows using redis, memcache or an in memory store. +- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. +- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress. +- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template. +- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. +- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support. +- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper. +- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models. +- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request +- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic. +- [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. +- [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 +- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic +- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation. From 55f860da2fce863a58d6ec9e78fd39ecec72d76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Thu, 22 Jun 2017 18:11:23 +0200 Subject: [PATCH 37/45] Added support for 'Authorization: Bearer ' header in `Request.token` property. Also added a test case for that kind of header. --- sanic/request.py | 12 ++++++++---- tests/test_requests.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 3cc9c10b..29cb83f6 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -86,11 +86,15 @@ class Request(dict): :return: token related to request """ + prefixes = ('Token ', 'Bearer ') auth_header = self.headers.get('Authorization') - if auth_header is not None and 'Token ' in auth_header: - return auth_header.partition('Token ')[-1] - else: - return auth_header + + if auth_header is not None: + for prefix in prefixes: + if prefix in auth_header: + return auth_header.partition(prefix)[-1] + + return auth_header @property def form(self): diff --git a/tests/test_requests.py b/tests/test_requests.py index 2351a3b0..671febeb 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -180,6 +180,16 @@ def test_token(): request, response = app.test_client.get('/', headers=headers) + assert request.token == token + + token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' + headers = { + 'content-type': 'application/json', + 'Authorization': 'Bearer {}'.format(token) + } + + request, response = app.test_client.get('/', headers=headers) + assert request.token == token # no Authorization headers From f049a4ca678f21d5f03535b4353e1cf5ffa47b6c Mon Sep 17 00:00:00 2001 From: 7 Date: Thu, 22 Jun 2017 13:26:50 -0700 Subject: [PATCH 38/45] Recycling gunicorn worker (#800) * add recycling feature to gunicorn worker * add unit tests * add more unit tests, and remove redundant trigger_events call * fixed up unit tests * make flake8 happy * address feedbacks * make flake8 happy * add doc --- docs/sanic/deploying.md | 6 +++ sanic/server.py | 18 ++++++--- sanic/worker.py | 22 +++++++++-- tests/test_worker.py | 82 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.md index 391c10da..d3652d0d 100644 --- a/docs/sanic/deploying.md +++ b/docs/sanic/deploying.md @@ -57,6 +57,12 @@ for Gunicorn `worker-class` argument: gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker ``` +If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker +after it has processed a given number of requests. This can be a convenient way to help limit the effects +of the memory leak. + +See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information. + ## Asynchronous support This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`. However be advised that this method does not support using multiple processes, and is not the preferred way diff --git a/sanic/server.py b/sanic/server.py index f3106226..369e790e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -75,7 +75,7 @@ class HttpProtocol(asyncio.Protocol): signal=Signal(), connections=set(), request_timeout=60, request_max_size=None, request_class=None, has_log=True, keep_alive=True, is_request_stream=False, router=None, - **kwargs): + state=None, **kwargs): self.loop = loop self.transport = None self.request = None @@ -99,6 +99,9 @@ class HttpProtocol(asyncio.Protocol): self._request_handler_task = None self._request_stream_task = None self._keep_alive = keep_alive + self.state = state if state else {} + if 'requests_count' not in self.state: + self.state['requests_count'] = 0 @property def keep_alive(self): @@ -154,6 +157,9 @@ class HttpProtocol(asyncio.Protocol): self.headers = [] self.parser = HttpRequestParser(self) + # requests count + self.state['requests_count'] = self.state['requests_count'] + 1 + # Parse request chunk or close connection try: self.parser.feed_data(data) @@ -389,7 +395,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, register_sys_signals=True, run_async=False, connections=None, signal=Signal(), request_class=None, has_log=True, keep_alive=True, is_request_stream=False, router=None, websocket_max_size=None, - websocket_max_queue=None): + websocket_max_queue=None, state=None): """Start asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -427,8 +433,6 @@ def serve(host, port, request_handler, error_handler, before_start=None, if debug: loop.set_debug(debug) - trigger_events(before_start, loop) - connections = connections if connections is not None else set() server = partial( protocol, @@ -445,7 +449,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, is_request_stream=is_request_stream, router=router, websocket_max_size=websocket_max_size, - websocket_max_queue=websocket_max_queue + websocket_max_queue=websocket_max_queue, + state=state ) server_coroutine = loop.create_server( @@ -457,6 +462,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, sock=sock, backlog=backlog ) + # Instead of pulling time at the end of every request, # pull it once per minute loop.call_soon(partial(update_current_time, loop)) @@ -464,6 +470,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, if run_async: return server_coroutine + trigger_events(before_start, loop) + try: http_server = loop.run_until_complete(server_coroutine) except: diff --git a/sanic/worker.py b/sanic/worker.py index 1d3e384b..876354ce 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -29,7 +29,7 @@ class GunicornWorker(base.Worker): self.ssl_context = self._create_ssl_context(cfg) else: self.ssl_context = None - self.servers = [] + self.servers = {} self.connections = set() self.exit_code = 0 self.signal = Signal() @@ -96,11 +96,16 @@ class GunicornWorker(base.Worker): async def _run(self): for sock in self.sockets: - self.servers.append(await serve( + state = dict(requests_count=0) + self._server_settings["host"] = None + self._server_settings["port"] = None + server = await serve( sock=sock, connections=self.connections, + state=state, **self._server_settings - )) + ) + self.servers[server] = state async def _check_alive(self): # If our parent changed then we shut down. @@ -109,7 +114,15 @@ class GunicornWorker(base.Worker): while self.alive: self.notify() - if pid == os.getpid() and self.ppid != os.getppid(): + req_count = sum( + self.servers[srv]["requests_count"] for srv in self.servers + ) + if self.max_requests and req_count > self.max_requests: + self.alive = False + self.log.info( + "Max requests exceeded, shutting down: %s", self + ) + elif pid == os.getpid() and self.ppid != os.getppid(): self.alive = False self.log.info("Parent changed, shutting down: %s", self) else: @@ -166,3 +179,4 @@ class GunicornWorker(base.Worker): self.alive = False self.exit_code = 1 self.cfg.worker_abort(self) + sys.exit(1) diff --git a/tests/test_worker.py b/tests/test_worker.py index 2c1a0123..e1a13368 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -3,7 +3,11 @@ import json import shlex import subprocess import urllib.request - +from unittest import mock +from sanic.worker import GunicornWorker +from sanic.app import Sanic +import asyncio +import logging import pytest @@ -20,3 +24,79 @@ def test_gunicorn_worker(gunicorn_worker): with urllib.request.urlopen('http://localhost:1337/') as f: res = json.loads(f.read(100).decode()) assert res['test'] + + +class GunicornTestWorker(GunicornWorker): + + def __init__(self): + self.app = mock.Mock() + self.app.callable = Sanic("test_gunicorn_worker") + self.servers = {} + self.exit_code = 0 + self.cfg = mock.Mock() + self.notify = mock.Mock() + + +@pytest.fixture +def worker(): + return GunicornTestWorker() + + +def test_worker_init_process(worker): + with mock.patch('sanic.worker.asyncio') as mock_asyncio: + try: + worker.init_process() + except TypeError: + pass + + assert mock_asyncio.get_event_loop.return_value.close.called + assert mock_asyncio.new_event_loop.called + assert mock_asyncio.set_event_loop.called + + +def test_worker_init_signals(worker): + worker.loop = mock.Mock() + worker.init_signals() + assert worker.loop.add_signal_handler.called + + +def test_handle_abort(worker): + with mock.patch('sanic.worker.sys') as mock_sys: + worker.handle_abort(object(), object()) + assert not worker.alive + assert worker.exit_code == 1 + mock_sys.exit.assert_called_with(1) + + +def test_handle_quit(worker): + worker.handle_quit(object(), object()) + assert not worker.alive + assert worker.exit_code == 0 + + +def test_run_max_requests_exceeded(worker): + loop = asyncio.new_event_loop() + worker.ppid = 1 + worker.alive = True + sock = mock.Mock() + sock.cfg_addr = ('localhost', 8080) + worker.sockets = [sock] + worker.wsgi = mock.Mock() + worker.connections = set() + worker.log = mock.Mock() + worker.loop = loop + worker.servers = { + "server1": {"requests_count": 14}, + "server2": {"requests_count": 15}, + } + worker.max_requests = 10 + worker._run = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None)) + + # exceeding request count + _runner = asyncio.ensure_future(worker._check_alive(), loop=loop) + loop.run_until_complete(_runner) + + assert worker.alive == False + worker.notify.assert_called_with() + worker.log.info.assert_called_with("Max requests exceeded, shutting down: %s", + worker) From cf1713b08561629080537309f7394ad1572226e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 16:12:15 +0200 Subject: [PATCH 39/45] Added a Unauthorized exception. Also added a few tests related to this new exception. --- sanic/exceptions.py | 28 ++++++++++++++++++++++++++++ tests/test_exceptions.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index e1136dd1..1cf28a8e 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -198,6 +198,34 @@ class InvalidRangeType(ContentRangeError): pass +@add_status_code(401) +class Unauthorized(SanicException): + """ + Unauthorized exception (401 HTTP status code). + + :param scheme: Name of the authentication scheme to be used. + :param realm: Description of the protected area. (optional) + :param others: A dict containing values to add to the WWW-Authenticate + header that is generated. This is especially useful when dealing with the + Digest scheme. (optional) + """ + pass + + def __init__(self, message, scheme, realm="", others=None): + super().__init__(message) + + adds = "" + + if others is not None: + values = ["{!s}={!r}".format(k, v) for k, v in others.items()] + adds = ', '.join(values) + adds = ', {}'.format(adds) + + self.headers = { + "WWW-Authenticate": "{} realm='{}'{}".format(scheme, realm, adds) + } + + def abort(status_code, message=None): """ Raise an exception based on SanicException. Returns the HTTP response diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index a2b8dc71..4330ef57 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -3,7 +3,8 @@ from bs4 import BeautifulSoup from sanic import Sanic from sanic.response import text -from sanic.exceptions import InvalidUsage, ServerError, NotFound, abort +from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized +from sanic.exceptions import abort class SanicExceptionTestException(Exception): @@ -26,6 +27,20 @@ def exception_app(): def handler_404(request): raise NotFound("OK") + @app.route('/401/basic') + def handler_401_basic(request): + raise Unauthorized("Unauthorized", "Basic", "Sanic") + + @app.route('/401/digest') + def handler_401_digest(request): + challenge = { + "qop": "auth, auth-int", + "algorithm": "MD5", + "nonce": "abcdef", + "opaque": "zyxwvu", + } + raise Unauthorized("Unauthorized", "Digest", "Sanic", challenge) + @app.route('/invalid') def handler_invalid(request): raise InvalidUsage("OK") @@ -49,8 +64,10 @@ def exception_app(): return app + def test_catch_exception_list(): app = Sanic('exception_list') + @app.exception([SanicExceptionTestException, NotFound]) def exception_list(request, exception): return text("ok") @@ -91,6 +108,24 @@ def test_not_found_exception(exception_app): assert response.status == 404 +def test_unauthorized_exception(exception_app): + """Test the built-in Unauthorized exception""" + request, response = exception_app.test_client.get('/401/basic') + assert response.status == 401 + assert response.headers.get('WWW-Authenticate') is not None + assert response.headers.get('WWW-Authenticate') == "Basic realm='Sanic'" + + request, response = exception_app.test_client.get('/401/digest') + assert response.status == 401 + + auth_header = response.headers.get('WWW-Authenticate') + expected = ("Digest realm='Sanic', qop='auth, auth-int', algorithm='MD5', " + "nonce='abcdef', opaque='zyxwvu'") + + assert auth_header is not None + assert auth_header == expected + + def test_handled_unhandled_exception(exception_app): """Test that an exception not built into sanic is handled""" request, response = exception_app.test_client.get('/divide_by_zero') From 9fcdacb62405cc2e1ae63a73a3be74d3fa926083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 16:29:04 +0200 Subject: [PATCH 40/45] Modified the name of an argument. --- sanic/exceptions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 1cf28a8e..95e41b4a 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -205,19 +205,19 @@ class Unauthorized(SanicException): :param scheme: Name of the authentication scheme to be used. :param realm: Description of the protected area. (optional) - :param others: A dict containing values to add to the WWW-Authenticate + :param challenge: A dict containing values to add to the WWW-Authenticate header that is generated. This is especially useful when dealing with the Digest scheme. (optional) """ pass - def __init__(self, message, scheme, realm="", others=None): + def __init__(self, message, scheme, realm="", challenge=None): super().__init__(message) adds = "" - if others is not None: - values = ["{!s}={!r}".format(k, v) for k, v in others.items()] + if challenge is not None: + values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()] adds = ', '.join(values) adds = ', {}'.format(adds) From 60aa60f48ea2536ac95ad61276dfa4ab64195a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 17:16:31 +0200 Subject: [PATCH 41/45] Fixed the test for the new Unauthorized exception. --- tests/test_exceptions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4330ef57..dcdecabd 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -119,11 +119,12 @@ def test_unauthorized_exception(exception_app): assert response.status == 401 auth_header = response.headers.get('WWW-Authenticate') - expected = ("Digest realm='Sanic', qop='auth, auth-int', algorithm='MD5', " - "nonce='abcdef', opaque='zyxwvu'") - assert auth_header is not None - assert auth_header == expected + assert auth_header.startswith('Digest') + assert "qop='auth, auth-int'" in auth_header + assert "algorithm='MD5'" in auth_header + assert "nonce='abcdef'" in auth_header + assert "opaque='zyxwvu'" in auth_header def test_handled_unhandled_exception(exception_app): From dc5a70b0de2abdbb1943450112c7f7607068806a Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Mon, 26 Jun 2017 21:05:23 +0900 Subject: [PATCH 42/45] Introduce debug mode for HTTP protocol --- sanic/server.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 369e790e..fd31680e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -75,7 +75,7 @@ class HttpProtocol(asyncio.Protocol): signal=Signal(), connections=set(), request_timeout=60, request_max_size=None, request_class=None, has_log=True, keep_alive=True, is_request_stream=False, router=None, - state=None, **kwargs): + state=None, debug=False, **kwargs): self.loop = loop self.transport = None self.request = None @@ -102,12 +102,14 @@ class HttpProtocol(asyncio.Protocol): self.state = state if state else {} if 'requests_count' not in self.state: self.state['requests_count'] = 0 + self._debug = debug @property def keep_alive(self): - return (self._keep_alive - and not self.signal.stopped - and self.parser.should_keep_alive()) + return ( + self._keep_alive and + not self.signal.stopped and + self.parser.should_keep_alive()) # -------------------------------------------- # # Connection @@ -164,7 +166,10 @@ class HttpProtocol(asyncio.Protocol): try: self.parser.feed_data(data) except HttpParserError: - exception = InvalidUsage('Bad Request') + message = 'Bad Request' + if self._debug: + message += '\n' + traceback.format_exc() + exception = InvalidUsage(message) self.write_error(exception) def on_url(self, url): @@ -450,7 +455,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, router=router, websocket_max_size=websocket_max_size, websocket_max_queue=websocket_max_queue, - state=state + state=state, + debug=debug, ) server_coroutine = loop.create_server( From ad8e1cbf62db2f6d3b45e520dcb8a6c6b8933b3f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Jun 2017 20:49:41 -0700 Subject: [PATCH 43/45] convert environment vars to int if digits --- sanic/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/config.py b/sanic/config.py index e3563bc1..b51f4d0c 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -201,4 +201,7 @@ class Config(dict): for k, v in os.environ.items(): if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) - self[config_key] = v + if v.isdigit(): + self[config_key] = int(v) + else: + self[config_key] = v From 4379a4b0670c9172a3a6af63aa7d0132142c1989 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Jun 2017 20:58:31 -0700 Subject: [PATCH 44/45] float logic --- sanic/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index b51f4d0c..ec4f9bf3 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -201,7 +201,11 @@ class Config(dict): for k, v in os.environ.items(): if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) - if v.isdigit(): - self[config_key] = int(v) + # This is a float or an int + if v.replace('.', '').isdigit(): + if '.' in v: + self[config_key] = float(v) + else: + self[config_key] = int(v) else: self[config_key] = v From 395d85a12f9b2be24dce112a6a4b0e873374de7f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Jun 2017 21:26:34 -0700 Subject: [PATCH 45/45] use try/except --- sanic/config.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index ec4f9bf3..f5649cfe 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -201,11 +201,10 @@ class Config(dict): for k, v in os.environ.items(): if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) - # This is a float or an int - if v.replace('.', '').isdigit(): - if '.' in v: + try: + self[config_key] = int(v) + except ValueError: + try: self[config_key] = float(v) - else: - self[config_key] = int(v) - else: - self[config_key] = v + except ValueError: + self[config_key] = v