From adb73316709748ad812dfd2312edee8195b7fa30 Mon Sep 17 00:00:00 2001 From: Shawn Niederriter Date: Tue, 11 Apr 2017 20:34:55 +0000 Subject: [PATCH 01/23] Updated examples for 0.5.0 --- examples/exception_monitoring.py | 8 +++----- examples/jinja_example.py | 6 +++--- examples/modify_header_example.py | 26 ++++++++++++++++++++++++++ examples/override_logging.py | 5 ++--- examples/request_timeout.py | 10 +++++----- examples/simple_server.py | 4 ++-- examples/try_everything.py | 20 ++++++++++---------- examples/vhosts.py | 14 +++++++------- 8 files changed, 58 insertions(+), 35 deletions(-) create mode 100644 examples/modify_header_example.py diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py index 37d5c89d..76d16d90 100644 --- a/examples/exception_monitoring.py +++ b/examples/exception_monitoring.py @@ -1,9 +1,7 @@ """ Example intercepting uncaught exceptions using Sanic's error handler framework. - This may be useful for developers wishing to use Sentry, Airbrake, etc. or a custom system to log and monitor unexpected errors in production. - First we create our own class inheriting from Handler in sanic.exceptions, and pass in an instance of it when we create our Sanic instance. Inside this class' default handler, we can do anything including sending exceptions to @@ -39,7 +37,7 @@ server's error_handler to an instance of our CustomHandler """ from sanic import Sanic -from sanic.response import json +from sanic import response app = Sanic(__name__) @@ -52,7 +50,7 @@ async def test(request): # Here, something occurs which causes an unexpected exception # This exception will flow to our custom handler. 1 / 0 - return json({"test": True}) + return response.json({"test": True}) -app.run(host="0.0.0.0", port=8000, debug=True) +app.run(host="0.0.0.0", port=8000, debug=True) \ No newline at end of file diff --git a/examples/jinja_example.py b/examples/jinja_example.py index 1f9bb1ba..ba8b3354 100644 --- a/examples/jinja_example.py +++ b/examples/jinja_example.py @@ -2,7 +2,7 @@ # curl -d '{"name": "John Doe"}' localhost:8000 from sanic import Sanic -from sanic.response import html +from sanic import response from jinja2 import Template template = Template('Hello {{ name }}!') @@ -12,7 +12,7 @@ app = Sanic(__name__) @app.route('/') async def test(request): data = request.json - return html(template.render(**data)) + return response.html(template.render(**data)) -app.run(host="0.0.0.0", port=8000) +app.run(host="0.0.0.0", port=8080, debug=True) \ No newline at end of file diff --git a/examples/modify_header_example.py b/examples/modify_header_example.py new file mode 100644 index 00000000..bb5efe8e --- /dev/null +++ b/examples/modify_header_example.py @@ -0,0 +1,26 @@ +""" +Modify header or status in response +""" + +from sanic import Sanic +from sanic import response + +app = Sanic(__name__) + +@app.route('/') +def handle_request(request): + return response.json( + {'message': 'Hello world!'}, + headers={'X-Served-By': 'sanic'}, + status=200 + ) + +@app.route('/unauthorized') +def handle_request(request): + return response.json( + {'message': 'You are not authorized'}, + headers={'X-Served-By': 'sanic'}, + status=404 + ) + +app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/examples/override_logging.py b/examples/override_logging.py index 117d63bf..e4d529e8 100644 --- a/examples/override_logging.py +++ b/examples/override_logging.py @@ -1,6 +1,5 @@ from sanic import Sanic -from sanic.response import text -import json +from sanic import response import logging logging_format = "[%(asctime)s] %(process)d-%(levelname)s " @@ -18,6 +17,6 @@ sanic = Sanic() @sanic.route("/") def test(request): log.info("received request; responding with 'hey'") - return text("hey") + return response.text("hey") sanic.run(host="0.0.0.0", port=8000) diff --git a/examples/request_timeout.py b/examples/request_timeout.py index 261f423a..fb2822ee 100644 --- a/examples/request_timeout.py +++ b/examples/request_timeout.py @@ -1,6 +1,6 @@ -from sanic import Sanic import asyncio -from sanic.response import text +from sanic import Sanic +from sanic import response from sanic.config import Config from sanic.exceptions import RequestTimeout @@ -11,11 +11,11 @@ app = Sanic(__name__) @app.route('/') async def test(request): await asyncio.sleep(3) - return text('Hello, world!') + return response.text('Hello, world!') @app.exception(RequestTimeout) def timeout(request, exception): - return text('RequestTimeout from error_handler.', 408) + return response.text('RequestTimeout from error_handler.', 408) -app.run(host='0.0.0.0', port=8000) +app.run(host='0.0.0.0', port=8000) \ No newline at end of file diff --git a/examples/simple_server.py b/examples/simple_server.py index a803feb8..948090c4 100644 --- a/examples/simple_server.py +++ b/examples/simple_server.py @@ -1,12 +1,12 @@ from sanic import Sanic -from sanic.response import json +from sanic import response app = Sanic(__name__) @app.route("/") async def test(request): - return json({"test": True}) + return response.json({"test": True}) if __name__ == '__main__': diff --git a/examples/try_everything.py b/examples/try_everything.py index da3cc515..d46b832e 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -2,7 +2,7 @@ import os from sanic import Sanic from sanic.log import log -from sanic.response import json, text, file +from sanic import response from sanic.exceptions import ServerError app = Sanic(__name__) @@ -10,17 +10,17 @@ app = Sanic(__name__) @app.route("/") async def test_async(request): - return json({"test": True}) + return response.json({"test": True}) @app.route("/sync", methods=['GET', 'POST']) def test_sync(request): - return json({"test": True}) + return response.json({"test": True}) @app.route("/dynamic//") def test_params(request, name, id): - return text("yeehaww {} {}".format(name, id)) + return response.text("yeehaww {} {}".format(name, id)) @app.route("/exception") @@ -31,11 +31,11 @@ def exception(request): async def test_await(request): import asyncio await asyncio.sleep(5) - return text("I'm feeling sleepy") + return response.text("I'm feeling sleepy") @app.route("/file") async def test_file(request): - return await file(os.path.abspath("setup.py")) + return await response.file(os.path.abspath("setup.py")) # ----------------------------------------------- # @@ -44,7 +44,7 @@ async def test_file(request): @app.exception(ServerError) async def test(request, exception): - return 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) # ----------------------------------------------- # @@ -53,17 +53,17 @@ async def test(request, exception): @app.route("/json") def post_json(request): - return json({"received": True, "message": request.json}) + return response.json({"received": True, "message": request.json}) @app.route("/form") def post_json(request): - return json({"received": True, "form_data": request.form, "test": request.form.get('test')}) + return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')}) @app.route("/query_string") def query_string(request): - return 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/vhosts.py b/examples/vhosts.py index 810dc513..50c9b6f6 100644 --- a/examples/vhosts.py +++ b/examples/vhosts.py @@ -1,4 +1,4 @@ -from sanic.response import text +from sanic import response from sanic import Sanic from sanic.blueprints import Blueprint @@ -15,25 +15,25 @@ bp = Blueprint("bp", host="bp.example.com") "somethingelse.com", "therestofyourdomains.com"]) async def hello(request): - return text("Some defaults") + return response.text("Some defaults") @app.route('/', host="example.com") async def hello(request): - return text("Answer") + return response.text("Answer") @app.route('/', host="sub.example.com") async def hello(request): - return text("42") + return response.text("42") @bp.route("/question") async def hello(request): - return text("What is the meaning of life?") + return response.text("What is the meaning of life?") @bp.route("/answer") async def hello(request): - return text("42") + return response.text("42") app.register_blueprint(bp) if __name__ == '__main__': - app.run(host="0.0.0.0", port=8000) + app.run(host="0.0.0.0", port=8000) \ No newline at end of file From 3c45c9170fc3b6a8be89e3f47047e1d9038fbc01 Mon Sep 17 00:00:00 2001 From: Shawn Niederriter Date: Tue, 11 Apr 2017 21:55:45 +0000 Subject: [PATCH 02/23] Fixed to merge with #626 --- examples/sanic_motor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/sanic_motor.py b/examples/sanic_motor.py index 495875a4..c7d2b60f 100644 --- a/examples/sanic_motor.py +++ b/examples/sanic_motor.py @@ -5,7 +5,7 @@ motor==1.1 sanic==0.2.0 """ from sanic import Sanic -from sanic.response import json +from sanic import response app = Sanic('motor_mongodb') @@ -25,7 +25,7 @@ async def get(request): for doc in docs: doc['id'] = str(doc['_id']) del doc['_id'] - return json(docs) + return response.json(docs) @app.route('/post', methods=['POST']) @@ -34,8 +34,8 @@ async def new(request): print(doc) db = get_db() object_id = await db.test_col.save(doc) - return json({'object_id': str(object_id)}) + return response.json({'object_id': str(object_id)}) if __name__ == "__main__": - app.run(host='127.0.0.1', port=8000) + app.run(host='0.0.0.0', port=8000, debug=True) From 0f10a36b4046f544869aa24d722336c00324fcde Mon Sep 17 00:00:00 2001 From: Shawn Niederriter Date: Wed, 12 Apr 2017 06:20:35 +0000 Subject: [PATCH 03/23] Added url_for example --- examples/url_for_example.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 examples/url_for_example.py diff --git a/examples/url_for_example.py b/examples/url_for_example.py new file mode 100644 index 00000000..c26debf4 --- /dev/null +++ b/examples/url_for_example.py @@ -0,0 +1,18 @@ +from sanic import Sanic +from sanic import response + +app = Sanic(__name__) + +@app.route('/') +async def index(request): + # generate a URL for the endpoint `post_handler` + url = app.url_for('post_handler', post_id=5) + # 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 From 35b92e1511867a03b1057da85b5d921fb86ed6b8 Mon Sep 17 00:00:00 2001 From: messense Date: Thu, 13 Apr 2017 11:34:35 +0800 Subject: [PATCH 04/23] Add path type for router --- sanic/router.py | 8 +++++--- tests/test_routes.py | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index f7877f15..39872469 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -16,6 +16,7 @@ REGEX_TYPES = { 'int': (int, r'\d+'), 'number': (float, r'[0-9\\.]+'), 'alpha': (str, r'[A-Za-z]+'), + 'path': (str, r'[^/].*?'), } ROUTER_CACHE_SIZE = 1024 @@ -71,7 +72,8 @@ class Router: self.routes_always_check = [] self.hosts = set() - def parse_parameter_string(self, parameter_string): + @classmethod + def parse_parameter_string(cls, parameter_string): """Parse a parameter string into its constituent name, type, and pattern @@ -161,10 +163,10 @@ class Router: parameters.append(parameter) # Mark the whole route as unhashable if it has the hash key in it - if re.search('(^|[^^]){1}/', pattern): + if re.search(r'(^|[^^]){1}/', pattern): properties['unhashable'] = True # Mark the route as unhashable if it matches the hash key - elif re.search(pattern, '/'): + elif re.search(r'/', pattern): properties['unhashable'] = True return '({})'.format(pattern) diff --git a/tests/test_routes.py b/tests/test_routes.py index 3506db66..b3e19355 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -238,6 +238,30 @@ def test_dynamic_route_regex(): assert response.status == 200 +def test_dynamic_route_path(): + app = Sanic('test_dynamic_route_path') + + @app.route('//info') + async def handler(request, path): + return text('OK') + + request, response = app.test_client.get('/path/1/info') + assert response.status == 200 + + request, response = app.test_client.get('/info') + assert response.status == 404 + + @app.route('/') + async def handler1(request, path): + return text('OK') + + request, response = app.test_client.get('/info') + assert response.status == 200 + + request, response = app.test_client.get('/whatever/you/set') + assert response.status == 200 + + def test_dynamic_route_unhashable(): app = Sanic('test_dynamic_route_unhashable') From 4c66cb18544244bc5431e7ed6759b06e7618575a Mon Sep 17 00:00:00 2001 From: messense Date: Thu, 13 Apr 2017 12:11:38 +0800 Subject: [PATCH 05/23] Fix static files router --- sanic/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index d2894ff2..0ff98423 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -288,7 +288,7 @@ class Sanic: attach_to=middleware_or_request) # Static Files - def static(self, uri, file_or_directory, pattern='.+', + def static(self, uri, file_or_directory, pattern=r'/?.+', use_modified_since=True, use_content_range=False): """Register a root to serve files from. The input can either be a file or a directory. See From afd51e0823524eec683b226a20f40d958253064f Mon Sep 17 00:00:00 2001 From: lazydog Date: Fri, 14 Apr 2017 02:55:39 +0800 Subject: [PATCH 06/23] fix directory traversal flaw --- sanic/static.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sanic/static.py b/sanic/static.py index adbdd0ea..3f95253c 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -48,14 +48,18 @@ def register(app, uri, file_or_directory, pattern, # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path - file_path = file_or_directory + root_path = file_or_directory if file_uri: file_path = path.join( file_or_directory, sub('^[/]*', '', file_uri)) # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) - file_path = unquote(file_path) + file_path = path.abspath(unquote(file_path)) + if not file_path.startswith(root_path): + raise FileNotFound('File not found', + path=file_or_directory, + relative_url=file_uri) try: headers = {} # Check if the client has been sent this file before From ae09dec05e10816b37eed425c87e193d230c5a73 Mon Sep 17 00:00:00 2001 From: lazydog Date: Fri, 14 Apr 2017 03:38:55 +0800 Subject: [PATCH 07/23] fixed `UnboundLocalError` --- sanic/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index 3f95253c..24fce4ff 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -48,7 +48,7 @@ def register(app, uri, file_or_directory, pattern, # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path - root_path = file_or_directory + root_path = file_path = file_or_directory if file_uri: file_path = path.join( file_or_directory, sub('^[/]*', '', file_uri)) From 5796f211c164c74da78ac5a28567b52af06e16ad Mon Sep 17 00:00:00 2001 From: Shawn Niederriter Date: Fri, 14 Apr 2017 17:17:23 +0000 Subject: [PATCH 08/23] Added detailed plotly example project --- examples/plotly_example/plotlyjs_example.py | 85 ++++++++++++++++++++ examples/plotly_example/requirements.txt | 5 ++ examples/plotly_example/templates/index.html | 0 3 files changed, 90 insertions(+) create mode 100644 examples/plotly_example/plotlyjs_example.py create mode 100644 examples/plotly_example/requirements.txt create mode 100644 examples/plotly_example/templates/index.html diff --git a/examples/plotly_example/plotlyjs_example.py b/examples/plotly_example/plotlyjs_example.py new file mode 100644 index 00000000..f536067c --- /dev/null +++ b/examples/plotly_example/plotlyjs_example.py @@ -0,0 +1,85 @@ +from sanic import Sanic + +from sanic_session import InMemorySessionInterface +from sanic_jinja2 import SanicJinja2 + +import json +import plotly + +import pandas as pd +import numpy as np + +app = Sanic(__name__) + +jinja = SanicJinja2(app) +session = InMemorySessionInterface(cookie_name=app.name, prefix=app.name) + +@app.middleware('request') +async def print_on_request(request): + print(request.headers) + await session.open(request) + +@app.middleware('response') +async def print_on_response(request, response): + await session.save(request, response) + + + +@app.route('/') +async def index(request): + rng = pd.date_range('1/1/2011', periods=7500, freq='H') + ts = pd.Series(np.random.randn(len(rng)), index=rng) + + graphs = [ + dict( + data=[ + dict( + x=[1, 2, 3], + y=[10, 20, 30], + type='scatter' + ), + ], + layout=dict( + title='first graph' + ) + ), + + dict( + data=[ + dict( + x=[1, 3, 5], + y=[10, 50, 30], + type='bar' + ), + ], + layout=dict( + title='second graph' + ) + ), + + dict( + data=[ + dict( + x=ts.index, # Can use the pandas data structures directly + y=ts + ) + ] + ) + ] + + # Add "ids" to each of the graphs to pass up to the client + # for templating + ids = ['graph-{}'.format(i) for i, _ in enumerate(graphs)] + + # Convert the figures to JSON + # PlotlyJSONEncoder appropriately converts pandas, datetime, etc + # objects to their JSON equivalents + graphJSON = json.dumps(graphs, cls=plotly.utils.PlotlyJSONEncoder) + + return jinja.render('index.html', request, + ids=ids, + graphJSON=graphJSON) + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8000, debug=True) \ No newline at end of file diff --git a/examples/plotly_example/requirements.txt b/examples/plotly_example/requirements.txt new file mode 100644 index 00000000..91875907 --- /dev/null +++ b/examples/plotly_example/requirements.txt @@ -0,0 +1,5 @@ +pandas==0.19.2 +plotly==2.0.7 +sanic==0.5.0 +sanic-jinja2==0.5.1 +sanic-session==0.1.3 \ No newline at end of file diff --git a/examples/plotly_example/templates/index.html b/examples/plotly_example/templates/index.html new file mode 100644 index 00000000..e69de29b From 2c45c2d3c053b7c0040882e1aac58df496d19c2c Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 14 Apr 2017 14:35:28 -0500 Subject: [PATCH 09/23] Add new contributing rules --- CONTRIBUTING.md | 62 ++++++++++++++++++++++++++++++++++++++ docs/sanic/contributing.md | 33 ++++++++++++++++++-- 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..95922693 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing + +Thank you for your interest! Sanic is always looking for contributors. If you +don't feel comfortable contributing code, adding docstrings to the source files +is very appreciated. + +## Installation + +To develop on sanic (and mainly to just run the tests) it is highly recommend to +install from sources. + +So assume you have already cloned the repo and are in the working directory with +a virtual environment already set up, then run: + +```bash +python setup.py develop && pip install -r requirements-dev.txt +``` + +## Running tests + +To run the tests for sanic it is recommended to use tox like so: + +```bash +tox +``` + +See it's that simple! + +## Pull requests! + +So the pull request approval rules are pretty simple: +1. All pull requests must pass unit tests +* All pull requests must be reviewed and approved by at least +one current collaborator on the project +* All pull requests must pass flake8 checks +* If you decide to remove/change anything from any common interface +a deprecation message should accompany it. +* If you implement a new feature you should have at least one unit +test to accompany it. + +## Documentation + +Sanic's documentation is built +using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in +Markdown and can be found in the `docs` folder, while the module reference is +automatically generated using `sphinx-apidoc`. + +To generate the documentation from scratch: + +```bash +sphinx-apidoc -fo docs/_api/ sanic +sphinx-build -b html docs docs/_build +``` + +The HTML documentation will be created in the `docs/_build` folder. + +## Warning + +One of the main goals of Sanic is speed. Code that lowers the performance of +Sanic without significant gains in usability, security, or features may not be +merged. Please don't let this intimidate you! If you have any concerns about an +idea, open an issue for discussion and help. diff --git a/docs/sanic/contributing.md b/docs/sanic/contributing.md index dbc64a02..95922693 100644 --- a/docs/sanic/contributing.md +++ b/docs/sanic/contributing.md @@ -4,10 +4,39 @@ Thank you for your interest! Sanic is always looking for contributors. If you don't feel comfortable contributing code, adding docstrings to the source files is very appreciated. +## Installation + +To develop on sanic (and mainly to just run the tests) it is highly recommend to +install from sources. + +So assume you have already cloned the repo and are in the working directory with +a virtual environment already set up, then run: + +```bash +python setup.py develop && pip install -r requirements-dev.txt +``` + ## Running tests -* `python -m pip install pytest` -* `python -m pytest tests` +To run the tests for sanic it is recommended to use tox like so: + +```bash +tox +``` + +See it's that simple! + +## Pull requests! + +So the pull request approval rules are pretty simple: +1. All pull requests must pass unit tests +* All pull requests must be reviewed and approved by at least +one current collaborator on the project +* All pull requests must pass flake8 checks +* If you decide to remove/change anything from any common interface +a deprecation message should accompany it. +* If you implement a new feature you should have at least one unit +test to accompany it. ## Documentation From 20d9ec1fd2e2266786efc0d30bc387dc012cc7c3 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 14 Apr 2017 14:38:45 -0500 Subject: [PATCH 10/23] Fix the numbered list --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95922693..367956b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,12 +30,12 @@ See it's that simple! So the pull request approval rules are pretty simple: 1. All pull requests must pass unit tests -* All pull requests must be reviewed and approved by at least +2. All pull requests must be reviewed and approved by at least one current collaborator on the project -* All pull requests must pass flake8 checks -* If you decide to remove/change anything from any common interface +3. All pull requests must pass flake8 checks +4. If you decide to remove/change anything from any common interface a deprecation message should accompany it. -* If you implement a new feature you should have at least one unit +5. If you implement a new feature you should have at least one unit test to accompany it. ## Documentation From c6aaa9b09c58cc964c5ec4877b43d014d1ae4566 Mon Sep 17 00:00:00 2001 From: Tom Haines Date: Sun, 16 Apr 2017 13:50:07 +0100 Subject: [PATCH 11/23] Use render_async and a template env with jinja2 --- examples/jinja_example.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/examples/jinja_example.py b/examples/jinja_example.py index ba8b3354..cf517030 100644 --- a/examples/jinja_example.py +++ b/examples/jinja_example.py @@ -1,18 +1,27 @@ -## To use this example: -# curl -d '{"name": "John Doe"}' localhost:8000 +# 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 Template - -template = Template('Hello {{ name }}!') +from jinja2 import Evironment, PackageLoader, select_autoescape app = Sanic(__name__) +# Load the template environment with async support +template_env = Environment( + loader=jinja2.PackageLoader('yourapplication', 'templates'), + autoescape=jinja2.select_autoescape(['html', 'xml']), + enable_async=True +) + +# Load the template from file +template = template_env.get_template("example_template.html") + + @app.route('/') async def test(request): data = request.json - return response.html(template.render(**data)) + rendered_template = await template.render_async(**data) + return response.html(rendered_template) app.run(host="0.0.0.0", port=8080, debug=True) \ No newline at end of file From ff0632001cde48e3f94ac7768422c2821aca034a Mon Sep 17 00:00:00 2001 From: aryeh Date: Sun, 16 Apr 2017 10:07:29 -0400 Subject: [PATCH 12/23] prevent crash for unknown response codes set text for unknown status code, otherwise when None is used exception occurs. --- sanic/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index bebb8071..0fc3c575 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -210,7 +210,7 @@ class HTTPResponse(BaseHTTPResponse): # Speeds up response rate 6% over pulling from all status = COMMON_STATUS_CODES.get(self.status) if not status: - status = ALL_STATUS_CODES.get(self.status) + status = ALL_STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE') return (b'HTTP/%b %d %b\r\n' b'Connection: %b\r\n' From 9eb48c2b0dcec3be097a165c8c1fc6feaccd203a Mon Sep 17 00:00:00 2001 From: Szucs Krisztian Date: Sun, 16 Apr 2017 18:11:24 +0200 Subject: [PATCH 13/23] dask distributed example --- examples/dask_distributed.py | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 examples/dask_distributed.py diff --git a/examples/dask_distributed.py b/examples/dask_distributed.py new file mode 100644 index 00000000..a7ee5079 --- /dev/null +++ b/examples/dask_distributed.py @@ -0,0 +1,40 @@ +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 be run somewhere else + with LocalCluster(scheduler_port=8786, processes=False) as cluster: + app.run(host="0.0.0.0", port=8000) From 5b22d1486a460b480656b4388963b530399cac85 Mon Sep 17 00:00:00 2001 From: Szucs Krisztian Date: Sun, 16 Apr 2017 18:13:00 +0200 Subject: [PATCH 14/23] fix syntax error in comment --- examples/dask_distributed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dask_distributed.py b/examples/dask_distributed.py index a7ee5079..6de7c8ca 100644 --- a/examples/dask_distributed.py +++ b/examples/dask_distributed.py @@ -35,6 +35,6 @@ async def test(request, value): if __name__ == '__main__': - # Distributed cluster should be run somewhere else + # Distributed cluster should run somewhere else with LocalCluster(scheduler_port=8786, processes=False) as cluster: app.run(host="0.0.0.0", port=8000) From 3e87314adf65f805e73ac12d6a405cad8ef41986 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 16 Apr 2017 21:58:10 -0700 Subject: [PATCH 15/23] use absolute path in static root --- sanic/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index 24fce4ff..64166d46 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -56,7 +56,7 @@ def register(app, uri, file_or_directory, pattern, # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) file_path = path.abspath(unquote(file_path)) - if not file_path.startswith(root_path): + if not file_path.startswith(path.abspath(unquote(root_path))): raise FileNotFound('File not found', path=file_or_directory, relative_url=file_uri) From 5ddb0488f23a266762731f0d0cf9d8749cb58190 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 16 Apr 2017 21:39:18 -0700 Subject: [PATCH 16/23] allow disabling keep alive --- sanic/app.py | 1 + sanic/config.py | 11 ++++++----- sanic/server.py | 11 ++++++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 0ff98423..b126fc5d 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -679,6 +679,7 @@ class Sanic: 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, + 'no_keep_alive': self.config.NO_KEEP_ALIVE, 'loop': loop, 'register_sys_signals': register_sys_signals, 'backlog': backlog diff --git a/sanic/config.py b/sanic/config.py index 406c44e6..ca08d8b0 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -6,7 +6,7 @@ SANIC_PREFIX = 'SANIC_' class Config(dict): - def __init__(self, defaults=None, load_env=True): + def __init__(self, defaults=None, load_env=True, no_keep_alive=False): super().__init__(defaults or {}) self.LOGO = """ ▄▄▄▄▄ @@ -31,6 +31,7 @@ class Config(dict): """ self.REQUEST_MAX_SIZE = 100000000 # 100 megababies self.REQUEST_TIMEOUT = 60 # 60 seconds + self.NO_KEEP_ALIVE = no_keep_alive if load_env: self.load_environment_vars() @@ -98,11 +99,11 @@ class Config(dict): self[key] = getattr(obj, key) def load_environment_vars(self): + """ + Looks for any SANIC_ prefixed environment variables and applies + them to the configuration if present. + """ for k, v in os.environ.items(): - """ - Looks for any SANIC_ prefixed environment variables and applies - them to the configuration if present. - """ if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) self[config_key] = v diff --git a/sanic/server.py b/sanic/server.py index 40c3ee69..9edda142 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -70,7 +70,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): + request_max_size=None, request_class=None, + no_keep_alive=False): self.loop = loop self.transport = None self.request = None @@ -88,10 +89,13 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = None self._last_request_time = None self._request_handler_task = None + self._no_keep_alive = no_keep_alive @property def keep_alive(self): - return self.parser.should_keep_alive() and not self.signal.stopped + return (not self._no_keep_alive + and not self.signal.stopped + and self.parser.should_keep_alive()) # -------------------------------------------- # # Connection @@ -322,7 +326,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=60, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, - signal=Signal(), request_class=None): + signal=Signal(), request_class=None, no_keep_alive=False): """Start asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -370,6 +374,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=request_timeout, request_max_size=request_max_size, request_class=request_class, + no_keep_alive=no_keep_alive, ) server_coroutine = loop.create_server( From 6f098b3d2134d742269d718a71321e341dc32089 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 16 Apr 2017 22:05:34 -0700 Subject: [PATCH 17/23] add no_keep_alive setting to docs --- docs/sanic/config.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index 3ed40fda..2bbce5d0 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -83,3 +83,4 @@ Out of the box there are just a few predefined values which can be overwritten w | ----------------- | --------- | --------------------------------- | | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | | REQUEST_TIMEOUT | 60 | How long a request can take (sec) | + | NO_KEEP_ALIVE | False | Disable keep-alive | From 81b6d988ecc10936a2388528244e7b6276fe44e6 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 16 Apr 2017 22:43:49 -0700 Subject: [PATCH 18/23] NO_KEEP_ALIVE -> KEEP_ALIVE --- docs/sanic/config.md | 2 +- sanic/app.py | 2 +- sanic/config.py | 4 ++-- sanic/server.py | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index 2bbce5d0..a93d2cf1 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -83,4 +83,4 @@ Out of the box there are just a few predefined values which can be overwritten w | ----------------- | --------- | --------------------------------- | | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | | REQUEST_TIMEOUT | 60 | How long a request can take (sec) | - | NO_KEEP_ALIVE | False | Disable keep-alive | + | KEEP_ALIVE | True | Disables keep-alive when False | diff --git a/sanic/app.py b/sanic/app.py index b126fc5d..1cb6554f 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -679,7 +679,7 @@ class Sanic: 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, - 'no_keep_alive': self.config.NO_KEEP_ALIVE, + 'keep_alive': self.config.KEEP_ALIVE, 'loop': loop, 'register_sys_signals': register_sys_signals, 'backlog': backlog diff --git a/sanic/config.py b/sanic/config.py index ca08d8b0..4c128b4b 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -6,7 +6,7 @@ SANIC_PREFIX = 'SANIC_' class Config(dict): - def __init__(self, defaults=None, load_env=True, no_keep_alive=False): + def __init__(self, defaults=None, load_env=True, keep_alive=True): super().__init__(defaults or {}) self.LOGO = """ ▄▄▄▄▄ @@ -31,7 +31,7 @@ class Config(dict): """ self.REQUEST_MAX_SIZE = 100000000 # 100 megababies self.REQUEST_TIMEOUT = 60 # 60 seconds - self.NO_KEEP_ALIVE = no_keep_alive + self.KEEP_ALIVE = keep_alive if load_env: self.load_environment_vars() diff --git a/sanic/server.py b/sanic/server.py index 9edda142..33763bbb 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -71,7 +71,7 @@ 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, - no_keep_alive=False): + keep_alive=True): self.loop = loop self.transport = None self.request = None @@ -89,11 +89,11 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = None self._last_request_time = None self._request_handler_task = None - self._no_keep_alive = no_keep_alive + self._keep_alive = keep_alive @property def keep_alive(self): - return (not self._no_keep_alive + return (self._keep_alive and not self.signal.stopped and self.parser.should_keep_alive()) @@ -326,7 +326,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=60, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, - signal=Signal(), request_class=None, no_keep_alive=False): + signal=Signal(), request_class=None, keep_alive=True): """Start asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -374,7 +374,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=request_timeout, request_max_size=request_max_size, request_class=request_class, - no_keep_alive=no_keep_alive, + keep_alive=keep_alive, ) server_coroutine = loop.create_server( From 1b939a6823343270f0e2dc09a8670dccc0a792a7 Mon Sep 17 00:00:00 2001 From: Szucs Krisztian Date: Mon, 17 Apr 2017 11:05:19 +0200 Subject: [PATCH 19/23] work with distributed 1.16.1 --- examples/dask_distributed.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/dask_distributed.py b/examples/dask_distributed.py index 6de7c8ca..ef3fe423 100644 --- a/examples/dask_distributed.py +++ b/examples/dask_distributed.py @@ -36,5 +36,6 @@ async def test(request, value): if __name__ == '__main__': # Distributed cluster should run somewhere else - with LocalCluster(scheduler_port=8786, processes=False) as cluster: + 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) From df914a92e40cd03dbf37a7ca7d6929d380b1a585 Mon Sep 17 00:00:00 2001 From: 38elements Date: Wed, 19 Apr 2017 11:19:01 +0900 Subject: [PATCH 20/23] Fix `this.signal.stopped` is `True` #639 --- sanic/app.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 0ff98423..f5fffba7 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -16,7 +16,7 @@ from sanic.handlers import ErrorHandler from sanic.log import log from sanic.response import HTTPResponse, StreamingHTTPResponse from sanic.router import Router -from sanic.server import serve, serve_multiple, HttpProtocol +from sanic.server import serve, serve_multiple, HttpProtocol, Signal from sanic.static import register as static_register from sanic.testing import SanicTestClient from sanic.views import CompositionView @@ -548,12 +548,13 @@ class Sanic: warnings.simplefilter('default') warnings.warn("stop_event will be removed from future versions.", DeprecationWarning) + signal = Signal() server_settings = self._helper( host=host, port=port, debug=debug, before_start=before_start, after_start=after_start, before_stop=before_stop, after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, loop=loop, protocol=protocol, backlog=backlog, - register_sys_signals=register_sys_signals) + register_sys_signals=register_sys_signals, signal=signal) try: self.is_running = True @@ -594,12 +595,13 @@ class Sanic: warnings.simplefilter('default') warnings.warn("stop_event will be removed from future versions.", DeprecationWarning) + signal = Signal() server_settings = self._helper( host=host, port=port, debug=debug, before_start=before_start, after_start=after_start, before_stop=before_stop, after_stop=after_stop, ssl=ssl, sock=sock, loop=loop or get_event_loop(), protocol=protocol, - backlog=backlog, run_async=True) + backlog=backlog, run_async=True, signal=signal) return await serve(**server_settings) @@ -629,7 +631,7 @@ class Sanic: before_start=None, after_start=None, before_stop=None, after_stop=None, ssl=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None, - register_sys_signals=True, run_async=False): + register_sys_signals=True, run_async=False, signal=None): """Helper function used by `run` and `create_server`.""" if isinstance(ssl, dict): @@ -674,6 +676,7 @@ class Sanic: 'port': port, 'sock': sock, 'ssl': ssl, + 'signal': signal, 'debug': debug, 'request_handler': self.handle_request, 'error_handler': self.error_handler, From bc035fca7816f1e609cec389d4b57a410488114b Mon Sep 17 00:00:00 2001 From: 38elements Date: Thu, 20 Apr 2017 18:27:28 +0900 Subject: [PATCH 21/23] Remove unnecessary variables --- sanic/app.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index f5fffba7..d4dc4e3d 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -548,13 +548,12 @@ class Sanic: warnings.simplefilter('default') warnings.warn("stop_event will be removed from future versions.", DeprecationWarning) - signal = Signal() server_settings = self._helper( host=host, port=port, debug=debug, before_start=before_start, after_start=after_start, before_stop=before_stop, after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, loop=loop, protocol=protocol, backlog=backlog, - register_sys_signals=register_sys_signals, signal=signal) + register_sys_signals=register_sys_signals) try: self.is_running = True @@ -595,13 +594,12 @@ class Sanic: warnings.simplefilter('default') warnings.warn("stop_event will be removed from future versions.", DeprecationWarning) - signal = Signal() server_settings = self._helper( host=host, port=port, debug=debug, before_start=before_start, after_start=after_start, before_stop=before_stop, after_stop=after_stop, ssl=ssl, sock=sock, loop=loop or get_event_loop(), protocol=protocol, - backlog=backlog, run_async=True, signal=signal) + backlog=backlog, run_async=True) return await serve(**server_settings) @@ -631,7 +629,7 @@ class Sanic: before_start=None, after_start=None, before_stop=None, after_stop=None, ssl=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None, - register_sys_signals=True, run_async=False, signal=None): + register_sys_signals=True, run_async=False): """Helper function used by `run` and `create_server`.""" if isinstance(ssl, dict): @@ -676,7 +674,7 @@ class Sanic: 'port': port, 'sock': sock, 'ssl': ssl, - 'signal': signal, + 'signal': Signal(), 'debug': debug, 'request_handler': self.handle_request, 'error_handler': self.error_handler, From a7d17fae449c296491ae84235c1d5ed76c882feb Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 21 Apr 2017 17:06:52 -0500 Subject: [PATCH 22/23] Fix duplicate signal settings for gunicorn worker --- sanic/worker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sanic/worker.py b/sanic/worker.py index 7a8303d8..b2f5af17 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -3,6 +3,7 @@ import sys import signal import asyncio import logging + try: import ssl except ImportError: @@ -50,8 +51,8 @@ class GunicornWorker(base.Worker): debug=is_debug, protocol=protocol, ssl=self.ssl_context, - run_async=True - ) + run_async=True) + self._server_settings['signal'] = self.signal self._server_settings.pop('sock') trigger_events(self._server_settings.get('before_start', []), self.loop) @@ -97,7 +98,6 @@ class GunicornWorker(base.Worker): self.servers.append(await serve( sock=sock, connections=self.connections, - signal=self.signal, **self._server_settings )) From a0cba1aee181ac9d23cb8967a006b9daccec1ea4 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Fri, 21 Apr 2017 22:36:02 -0700 Subject: [PATCH 23/23] accept token directly in auth header --- sanic/request.py | 7 ++++--- tests/test_requests.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 4a15c22f..31b6a08f 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -78,9 +78,10 @@ class Request(dict): :return: token related to request """ auth_header = self.headers.get('Authorization') - if auth_header is not None: - return auth_header.split()[1] - return auth_header + if 'Token ' in auth_header: + return auth_header.partition('Token ')[-1] + else: + return auth_header @property def form(self): diff --git a/tests/test_requests.py b/tests/test_requests.py index 7b453fc1..c0824850 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -141,6 +141,16 @@ def test_token(): return text('OK') # uuid4 generated token. + token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' + headers = { + 'content-type': 'application/json', + 'Authorization': '{}'.format(token) + } + + request, response = app.test_client.get('/', headers=headers) + + assert request.token == token + token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' headers = { 'content-type': 'application/json', @@ -151,6 +161,18 @@ def test_token(): assert request.token == token + token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' + headers = { + 'content-type': 'application/json', + 'Authorization': 'Bearer Token {}'.format(token) + } + + request, response = app.test_client.get('/', headers=headers) + + assert request.token == token + + + # ------------------------------------------------------------ # # POST # ------------------------------------------------------------ #