merge upstream
This commit is contained in:
commit
be1016ace6
27
docs/conf.py
27
docs/conf.py
|
@ -22,7 +22,7 @@ import sanic
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
extensions = []
|
extensions = ['sphinx.ext.autodoc']
|
||||||
|
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
@ -68,7 +68,6 @@ pygments_style = 'sphinx'
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
todo_include_todos = False
|
todo_include_todos = False
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# 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".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTMLHelp output ------------------------------------------
|
# -- Options for HTMLHelp output ------------------------------------------
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = 'Sanicdoc'
|
htmlhelp_basename = 'Sanicdoc'
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
latex_elements = {
|
latex_elements = {
|
||||||
|
@ -110,21 +107,14 @@ latex_elements = {
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation',
|
||||||
(master_doc, 'Sanic.tex', 'Sanic Documentation',
|
'Sanic contributors', 'manual'), ]
|
||||||
'Sanic contributors', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for manual page output ---------------------------------------
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
|
||||||
(master_doc, 'sanic', 'Sanic Documentation',
|
|
||||||
[author], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output -------------------------------------------
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
@ -132,13 +122,10 @@ man_pages = [
|
||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
(master_doc, 'Sanic', 'Sanic Documentation',
|
(master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic',
|
||||||
author, 'Sanic', 'One line description of project.',
|
'One line description of project.', 'Miscellaneous'),
|
||||||
'Miscellaneous'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Epub output ----------------------------------------------
|
# -- Options for Epub output ----------------------------------------------
|
||||||
|
|
||||||
# Bibliographic Dublin Core info.
|
# Bibliographic Dublin Core info.
|
||||||
|
@ -150,8 +137,6 @@ epub_copyright = copyright
|
||||||
# A list of files that should not be packed into the epub file.
|
# A list of files that should not be packed into the epub file.
|
||||||
epub_exclude_files = ['search.html']
|
epub_exclude_files = ['search.html']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -- Custom Settings -------------------------------------------------------
|
# -- Custom Settings -------------------------------------------------------
|
||||||
|
|
||||||
suppress_warnings = ['image.nonlocal_uri']
|
suppress_warnings = ['image.nonlocal_uri']
|
||||||
|
|
|
@ -16,6 +16,7 @@ Guides
|
||||||
sanic/blueprints
|
sanic/blueprints
|
||||||
sanic/config
|
sanic/config
|
||||||
sanic/cookies
|
sanic/cookies
|
||||||
|
sanic/streaming
|
||||||
sanic/class_based_views
|
sanic/class_based_views
|
||||||
sanic/custom_protocol
|
sanic/custom_protocol
|
||||||
sanic/ssl
|
sanic/ssl
|
||||||
|
|
|
@ -66,7 +66,7 @@ Using blueprints allows you to also register middleware globally.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@bp.middleware
|
@bp.middleware
|
||||||
async def halt_request(request):
|
async def print_on_request(request):
|
||||||
print("I am a spy")
|
print("I am a spy")
|
||||||
|
|
||||||
@bp.middleware('request')
|
@bp.middleware('request')
|
||||||
|
|
|
@ -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
|
## URL parameters
|
||||||
|
|
||||||
If you need any URL parameters, as discussed in the routing guide, include them
|
If you need any URL parameters, as discussed in the routing guide, include them
|
||||||
|
|
|
@ -75,4 +75,3 @@ 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_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||||
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
||||||
|
|
|
@ -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](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
|
||||||
`Babel` library
|
`Babel` library
|
||||||
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
|
- [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.
|
||||||
|
|
|
@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
|
||||||
|
|
||||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||||
|
|
||||||
Sanic aspires to be simple:
|
Sanic aspires to be simple
|
||||||
-------------------
|
---------------------------
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
|
|
@ -15,4 +15,5 @@ dependencies:
|
||||||
- httptools>=0.0.9
|
- httptools>=0.0.9
|
||||||
- ujson>=1.35
|
- ujson>=1.35
|
||||||
- aiofiles>=0.3.0
|
- aiofiles>=0.3.0
|
||||||
|
- websockets>=3.2
|
||||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
- https://github.com/channelcat/docutils-fork/zipball/master
|
62
examples/sanic_aiomysql_with_global_pool.py
Normal file
62
examples/sanic_aiomysql_with_global_pool.py
Normal file
|
@ -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)
|
34
examples/sanic_aioredis_example.py
Normal file
34
examples/sanic_aioredis_example.py
Normal file
|
@ -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)
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
## You need the following additional packages for this example
|
## You need the following additional packages for this example
|
||||||
# aiopg
|
# aiopg
|
||||||
# peewee_async
|
# peewee_async
|
||||||
|
@ -10,8 +11,9 @@ from sanic.response import json
|
||||||
|
|
||||||
## peewee_async related imports
|
## peewee_async related imports
|
||||||
import peewee
|
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
|
# we instantiate a custom loop so we can pass it to our db manager
|
||||||
|
|
||||||
## from peewee_async docs:
|
## 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
|
# with manager! It’s all automatic. But you can run Manager.connect() or
|
||||||
# Manager.close() when you need it.
|
# 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:
|
database.allow_sync = False
|
||||||
class KeyValue(peewee.Model):
|
"""
|
||||||
key = peewee.CharField(max_length=40, unique=True)
|
|
||||||
text = peewee.TextField(default='')
|
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:
|
class Meta:
|
||||||
database = database
|
database=db
|
||||||
|
return _Base
|
||||||
# create table synchronously
|
|
||||||
KeyValue.create_table(True)
|
|
||||||
|
|
||||||
# OPTIONAL: close synchronous connection
|
|
||||||
database.close()
|
|
||||||
|
|
||||||
# OPTIONAL: disable any future syncronous calls
|
|
||||||
objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
|
|
||||||
|
|
||||||
|
|
||||||
app = Sanic('peewee_example')
|
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)
|
||||||
|
|
||||||
@app.listener('before_server_start')
|
|
||||||
def setup(app, loop):
|
AsyncBaseModel = declarative_base(database='test',
|
||||||
database = PostgresqlDatabase(database='test',
|
|
||||||
host='127.0.0.1',
|
host='127.0.0.1',
|
||||||
user='postgres',
|
user='postgres',
|
||||||
password='mysecretpassword')
|
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/<key>/<value>')
|
@app.route('/post/<key>/<value>')
|
||||||
async def post(request, key, value):
|
async def post(request, key, value):
|
||||||
"""
|
"""
|
||||||
Save get parameters to database
|
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})
|
return json({'object_id': obj.id})
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,7 +100,7 @@ async def get(request):
|
||||||
"""
|
"""
|
||||||
Load all objects from database
|
Load all objects from database
|
||||||
"""
|
"""
|
||||||
all_objects = await objects.execute(KeyValue.select())
|
all_objects = await KeyValue.objects.execute(KeyValue.select())
|
||||||
serialized_obj = []
|
serialized_obj = []
|
||||||
for obj in all_objects:
|
for obj in all_objects:
|
||||||
serialized_obj.append({
|
serialized_obj.append({
|
||||||
|
|
|
@ -9,4 +9,5 @@ async def test(request):
|
||||||
return json({"test": True})
|
return json({"test": True})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|
|
@ -70,6 +70,11 @@ def query_string(request):
|
||||||
# Run Server
|
# Run Server
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
|
@app.listener('before_server_start')
|
||||||
|
def before_start(app, loop):
|
||||||
|
log.info("SERVER STARTING")
|
||||||
|
|
||||||
|
|
||||||
@app.listener('after_server_start')
|
@app.listener('after_server_start')
|
||||||
def after_start(app, loop):
|
def after_start(app, loop):
|
||||||
log.info("OH OH OH OH OHHHHHHHH")
|
log.info("OH OH OH OH OHHHHHHHH")
|
||||||
|
@ -77,7 +82,13 @@ def after_start(app, loop):
|
||||||
|
|
||||||
@app.listener('before_server_stop')
|
@app.listener('before_server_stop')
|
||||||
def before_stop(app, loop):
|
def before_stop(app, loop):
|
||||||
|
log.info("SERVER STOPPING")
|
||||||
|
|
||||||
|
|
||||||
|
@app.listener('after_server_stop')
|
||||||
|
def after_stop(app, loop):
|
||||||
log.info("TRIED EVERYTHING")
|
log.info("TRIED EVERYTHING")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|
89
sanic/app.py
89
sanic/app.py
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from asyncio import get_event_loop
|
from asyncio import get_event_loop, ensure_future, CancelledError
|
||||||
from collections import deque, defaultdict
|
from collections import deque, defaultdict
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable, stack, getmodulename
|
from inspect import isawaitable, stack, getmodulename
|
||||||
|
@ -54,6 +54,7 @@ class Sanic:
|
||||||
self.listeners = defaultdict(list)
|
self.listeners = defaultdict(list)
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.websocket_enabled = False
|
self.websocket_enabled = False
|
||||||
|
self.websocket_tasks = []
|
||||||
|
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
|
@ -101,7 +102,8 @@ class Sanic:
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
# 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
|
"""Decorate a function to be registered as a route
|
||||||
|
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
|
@ -117,34 +119,42 @@ class Sanic:
|
||||||
|
|
||||||
def response(handler):
|
def response(handler):
|
||||||
self.router.add(uri=uri, methods=methods, handler=handler,
|
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||||
host=host)
|
host=host, strict_slashes=strict_slashes)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Shorthand method decorators
|
# Shorthand method decorators
|
||||||
def get(self, uri, host=None):
|
def get(self, uri, host=None, strict_slashes=False):
|
||||||
return self.route(uri, methods=frozenset({"GET"}), host=host)
|
return self.route(uri, methods=frozenset({"GET"}), host=host,
|
||||||
|
strict_slashes=strict_slashes)
|
||||||
|
|
||||||
def post(self, uri, host=None):
|
def post(self, uri, host=None, strict_slashes=False):
|
||||||
return self.route(uri, methods=frozenset({"POST"}), host=host)
|
return self.route(uri, methods=frozenset({"POST"}), host=host,
|
||||||
|
strict_slashes=strict_slashes)
|
||||||
|
|
||||||
def put(self, uri, host=None):
|
def put(self, uri, host=None, strict_slashes=False):
|
||||||
return self.route(uri, methods=frozenset({"PUT"}), host=host)
|
return self.route(uri, methods=frozenset({"PUT"}), host=host,
|
||||||
|
strict_slashes=strict_slashes)
|
||||||
|
|
||||||
def head(self, uri, host=None):
|
def head(self, uri, host=None, strict_slashes=False):
|
||||||
return self.route(uri, methods=frozenset({"HEAD"}), host=host)
|
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
|
||||||
|
strict_slashes=strict_slashes)
|
||||||
|
|
||||||
def options(self, uri, host=None):
|
def options(self, uri, host=None, strict_slashes=False):
|
||||||
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
|
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
|
||||||
|
strict_slashes=strict_slashes)
|
||||||
|
|
||||||
def patch(self, uri, host=None):
|
def patch(self, uri, host=None, strict_slashes=False):
|
||||||
return self.route(uri, methods=frozenset({"PATCH"}), host=host)
|
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
|
||||||
|
strict_slashes=strict_slashes)
|
||||||
|
|
||||||
def delete(self, uri, host=None):
|
def delete(self, uri, host=None, strict_slashes=False):
|
||||||
return self.route(uri, methods=frozenset({"DELETE"}), host=host)
|
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
|
"""A helper method to register class instance or
|
||||||
functions as a handler to the application url
|
functions as a handler to the application url
|
||||||
routes.
|
routes.
|
||||||
|
@ -168,17 +178,18 @@ class Sanic:
|
||||||
if isinstance(handler, CompositionView):
|
if isinstance(handler, CompositionView):
|
||||||
methods = handler.handlers.keys()
|
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
|
return handler
|
||||||
|
|
||||||
# Decorator
|
# 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
|
"""Decorate a function to be registered as a websocket route
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
:param host:
|
:param host:
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
self.websocket_enabled = True
|
self.enable_websocket()
|
||||||
|
|
||||||
# Fix case where the user did not prefix the URL with a /
|
# Fix case where the user did not prefix the URL with a /
|
||||||
# and will probably get confused as to why it's not working
|
# and will probably get confused as to why it's not working
|
||||||
|
@ -190,22 +201,31 @@ class Sanic:
|
||||||
request.app = self
|
request.app = self
|
||||||
protocol = request.transport.get_protocol()
|
protocol = request.transport.get_protocol()
|
||||||
ws = await protocol.websocket_handshake(request)
|
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:
|
try:
|
||||||
# invoke the application handler
|
await fut
|
||||||
await handler(request, ws, *args, **kwargs)
|
except (CancelledError, ConnectionClosed):
|
||||||
except ConnectionClosed:
|
|
||||||
pass
|
pass
|
||||||
|
self.websocket_tasks.remove(fut)
|
||||||
await ws.close()
|
await ws.close()
|
||||||
|
|
||||||
self.router.add(uri=uri, handler=websocket_handler,
|
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 handler
|
||||||
|
|
||||||
return response
|
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."""
|
"""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):
|
def enable_websocket(self, enable=True):
|
||||||
"""Enable or disable the support for websocket.
|
"""Enable or disable the support for websocket.
|
||||||
|
@ -213,6 +233,14 @@ class Sanic:
|
||||||
Websocket is enabled automatically if websocket routes are
|
Websocket is enabled automatically if websocket routes are
|
||||||
added to the application.
|
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
|
self.websocket_enabled = enable
|
||||||
|
|
||||||
def remove_route(self, uri, clean_cache=True, host=None):
|
def remove_route(self, uri, clean_cache=True, host=None):
|
||||||
|
@ -305,7 +333,7 @@ class Sanic:
|
||||||
the output URL's query string.
|
the output URL's query string.
|
||||||
|
|
||||||
:param view_name: string referencing the view name
|
: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.
|
parameters and query string arguments.
|
||||||
|
|
||||||
:return: the built URL
|
:return: the built URL
|
||||||
|
@ -550,6 +578,10 @@ class Sanic:
|
||||||
"""This kills the Sanic"""
|
"""This kills the Sanic"""
|
||||||
get_event_loop().stop()
|
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,
|
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
|
||||||
before_start=None, after_start=None,
|
before_start=None, after_start=None,
|
||||||
before_stop=None, after_stop=None, ssl=None,
|
before_stop=None, after_stop=None, ssl=None,
|
||||||
|
@ -658,6 +690,7 @@ class Sanic:
|
||||||
server_settings['run_async'] = True
|
server_settings['run_async'] = True
|
||||||
|
|
||||||
# Serve
|
# Serve
|
||||||
|
if host and port:
|
||||||
proto = "http"
|
proto = "http"
|
||||||
if ssl is not None:
|
if ssl is not None:
|
||||||
proto = "https"
|
proto = "https"
|
||||||
|
|
|
@ -19,7 +19,7 @@ _Translator.update({
|
||||||
|
|
||||||
|
|
||||||
def _quote(str):
|
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
|
If the string does not need to be double-quoted, then just return the
|
||||||
string. Otherwise, surround the string in doublequotes and quote
|
string. Otherwise, surround the string in doublequotes and quote
|
||||||
(with a \) special characters.
|
(with a \) special characters.
|
||||||
|
|
|
@ -251,8 +251,7 @@ def text(body, status=200, headers=None,
|
||||||
:param body: Response data to be encoded.
|
:param body: Response data to be encoded.
|
||||||
:param status: Response code.
|
:param status: Response code.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param content_type:
|
:param content_type: the content type (string) of the response
|
||||||
the content type (string) of the response
|
|
||||||
"""
|
"""
|
||||||
return HTTPResponse(
|
return HTTPResponse(
|
||||||
body, status=status, headers=headers,
|
body, status=status, headers=headers,
|
||||||
|
@ -266,8 +265,7 @@ def raw(body, status=200, headers=None,
|
||||||
:param body: Response data.
|
:param body: Response data.
|
||||||
:param status: Response code.
|
:param status: Response code.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param content_type:
|
:param content_type: the content type (string) of the response.
|
||||||
the content type (string) of the response
|
|
||||||
"""
|
"""
|
||||||
return HTTPResponse(body_bytes=body, status=status, headers=headers,
|
return HTTPResponse(body_bytes=body, status=status, headers=headers,
|
||||||
content_type=content_type)
|
content_type=content_type)
|
||||||
|
@ -316,9 +314,9 @@ def stream(
|
||||||
content_type="text/plain; charset=utf-8"):
|
content_type="text/plain; charset=utf-8"):
|
||||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||||
Example usage:
|
|
||||||
|
|
||||||
```
|
Example usage::
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def index(request):
|
async def index(request):
|
||||||
async def streaming_fn(response):
|
async def streaming_fn(response):
|
||||||
|
@ -326,7 +324,6 @@ def stream(
|
||||||
await response.write('bar')
|
await response.write('bar')
|
||||||
|
|
||||||
return stream(streaming_fn, content_type='text/plain')
|
return stream(streaming_fn, content_type='text/plain')
|
||||||
```
|
|
||||||
|
|
||||||
:param streaming_fn: A coroutine accepts a response and
|
:param streaming_fn: A coroutine accepts a response and
|
||||||
writes content to that response.
|
writes content to that response.
|
||||||
|
|
|
@ -75,8 +75,9 @@ class Router:
|
||||||
"""Parse a parameter string into its constituent name, type, and
|
"""Parse a parameter string into its constituent name, type, and
|
||||||
pattern
|
pattern
|
||||||
|
|
||||||
For example:
|
For example::
|
||||||
`parse_parameter_string('<param_one:[A-z]>')` ->
|
|
||||||
|
parse_parameter_string('<param_one:[A-z]>')` ->
|
||||||
('param_one', str, '[A-z]')
|
('param_one', str, '[A-z]')
|
||||||
|
|
||||||
:param parameter_string: String to parse
|
:param parameter_string: String to parse
|
||||||
|
@ -95,9 +96,15 @@ class Router:
|
||||||
|
|
||||||
return name, _type, pattern
|
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
|
# add regular version
|
||||||
self._add(uri, methods, handler, host)
|
self._add(uri, methods, handler, host)
|
||||||
|
|
||||||
|
if strict_slashes:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add versions with and without trailing /
|
||||||
slash_is_missing = (
|
slash_is_missing = (
|
||||||
not uri[-1] == '/'
|
not uri[-1] == '/'
|
||||||
and not self.routes_all.get(uri + '/', False)
|
and not self.routes_all.get(uri + '/', False)
|
||||||
|
|
|
@ -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,
|
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||||
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
||||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
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.
|
"""Start asynchronous HTTP Server on an individual process.
|
||||||
|
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
|
@ -349,8 +350,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
|
|
||||||
trigger_events(before_start, loop)
|
trigger_events(before_start, loop)
|
||||||
|
|
||||||
connections = set()
|
connections = connections if connections is not None else set()
|
||||||
signal = Signal()
|
|
||||||
server = partial(
|
server = partial(
|
||||||
protocol,
|
protocol,
|
||||||
loop=loop,
|
loop=loop,
|
||||||
|
|
166
sanic/worker.py
Normal file
166
sanic/worker.py
Normal file
|
@ -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)
|
|
@ -23,6 +23,29 @@ def test_shorthand_routes_get():
|
||||||
request, response = app.test_client.post('/get')
|
request, response = app.test_client.post('/get')
|
||||||
assert response.status == 405
|
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():
|
def test_route_optional_slash():
|
||||||
app = Sanic('test_route_optional_slash')
|
app = Sanic('test_route_optional_slash')
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,13 @@ def test_methods(method):
|
||||||
|
|
||||||
class DummyView(HTTPMethodView):
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
def get(self, request):
|
async def get(self, request):
|
||||||
return text('', headers={'method': 'GET'})
|
return text('', headers={'method': 'GET'})
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
return text('', headers={'method': 'POST'})
|
return text('', headers={'method': 'POST'})
|
||||||
|
|
||||||
def put(self, request):
|
async def put(self, request):
|
||||||
return text('', headers={'method': 'PUT'})
|
return text('', headers={'method': 'PUT'})
|
||||||
|
|
||||||
def head(self, request):
|
def head(self, request):
|
||||||
|
@ -30,7 +30,7 @@ def test_methods(method):
|
||||||
def options(self, request):
|
def options(self, request):
|
||||||
return text('', headers={'method': 'OPTIONS'})
|
return text('', headers={'method': 'OPTIONS'})
|
||||||
|
|
||||||
def patch(self, request):
|
async def patch(self, request):
|
||||||
return text('', headers={'method': 'PATCH'})
|
return text('', headers={'method': 'PATCH'})
|
||||||
|
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user