diff --git a/docs/conf.py b/docs/conf.py
index 21b9b9cf..c97f3c19 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -22,7 +22,7 @@ import sanic
# -- General configuration ------------------------------------------------
-extensions = []
+extensions = ['sphinx.ext.autodoc']
templates_path = ['_templates']
@@ -68,7 +68,6 @@ pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
-
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
@@ -80,13 +79,11 @@ html_theme = 'sphinx_rtd_theme'
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
-
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'Sanicdoc'
-
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
@@ -110,21 +107,14 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
-latex_documents = [
- (master_doc, 'Sanic.tex', 'Sanic Documentation',
- 'Sanic contributors', 'manual'),
-]
-
+latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation',
+ 'Sanic contributors', 'manual'), ]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
-man_pages = [
- (master_doc, 'sanic', 'Sanic Documentation',
- [author], 1)
-]
-
+man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
# -- Options for Texinfo output -------------------------------------------
@@ -132,13 +122,10 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'Sanic', 'Sanic Documentation',
- author, 'Sanic', 'One line description of project.',
- 'Miscellaneous'),
+ (master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic',
+ 'One line description of project.', 'Miscellaneous'),
]
-
-
# -- Options for Epub output ----------------------------------------------
# Bibliographic Dublin Core info.
@@ -150,8 +137,6 @@ epub_copyright = copyright
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
-
-
# -- Custom Settings -------------------------------------------------------
suppress_warnings = ['image.nonlocal_uri']
diff --git a/docs/index.rst b/docs/index.rst
index 43cd0ba2..3fa63d5c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -16,6 +16,7 @@ Guides
sanic/blueprints
sanic/config
sanic/cookies
+ sanic/streaming
sanic/class_based_views
sanic/custom_protocol
sanic/ssl
diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md
index 4b704422..5fe20e54 100644
--- a/docs/sanic/blueprints.md
+++ b/docs/sanic/blueprints.md
@@ -66,7 +66,7 @@ Using blueprints allows you to also register middleware globally.
```python
@bp.middleware
-async def halt_request(request):
+async def print_on_request(request):
print("I am a spy")
@bp.middleware('request')
@@ -116,7 +116,7 @@ bp = Blueprint('my_blueprint')
async def setup_connection(app, loop):
global database
database = mysql.connect(host='127.0.0.1'...)
-
+
@bp.listener('after_server_stop')
async def close_connection(app, loop):
await database.close()
@@ -142,7 +142,7 @@ blueprint_v2 = Blueprint('v2', url_prefix='/v2')
@blueprint_v1.route('/')
async def api_v1_root(request):
return text('Welcome to version 1 of our documentation')
-
+
@blueprint_v2.route('/')
async def api_v2_root(request):
return text('Welcome to version 2 of our documentation')
diff --git a/docs/sanic/class_based_views.md b/docs/sanic/class_based_views.md
index 02b02140..ace8bf9c 100644
--- a/docs/sanic/class_based_views.md
+++ b/docs/sanic/class_based_views.md
@@ -48,6 +48,24 @@ app.add_route(SimpleView.as_view(), '/')
```
+You can also use `async` syntax.
+
+```python
+from sanic import Sanic
+from sanic.views import HTTPMethodView
+from sanic.response import text
+
+app = Sanic('some_name')
+
+class SimpleAsyncView(HTTPMethodView):
+
+ async def get(self, request):
+ return text('I am async get method')
+
+app.add_route(SimpleAsyncView.as_view(), '/')
+
+```
+
## URL parameters
If you need any URL parameters, as discussed in the routing guide, include them
@@ -128,4 +146,4 @@ view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
app.add_route(view, '/')
```
-Note: currently you cannot build a URL for a CompositionView using `url_for`.
+Note: currently you cannot build a URL for a CompositionView using `url_for`.
diff --git a/docs/config.md b/docs/sanic/config.md
similarity index 89%
rename from docs/config.md
rename to docs/sanic/config.md
index f5d56467..0c22de4b 100644
--- a/docs/config.md
+++ b/docs/sanic/config.md
@@ -71,8 +71,7 @@ DB_USER = 'appuser'
Out of the box there are just a few predefined values which can be overwritten when creating the application.
-| Variable | Default | Description |
-| ----------------- | --------- | --------------------------------- |
-| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
-| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
-
+ | Variable | Default | Description |
+ | ----------------- | --------- | --------------------------------- |
+ | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
+ | REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md
index d3bdda5b..b11470d9 100644
--- a/docs/sanic/extensions.md
+++ b/docs/sanic/extensions.md
@@ -17,3 +17,4 @@ A list of Sanic extensions created by the community.
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
`Babel` library
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
+- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
diff --git a/docs/sanic/index.rst b/docs/sanic/index.rst
index eb9eb286..08797e9f 100644
--- a/docs/sanic/index.rst
+++ b/docs/sanic/index.rst
@@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
Sanic is developed `on GitHub `_. Contributions are welcome!
-Sanic aspires to be simple:
--------------------
+Sanic aspires to be simple
+---------------------------
.. code:: python
diff --git a/environment.yml b/environment.yml
index 7eee43a5..298ea552 100644
--- a/environment.yml
+++ b/environment.yml
@@ -15,4 +15,5 @@ dependencies:
- httptools>=0.0.9
- ujson>=1.35
- aiofiles>=0.3.0
+ - websockets>=3.2
- https://github.com/channelcat/docutils-fork/zipball/master
\ No newline at end of file
diff --git a/examples/sanic_aiomysql_with_global_pool.py b/examples/sanic_aiomysql_with_global_pool.py
new file mode 100644
index 00000000..65d5832d
--- /dev/null
+++ b/examples/sanic_aiomysql_with_global_pool.py
@@ -0,0 +1,62 @@
+# 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():
+ result = []
+ data = {}
+ async with app.pool['aiomysql'].acquire() as conn:
+ async with conn.cursor() as cur:
+ await cur.execute("SELECT question, pub_date FROM sanic_polls")
+ async for row in cur:
+ result.append({"question": row[0], "pub_date": row[1]})
+ if result or len(result) > 0:
+ data['data'] = res
+ return json(data)
+
+
+if __name__ == '__main__':
+ app.run(host="127.0.0.1", workers=4, port=12000)
diff --git a/examples/sanic_aioredis_example.py b/examples/sanic_aioredis_example.py
new file mode 100644
index 00000000..8ba51617
--- /dev/null
+++ b/examples/sanic_aioredis_example.py
@@ -0,0 +1,34 @@
+""" To run this example you need additional aioredis package
+"""
+from sanic import Sanic, response
+import aioredis
+
+app = Sanic(__name__)
+
+
+@app.route("/")
+async def handle(request):
+ async with request.app.redis_pool.get() as redis:
+ await redis.set('test-my-key', 'value')
+ val = await redis.get('test-my-key')
+ return response.text(val.decode('utf-8'))
+
+
+@app.listener('before_server_start')
+async def before_server_start(app, loop):
+ app.redis_pool = await aioredis.create_pool(
+ ('localhost', 6379),
+ minsize=5,
+ maxsize=10,
+ loop=loop
+ )
+
+
+@app.listener('after_server_stop')
+async def after_server_stop(app, loop):
+ app.redis_pool.close()
+ await app.redis_pool.wait_closed()
+
+
+if __name__ == '__main__':
+ app.run(host="0.0.0.0", port=8000)
diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py
index 8db8ddff..aaa139f1 100644
--- a/examples/sanic_peewee.py
+++ b/examples/sanic_peewee.py
@@ -1,3 +1,4 @@
+
## You need the following additional packages for this example
# aiopg
# peewee_async
@@ -10,8 +11,9 @@ from sanic.response import json
## peewee_async related imports
import peewee
-from peewee_async import Manager, PostgresqlDatabase
-
+from peewee import Model, BaseModel
+from peewee_async import Manager, PostgresqlDatabase, execute
+from functools import partial
# we instantiate a custom loop so we can pass it to our db manager
## from peewee_async docs:
@@ -19,42 +21,77 @@ from peewee_async import Manager, PostgresqlDatabase
# with manager! It’s all automatic. But you can run Manager.connect() or
# Manager.close() when you need it.
+class AsyncManager(Manager):
+ """Inherit the peewee_async manager with our own object
+ configuration
-# let's create a simple key value store:
-class KeyValue(peewee.Model):
- key = peewee.CharField(max_length=40, unique=True)
- text = peewee.TextField(default='')
+ database.allow_sync = False
+ """
- class Meta:
- database = database
+ def __init__(self, _model_class, *args, **kwargs):
+ super(AsyncManager, self).__init__(*args, **kwargs)
+ self._model_class = _model_class
+ self.database.allow_sync = False
-# create table synchronously
-KeyValue.create_table(True)
+ def _do_fill(self, method, *args, **kwargs):
+ _class_method = getattr(super(AsyncManager, self), method)
+ pf = partial(_class_method, self._model_class)
+ return pf(*args, **kwargs)
-# OPTIONAL: close synchronous connection
-database.close()
+ def new(self, *args, **kwargs):
+ return self._do_fill('create', *args, **kwargs)
-# OPTIONAL: disable any future syncronous calls
-objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
+ def get(self, *args, **kwargs):
+ return self._do_fill('get', *args, **kwargs)
+
+ def execute(self, query):
+ return execute(query)
-app = Sanic('peewee_example')
+def _get_meta_db_class(db):
+ """creating a declartive class model for db"""
+ class _BlockedMeta(BaseModel):
+ def __new__(cls, name, bases, attrs):
+ _instance = super(_BlockedMeta, cls).__new__(cls, name, bases, attrs)
+ _instance.objects = AsyncManager(_instance, db)
+ return _instance
-@app.listener('before_server_start')
-def setup(app, loop):
- database = PostgresqlDatabase(database='test',
+ class _Base(Model, metaclass=_BlockedMeta):
+
+ def to_dict(self):
+ return self._data
+
+ class Meta:
+ database=db
+ return _Base
+
+
+def declarative_base(*args, **kwargs):
+ """Returns a new Modeled Class after inheriting meta and Model classes"""
+ db = PostgresqlDatabase(*args, **kwargs)
+ return _get_meta_db_class(db)
+
+
+AsyncBaseModel = declarative_base(database='test',
host='127.0.0.1',
user='postgres',
password='mysecretpassword')
- objects = Manager(database, loop=loop)
+# let's create a simple key value store:
+class KeyValue(AsyncBaseModel):
+ key = peewee.CharField(max_length=40, unique=True)
+ text = peewee.TextField(default='')
+
+
+app = Sanic('peewee_example')
+
@app.route('/post//')
async def post(request, key, value):
"""
Save get parameters to database
"""
- obj = await objects.create(KeyValue, key=key, text=value)
+ obj = await KeyValue.objects.new(key=key, text=value)
return json({'object_id': obj.id})
@@ -63,7 +100,7 @@ async def get(request):
"""
Load all objects from database
"""
- all_objects = await objects.execute(KeyValue.select())
+ all_objects = await KeyValue.objects.execute(KeyValue.select())
serialized_obj = []
for obj in all_objects:
serialized_obj.append({
diff --git a/examples/simple_server.py b/examples/simple_server.py
index 24e3570f..a803feb8 100644
--- a/examples/simple_server.py
+++ b/examples/simple_server.py
@@ -9,4 +9,5 @@ async def test(request):
return json({"test": True})
-app.run(host="0.0.0.0", port=8000)
+if __name__ == '__main__':
+ app.run(host="0.0.0.0", port=8000)
diff --git a/examples/try_everything.py b/examples/try_everything.py
index f7191ecc..da3cc515 100644
--- a/examples/try_everything.py
+++ b/examples/try_everything.py
@@ -70,6 +70,11 @@ def query_string(request):
# Run Server
# ----------------------------------------------- #
+@app.listener('before_server_start')
+def before_start(app, loop):
+ log.info("SERVER STARTING")
+
+
@app.listener('after_server_start')
def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH")
@@ -77,7 +82,13 @@ def after_start(app, loop):
@app.listener('before_server_stop')
def before_stop(app, loop):
+ log.info("SERVER STOPPING")
+
+
+@app.listener('after_server_stop')
+def after_stop(app, loop):
log.info("TRIED EVERYTHING")
-app.run(host="0.0.0.0", port=8000, debug=True)
+if __name__ == '__main__':
+ app.run(host="0.0.0.0", port=8000, debug=True)
diff --git a/sanic/app.py b/sanic/app.py
index 65a6196f..e101caf5 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -1,7 +1,7 @@
import logging
import re
import warnings
-from asyncio import get_event_loop
+from asyncio import get_event_loop, ensure_future, CancelledError
from collections import deque, defaultdict
from functools import partial
from inspect import isawaitable, stack, getmodulename
@@ -54,6 +54,7 @@ class Sanic:
self.listeners = defaultdict(list)
self.is_running = False
self.websocket_enabled = False
+ self.websocket_tasks = []
# Register alternative method names
self.go_fast = self.run
@@ -101,7 +102,8 @@ class Sanic:
return decorator
# Decorator
- def route(self, uri, methods=frozenset({'GET'}), host=None):
+ def route(self, uri, methods=frozenset({'GET'}), host=None,
+ strict_slashes=False):
"""Decorate a function to be registered as a route
:param uri: path of the URL
@@ -117,34 +119,42 @@ class Sanic:
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler,
- host=host)
+ host=host, strict_slashes=strict_slashes)
return handler
return response
# Shorthand method decorators
- def get(self, uri, host=None):
- return self.route(uri, methods=frozenset({"GET"}), host=host)
+ def get(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"GET"}), host=host,
+ strict_slashes=strict_slashes)
- def post(self, uri, host=None):
- return self.route(uri, methods=frozenset({"POST"}), host=host)
+ def post(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"POST"}), host=host,
+ strict_slashes=strict_slashes)
- def put(self, uri, host=None):
- return self.route(uri, methods=frozenset({"PUT"}), host=host)
+ def put(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"PUT"}), host=host,
+ strict_slashes=strict_slashes)
- def head(self, uri, host=None):
- return self.route(uri, methods=frozenset({"HEAD"}), host=host)
+ def head(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"HEAD"}), host=host,
+ strict_slashes=strict_slashes)
- def options(self, uri, host=None):
- return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
+ def options(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
+ strict_slashes=strict_slashes)
- def patch(self, uri, host=None):
- return self.route(uri, methods=frozenset({"PATCH"}), host=host)
+ def patch(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"PATCH"}), host=host,
+ strict_slashes=strict_slashes)
- def delete(self, uri, host=None):
- return self.route(uri, methods=frozenset({"DELETE"}), host=host)
+ def delete(self, uri, host=None, strict_slashes=False):
+ return self.route(uri, methods=frozenset({"DELETE"}), host=host,
+ strict_slashes=strict_slashes)
- def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
+ def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
+ strict_slashes=False):
"""A helper method to register class instance or
functions as a handler to the application url
routes.
@@ -168,17 +178,18 @@ class Sanic:
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
- self.route(uri=uri, methods=methods, host=host)(handler)
+ self.route(uri=uri, methods=methods, host=host,
+ strict_slashes=strict_slashes)(handler)
return handler
# Decorator
- def websocket(self, uri, host=None):
+ def websocket(self, uri, host=None, strict_slashes=False):
"""Decorate a function to be registered as a websocket route
:param uri: path of the URL
:param host:
:return: decorated function
"""
- self.websocket_enabled = True
+ self.enable_websocket()
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
@@ -190,22 +201,31 @@ class Sanic:
request.app = self
protocol = request.transport.get_protocol()
ws = await protocol.websocket_handshake(request)
+
+ # schedule the application handler
+ # its future is kept in self.websocket_tasks in case it
+ # needs to be cancelled due to the server being stopped
+ fut = ensure_future(handler(request, ws, *args, **kwargs))
+ self.websocket_tasks.append(fut)
try:
- # invoke the application handler
- await handler(request, ws, *args, **kwargs)
- except ConnectionClosed:
+ await fut
+ except (CancelledError, ConnectionClosed):
pass
+ self.websocket_tasks.remove(fut)
await ws.close()
self.router.add(uri=uri, handler=websocket_handler,
- methods=frozenset({'GET'}), host=host)
+ methods=frozenset({'GET'}), host=host,
+ strict_slashes=strict_slashes)
return handler
return response
- def add_websocket_route(self, handler, uri, host=None):
+ def add_websocket_route(self, handler, uri, host=None,
+ strict_slashes=False):
"""A helper method to register a function as a websocket route."""
- return self.websocket(uri, host=host)(handler)
+ return self.websocket(uri, host=host,
+ strict_slashes=strict_slashes)(handler)
def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket.
@@ -213,6 +233,14 @@ class Sanic:
Websocket is enabled automatically if websocket routes are
added to the application.
"""
+ if not self.websocket_enabled:
+ # if the server is stopped, we want to cancel any ongoing
+ # websocket tasks, to allow the server to exit promptly
+ @self.listener('before_server_stop')
+ def cancel_websocket_tasks(app, loop):
+ for task in self.websocket_tasks:
+ task.cancel()
+
self.websocket_enabled = enable
def remove_route(self, uri, clean_cache=True, host=None):
@@ -305,7 +333,7 @@ class Sanic:
the output URL's query string.
:param view_name: string referencing the view name
- :param **kwargs: keys and values that are used to build request
+ :param \*\*kwargs: keys and values that are used to build request
parameters and query string arguments.
:return: the built URL
@@ -550,6 +578,10 @@ class Sanic:
"""This kills the Sanic"""
get_event_loop().stop()
+ def __call__(self):
+ """gunicorn compatibility"""
+ return self
+
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, after_start=None,
before_stop=None, after_stop=None, ssl=None,
@@ -658,9 +690,10 @@ class Sanic:
server_settings['run_async'] = True
# Serve
- proto = "http"
- if ssl is not None:
- proto = "https"
- log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
+ if host and port:
+ proto = "http"
+ if ssl is not None:
+ proto = "https"
+ log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
return server_settings
diff --git a/sanic/cookies.py b/sanic/cookies.py
index ae77bb44..ce096cd2 100644
--- a/sanic/cookies.py
+++ b/sanic/cookies.py
@@ -19,7 +19,7 @@ _Translator.update({
def _quote(str):
- r"""Quote a string for use in a cookie header.
+ """Quote a string for use in a cookie header.
If the string does not need to be double-quoted, then just return the
string. Otherwise, surround the string in doublequotes and quote
(with a \) special characters.
diff --git a/sanic/response.py b/sanic/response.py
index eb2d3e49..c129884f 100644
--- a/sanic/response.py
+++ b/sanic/response.py
@@ -251,8 +251,7 @@ def text(body, status=200, headers=None,
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
- :param content_type:
- the content type (string) of the response
+ :param content_type: the content type (string) of the response
"""
return HTTPResponse(
body, status=status, headers=headers,
@@ -266,8 +265,7 @@ def raw(body, status=200, headers=None,
:param body: Response data.
:param status: Response code.
:param headers: Custom Headers.
- :param content_type:
- the content type (string) of the response
+ :param content_type: the content type (string) of the response.
"""
return HTTPResponse(body_bytes=body, status=status, headers=headers,
content_type=content_type)
@@ -316,17 +314,16 @@ def stream(
content_type="text/plain; charset=utf-8"):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
- Example usage:
- ```
- @app.route("/")
- async def index(request):
- async def streaming_fn(response):
- await response.write('foo')
- await response.write('bar')
+ Example usage::
- return stream(streaming_fn, content_type='text/plain')
- ```
+ @app.route("/")
+ async def index(request):
+ async def streaming_fn(response):
+ await response.write('foo')
+ await response.write('bar')
+
+ return stream(streaming_fn, content_type='text/plain')
:param streaming_fn: A coroutine accepts a response and
writes content to that response.
diff --git a/sanic/router.py b/sanic/router.py
index 38b1c029..f7877f15 100644
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -75,9 +75,10 @@ class Router:
"""Parse a parameter string into its constituent name, type, and
pattern
- For example:
- `parse_parameter_string('')` ->
- ('param_one', str, '[A-z]')
+ For example::
+
+ parse_parameter_string('')` ->
+ ('param_one', str, '[A-z]')
:param parameter_string: String to parse
:return: tuple containing
@@ -95,9 +96,15 @@ class Router:
return name, _type, pattern
- def add(self, uri, methods, handler, host=None):
+ def add(self, uri, methods, handler, host=None, strict_slashes=False):
+
# add regular version
self._add(uri, methods, handler, host)
+
+ if strict_slashes:
+ return
+
+ # Add versions with and without trailing /
slash_is_missing = (
not uri[-1] == '/'
and not self.routes_all.get(uri + '/', False)
diff --git a/sanic/server.py b/sanic/server.py
index 39816e28..00bb2331 100644
--- a/sanic/server.py
+++ b/sanic/server.py
@@ -313,7 +313,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, debug=False,
request_timeout=60, ssl=None, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
- register_sys_signals=True, run_async=False):
+ register_sys_signals=True, run_async=False, connections=None,
+ signal=Signal()):
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
@@ -329,7 +330,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
`app` instance and `loop`
:param after_stop: function to be executed when a stop signal is
received after it is respected. Takes arguments
- `app` instance and `loop`
+ `app` instance and `loop`
:param debug: enables debug output (slows server)
:param request_timeout: time in seconds
:param ssl: SSLContext
@@ -349,8 +350,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(before_start, loop)
- connections = set()
- signal = Signal()
+ connections = connections if connections is not None else set()
server = partial(
protocol,
loop=loop,
diff --git a/sanic/worker.py b/sanic/worker.py
new file mode 100644
index 00000000..7a8303d8
--- /dev/null
+++ b/sanic/worker.py
@@ -0,0 +1,166 @@
+import os
+import sys
+import signal
+import asyncio
+import logging
+try:
+ import ssl
+except ImportError:
+ ssl = None
+
+import uvloop
+import gunicorn.workers.base as base
+
+from sanic.server import trigger_events, serve, HttpProtocol, Signal
+from sanic.websocket import WebSocketProtocol
+
+
+class GunicornWorker(base.Worker):
+
+ def __init__(self, *args, **kw): # pragma: no cover
+ super().__init__(*args, **kw)
+ cfg = self.cfg
+ if cfg.is_ssl:
+ self.ssl_context = self._create_ssl_context(cfg)
+ else:
+ self.ssl_context = None
+ self.servers = []
+ self.connections = set()
+ self.exit_code = 0
+ self.signal = Signal()
+
+ def init_process(self):
+ # create new event_loop after fork
+ asyncio.get_event_loop().close()
+
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
+
+ super().init_process()
+
+ def run(self):
+ is_debug = self.log.loglevel == logging.DEBUG
+ protocol = (WebSocketProtocol if self.app.callable.websocket_enabled
+ else HttpProtocol)
+ self._server_settings = self.app.callable._helper(
+ host=None,
+ port=None,
+ loop=self.loop,
+ debug=is_debug,
+ protocol=protocol,
+ ssl=self.ssl_context,
+ run_async=True
+ )
+ self._server_settings.pop('sock')
+ trigger_events(self._server_settings.get('before_start', []),
+ self.loop)
+ self._server_settings['before_start'] = ()
+
+ self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
+ try:
+ self.loop.run_until_complete(self._runner)
+ self.app.callable.is_running = True
+ trigger_events(self._server_settings.get('after_start', []),
+ self.loop)
+ self.loop.run_until_complete(self._check_alive())
+ trigger_events(self._server_settings.get('before_stop', []),
+ self.loop)
+ self.loop.run_until_complete(self.close())
+ finally:
+ trigger_events(self._server_settings.get('after_stop', []),
+ self.loop)
+ self.loop.close()
+
+ sys.exit(self.exit_code)
+
+ async def close(self):
+ if self.servers:
+ # stop accepting connections
+ self.log.info("Stopping server: %s, connections: %s",
+ self.pid, len(self.connections))
+ for server in self.servers:
+ server.close()
+ await server.wait_closed()
+ self.servers.clear()
+
+ # prepare connections for closing
+ self.signal.stopped = True
+ for conn in self.connections:
+ conn.close_if_idle()
+
+ while self.connections:
+ await asyncio.sleep(0.1)
+
+ async def _run(self):
+ for sock in self.sockets:
+ self.servers.append(await serve(
+ sock=sock,
+ connections=self.connections,
+ signal=self.signal,
+ **self._server_settings
+ ))
+
+ async def _check_alive(self):
+ # If our parent changed then we shut down.
+ pid = os.getpid()
+ try:
+ while self.alive:
+ self.notify()
+
+ if pid == os.getpid() and self.ppid != os.getppid():
+ self.alive = False
+ self.log.info("Parent changed, shutting down: %s", self)
+ else:
+ await asyncio.sleep(1.0, loop=self.loop)
+ except (Exception, BaseException, GeneratorExit, KeyboardInterrupt):
+ pass
+
+ @staticmethod
+ def _create_ssl_context(cfg):
+ """ Creates SSLContext instance for usage in asyncio.create_server.
+ See ssl.SSLSocket.__init__ for more details.
+ """
+ ctx = ssl.SSLContext(cfg.ssl_version)
+ ctx.load_cert_chain(cfg.certfile, cfg.keyfile)
+ ctx.verify_mode = cfg.cert_reqs
+ if cfg.ca_certs:
+ ctx.load_verify_locations(cfg.ca_certs)
+ if cfg.ciphers:
+ ctx.set_ciphers(cfg.ciphers)
+ return ctx
+
+ def init_signals(self):
+ # Set up signals through the event loop API.
+
+ self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit,
+ signal.SIGQUIT, None)
+
+ self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit,
+ signal.SIGTERM, None)
+
+ self.loop.add_signal_handler(signal.SIGINT, self.handle_quit,
+ signal.SIGINT, None)
+
+ self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch,
+ signal.SIGWINCH, None)
+
+ self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1,
+ signal.SIGUSR1, None)
+
+ self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort,
+ signal.SIGABRT, None)
+
+ # Don't let SIGTERM and SIGUSR1 disturb active requests
+ # by interrupting system calls
+ signal.siginterrupt(signal.SIGTERM, False)
+ signal.siginterrupt(signal.SIGUSR1, False)
+
+ def handle_quit(self, sig, frame):
+ self.alive = False
+ self.cfg.worker_int(self)
+
+ def handle_abort(self, sig, frame):
+ self.alive = False
+ self.exit_code = 1
+ self.cfg.worker_abort(self)
diff --git a/tests/test_routes.py b/tests/test_routes.py
index afefe4a7..3506db66 100644
--- a/tests/test_routes.py
+++ b/tests/test_routes.py
@@ -23,6 +23,29 @@ def test_shorthand_routes_get():
request, response = app.test_client.post('/get')
assert response.status == 405
+def test_route_strict_slash():
+ app = Sanic('test_route_strict_slash')
+
+ @app.get('/get', strict_slashes=True)
+ def handler(request):
+ return text('OK')
+
+ @app.post('/post/', strict_slashes=True)
+ def handler(request):
+ return text('OK')
+
+ request, response = app.test_client.get('/get')
+ assert response.text == 'OK'
+
+ request, response = app.test_client.get('/get/')
+ assert response.status == 404
+
+ request, response = app.test_client.post('/post/')
+ assert response.text == 'OK'
+
+ request, response = app.test_client.post('/post')
+ assert response.status == 404
+
def test_route_optional_slash():
app = Sanic('test_route_optional_slash')
diff --git a/tests/test_views.py b/tests/test_views.py
index 627a3b6c..40fc1adf 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -15,13 +15,13 @@ def test_methods(method):
class DummyView(HTTPMethodView):
- def get(self, request):
+ async def get(self, request):
return text('', headers={'method': 'GET'})
def post(self, request):
return text('', headers={'method': 'POST'})
- def put(self, request):
+ async def put(self, request):
return text('', headers={'method': 'PUT'})
def head(self, request):
@@ -30,7 +30,7 @@ def test_methods(method):
def options(self, request):
return text('', headers={'method': 'OPTIONS'})
- def patch(self, request):
+ async def patch(self, request):
return text('', headers={'method': 'PATCH'})
def delete(self, request):