Merge branch 'master' into 420
This commit is contained in:
commit
1cf1024332
|
@ -78,10 +78,7 @@ Documentation
|
||||||
TODO
|
TODO
|
||||||
----
|
----
|
||||||
* Streamed file processing
|
* Streamed file processing
|
||||||
* File output
|
* http2
|
||||||
* Examples of integrations with 3rd-party modules
|
|
||||||
* RESTful router
|
|
||||||
|
|
||||||
Limitations
|
Limitations
|
||||||
-----------
|
-----------
|
||||||
* No wheels for uvloop and httptools on Windows :(
|
* No wheels for uvloop and httptools on Windows :(
|
||||||
|
|
|
@ -131,8 +131,8 @@ can be used to implement our API versioning scheme.
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic import Blueprint
|
from sanic import Blueprint
|
||||||
|
|
||||||
blueprint_v1 = Blueprint('v1')
|
blueprint_v1 = Blueprint('v1', url_prefix='/v1')
|
||||||
blueprint_v2 = Blueprint('v2')
|
blueprint_v2 = Blueprint('v2', url_prefix='/v2')
|
||||||
|
|
||||||
@blueprint_v1.route('/')
|
@blueprint_v1.route('/')
|
||||||
async def api_v1_root(request):
|
async def api_v1_root(request):
|
||||||
|
|
|
@ -7,15 +7,6 @@ keyword arguments:
|
||||||
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
||||||
- `port` *(default `8000`)*: Port to host the server on.
|
- `port` *(default `8000`)*: Port to host the server on.
|
||||||
- `debug` *(default `False`)*: Enables debug output (slows server).
|
- `debug` *(default `False`)*: Enables debug output (slows server).
|
||||||
- `before_start` *(default `None`)*: Function or list of functions to be executed
|
|
||||||
before the server starts accepting connections.
|
|
||||||
- `after_start` *(default `None`)*: Function or list of functions to be executed
|
|
||||||
after the server starts accepting connections.
|
|
||||||
- `before_stop` *(default `None`)*: Function or list of functions to be
|
|
||||||
executed when a stop signal is received before it is
|
|
||||||
respected.
|
|
||||||
- `after_stop` *(default `None`)*: Function or list of functions to be executed
|
|
||||||
when all requests are complete.
|
|
||||||
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
|
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
|
||||||
- `sock` *(default `None`)*: Socket for the server to accept connections from.
|
- `sock` *(default `None`)*: Socket for the server to accept connections from.
|
||||||
- `workers` *(default `1`)*: Number of worker processes to spawn.
|
- `workers` *(default `1`)*: Number of worker processes to spawn.
|
||||||
|
|
|
@ -7,3 +7,4 @@ A list of Sanic extensions created by the community.
|
||||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||||
|
- [Websockets](https://github.com/r0fls/sanic-websockets): Minimal wrapper to work with [Websockets](https://github.com/aaugustin/websockets)
|
||||||
|
|
|
@ -145,6 +145,28 @@ Other things to keep in mind when using `url_for`:
|
||||||
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
|
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
|
||||||
# /posts/5?arg_one=one&arg_two=two
|
# /posts/5?arg_one=one&arg_two=two
|
||||||
```
|
```
|
||||||
|
- Multivalue argument can be passed to `url_for`. For example:
|
||||||
|
```python
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
|
||||||
|
# /posts/5?arg_one=one&arg_one=two
|
||||||
|
```
|
||||||
|
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not support now and will be ignored). For example:
|
||||||
|
```python
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
|
||||||
|
# /posts/5?arg_one=one#anchor
|
||||||
|
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
|
||||||
|
# //server/posts/5?arg_one=one
|
||||||
|
# _external requires passed argument _server or SERVER_NAME in app.config or url will be same as no _external
|
||||||
|
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
|
||||||
|
# http://server/posts/5?arg_one=one
|
||||||
|
# when specifying _scheme, _external must be True
|
||||||
|
|
||||||
|
# you can pass all special arguments one time
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
|
||||||
|
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
|
||||||
|
```
|
||||||
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.
|
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Example of caching using aiocache package. To run it you will need a Redis
|
Example of caching using aiocache package. To run it you will need a Redis
|
||||||
instance running in localhost:6379.
|
instance running in localhost:6379. You can also try with SimpleMemoryCache.
|
||||||
|
|
||||||
Running this example you will see that the first call lasts 3 seconds and
|
Running this example you will see that the first call lasts 3 seconds and
|
||||||
the rest are instant because the value is retrieved from the Redis.
|
the rest are instant because the value is retrieved from the Redis.
|
||||||
|
@ -20,9 +20,14 @@ from aiocache.serializers import JsonSerializer
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
aiocache.settings.set_defaults(
|
|
||||||
class_="aiocache.RedisCache"
|
@app.listener('before_server_start')
|
||||||
)
|
def init_cache(sanic, loop):
|
||||||
|
aiocache.settings.set_defaults(
|
||||||
|
class_="aiocache.RedisCache",
|
||||||
|
# class_="aiocache.SimpleMemoryCache",
|
||||||
|
loop=loop
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@cached(key="my_custom_key", serializer=JsonSerializer())
|
@cached(key="my_custom_key", serializer=JsonSerializer())
|
||||||
|
@ -38,4 +43,4 @@ async def test(request):
|
||||||
return json(await expensive_call())
|
return json(await expensive_call())
|
||||||
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop())
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|
|
@ -8,6 +8,7 @@ app = Sanic(__name__)
|
||||||
|
|
||||||
sem = None
|
sem = None
|
||||||
|
|
||||||
|
@app.listener('before_server_start')
|
||||||
def init(sanic, loop):
|
def init(sanic, loop):
|
||||||
global sem
|
global sem
|
||||||
CONCURRENCY_PER_WORKER = 4
|
CONCURRENCY_PER_WORKER = 4
|
||||||
|
@ -33,4 +34,4 @@ async def test(request):
|
||||||
return json(response)
|
return json(response)
|
||||||
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000, workers=2, before_start=init)
|
app.run(host="0.0.0.0", port=8000, workers=2)
|
||||||
|
|
|
@ -26,6 +26,7 @@ async def get_pool():
|
||||||
|
|
||||||
app = Sanic(name=__name__)
|
app = Sanic(name=__name__)
|
||||||
|
|
||||||
|
@app.listener('before_server_start')
|
||||||
async def prepare_db(app, loop):
|
async def prepare_db(app, loop):
|
||||||
"""
|
"""
|
||||||
Let's create some table and add some data
|
Let's create some table and add some data
|
||||||
|
@ -61,5 +62,4 @@ async def handle(request):
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0',
|
app.run(host='0.0.0.0',
|
||||||
port=8000,
|
port=8000,
|
||||||
debug=True,
|
debug=True)
|
||||||
before_start=prepare_db)
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ polls = sa.Table('sanic_polls', metadata,
|
||||||
|
|
||||||
app = Sanic(name=__name__)
|
app = Sanic(name=__name__)
|
||||||
|
|
||||||
|
@app.listener('before_server_start')
|
||||||
async def prepare_db(app, loop):
|
async def prepare_db(app, loop):
|
||||||
""" Let's add some data
|
""" Let's add some data
|
||||||
|
|
||||||
|
@ -58,9 +58,10 @@ async def handle(request):
|
||||||
async with engine.acquire() as conn:
|
async with engine.acquire() as conn:
|
||||||
result = []
|
result = []
|
||||||
async for row in conn.execute(polls.select()):
|
async for row in conn.execute(polls.select()):
|
||||||
result.append({"question": row.question, "pub_date": row.pub_date})
|
result.append({"question": row.question,
|
||||||
|
"pub_date": row.pub_date})
|
||||||
return json({"polls": result})
|
return json({"polls": result})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=8000, before_start=prepare_db)
|
app.run(host='0.0.0.0', port=8000)
|
||||||
|
|
|
@ -27,6 +27,7 @@ def jsonify(records):
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
@app.listener('before_server_start')
|
||||||
async def create_db(app, loop):
|
async def create_db(app, loop):
|
||||||
"""
|
"""
|
||||||
Create some table and add some data
|
Create some table and add some data
|
||||||
|
@ -55,4 +56,4 @@ async def handler(request):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=8000, before_start=create_db)
|
app.run(host='0.0.0.0', port=8000)
|
||||||
|
|
|
@ -14,14 +14,6 @@ from peewee_async import Manager, PostgresqlDatabase
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
def setup(app, loop):
|
|
||||||
database = PostgresqlDatabase(database='test',
|
|
||||||
host='127.0.0.1',
|
|
||||||
user='postgres',
|
|
||||||
password='mysecretpassword')
|
|
||||||
|
|
||||||
objects = Manager(database, loop=loop)
|
|
||||||
|
|
||||||
## from peewee_async docs:
|
## from peewee_async docs:
|
||||||
# Also there’s no need to connect and re-connect before executing async queries
|
# Also there’s no need to connect and re-connect before executing async queries
|
||||||
# with manager! It’s all automatic. But you can run Manager.connect() or
|
# with manager! It’s all automatic. But you can run Manager.connect() or
|
||||||
|
@ -48,6 +40,15 @@ objects.database.allow_sync = False # this will raise AssertionError on ANY sync
|
||||||
|
|
||||||
app = Sanic('peewee_example')
|
app = Sanic('peewee_example')
|
||||||
|
|
||||||
|
@app.listener('before_server_start')
|
||||||
|
def setup(app, loop):
|
||||||
|
database = PostgresqlDatabase(database='test',
|
||||||
|
host='127.0.0.1',
|
||||||
|
user='postgres',
|
||||||
|
password='mysecretpassword')
|
||||||
|
|
||||||
|
objects = Manager(database, loop=loop)
|
||||||
|
|
||||||
@app.route('/post/<key>/<value>')
|
@app.route('/post/<key>/<value>')
|
||||||
async def post(request, key, value):
|
async def post(request, key, value):
|
||||||
"""
|
"""
|
||||||
|
@ -75,4 +76,4 @@ async def get(request):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host='0.0.0.0', port=8000, before_start=setup)
|
app.run(host='0.0.0.0', port=8000)
|
||||||
|
|
|
@ -64,12 +64,14 @@ def query_string(request):
|
||||||
# Run Server
|
# Run Server
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
|
@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")
|
||||||
|
|
||||||
|
|
||||||
|
@app.listener('before_server_stop')
|
||||||
def before_stop(app, loop):
|
def before_stop(app, loop):
|
||||||
log.info("TRIED EVERYTHING")
|
log.info("TRIED EVERYTHING")
|
||||||
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True, after_start=after_start, before_stop=before_stop)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
|
|
||||||
|
from sanic.constants import HTTP_METHODS
|
||||||
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
|
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
|
||||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||||
|
@ -11,9 +13,9 @@ FutureStatic = namedtuple('Route',
|
||||||
|
|
||||||
class Blueprint:
|
class Blueprint:
|
||||||
def __init__(self, name, url_prefix=None, host=None):
|
def __init__(self, name, url_prefix=None, host=None):
|
||||||
"""
|
"""Create a new blueprint
|
||||||
Creates a new blueprint
|
|
||||||
:param name: Unique name of the blueprint
|
:param name: unique name of the blueprint
|
||||||
:param url_prefix: URL to be prefixed before all route URLs
|
:param url_prefix: URL to be prefixed before all route URLs
|
||||||
"""
|
"""
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -27,9 +29,7 @@ class Blueprint:
|
||||||
self.statics = []
|
self.statics = []
|
||||||
|
|
||||||
def register(self, app, options):
|
def register(self, app, options):
|
||||||
"""
|
"""Register the blueprint to the sanic app."""
|
||||||
Registers the blueprint to the sanic app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url_prefix = options.get('url_prefix', self.url_prefix)
|
url_prefix = options.get('url_prefix', self.url_prefix)
|
||||||
|
|
||||||
|
@ -65,11 +65,16 @@ class Blueprint:
|
||||||
app.static(uri, future.file_or_directory,
|
app.static(uri, future.file_or_directory,
|
||||||
*future.args, **future.kwargs)
|
*future.args, **future.kwargs)
|
||||||
|
|
||||||
|
# Event listeners
|
||||||
|
for event, listeners in self.listeners.items():
|
||||||
|
for listener in listeners:
|
||||||
|
app.listener(event)(listener)
|
||||||
|
|
||||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||||
"""
|
"""Create a blueprint route from a decorated function.
|
||||||
Creates a blueprint route from a decorated function.
|
|
||||||
:param uri: Endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
:param methods: List of acceptable HTTP methods.
|
:param methods: list of acceptable HTTP methods.
|
||||||
"""
|
"""
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
route = FutureRoute(handler, uri, methods, host)
|
route = FutureRoute(handler, uri, methods, host)
|
||||||
|
@ -77,20 +82,33 @@ class Blueprint:
|
||||||
return handler
|
return handler
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods=None, host=None):
|
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
|
||||||
|
"""Create a blueprint route from a function.
|
||||||
|
|
||||||
|
:param handler: function for handling uri requests. Accepts function,
|
||||||
|
or class instance with a view_class method.
|
||||||
|
:param uri: endpoint at which the route will be accessible.
|
||||||
|
:param methods: list of acceptable HTTP methods.
|
||||||
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
Creates a blueprint route from a function.
|
# Handle HTTPMethodView differently
|
||||||
:param handler: Function to handle uri request.
|
if hasattr(handler, 'view_class'):
|
||||||
:param uri: Endpoint at which the route will be accessible.
|
methods = set()
|
||||||
:param methods: List of acceptable HTTP methods.
|
|
||||||
"""
|
for method in HTTP_METHODS:
|
||||||
route = FutureRoute(handler, uri, methods, host)
|
if getattr(handler.view_class, method.lower(), None):
|
||||||
self.routes.append(route)
|
methods.add(method)
|
||||||
|
|
||||||
|
# handle composition view differently
|
||||||
|
if isinstance(handler, CompositionView):
|
||||||
|
methods = handler.handlers.keys()
|
||||||
|
|
||||||
|
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def listener(self, event):
|
def listener(self, event):
|
||||||
"""
|
"""Create a listener from a decorated function.
|
||||||
Create a listener from a decorated function.
|
|
||||||
:param event: Event to listen to.
|
:param event: Event to listen to.
|
||||||
"""
|
"""
|
||||||
def decorator(listener):
|
def decorator(listener):
|
||||||
|
@ -99,9 +117,7 @@ class Blueprint:
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def middleware(self, *args, **kwargs):
|
def middleware(self, *args, **kwargs):
|
||||||
"""
|
"""Create a blueprint middleware from a decorated function."""
|
||||||
Creates a blueprint middleware from a decorated function.
|
|
||||||
"""
|
|
||||||
def register_middleware(_middleware):
|
def register_middleware(_middleware):
|
||||||
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
||||||
self.middlewares.append(future_middleware)
|
self.middlewares.append(future_middleware)
|
||||||
|
@ -116,9 +132,7 @@ class Blueprint:
|
||||||
return register_middleware
|
return register_middleware
|
||||||
|
|
||||||
def exception(self, *args, **kwargs):
|
def exception(self, *args, **kwargs):
|
||||||
"""
|
"""Create a blueprint exception from a decorated function."""
|
||||||
Creates a blueprint exception from a decorated function.
|
|
||||||
"""
|
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
exception = FutureException(handler, args, kwargs)
|
exception = FutureException(handler, args, kwargs)
|
||||||
self.exceptions.append(exception)
|
self.exceptions.append(exception)
|
||||||
|
@ -126,9 +140,9 @@ class Blueprint:
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||||
"""
|
"""Create a blueprint static route from a decorated function.
|
||||||
Creates a blueprint static route from a decorated function.
|
|
||||||
:param uri: Endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
:param file_or_directory: Static asset.
|
:param file_or_directory: Static asset.
|
||||||
"""
|
"""
|
||||||
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
||||||
|
|
|
@ -39,8 +39,9 @@ class Config(dict):
|
||||||
self[attr] = value
|
self[attr] = value
|
||||||
|
|
||||||
def from_envvar(self, variable_name):
|
def from_envvar(self, variable_name):
|
||||||
"""Loads a configuration from an environment variable pointing to
|
"""Load a configuration from an environment variable pointing to
|
||||||
a configuration file.
|
a configuration file.
|
||||||
|
|
||||||
:param variable_name: name of the environment variable
|
:param variable_name: name of the environment variable
|
||||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
||||||
"""
|
"""
|
||||||
|
@ -52,8 +53,9 @@ class Config(dict):
|
||||||
return self.from_pyfile(config_file)
|
return self.from_pyfile(config_file)
|
||||||
|
|
||||||
def from_pyfile(self, filename):
|
def from_pyfile(self, filename):
|
||||||
"""Updates the values in the config from a Python file. Only the uppercase
|
"""Update the values in the config from a Python file.
|
||||||
variables in that module are stored in the config.
|
Only the uppercase variables in that module are stored in the config.
|
||||||
|
|
||||||
:param filename: an absolute path to the config file
|
:param filename: an absolute path to the config file
|
||||||
"""
|
"""
|
||||||
module = types.ModuleType('config')
|
module = types.ModuleType('config')
|
||||||
|
@ -69,7 +71,7 @@ class Config(dict):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def from_object(self, obj):
|
def from_object(self, obj):
|
||||||
"""Updates the values from the given object.
|
"""Update the values from the given object.
|
||||||
Objects are usually either modules or classes.
|
Objects are usually either modules or classes.
|
||||||
|
|
||||||
Just the uppercase variables in that object are stored in the config.
|
Just the uppercase variables in that object are stored in the config.
|
||||||
|
|
|
@ -39,8 +39,7 @@ _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||||
|
|
||||||
|
|
||||||
class CookieJar(dict):
|
class CookieJar(dict):
|
||||||
"""
|
"""CookieJar dynamically writes headers as cookies are added and removed
|
||||||
CookieJar dynamically writes headers as cookies are added and removed
|
|
||||||
It gets around the limitation of one header per name by using the
|
It gets around the limitation of one header per name by using the
|
||||||
MultiHeader class to provide a unique key that encodes to Set-Cookie.
|
MultiHeader class to provide a unique key that encodes to Set-Cookie.
|
||||||
"""
|
"""
|
||||||
|
@ -75,9 +74,7 @@ class CookieJar(dict):
|
||||||
|
|
||||||
|
|
||||||
class Cookie(dict):
|
class Cookie(dict):
|
||||||
"""
|
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
|
||||||
This is a stripped down version of Morsel from SimpleCookie #gottagofast
|
|
||||||
"""
|
|
||||||
_keys = {
|
_keys = {
|
||||||
"expires": "expires",
|
"expires": "expires",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
|
@ -128,9 +125,8 @@ class Cookie(dict):
|
||||||
|
|
||||||
|
|
||||||
class MultiHeader:
|
class MultiHeader:
|
||||||
"""
|
"""String-holding object which allow us to set a header within response
|
||||||
Allows us to set a header within response that has a unique key,
|
that has a unique key, but may contain duplicate header names
|
||||||
but may contain duplicate header names
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
|
@ -35,8 +35,8 @@ class ErrorHandler:
|
||||||
self.handlers[exception] = handler
|
self.handlers[exception] = handler
|
||||||
|
|
||||||
def response(self, request, exception):
|
def response(self, request, exception):
|
||||||
"""
|
"""Fetches and executes an exception handler and returns a response
|
||||||
Fetches and executes an exception handler and returns a response object
|
object
|
||||||
|
|
||||||
:param request: Request
|
:param request: Request
|
||||||
:param exception: Exception to handle
|
:param exception: Exception to handle
|
||||||
|
@ -86,9 +86,7 @@ class ErrorHandler:
|
||||||
|
|
||||||
|
|
||||||
class ContentRangeHandler:
|
class ContentRangeHandler:
|
||||||
"""
|
"""Class responsible for parsing request header"""
|
||||||
This class is for parsing the request header
|
|
||||||
"""
|
|
||||||
__slots__ = ('start', 'end', 'size', 'total', 'headers')
|
__slots__ = ('start', 'end', 'size', 'total', 'headers')
|
||||||
|
|
||||||
def __init__(self, request, stats):
|
def __init__(self, request, stats):
|
||||||
|
|
|
@ -16,8 +16,7 @@ DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||||
|
|
||||||
|
|
||||||
class RequestParameters(dict):
|
class RequestParameters(dict):
|
||||||
"""
|
"""Hosts a dict with lists as values where get returns the first
|
||||||
Hosts a dict with lists as values where get returns the first
|
|
||||||
value of the list and getlist returns the whole shebang
|
value of the list and getlist returns the whole shebang
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -31,9 +30,7 @@ class RequestParameters(dict):
|
||||||
|
|
||||||
|
|
||||||
class Request(dict):
|
class Request(dict):
|
||||||
"""
|
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||||
Properties of an HTTP request such as URL, headers, etc.
|
|
||||||
"""
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||||
'query_string', 'body',
|
'query_string', 'body',
|
||||||
|
@ -73,8 +70,8 @@ class Request(dict):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def token(self):
|
def token(self):
|
||||||
"""
|
"""Attempt to return the auth header token.
|
||||||
Attempts to return the auth header token.
|
|
||||||
:return: token related to request
|
:return: token related to request
|
||||||
"""
|
"""
|
||||||
auth_header = self.headers.get('Authorization')
|
auth_header = self.headers.get('Authorization')
|
||||||
|
@ -146,11 +143,10 @@ File = namedtuple('File', ['type', 'body', 'name'])
|
||||||
|
|
||||||
|
|
||||||
def parse_multipart_form(body, boundary):
|
def parse_multipart_form(body, boundary):
|
||||||
"""
|
"""Parse a request body and returns fields and files
|
||||||
Parses a request body and returns fields and files
|
|
||||||
|
|
||||||
:param body: Bytes request body
|
:param body: bytes request body
|
||||||
:param boundary: Bytes multipart boundary
|
:param boundary: bytes multipart boundary
|
||||||
:return: fields (RequestParameters), files (RequestParameters)
|
:return: fields (RequestParameters), files (RequestParameters)
|
||||||
"""
|
"""
|
||||||
files = RequestParameters()
|
files = RequestParameters()
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from collections import ChainMap
|
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
from ujson import dumps as json_dumps
|
from ujson import dumps as json_dumps
|
||||||
|
@ -98,17 +97,15 @@ class HTTPResponse:
|
||||||
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||||
# This is all returned in a kind-of funky way
|
# This is all returned in a kind-of funky way
|
||||||
# We tried to make this as fast as possible in pure python
|
# We tried to make this as fast as possible in pure python
|
||||||
default_header = dict()
|
timeout_header = b''
|
||||||
if keep_alive:
|
if keep_alive and keep_alive_timeout is not None:
|
||||||
if keep_alive_timeout:
|
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
|
||||||
default_header['Keep-Alive'] = keep_alive_timeout
|
self.headers['Content-Length'] = self.headers.get(
|
||||||
default_header['Connection'] = 'keep-alive'
|
'Content-Length', len(self.body))
|
||||||
else:
|
self.headers['Content-Type'] = self.headers.get(
|
||||||
default_header['Connection'] = 'close'
|
'Content-Type', self.content_type)
|
||||||
default_header['Content-Length'] = len(self.body)
|
|
||||||
default_header['Content-Type'] = self.content_type
|
|
||||||
headers = b''
|
headers = b''
|
||||||
for name, value in ChainMap(self.headers, default_header).items():
|
for name, value in self.headers.items():
|
||||||
try:
|
try:
|
||||||
headers += (
|
headers += (
|
||||||
b'%b: %b\r\n' % (
|
b'%b: %b\r\n' % (
|
||||||
|
@ -117,6 +114,7 @@ class HTTPResponse:
|
||||||
headers += (
|
headers += (
|
||||||
b'%b: %b\r\n' % (
|
b'%b: %b\r\n' % (
|
||||||
str(name).encode(), str(value).encode('utf-8')))
|
str(name).encode(), str(value).encode('utf-8')))
|
||||||
|
|
||||||
# Try to pull from the common codes first
|
# Try to pull from the common codes first
|
||||||
# Speeds up response rate 6% over pulling from all
|
# Speeds up response rate 6% over pulling from all
|
||||||
status = COMMON_STATUS_CODES.get(self.status)
|
status = COMMON_STATUS_CODES.get(self.status)
|
||||||
|
@ -124,11 +122,15 @@ class HTTPResponse:
|
||||||
status = ALL_STATUS_CODES.get(self.status)
|
status = ALL_STATUS_CODES.get(self.status)
|
||||||
|
|
||||||
return (b'HTTP/%b %d %b\r\n'
|
return (b'HTTP/%b %d %b\r\n'
|
||||||
|
b'Connection: %b\r\n'
|
||||||
|
b'%b'
|
||||||
b'%b\r\n'
|
b'%b\r\n'
|
||||||
b'%b') % (
|
b'%b') % (
|
||||||
version.encode(),
|
version.encode(),
|
||||||
self.status,
|
self.status,
|
||||||
status,
|
status,
|
||||||
|
b'keep-alive' if keep_alive else b'close',
|
||||||
|
timeout_header,
|
||||||
headers,
|
headers,
|
||||||
self.body
|
self.body
|
||||||
)
|
)
|
||||||
|
@ -152,15 +154,32 @@ def json(body, status=200, headers=None, **kwargs):
|
||||||
status=status, content_type="application/json")
|
status=status, content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
def text(body, status=200, headers=None):
|
def text(body, status=200, headers=None,
|
||||||
|
content_type="text/plain; charset=utf-8"):
|
||||||
"""
|
"""
|
||||||
Returns response object with body in text format.
|
Returns response object with body in text format.
|
||||||
: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:
|
||||||
|
the content type (string) of the response
|
||||||
"""
|
"""
|
||||||
return HTTPResponse(body, status=status, headers=headers,
|
return HTTPResponse(body, status=status, headers=headers,
|
||||||
content_type="text/plain; charset=utf-8")
|
content_type=content_type)
|
||||||
|
|
||||||
|
|
||||||
|
def raw(body, status=200, headers=None,
|
||||||
|
content_type="application/octet-stream"):
|
||||||
|
"""
|
||||||
|
Returns response object without encoding the body.
|
||||||
|
:param body: Response data.
|
||||||
|
:param status: Response code.
|
||||||
|
:param headers: Custom Headers.
|
||||||
|
:param content_type:
|
||||||
|
the content type (string) of the response
|
||||||
|
"""
|
||||||
|
return HTTPResponse(body_bytes=body, status=status, headers=headers,
|
||||||
|
content_type=content_type)
|
||||||
|
|
||||||
|
|
||||||
def html(body, status=200, headers=None):
|
def html(body, status=200, headers=None):
|
||||||
|
@ -175,8 +194,8 @@ def html(body, status=200, headers=None):
|
||||||
|
|
||||||
|
|
||||||
async def file(location, mime_type=None, headers=None, _range=None):
|
async def file(location, mime_type=None, headers=None, _range=None):
|
||||||
"""
|
"""Return a response object with file data.
|
||||||
Returns response object with file data.
|
|
||||||
:param location: Location of file on system.
|
:param location: Location of file on system.
|
||||||
:param mime_type: Specific mime_type.
|
:param mime_type: Specific mime_type.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
|
@ -203,14 +222,12 @@ async def file(location, mime_type=None, headers=None, _range=None):
|
||||||
|
|
||||||
def redirect(to, headers=None, status=302,
|
def redirect(to, headers=None, status=302,
|
||||||
content_type="text/html; charset=utf-8"):
|
content_type="text/html; charset=utf-8"):
|
||||||
"""
|
"""Abort execution and cause a 302 redirect (by default).
|
||||||
Aborts execution and causes a 302 redirect (by default).
|
|
||||||
|
|
||||||
:param to: path or fully qualified URL to redirect to
|
:param to: path or fully qualified URL to redirect to
|
||||||
:param headers: optional dict of headers to include in the new request
|
:param headers: optional dict of headers to include in the new request
|
||||||
:param status: status code (int) of the new request, defaults to 302
|
:param status: status code (int) of the new request, defaults to 302
|
||||||
:param content_type:
|
:param content_type: the content type (string) of the response
|
||||||
the content type (string) of the response
|
|
||||||
:returns: the redirecting Response
|
:returns: the redirecting Response
|
||||||
"""
|
"""
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
|
|
|
@ -32,8 +32,7 @@ class RouteDoesNotExist(Exception):
|
||||||
|
|
||||||
|
|
||||||
class Router:
|
class Router:
|
||||||
"""
|
"""Router supports basic routing with parameters and method checks
|
||||||
Router supports basic routing with parameters and method checks
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
|
@ -71,8 +70,9 @@ class Router:
|
||||||
self.hosts = None
|
self.hosts = None
|
||||||
|
|
||||||
def parse_parameter_string(self, parameter_string):
|
def parse_parameter_string(self, parameter_string):
|
||||||
"""
|
"""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]')
|
||||||
|
@ -94,14 +94,13 @@ 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):
|
||||||
"""
|
"""Add a handler to the route list
|
||||||
Adds a handler to the route list
|
|
||||||
|
|
||||||
:param uri: Path to match
|
:param uri: path to match
|
||||||
:param methods: Array of accepted method names.
|
:param methods: sequence of accepted method names. If none are
|
||||||
If none are provided, any method is allowed
|
provided, any method is allowed
|
||||||
:param handler: Request handler function.
|
:param handler: request handler function.
|
||||||
When executed, it should provide a response object.
|
When executed, it should provide a response object.
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -239,8 +238,7 @@ class Router:
|
||||||
|
|
||||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||||
def find_route_by_view_name(self, view_name):
|
def find_route_by_view_name(self, view_name):
|
||||||
"""
|
"""Find a route in the router based on the specified view name.
|
||||||
Find a route in the router based on the specified view name.
|
|
||||||
|
|
||||||
:param view_name: string of view name to search by
|
:param view_name: string of view name to search by
|
||||||
:return: tuple containing (uri, Route)
|
:return: tuple containing (uri, Route)
|
||||||
|
@ -255,8 +253,7 @@ class Router:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""
|
"""Get a request handler based on the URL of the request, or raises an
|
||||||
Gets a request handler based on the URL of the request, or raises an
|
|
||||||
error
|
error
|
||||||
|
|
||||||
:param request: Request object
|
:param request: Request object
|
||||||
|
@ -270,11 +267,11 @@ class Router:
|
||||||
|
|
||||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||||
def _get(self, url, method, host):
|
def _get(self, url, method, host):
|
||||||
"""
|
"""Get a request handler based on the URL of the request, or raises an
|
||||||
Gets a request handler based on the URL of the request, or raises an
|
|
||||||
error. Internal method for caching.
|
error. Internal method for caching.
|
||||||
:param url: Request URL
|
|
||||||
:param method: Request method
|
:param url: request URL
|
||||||
|
:param method: request method
|
||||||
:return: handler, arguments, keyword arguments
|
:return: handler, arguments, keyword arguments
|
||||||
"""
|
"""
|
||||||
url = host + url
|
url = host + url
|
||||||
|
|
169
sanic/sanic.py
169
sanic/sanic.py
|
@ -2,7 +2,7 @@ import logging
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from asyncio import get_event_loop
|
from asyncio import get_event_loop
|
||||||
from collections import deque
|
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
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
@ -10,18 +10,19 @@ from urllib.parse import urlencode, urlunparse
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .constants import HTTP_METHODS
|
from .constants import HTTP_METHODS
|
||||||
from .handlers import ErrorHandler
|
|
||||||
from .exceptions import ServerError, URLBuildError
|
from .exceptions import ServerError, URLBuildError
|
||||||
|
from .handlers import ErrorHandler
|
||||||
from .log import log
|
from .log import log
|
||||||
from .response import HTTPResponse
|
from .response import HTTPResponse
|
||||||
from .router import Router
|
from .router import Router
|
||||||
from .server import serve, serve_multiple, HttpProtocol
|
from .server import serve, serve_multiple, HttpProtocol
|
||||||
from .static import register as static_register
|
from .static import register as static_register
|
||||||
|
from .views import CompositionView
|
||||||
|
|
||||||
|
|
||||||
class Sanic:
|
class Sanic:
|
||||||
def __init__(self, name=None, router=None,
|
|
||||||
error_handler=None):
|
def __init__(self, name=None, router=None, error_handler=None):
|
||||||
# Only set up a default log handler if the
|
# Only set up a default log handler if the
|
||||||
# end-user application didn't set anything up.
|
# end-user application didn't set anything up.
|
||||||
if not logging.root.handlers and log.level == logging.NOTSET:
|
if not logging.root.handlers and log.level == logging.NOTSET:
|
||||||
|
@ -31,9 +32,12 @@ class Sanic:
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
log.addHandler(handler)
|
log.addHandler(handler)
|
||||||
log.setLevel(logging.INFO)
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# Get name from previous stack frame
|
||||||
if name is None:
|
if name is None:
|
||||||
frame_records = stack()[1]
|
frame_records = stack()[1]
|
||||||
name = getmodulename(frame_records[1])
|
name = getmodulename(frame_records[1])
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.router = router or Router()
|
self.router = router or Router()
|
||||||
self.error_handler = error_handler or ErrorHandler()
|
self.error_handler = error_handler or ErrorHandler()
|
||||||
|
@ -44,19 +48,49 @@ class Sanic:
|
||||||
self._blueprint_order = []
|
self._blueprint_order = []
|
||||||
self.debug = None
|
self.debug = None
|
||||||
self.sock = None
|
self.sock = None
|
||||||
self.processes = None
|
self.listeners = defaultdict(list)
|
||||||
|
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loop(self):
|
||||||
|
"""Synonymous with asyncio.get_event_loop()."""
|
||||||
|
return get_event_loop()
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Registration
|
# Registration
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def add_task(self, task):
|
||||||
|
"""Schedule a task to run later, after the loop has started.
|
||||||
|
Different from asyncio.ensure_future in that it does not
|
||||||
|
also return a future, and the actual ensure_future call
|
||||||
|
is delayed until before server start.
|
||||||
|
|
||||||
|
:param task: future, couroutine or awaitable
|
||||||
|
"""
|
||||||
|
@self.listener('before_server_start')
|
||||||
|
def run(app, loop):
|
||||||
|
if callable(task):
|
||||||
|
loop.create_task(task())
|
||||||
|
else:
|
||||||
|
loop.create_task(task)
|
||||||
|
|
||||||
|
# Decorator
|
||||||
|
def listener(self, event):
|
||||||
|
"""Create a listener from a decorated function.
|
||||||
|
|
||||||
|
:param event: event to listen to
|
||||||
|
"""
|
||||||
|
def decorator(listener):
|
||||||
|
self.listeners[event].append(listener)
|
||||||
|
return listener
|
||||||
|
return decorator
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||||
"""
|
"""Decorate a function to be registered as a route
|
||||||
Decorates a function to be registered as a route
|
|
||||||
|
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
:param methods: list or tuple of methods allowed
|
:param methods: list or tuple of methods allowed
|
||||||
|
@ -99,8 +133,7 @@ class Sanic:
|
||||||
return self.route(uri, methods=frozenset({"DELETE"}), host=host)
|
return self.route(uri, methods=frozenset({"DELETE"}), host=host)
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
|
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
|
||||||
"""
|
"""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.
|
||||||
|
|
||||||
|
@ -113,7 +146,16 @@ class Sanic:
|
||||||
"""
|
"""
|
||||||
# Handle HTTPMethodView differently
|
# Handle HTTPMethodView differently
|
||||||
if hasattr(handler, 'view_class'):
|
if hasattr(handler, 'view_class'):
|
||||||
methods = frozenset(HTTP_METHODS)
|
methods = set()
|
||||||
|
|
||||||
|
for method in HTTP_METHODS:
|
||||||
|
if getattr(handler.view_class, method.lower(), None):
|
||||||
|
methods.add(method)
|
||||||
|
|
||||||
|
# handle composition view differently
|
||||||
|
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)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
@ -122,8 +164,7 @@ class Sanic:
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def exception(self, *exceptions):
|
def exception(self, *exceptions):
|
||||||
"""
|
"""Decorate a function to be registered as a handler for exceptions
|
||||||
Decorates a function to be registered as a handler for exceptions
|
|
||||||
|
|
||||||
:param exceptions: exceptions
|
:param exceptions: exceptions
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
|
@ -137,14 +178,11 @@ class Sanic:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def middleware(self, *args, **kwargs):
|
def middleware(self, middleware_or_request):
|
||||||
|
"""Decorate and register middleware to be called before a request.
|
||||||
|
Can either be called as @app.middleware or @app.middleware('request')
|
||||||
"""
|
"""
|
||||||
Decorates and registers middleware to be called before a request
|
def register_middleware(middleware, attach_to='request'):
|
||||||
can either be called as @app.middleware or @app.middleware('request')
|
|
||||||
"""
|
|
||||||
attach_to = 'request'
|
|
||||||
|
|
||||||
def register_middleware(middleware):
|
|
||||||
if attach_to == 'request':
|
if attach_to == 'request':
|
||||||
self.request_middleware.append(middleware)
|
self.request_middleware.append(middleware)
|
||||||
if attach_to == 'response':
|
if attach_to == 'response':
|
||||||
|
@ -152,25 +190,24 @@ class Sanic:
|
||||||
return middleware
|
return middleware
|
||||||
|
|
||||||
# Detect which way this was called, @middleware or @middleware('AT')
|
# Detect which way this was called, @middleware or @middleware('AT')
|
||||||
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
if callable(middleware_or_request):
|
||||||
return register_middleware(args[0])
|
return register_middleware(middleware_or_request)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
attach_to = args[0]
|
return partial(register_middleware,
|
||||||
return register_middleware
|
attach_to=middleware_or_request)
|
||||||
|
|
||||||
# Static Files
|
# Static Files
|
||||||
def static(self, uri, file_or_directory, pattern='.+',
|
def static(self, uri, file_or_directory, pattern='.+',
|
||||||
use_modified_since=True, use_content_range=False):
|
use_modified_since=True, use_content_range=False):
|
||||||
"""
|
"""Register a root to serve files from. The input can either be a
|
||||||
Registers a root to serve files from. The input can either be a file
|
file or a directory. See
|
||||||
or a directory. See
|
|
||||||
"""
|
"""
|
||||||
static_register(self, uri, file_or_directory, pattern,
|
static_register(self, uri, file_or_directory, pattern,
|
||||||
use_modified_since, use_content_range)
|
use_modified_since, use_content_range)
|
||||||
|
|
||||||
def blueprint(self, blueprint, **options):
|
def blueprint(self, blueprint, **options):
|
||||||
"""
|
"""Register a blueprint on the application.
|
||||||
Registers a blueprint on the application.
|
|
||||||
|
|
||||||
:param blueprint: Blueprint object
|
:param blueprint: Blueprint object
|
||||||
:param options: option dictionary with blueprint defaults
|
:param options: option dictionary with blueprint defaults
|
||||||
|
@ -197,7 +234,7 @@ class Sanic:
|
||||||
return self.blueprint(*args, **kwargs)
|
return self.blueprint(*args, **kwargs)
|
||||||
|
|
||||||
def url_for(self, view_name: str, **kwargs):
|
def url_for(self, view_name: str, **kwargs):
|
||||||
"""Builds a URL based on a view name and the values provided.
|
"""Build a URL based on a view name and the values provided.
|
||||||
|
|
||||||
In order to build a URL, all request parameters must be supplied as
|
In order to build a URL, all request parameters must be supplied as
|
||||||
keyword arguments, and each parameter must pass the test for the
|
keyword arguments, and each parameter must pass the test for the
|
||||||
|
@ -207,7 +244,7 @@ class Sanic:
|
||||||
Keyword arguments that are not request parameters will be included in
|
Keyword arguments that are not request parameters will be included in
|
||||||
the output URL's query string.
|
the output URL's query string.
|
||||||
|
|
||||||
:param view_name: A 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.
|
||||||
|
|
||||||
|
@ -230,6 +267,19 @@ class Sanic:
|
||||||
matched_params = re.findall(
|
matched_params = re.findall(
|
||||||
self.router.parameter_pattern, uri)
|
self.router.parameter_pattern, uri)
|
||||||
|
|
||||||
|
# _method is only a placeholder now, don't know how to support it
|
||||||
|
kwargs.pop('_method', None)
|
||||||
|
anchor = kwargs.pop('_anchor', '')
|
||||||
|
# _external need SERVER_NAME in config or pass _server arg
|
||||||
|
external = kwargs.pop('_external', False)
|
||||||
|
scheme = kwargs.pop('_scheme', '')
|
||||||
|
if scheme and not external:
|
||||||
|
raise ValueError('When specifying _scheme, _external must be True')
|
||||||
|
|
||||||
|
netloc = kwargs.pop('_server', None)
|
||||||
|
if netloc is None and external:
|
||||||
|
netloc = self.config.get('SERVER_NAME', '')
|
||||||
|
|
||||||
for match in matched_params:
|
for match in matched_params:
|
||||||
name, _type, pattern = self.router.parse_parameter_string(
|
name, _type, pattern = self.router.parse_parameter_string(
|
||||||
match)
|
match)
|
||||||
|
@ -270,12 +320,9 @@ class Sanic:
|
||||||
replacement_regex, supplied_param, out)
|
replacement_regex, supplied_param, out)
|
||||||
|
|
||||||
# parse the remainder of the keyword arguments into a querystring
|
# parse the remainder of the keyword arguments into a querystring
|
||||||
if kwargs:
|
query_string = urlencode(kwargs, doseq=True) if kwargs else ''
|
||||||
query_string = urlencode(kwargs)
|
# scheme://netloc/path;parameters?query#fragment
|
||||||
out = urlunparse((
|
out = urlunparse((scheme, netloc, out, '', query_string, anchor))
|
||||||
'', '', out,
|
|
||||||
'', query_string, ''
|
|
||||||
))
|
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@ -287,9 +334,8 @@ class Sanic:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def handle_request(self, request, response_callback):
|
async def handle_request(self, request, response_callback):
|
||||||
"""
|
"""Take a request from the HTTP Server and return a response object
|
||||||
Takes a request from the HTTP Server and returns a response object to
|
to be sent back The HTTP Server only expects a response object, so
|
||||||
be sent back The HTTP Server only expects a response object, so
|
|
||||||
exception handling must be done here
|
exception handling must be done here
|
||||||
|
|
||||||
:param request: HTTP Request object
|
:param request: HTTP Request object
|
||||||
|
@ -371,9 +417,8 @@ class Sanic:
|
||||||
after_start=None, before_stop=None, after_stop=None, ssl=None,
|
after_start=None, before_stop=None, after_stop=None, ssl=None,
|
||||||
sock=None, workers=1, loop=None, protocol=HttpProtocol,
|
sock=None, workers=1, loop=None, protocol=HttpProtocol,
|
||||||
backlog=100, stop_event=None, register_sys_signals=True):
|
backlog=100, stop_event=None, register_sys_signals=True):
|
||||||
"""
|
"""Run the HTTP Server and listen until keyboard interrupt or term
|
||||||
Runs the HTTP Server and listens until keyboard interrupt or term
|
signal. On termination, drain connections before closing.
|
||||||
signal. On termination, drains connections before closing.
|
|
||||||
|
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
:param port: Port to host on
|
:param port: Port to host on
|
||||||
|
@ -403,6 +448,7 @@ class Sanic:
|
||||||
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
|
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
|
||||||
loop=loop, protocol=protocol, backlog=backlog,
|
loop=loop, protocol=protocol, backlog=backlog,
|
||||||
stop_event=stop_event, register_sys_signals=register_sys_signals)
|
stop_event=stop_event, register_sys_signals=register_sys_signals)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if workers == 1:
|
if workers == 1:
|
||||||
serve(**server_settings)
|
serve(**server_settings)
|
||||||
|
@ -422,9 +468,7 @@ class Sanic:
|
||||||
before_stop=None, after_stop=None, ssl=None,
|
before_stop=None, after_stop=None, ssl=None,
|
||||||
sock=None, loop=None, protocol=HttpProtocol,
|
sock=None, loop=None, protocol=HttpProtocol,
|
||||||
backlog=100, stop_event=None):
|
backlog=100, stop_event=None):
|
||||||
"""
|
"""Asynchronous version of `run`."""
|
||||||
Asynchronous version of `run`.
|
|
||||||
"""
|
|
||||||
server_settings = self._helper(
|
server_settings = self._helper(
|
||||||
host=host, port=port, debug=debug, before_start=before_start,
|
host=host, port=port, debug=debug, before_start=before_start,
|
||||||
after_start=after_start, before_stop=before_stop,
|
after_start=after_start, before_stop=before_stop,
|
||||||
|
@ -445,9 +489,7 @@ class Sanic:
|
||||||
after_stop=None, ssl=None, sock=None, workers=1, loop=None,
|
after_stop=None, ssl=None, sock=None, workers=1, loop=None,
|
||||||
protocol=HttpProtocol, backlog=100, stop_event=None,
|
protocol=HttpProtocol, backlog=100, stop_event=None,
|
||||||
register_sys_signals=True, run_async=False):
|
register_sys_signals=True, run_async=False):
|
||||||
"""
|
"""Helper function used by `run` and `create_server`."""
|
||||||
Helper function used by `run` and `create_server`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if loop is not None:
|
if loop is not None:
|
||||||
if debug:
|
if debug:
|
||||||
|
@ -457,9 +499,19 @@ class Sanic:
|
||||||
"pull/335 has more information.",
|
"pull/335 has more information.",
|
||||||
DeprecationWarning)
|
DeprecationWarning)
|
||||||
|
|
||||||
|
# Deprecate this
|
||||||
|
if any(arg is not None for arg in (after_stop, after_start,
|
||||||
|
before_start, before_stop)):
|
||||||
|
if debug:
|
||||||
|
warnings.simplefilter('default')
|
||||||
|
warnings.warn("Passing a before_start, before_stop, after_start or"
|
||||||
|
"after_stop callback will be deprecated in next "
|
||||||
|
"major version after 0.4.0",
|
||||||
|
DeprecationWarning)
|
||||||
|
|
||||||
self.error_handler.debug = debug
|
self.error_handler.debug = debug
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.loop = loop = get_event_loop()
|
loop = self.loop
|
||||||
|
|
||||||
server_settings = {
|
server_settings = {
|
||||||
'protocol': protocol,
|
'protocol': protocol,
|
||||||
|
@ -481,19 +533,18 @@ class Sanic:
|
||||||
# Register start/stop events
|
# Register start/stop events
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
|
||||||
for event_name, settings_name, args, reverse in (
|
for event_name, settings_name, reverse, args in (
|
||||||
("before_server_start", "before_start", before_start, False),
|
("before_server_start", "before_start", False, before_start),
|
||||||
("after_server_start", "after_start", after_start, False),
|
("after_server_start", "after_start", False, after_start),
|
||||||
("before_server_stop", "before_stop", before_stop, True),
|
("before_server_stop", "before_stop", True, before_stop),
|
||||||
("after_server_stop", "after_stop", after_stop, True),
|
("after_server_stop", "after_stop", True, after_stop),
|
||||||
):
|
):
|
||||||
listeners = []
|
listeners = self.listeners[event_name].copy()
|
||||||
for blueprint in self.blueprints.values():
|
|
||||||
listeners += blueprint.listeners[event_name]
|
|
||||||
if args:
|
if args:
|
||||||
if callable(args):
|
if callable(args):
|
||||||
args = [args]
|
listeners.append(args)
|
||||||
listeners += args
|
else:
|
||||||
|
listeners.extend(args)
|
||||||
if reverse:
|
if reverse:
|
||||||
listeners.reverse()
|
listeners.reverse()
|
||||||
# Prepend sanic to the arguments when listeners are triggered
|
# Prepend sanic to the arguments when listeners are triggered
|
||||||
|
|
|
@ -33,8 +33,7 @@ class Signal:
|
||||||
|
|
||||||
|
|
||||||
class CIDict(dict):
|
class CIDict(dict):
|
||||||
"""
|
"""Case Insensitive dict where all keys are converted to lowercase
|
||||||
Case Insensitive dict where all keys are converted to lowercase
|
|
||||||
This does not maintain the inputted case when calling items() or keys()
|
This does not maintain the inputted case when calling items() or keys()
|
||||||
in favor of speed, since headers are case insensitive
|
in favor of speed, since headers are case insensitive
|
||||||
"""
|
"""
|
||||||
|
@ -228,8 +227,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self._total_request_size = 0
|
self._total_request_size = 0
|
||||||
|
|
||||||
def close_if_idle(self):
|
def close_if_idle(self):
|
||||||
"""
|
"""Close the connection if a request is not being sent or received
|
||||||
Close the connection if a request is not being sent or received
|
|
||||||
:return: boolean - True if closed, false if staying open
|
:return: boolean - True if closed, false if staying open
|
||||||
"""
|
"""
|
||||||
if not self.parser:
|
if not self.parser:
|
||||||
|
@ -239,9 +238,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
|
|
||||||
|
|
||||||
def update_current_time(loop):
|
def update_current_time(loop):
|
||||||
"""
|
"""Cache the current time, since it is needed at the end of every
|
||||||
Caches the current time, since it is needed
|
keep-alive request to update the request timeout time
|
||||||
at the end of every keep-alive request to update the request timeout time
|
|
||||||
|
|
||||||
:param loop:
|
:param loop:
|
||||||
:return:
|
:return:
|
||||||
|
@ -252,17 +250,15 @@ def update_current_time(loop):
|
||||||
|
|
||||||
|
|
||||||
def trigger_events(events, loop):
|
def trigger_events(events, loop):
|
||||||
"""
|
"""Trigger event callbacks (functions or async)
|
||||||
|
|
||||||
:param events: one or more sync or async functions to execute
|
:param events: one or more sync or async functions to execute
|
||||||
:param loop: event loop
|
:param loop: event loop
|
||||||
"""
|
"""
|
||||||
if events:
|
for event in events:
|
||||||
if not isinstance(events, list):
|
result = event(loop)
|
||||||
events = [events]
|
if isawaitable(result):
|
||||||
for event in events:
|
loop.run_until_complete(result)
|
||||||
result = event(loop)
|
|
||||||
if isawaitable(result):
|
|
||||||
loop.run_until_complete(result)
|
|
||||||
|
|
||||||
|
|
||||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
|
@ -270,31 +266,30 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
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):
|
||||||
"""
|
"""Start asynchronous HTTP Server on an individual process.
|
||||||
Starts asynchronous HTTP Server on an individual process.
|
|
||||||
|
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
:param port: Port to host on
|
:param port: Port to host on
|
||||||
:param request_handler: Sanic request handler with middleware
|
:param request_handler: Sanic request handler with middleware
|
||||||
:param error_handler: Sanic error handler with middleware
|
:param error_handler: Sanic error handler with middleware
|
||||||
:param before_start: Function to be executed before the server starts
|
:param before_start: function to be executed before the server starts
|
||||||
listening. Takes arguments `app` instance and `loop`
|
listening. Takes arguments `app` instance and `loop`
|
||||||
:param after_start: Function to be executed after the server starts
|
:param after_start: function to be executed after the server starts
|
||||||
listening. Takes arguments `app` instance and `loop`
|
listening. Takes arguments `app` instance and `loop`
|
||||||
:param before_stop: Function to be executed when a stop signal is
|
:param before_stop: function to be executed when a stop signal is
|
||||||
received before it is respected. Takes arguments
|
received before it is respected. Takes arguments
|
||||||
`app` instance and `loop`
|
`app` instance and `loop`
|
||||||
:param after_stop: Function to be executed when a stop signal is
|
:param after_stop: function to be executed when a stop signal is
|
||||||
received after it is respected. Takes arguments
|
received after it is respected. Takes arguments
|
||||||
`app` instance and `loop`
|
`app` instance and `loop`
|
||||||
:param debug: Enables debug output (slows server)
|
:param debug: enables debug output (slows server)
|
||||||
:param request_timeout: time in seconds
|
:param request_timeout: time in seconds
|
||||||
:param ssl: SSLContext
|
:param ssl: SSLContext
|
||||||
:param sock: Socket for the server to accept connections from
|
:param sock: Socket for the server to accept connections from
|
||||||
:param request_max_size: size in bytes, `None` for no limit
|
:param request_max_size: size in bytes, `None` for no limit
|
||||||
:param reuse_port: `True` for multiple workers
|
:param reuse_port: `True` for multiple workers
|
||||||
:param loop: asyncio compatible event loop
|
:param loop: asyncio compatible event loop
|
||||||
:param protocol: Subclass of asyncio protocol class
|
:param protocol: subclass of asyncio protocol class
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if not run_async:
|
if not run_async:
|
||||||
|
@ -349,9 +344,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
try:
|
try:
|
||||||
loop.add_signal_handler(_signal, loop.stop)
|
loop.add_signal_handler(_signal, loop.stop)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
log.warn(('Sanic tried to use loop.add_signal_handler')
|
log.warn('Sanic tried to use loop.add_signal_handler but it is'
|
||||||
('but it is not implemented on this platform.'))
|
' not implemented on this platform.')
|
||||||
|
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
try:
|
try:
|
||||||
log.info('Starting worker [{}]'.format(pid))
|
log.info('Starting worker [{}]'.format(pid))
|
||||||
|
@ -380,9 +374,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
|
|
||||||
|
|
||||||
def serve_multiple(server_settings, workers, stop_event=None):
|
def serve_multiple(server_settings, workers, stop_event=None):
|
||||||
"""
|
"""Start multiple server processes simultaneously. Stop on interrupt
|
||||||
Starts multiple server processes simultaneously. Stops on interrupt
|
and terminate signals, and drain connections when complete.
|
||||||
and terminate signals, and drains connections when complete.
|
|
||||||
|
|
||||||
:param server_settings: kw arguments to be passed to the serve function
|
:param server_settings: kw arguments to be passed to the serve function
|
||||||
:param workers: number of workers to launch
|
:param workers: number of workers to launch
|
||||||
|
|
|
@ -18,7 +18,7 @@ def register(app, uri, file_or_directory, pattern,
|
||||||
# make a good effort here. Modified-since is nice, but we could
|
# make a good effort here. Modified-since is nice, but we could
|
||||||
# also look into etags, expires, and caching
|
# also look into etags, expires, and caching
|
||||||
"""
|
"""
|
||||||
Registers a static directory handler with Sanic by adding a route to the
|
Register a static directory handler with Sanic by adding a route to the
|
||||||
router and registering a handler.
|
router and registering a handler.
|
||||||
|
|
||||||
:param app: Sanic
|
:param app: Sanic
|
||||||
|
|
|
@ -6,7 +6,11 @@ PORT = 42101
|
||||||
|
|
||||||
|
|
||||||
async def local_request(method, uri, cookies=None, *args, **kwargs):
|
async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||||
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
|
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
|
||||||
|
url = uri
|
||||||
|
else:
|
||||||
|
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
|
||||||
|
|
||||||
log.info(url)
|
log.info(url)
|
||||||
async with aiohttp.ClientSession(cookies=cookies) as session:
|
async with aiohttp.ClientSession(cookies=cookies) as session:
|
||||||
async with getattr(
|
async with getattr(
|
||||||
|
@ -17,8 +21,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
debug=False, server_kwargs={},
|
debug=False, server_kwargs={}, *request_args,
|
||||||
*request_args, **request_kwargs):
|
**request_kwargs):
|
||||||
results = [None, None]
|
results = [None, None]
|
||||||
exceptions = []
|
exceptions = []
|
||||||
|
|
||||||
|
@ -28,6 +32,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
results[0] = request
|
results[0] = request
|
||||||
app.request_middleware.appendleft(_collect_request)
|
app.request_middleware.appendleft(_collect_request)
|
||||||
|
|
||||||
|
@app.listener('after_server_start')
|
||||||
async def _collect_response(sanic, loop):
|
async def _collect_response(sanic, loop):
|
||||||
try:
|
try:
|
||||||
response = await local_request(method, uri, *request_args,
|
response = await local_request(method, uri, *request_args,
|
||||||
|
@ -37,8 +42,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
exceptions.append(e)
|
exceptions.append(e)
|
||||||
app.stop()
|
app.stop()
|
||||||
|
|
||||||
app.run(host=HOST, debug=debug, port=PORT,
|
app.run(host=HOST, debug=debug, port=PORT, **server_kwargs)
|
||||||
after_start=_collect_response, **server_kwargs)
|
app.listeners['after_server_start'].pop()
|
||||||
|
|
||||||
if exceptions:
|
if exceptions:
|
||||||
raise ValueError("Exception during request: {}".format(exceptions))
|
raise ValueError("Exception during request: {}".format(exceptions))
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from .exceptions import InvalidUsage
|
from .exceptions import InvalidUsage
|
||||||
|
from .constants import HTTP_METHODS
|
||||||
|
|
||||||
|
|
||||||
class HTTPMethodView:
|
class HTTPMethodView:
|
||||||
""" Simple class based implementation of view for the sanic.
|
"""Simple class based implementation of view for the sanic.
|
||||||
You should implement methods (get, post, put, patch, delete) for the class
|
You should implement methods (get, post, put, patch, delete) for the class
|
||||||
to every HTTP method you want to support.
|
to every HTTP method you want to support.
|
||||||
|
|
||||||
|
@ -40,17 +41,12 @@ class HTTPMethodView:
|
||||||
|
|
||||||
def dispatch_request(self, request, *args, **kwargs):
|
def dispatch_request(self, request, *args, **kwargs):
|
||||||
handler = getattr(self, request.method.lower(), None)
|
handler = getattr(self, request.method.lower(), None)
|
||||||
if handler:
|
return handler(request, *args, **kwargs)
|
||||||
return handler(request, *args, **kwargs)
|
|
||||||
raise InvalidUsage(
|
|
||||||
'Method {} not allowed for URL {}'.format(
|
|
||||||
request.method, request.url), status_code=405)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, *class_args, **class_kwargs):
|
def as_view(cls, *class_args, **class_kwargs):
|
||||||
""" Converts the class into an actual view function that can be used
|
"""Return view function for use with the routing system, that
|
||||||
with the routing system.
|
dispatches request to appropriate handler method.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def view(*args, **kwargs):
|
def view(*args, **kwargs):
|
||||||
self = view.view_class(*class_args, **class_kwargs)
|
self = view.view_class(*class_args, **class_kwargs)
|
||||||
|
@ -69,7 +65,7 @@ class HTTPMethodView:
|
||||||
|
|
||||||
|
|
||||||
class CompositionView:
|
class CompositionView:
|
||||||
""" Simple method-function mapped view for the sanic.
|
"""Simple method-function mapped view for the sanic.
|
||||||
You can add handler functions to methods (get, post, put, patch, delete)
|
You can add handler functions to methods (get, post, put, patch, delete)
|
||||||
for every HTTP method you want to support.
|
for every HTTP method you want to support.
|
||||||
|
|
||||||
|
@ -89,15 +85,15 @@ class CompositionView:
|
||||||
|
|
||||||
def add(self, methods, handler):
|
def add(self, methods, handler):
|
||||||
for method in methods:
|
for method in methods:
|
||||||
|
if method not in HTTP_METHODS:
|
||||||
|
raise InvalidUsage(
|
||||||
|
'{} is not a valid HTTP method.'.format(method))
|
||||||
|
|
||||||
if method in self.handlers:
|
if method in self.handlers:
|
||||||
raise KeyError(
|
raise InvalidUsage(
|
||||||
'Method {} already is registered.'.format(method))
|
'Method {} is already registered.'.format(method))
|
||||||
self.handlers[method] = handler
|
self.handlers[method] = handler
|
||||||
|
|
||||||
def __call__(self, request, *args, **kwargs):
|
def __call__(self, request, *args, **kwargs):
|
||||||
handler = self.handlers.get(request.method.upper(), None)
|
handler = self.handlers[request.method.upper()]
|
||||||
if handler is None:
|
|
||||||
raise InvalidUsage(
|
|
||||||
'Method {} not allowed for URL {}'.format(
|
|
||||||
request.method, request.url), status_code=405)
|
|
||||||
return handler(request, *args, **kwargs)
|
return handler(request, *args, **kwargs)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from sanic import Sanic
|
||||||
def test_bad_request_response():
|
def test_bad_request_response():
|
||||||
app = Sanic('test_bad_request_response')
|
app = Sanic('test_bad_request_response')
|
||||||
lines = []
|
lines = []
|
||||||
|
@app.listener('after_server_start')
|
||||||
async def _request(sanic, loop):
|
async def _request(sanic, loop):
|
||||||
connect = asyncio.open_connection('127.0.0.1', 42101)
|
connect = asyncio.open_connection('127.0.0.1', 42101)
|
||||||
reader, writer = await connect
|
reader, writer = await connect
|
||||||
|
@ -15,6 +16,6 @@ def test_bad_request_response():
|
||||||
break
|
break
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
app.stop()
|
app.stop()
|
||||||
app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request)
|
app.run(host='127.0.0.1', port=42101, debug=False)
|
||||||
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
|
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
|
||||||
assert lines[-1] == b'Error: Bad Request'
|
assert lines[-1] == b'Error: Bad Request'
|
||||||
|
|
30
tests/test_create_task.py
Normal file
30
tests/test_create_task.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import sanic
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
from sanic.response import text
|
||||||
|
from threading import Event
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
def test_create_task():
|
||||||
|
e = Event()
|
||||||
|
async def coro():
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
e.set()
|
||||||
|
|
||||||
|
app = sanic.Sanic()
|
||||||
|
app.add_task(coro)
|
||||||
|
|
||||||
|
@app.route('/early')
|
||||||
|
def not_set(request):
|
||||||
|
return text(e.is_set())
|
||||||
|
|
||||||
|
@app.route('/late')
|
||||||
|
async def set(request):
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
return text(e.is_set())
|
||||||
|
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/early')
|
||||||
|
assert response.body == b'False'
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/late')
|
||||||
|
assert response.body == b'True'
|
|
@ -9,10 +9,10 @@ from sanic import Sanic
|
||||||
from sanic.utils import HOST, PORT
|
from sanic.utils import HOST, PORT
|
||||||
|
|
||||||
AVAILABLE_LISTENERS = [
|
AVAILABLE_LISTENERS = [
|
||||||
'before_start',
|
'before_server_start',
|
||||||
'after_start',
|
'after_server_start',
|
||||||
'before_stop',
|
'before_server_stop',
|
||||||
'after_stop'
|
'after_server_stop'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,9 +42,10 @@ def test_single_listener(listener_name):
|
||||||
random_name_app = Sanic(''.join(
|
random_name_app = Sanic(''.join(
|
||||||
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||||
output = list()
|
output = list()
|
||||||
start_stop_app(
|
# Register listener
|
||||||
random_name_app,
|
random_name_app.listener(listener_name)(
|
||||||
**{listener_name: create_listener(listener_name, output)})
|
create_listener(listener_name, output))
|
||||||
|
start_stop_app(random_name_app)
|
||||||
assert random_name_app.name + listener_name == output.pop()
|
assert random_name_app.name + listener_name == output.pop()
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,9 +53,9 @@ def test_all_listeners():
|
||||||
random_name_app = Sanic(''.join(
|
random_name_app = Sanic(''.join(
|
||||||
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||||
output = list()
|
output = list()
|
||||||
start_stop_app(
|
for listener_name in AVAILABLE_LISTENERS:
|
||||||
random_name_app,
|
listener = create_listener(listener_name, output)
|
||||||
**{listener_name: create_listener(listener_name, output)
|
random_name_app.listener(listener_name)(listener)
|
||||||
for listener_name in AVAILABLE_LISTENERS})
|
start_stop_app(random_name_app)
|
||||||
for listener_name in AVAILABLE_LISTENERS:
|
for listener_name in AVAILABLE_LISTENERS:
|
||||||
assert random_name_app.name + listener_name == output.pop()
|
assert random_name_app.name + listener_name == output.pop()
|
||||||
|
|
|
@ -27,10 +27,11 @@ def test_register_system_signals():
|
||||||
async def hello_route(request):
|
async def hello_route(request):
|
||||||
return HTTPResponse()
|
return HTTPResponse()
|
||||||
|
|
||||||
app.run(HOST, PORT,
|
app.listener('after_server_start')(stop)
|
||||||
before_start=set_loop,
|
app.listener('before_server_start')(set_loop)
|
||||||
after_start=stop,
|
app.listener('after_server_stop')(after)
|
||||||
after_stop=after)
|
|
||||||
|
app.run(HOST, PORT)
|
||||||
assert calledq.get() == True
|
assert calledq.get() == True
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,9 +43,9 @@ def test_dont_register_system_signals():
|
||||||
async def hello_route(request):
|
async def hello_route(request):
|
||||||
return HTTPResponse()
|
return HTTPResponse()
|
||||||
|
|
||||||
app.run(HOST, PORT,
|
app.listener('after_server_start')(stop)
|
||||||
before_start=set_loop,
|
app.listener('before_server_start')(set_loop)
|
||||||
after_start=stop,
|
app.listener('after_server_stop')(after)
|
||||||
after_stop=after,
|
|
||||||
register_sys_signals=False)
|
app.run(HOST, PORT, register_sys_signals=False)
|
||||||
assert calledq.get() == False
|
assert calledq.get() == False
|
||||||
|
|
|
@ -5,11 +5,19 @@ from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test, PORT as test_port
|
||||||
from sanic.exceptions import URLBuildError
|
from sanic.exceptions import URLBuildError
|
||||||
|
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
URL_FOR_ARGS1 = dict(arg1=['v1', 'v2'])
|
||||||
|
URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2'
|
||||||
|
URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
|
||||||
|
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
|
||||||
|
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
|
||||||
|
_server='localhost:{}'.format(test_port), _external=True)
|
||||||
|
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port)
|
||||||
|
|
||||||
|
|
||||||
def _generate_handlers_from_names(app, l):
|
def _generate_handlers_from_names(app, l):
|
||||||
for name in l:
|
for name in l:
|
||||||
|
@ -39,6 +47,23 @@ def test_simple_url_for_getting(simple_app):
|
||||||
assert response.text == letter
|
assert response.text == letter
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('args,url',
|
||||||
|
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
|
||||||
|
(URL_FOR_ARGS2, URL_FOR_VALUE2),
|
||||||
|
(URL_FOR_ARGS3, URL_FOR_VALUE3)])
|
||||||
|
def test_simple_url_for_getting_with_more_params(args, url):
|
||||||
|
app = Sanic('more_url_build')
|
||||||
|
|
||||||
|
@app.route('/myurl')
|
||||||
|
def passes(request):
|
||||||
|
return text('this should pass')
|
||||||
|
|
||||||
|
assert url == app.url_for('passes', **args)
|
||||||
|
request, response = sanic_endpoint_test(app, uri=url)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == 'this should pass'
|
||||||
|
|
||||||
|
|
||||||
def test_fails_if_endpoint_not_found():
|
def test_fails_if_endpoint_not_found():
|
||||||
app = Sanic('fail_url_build')
|
app = Sanic('fail_url_build')
|
||||||
|
|
||||||
|
@ -75,6 +100,19 @@ def test_fails_url_build_if_param_not_passed():
|
||||||
assert 'Required parameter `Z` was not passed to url_for' in str(e.value)
|
assert 'Required parameter `Z` was not passed to url_for' in str(e.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fails_url_build_if_params_not_passed():
|
||||||
|
app = Sanic('fail_url_build')
|
||||||
|
|
||||||
|
@app.route('/fail')
|
||||||
|
def fail():
|
||||||
|
return text('this should fail')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as e:
|
||||||
|
app.url_for('fail', _scheme='http')
|
||||||
|
|
||||||
|
assert str(e.value) == 'When specifying _scheme, _external must be True'
|
||||||
|
|
||||||
|
|
||||||
COMPLEX_PARAM_URL = (
|
COMPLEX_PARAM_URL = (
|
||||||
'/<foo:int>/<four_letter_string:[A-z]{4}>/'
|
'/<foo:int>/<four_letter_string:[A-z]{4}>/'
|
||||||
'<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>')
|
'<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>')
|
||||||
|
@ -179,11 +217,11 @@ def blueprint_app():
|
||||||
return text(
|
return text(
|
||||||
'foo from first : {}'.format(param))
|
'foo from first : {}'.format(param))
|
||||||
|
|
||||||
@second_print.route('/foo') # noqa
|
@second_print.route('/foo') # noqa
|
||||||
def foo():
|
def foo():
|
||||||
return text('foo from second')
|
return text('foo from second')
|
||||||
|
|
||||||
@second_print.route('/foo/<param>') # noqa
|
@second_print.route('/foo/<param>') # noqa
|
||||||
def foo_with_param(request, param):
|
def foo_with_param(request, param):
|
||||||
return text(
|
return text(
|
||||||
'foo from second : {}'.format(param))
|
'foo from second : {}'.format(param))
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import pytest as pytest
|
import pytest as pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
from sanic.exceptions import InvalidUsage
|
||||||
from sanic.response import text, HTTPResponse
|
from sanic.response import text, HTTPResponse
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView, CompositionView
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
@ -196,3 +197,65 @@ def test_with_decorator():
|
||||||
request, response = sanic_endpoint_test(app, method="get")
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
assert response.text == 'I am get method'
|
assert response.text == 'I am get method'
|
||||||
assert results[0] == 1
|
assert results[0] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_composition_view_rejects_incorrect_methods():
|
||||||
|
def foo(request):
|
||||||
|
return text('Foo')
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidUsage) as e:
|
||||||
|
view.add(['GET', 'FOO'], foo)
|
||||||
|
|
||||||
|
assert str(e.value) == 'FOO is not a valid HTTP method.'
|
||||||
|
|
||||||
|
|
||||||
|
def test_composition_view_rejects_duplicate_methods():
|
||||||
|
def foo(request):
|
||||||
|
return text('Foo')
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidUsage) as e:
|
||||||
|
view.add(['GET', 'POST', 'GET'], foo)
|
||||||
|
|
||||||
|
assert str(e.value) == 'Method GET is already registered.'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||||
|
def test_composition_view_runs_methods_as_expected(method):
|
||||||
|
app = Sanic('test_composition_view')
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
|
||||||
|
view.add(['DELETE', 'PATCH'], lambda x: text('second method'))
|
||||||
|
|
||||||
|
app.add_route(view, '/')
|
||||||
|
|
||||||
|
if method in ['GET', 'POST', 'PUT']:
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/', method=method)
|
||||||
|
assert response.text == 'first method'
|
||||||
|
|
||||||
|
if method in ['DELETE', 'PATCH']:
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/', method=method)
|
||||||
|
assert response.text == 'second method'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||||
|
def test_composition_view_rejects_invalid_methods(method):
|
||||||
|
app = Sanic('test_composition_view')
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
|
||||||
|
|
||||||
|
app.add_route(view, '/')
|
||||||
|
|
||||||
|
if method in ['GET', 'POST', 'PUT']:
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/', method=method)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == 'first method'
|
||||||
|
|
||||||
|
if method in ['DELETE', 'PATCH']:
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/', method=method)
|
||||||
|
assert response.status == 405
|
||||||
|
|
Loading…
Reference in New Issue
Block a user