Merge branch 'master' into improved_config
This commit is contained in:
commit
5bba3388a0
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -11,3 +11,5 @@ settings.py
|
|||
.idea/*
|
||||
.cache/*
|
||||
.python-version
|
||||
docs/_build/
|
||||
docs/_api/
|
98
README.md
98
README.md
|
@ -1,98 +0,0 @@
|
|||
# Sanic
|
||||
|
||||
[![Join the chat at https://gitter.im/sanic-python/Lobby](https://badges.gitter.im/sanic-python/Lobby.svg)](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
|
||||
[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/)
|
||||
[![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/)
|
||||
|
||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/.
|
||||
|
||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
All tests were run on an AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for Falcon and Flask but did not speed up requests.
|
||||
|
||||
|
||||
|
||||
| Server | Implementation | Requests/sec | Avg Latency |
|
||||
| ------- | ------------------- | ------------:| -----------:|
|
||||
| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms |
|
||||
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
|
||||
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
|
||||
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
|
||||
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
|
||||
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
|
||||
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
|
||||
| Tornado | Python 3.5 | 2,138 | 46.66ms |
|
||||
|
||||
## Hello World
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
```
|
||||
|
||||
## Installation
|
||||
* `python -m pip install sanic`
|
||||
|
||||
## Documentation
|
||||
* [Getting started](docs/getting_started.md)
|
||||
* [Request Data](docs/request_data.md)
|
||||
* [Routing](docs/routing.md)
|
||||
* [Middleware](docs/middleware.md)
|
||||
* [Exceptions](docs/exceptions.md)
|
||||
* [Blueprints](docs/blueprints.md)
|
||||
* [Class Based Views](docs/class_based_views.md)
|
||||
* [Cookies](docs/cookies.md)
|
||||
* [Static Files](docs/static_files.md)
|
||||
* [Configuration](docs/config.md)
|
||||
* [Custom Protocol](docs/custom_protocol.md)
|
||||
* [Testing](docs/testing.md)
|
||||
* [Deploying](docs/deploying.md)
|
||||
* [Contributing](docs/contributing.md)
|
||||
* [License](LICENSE)
|
||||
|
||||
## TODO:
|
||||
* Streamed file processing
|
||||
* File output
|
||||
* Examples of integrations with 3rd-party modules
|
||||
* RESTful router
|
||||
|
||||
## Limitations:
|
||||
* No wheels for uvloop and httptools on Windows :(
|
||||
|
||||
## Final Thoughts:
|
||||
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
127
README.rst
Normal file
127
README.rst
Normal file
|
@ -0,0 +1,127 @@
|
|||
Sanic
|
||||
=================================
|
||||
|
||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
|
||||
|
||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
|
||||
|
||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
|
||||
Benchmarks
|
||||
----------
|
||||
|
||||
All tests were run on an AWS medium instance running ubuntu, using 1
|
||||
process. Each script delivered a small JSON response and was tested with
|
||||
wrk using 100 connections. Pypy was tested for Falcon and Flask but did
|
||||
not speed up requests.
|
||||
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Server | Implementation | Requests/sec | Avg Latency |
|
||||
+===========+=======================+================+===============+
|
||||
| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Tornado | Python 3.5 | 2,138 | 46.66ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
|
||||
Hello World Example
|
||||
-------------------
|
||||
|
||||
.. code:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
SSL Example
|
||||
-----------
|
||||
|
||||
Optionally pass in an SSLContext:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import ssl
|
||||
certificate = "/path/to/certificate"
|
||||
keyfile = "/path/to/keyfile"
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(certificate, keyfile=keyfile)
|
||||
|
||||
app.run(host="0.0.0.0", port=8443, ssl=context)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
- ``python -m pip install sanic``
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation can be found in the ``docs`` directory.
|
||||
|
||||
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
|
||||
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master
|
||||
:target: https://travis-ci.org/channelcat/sanic
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
|
||||
TODO
|
||||
----
|
||||
* Streamed file processing
|
||||
* File output
|
||||
* Examples of integrations with 3rd-party modules
|
||||
* RESTful router
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
* No wheels for uvloop and httptools on Windows :(
|
||||
|
||||
Final Thoughts
|
||||
--------------
|
||||
|
||||
::
|
||||
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
|
@ -1,25 +1,20 @@
|
|||
# Blueprints
|
||||
|
||||
Blueprints are objects that can be used for sub-routing within an application.
|
||||
Instead of adding routes to the application object, blueprints define similar
|
||||
Instead of adding routes to the application instance, blueprints define similar
|
||||
methods for adding routes, which are then registered with the application in a
|
||||
flexible and pluggable manner.
|
||||
|
||||
## Why?
|
||||
|
||||
Blueprints are especially useful for larger applications, where your application
|
||||
logic can be broken down into several groups or areas of responsibility.
|
||||
|
||||
It is also useful for API versioning, where one blueprint may point at
|
||||
`/v1/<routes>`, and another pointing at `/v2/<routes>`.
|
||||
|
||||
Blueprints are especially useful for larger applications, where your
|
||||
application logic can be broken down into several groups or areas of
|
||||
responsibility.
|
||||
|
||||
## My First Blueprint
|
||||
|
||||
The following shows a very simple blueprint that registers a handler-function at
|
||||
the root `/` of your application.
|
||||
|
||||
Suppose you save this file as `my_blueprint.py`, this can be imported in your
|
||||
Suppose you save this file as `my_blueprint.py`, which can be imported into your
|
||||
main application later.
|
||||
|
||||
```python
|
||||
|
@ -34,7 +29,8 @@ async def bp_root(request):
|
|||
|
||||
```
|
||||
|
||||
## Registering Blueprints
|
||||
## Registering blueprints
|
||||
|
||||
Blueprints must be registered with the application.
|
||||
|
||||
```python
|
||||
|
@ -48,14 +44,19 @@ app.run(host='0.0.0.0', port=8000, debug=True)
|
|||
```
|
||||
|
||||
This will add the blueprint to the application and register any routes defined
|
||||
by that blueprint.
|
||||
In this example, the registered routes in the `app.router` will look like:
|
||||
by that blueprint. In this example, the registered routes in the `app.router`
|
||||
will look like:
|
||||
|
||||
```python
|
||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
||||
```
|
||||
|
||||
## Middleware
|
||||
## Using blueprints
|
||||
|
||||
Blueprints have much the same functionality as an application instance.
|
||||
|
||||
### Middleware
|
||||
|
||||
Using blueprints allows you to also register middleware globally.
|
||||
|
||||
```python
|
||||
|
@ -72,30 +73,36 @@ async def halt_response(request, response):
|
|||
return text('I halted the response')
|
||||
```
|
||||
|
||||
## Exceptions
|
||||
Exceptions can also be applied exclusively to blueprints globally.
|
||||
### Exceptions
|
||||
|
||||
Exceptions can be applied exclusively to blueprints globally.
|
||||
|
||||
```python
|
||||
@bp.exception(NotFound)
|
||||
def ignore_404s(request, exception):
|
||||
return text("Yep, I totally found the page: {}".format(request.url))
|
||||
```
|
||||
|
||||
## Static files
|
||||
Static files can also be served globally, under the blueprint prefix.
|
||||
### Static files
|
||||
|
||||
Static files can be served globally, under the blueprint prefix.
|
||||
|
||||
```python
|
||||
bp.static('/folder/to/serve', '/web/path')
|
||||
```
|
||||
|
||||
## Start and Stop
|
||||
Blueprints and run functions during the start and stop process of the server.
|
||||
If running in multiprocessor mode (more than 1 worker), these are triggered after the workers fork
|
||||
## Start and stop
|
||||
|
||||
Blueprints can run functions during the start and stop process of the server.
|
||||
If running in multiprocessor mode (more than 1 worker), these are triggered
|
||||
after the workers fork.
|
||||
|
||||
Available events are:
|
||||
|
||||
* before_server_start - Executed before the server begins to accept connections
|
||||
* after_server_start - Executed after the server begins to accept connections
|
||||
* before_server_stop - Executed before the server stops accepting connections
|
||||
* after_server_stop - Executed after the server is stopped and all requests are complete
|
||||
- `before_server_start`: Executed before the server begins to accept connections
|
||||
- `after_server_start`: Executed after the server begins to accept connections
|
||||
- `before_server_stop`: Executed before the server stops accepting connections
|
||||
- `after_server_stop`: Executed after the server is stopped and all requests are complete
|
||||
|
||||
```python
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
@ -109,3 +116,49 @@ async def setup_connection():
|
|||
async def close_connection():
|
||||
await database.close()
|
||||
```
|
||||
|
||||
## Use-case: API versioning
|
||||
|
||||
Blueprints can be very useful for API versioning, where one blueprint may point
|
||||
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
|
||||
|
||||
When a blueprint is initialised, it can take an optional `url_prefix` argument,
|
||||
which will be prepended to all routes defined on the blueprint. This feature
|
||||
can be used to implement our API versioning scheme.
|
||||
|
||||
```python
|
||||
# blueprints.py
|
||||
from sanic.response import text
|
||||
from sanic import Blueprint
|
||||
|
||||
blueprint_v1 = Blueprint('v1')
|
||||
blueprint_v2 = Blueprint('v2')
|
||||
|
||||
@blueprint_v1.route('/')
|
||||
async def api_v1_root(request):
|
||||
return text('Welcome to version 1 of our documentation')
|
||||
|
||||
@blueprint_v2.route('/')
|
||||
async def api_v2_root(request):
|
||||
return text('Welcome to version 2 of our documentation')
|
||||
```
|
||||
|
||||
When we register our blueprints on the app, the routes `/v1` and `/v2` will now
|
||||
point to the individual blueprints, which allows the creation of *sub-sites*
|
||||
for each API version.
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from sanic import Sanic
|
||||
from blueprints import blueprint_v1, blueprint_v2
|
||||
|
||||
app = Sanic(__name__)
|
||||
app.blueprint(blueprint_v1)
|
||||
app.blueprint(blueprint_v2)
|
||||
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
```
|
||||
|
||||
**Previous:** [Exceptions](exceptions.md)
|
||||
|
||||
**Next:** [Class-based views](class_based_views.md)
|
||||
|
|
|
@ -1,8 +1,25 @@
|
|||
# Class based views
|
||||
# Class-Based Views
|
||||
|
||||
Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response.
|
||||
Class-based views are simply classes which implement response behaviour to
|
||||
requests. They provide a way to compartmentalise handling of different HTTP
|
||||
request types at the same endpoint. Rather than defining and decorating three
|
||||
different handler functions, one for each of an endpoint's supported request
|
||||
type, the endpoint can be assigned a class-based view.
|
||||
|
||||
## Defining views
|
||||
|
||||
A class-based view should subclass `HTTPMethodView`. You can then implement
|
||||
class methods for every HTTP request type you want to support. If a request is
|
||||
received that has no defined method, a `405: Method not allowed` response will
|
||||
be generated.
|
||||
|
||||
To register a class-based view on an endpoint, the `app.add_route` method is
|
||||
used. The first argument should be the defined class with the method `as_view`
|
||||
invoked, and the second should be the URL endpoint.
|
||||
|
||||
The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class
|
||||
using all these methods would look like the following.
|
||||
|
||||
## Examples
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import HTTPMethodView
|
||||
|
@ -10,7 +27,6 @@ from sanic.response import text
|
|||
|
||||
app = Sanic('some_name')
|
||||
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
|
@ -32,7 +48,10 @@ app.add_route(SimpleView.as_view(), '/')
|
|||
|
||||
```
|
||||
|
||||
If you need any url params just mention them in method definition:
|
||||
## URL parameters
|
||||
|
||||
If you need any URL parameters, as discussed in the routing guide, include them
|
||||
in the method definition.
|
||||
|
||||
```python
|
||||
class NameView(HTTPMethodView):
|
||||
|
@ -41,10 +60,12 @@ class NameView(HTTPMethodView):
|
|||
return text('Hello {}'.format(name))
|
||||
|
||||
app.add_route(NameView.as_view(), '/<name>')
|
||||
|
||||
```
|
||||
|
||||
If you want to add decorator for class, you could set decorators variable
|
||||
## Decorators
|
||||
|
||||
If you want to add any decorators to the class, you can set the `decorators`
|
||||
class variable. These will be applied to the class when `as_view` is called.
|
||||
|
||||
```
|
||||
class ViewWithDecorator(HTTPMethodView):
|
||||
|
@ -54,5 +75,8 @@ class ViewWithDecorator(HTTPMethodView):
|
|||
return text('Hello I have a decorator')
|
||||
|
||||
app.add_route(ViewWithDecorator.as_view(), '/url')
|
||||
|
||||
```
|
||||
|
||||
**Previous:** [Blueprints](blueprints.md)
|
||||
|
||||
**Next:** [Cookies](cookies.md)
|
||||
|
|
155
docs/conf.py
Normal file
155
docs/conf.py
Normal file
|
@ -0,0 +1,155 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Sanic documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Dec 25 18:07:21 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add support for Markdown documentation using Recommonmark
|
||||
from recommonmark.parser import CommonMarkParser
|
||||
|
||||
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
|
||||
# autogenerate documentation from docstrings
|
||||
root_directory = os.path.dirname(os.getcwd())
|
||||
sys.path.insert(0, root_directory)
|
||||
|
||||
import sanic
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.githubpages']
|
||||
|
||||
templates_path = ['_templates']
|
||||
|
||||
# Enable support for both Restructured Text and Markdown
|
||||
source_parsers = {'.md': CommonMarkParser}
|
||||
source_suffix = ['.rst', '.md']
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Sanic'
|
||||
copyright = '2016, Sanic contributors'
|
||||
author = 'Sanic contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = sanic.__version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = sanic.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
#
|
||||
# modules.rst is generated by sphinx-apidoc but is unused. This suppresses
|
||||
# a warning about it.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'modules.rst']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Sanicdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Sanic.tex', 'Sanic Documentation',
|
||||
'Sanic contributors', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'sanic', 'Sanic Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Sanic', 'Sanic Documentation',
|
||||
author, 'Sanic', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
# -- Options for Epub output ----------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
epub_author = author
|
||||
epub_publisher = author
|
||||
epub_copyright = copyright
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
|
@ -1,10 +1,35 @@
|
|||
# How to contribute to Sanic
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest!
|
||||
Thank you for your interest! Sanic is always looking for contributors. If you
|
||||
don't feel comfortable contributing code, adding docstrings to the source files
|
||||
is very appreciated.
|
||||
|
||||
## Running tests
|
||||
|
||||
* `python -m pip install pytest`
|
||||
* `python -m pytest tests`
|
||||
|
||||
## Documentation
|
||||
|
||||
Sanic's documentation is built
|
||||
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
|
||||
Markdown and can be found in the `docs` folder, while the module reference is
|
||||
automatically generated using `sphinx-apidoc`.
|
||||
|
||||
To generate the documentation from scratch:
|
||||
|
||||
```bash
|
||||
sphinx-apidoc -fo docs/_api/ sanic
|
||||
sphinx-build -b html docs docs/_build
|
||||
```
|
||||
|
||||
The HTML documentation will be created in the `docs/_build` folder.
|
||||
|
||||
## Warning
|
||||
One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged.
|
||||
|
||||
One of the main goals of Sanic is speed. Code that lowers the performance of
|
||||
Sanic without significant gains in usability, security, or features may not be
|
||||
merged. Please don't let this intimidate you! If you have any concerns about an
|
||||
idea, open an issue for discussion and help.
|
||||
|
||||
**Previous:** [Sanic extensions](extensions.md)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# Cookies
|
||||
|
||||
## Request
|
||||
Cookies are pieces of data which persist inside a user's browser. Sanic can
|
||||
both read and write cookies, which are stored as key-value pairs.
|
||||
|
||||
Request cookies can be accessed via the request.cookie dictionary
|
||||
## Reading cookies
|
||||
|
||||
### Example
|
||||
A user's cookies can be accessed `Request` object's `cookie` dictionary.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
|
@ -16,28 +16,11 @@ async def test(request):
|
|||
return text("Test cookie set to: {}".format(test_cookie))
|
||||
```
|
||||
|
||||
## Response
|
||||
## Writing cookies
|
||||
|
||||
Response cookies can be set like dictionary values and
|
||||
have the following parameters available:
|
||||
|
||||
* expires - datetime - Time for cookie to expire on the client's browser
|
||||
* path - string - The Path attribute specifies the subset of URLs to
|
||||
which this cookie applies
|
||||
* comment - string - Cookie comment (metadata)
|
||||
* domain - string - Specifies the domain for which the
|
||||
cookie is valid. An explicitly specified domain must always
|
||||
start with a dot.
|
||||
* max-age - number - Number of seconds the cookie should live for
|
||||
* secure - boolean - Specifies whether the cookie will only be sent via
|
||||
HTTPS
|
||||
* httponly - boolean - Specifies whether the cookie cannot be read
|
||||
by javascript
|
||||
|
||||
### Example
|
||||
When returning a response, cookies can be set on the `Response` object.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
|
@ -48,3 +31,22 @@ async def test(request):
|
|||
response.cookies['test']['httponly'] = True
|
||||
return response
|
||||
```
|
||||
|
||||
Response cookies can be set like dictionary values and have the following
|
||||
parameters available:
|
||||
|
||||
- `expires` (datetime): The time for the cookie to expire on the
|
||||
client's browser.
|
||||
- `path` (string): The subset of URLs to which this cookie applies.
|
||||
- `comment` (string): A comment (metadata).
|
||||
- `domain` (string): Specifies the domain for which the cookie is valid. An
|
||||
explicitly specified domain must always start with a dot.
|
||||
- `max-age` (number): Number of seconds the cookie should live for.
|
||||
- `secure` (boolean): Specifies whether the cookie will only be sent via
|
||||
HTTPS.
|
||||
- `httponly` (boolean): Specifies whether the cookie cannot be read by
|
||||
Javascript.
|
||||
|
||||
**Previous:** [Class-based views](class_based_views.md)
|
||||
|
||||
**Next:** [Custom protocols](custom_protocol.md)
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
# Custom Protocol
|
||||
# Custom Protocols
|
||||
|
||||
You can change the behavior of protocol by using custom protocol.
|
||||
If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic.
|
||||
*Note: this is advanced usage, and most readers will not need such functionality.*
|
||||
|
||||
* loop
|
||||
`loop` is an asyncio compatible event loop.
|
||||
You can change the behavior of Sanic's protocol by specifying a custom
|
||||
protocol, which should be a subclass
|
||||
of
|
||||
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
||||
This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method.
|
||||
|
||||
* connections
|
||||
`connections` is a `set object` to store protocol objects.
|
||||
When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections.
|
||||
The constructor of the custom protocol class receives the following keyword
|
||||
arguments from Sanic.
|
||||
|
||||
* signal
|
||||
`signal` is a `sanic.server.Signal object` with `stopped attribute`.
|
||||
When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`.
|
||||
|
||||
* request_handler
|
||||
`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments.
|
||||
|
||||
* error_handler
|
||||
`error_handler` is a `sanic.exceptions.Handler` object.
|
||||
|
||||
* request_timeout
|
||||
`request_timeout` is seconds for timeout.
|
||||
|
||||
* request_max_size
|
||||
`request_max_size` is bytes of max request size.
|
||||
- `loop`: an `asyncio`-compatible event loop.
|
||||
- `connections`: a `set` to store protocol objects. When Sanic receives
|
||||
`SIGINT` or `SIGTERM`, it executes `protocol.close_if_idle` for all protocol
|
||||
objects stored in this set.
|
||||
- `signal`: a `sanic.server.Signal` object with the `stopped` attribute. When
|
||||
Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` is assigned `True`.
|
||||
- `request_handler`: a coroutine that takes a `sanic.request.Request` object
|
||||
and a `response` callback as arguments.
|
||||
- `error_handler`: a `sanic.exceptions.Handler` which is called when exceptions
|
||||
are raised.
|
||||
- `request_timeout`: the number of seconds before a request times out.
|
||||
- `request_max_size`: an integer specifying the maximum size of a request, in bytes.
|
||||
|
||||
## Example
|
||||
By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.
|
||||
In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`.
|
||||
|
||||
An error occurs in the default protocol if a handler function does not return
|
||||
an `HTTPResponse` object.
|
||||
|
||||
By overriding the `write_response` protocol method, if a handler returns a
|
||||
string it will be converted to an `HTTPResponse object`.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
@ -68,3 +70,7 @@ async def response(request):
|
|||
|
||||
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
||||
```
|
||||
|
||||
**Previous:** [Cookies](cookies.md)
|
||||
|
||||
**Next:** [Testing](testing.md)
|
||||
|
|
|
@ -1,35 +1,59 @@
|
|||
# Deploying
|
||||
|
||||
When it comes to deploying Sanic, there's not much to it, but there are
|
||||
a few things to take note of.
|
||||
Deploying Sanic is made simple by the inbuilt webserver. After defining an
|
||||
instance of `sanic.Sanic`, we can call the `run` method with the following
|
||||
keyword arguments:
|
||||
|
||||
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
||||
- `port` *(default `8000`)*: Port to host the server on.
|
||||
- `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).
|
||||
- `sock` *(default `None`)*: Socket for the server to accept connections from.
|
||||
- `workers` *(default `1`)*: Number of worker processes to spawn.
|
||||
- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is
|
||||
specified, Sanic creates its own event loop.
|
||||
- `protocol` *(default `HttpProtocol`)*: Subclass
|
||||
of
|
||||
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
||||
|
||||
## Workers
|
||||
|
||||
By default, Sanic listens in the main process using only 1 CPU core.
|
||||
To crank up the juice, just specify the number of workers in the run
|
||||
arguments like so:
|
||||
By default, Sanic listens in the main process using only one CPU core. To crank
|
||||
up the juice, just specify the number of workers in the `run` arguments.
|
||||
|
||||
```python
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
|
||||
Sanic will automatically spin up multiple processes and route
|
||||
traffic between them. We recommend as many workers as you have
|
||||
available cores.
|
||||
Sanic will automatically spin up multiple processes and route traffic between
|
||||
them. We recommend as many workers as you have available cores.
|
||||
|
||||
## Running via Command
|
||||
## Running via command
|
||||
|
||||
If you like using command line arguments, you can launch a sanic server
|
||||
by executing the module. For example, if you initialized sanic as
|
||||
app in a file named server.py, you could run the server like so:
|
||||
If you like using command line arguments, you can launch a Sanic server by
|
||||
executing the module. For example, if you initialized Sanic as `app` in a file
|
||||
named `server.py`, you could run the server like so:
|
||||
|
||||
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
|
||||
|
||||
With this way of running sanic, it is not necessary to run app.run in
|
||||
your python file. If you do, just make sure you wrap it in name == main
|
||||
like so:
|
||||
With this way of running sanic, it is not necessary to invoke `app.run` in your
|
||||
Python file. If you do, make sure you wrap it so that it only executes when
|
||||
directly run by the interpreter.
|
||||
|
||||
```python
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
|
||||
**Previous:** [Request Data](request_data.md)
|
||||
|
||||
**Next:** [Static Files](static_files.md)
|
||||
|
|
|
@ -1,24 +1,31 @@
|
|||
# Exceptions
|
||||
|
||||
Exceptions can be thrown from within request handlers and will automatically be handled by Sanic. Exceptions take a message as their first argument, and can also take a status_code to be passed back in the HTTP response. Check sanic.exceptions for the full list of exceptions to throw.
|
||||
Exceptions can be thrown from within request handlers and will automatically be
|
||||
handled by Sanic. Exceptions take a message as their first argument, and can
|
||||
also take a status code to be passed back in the HTTP response.
|
||||
|
||||
## Throwing an exception
|
||||
|
||||
To throw an exception, simply `raise` the relevant exception from the
|
||||
`sanic.exceptions` module.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
@app.route('/killme')
|
||||
def i_am_ready_to_die(request):
|
||||
raise ServerError("Something bad happened")
|
||||
raise ServerError("Something bad happened", status_code=500)
|
||||
```
|
||||
|
||||
## Handling Exceptions
|
||||
## Handling exceptions
|
||||
|
||||
Just use the @exception decorator. The decorator expects a list of exceptions to handle as arguments. You can pass SanicException to catch them all! The exception handler must expect a request and exception object as arguments.
|
||||
To override Sanic's default handling of an exception, the `@app.exception`
|
||||
decorator is used. The decorator expects a list of exceptions to handle as
|
||||
arguments. You can pass `SanicException` to catch them all! The decorated
|
||||
exception handler function must take a `Request` and `Exception` object as
|
||||
arguments.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import NotFound
|
||||
|
||||
|
@ -26,3 +33,17 @@ from sanic.exceptions import NotFound
|
|||
def ignore_404s(request, exception):
|
||||
return text("Yep, I totally found the page: {}".format(request.url))
|
||||
```
|
||||
|
||||
## Useful exceptions
|
||||
|
||||
Some of the most useful exceptions are presented below:
|
||||
|
||||
- `NotFound`: called when a suitable route for the request isn't found.
|
||||
- `ServerError`: called when something goes wrong inside the server. This
|
||||
usually occurs if there is an exception raised in user code.
|
||||
|
||||
See the `sanic.exceptions` module for the full list of exceptions to throw.
|
||||
|
||||
**Previous:** [Middleware](middleware.md)
|
||||
|
||||
**Next:** [Blueprints](blueprints.md)
|
||||
|
|
12
docs/extensions.md
Normal file
12
docs/extensions.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Sanic Extensions
|
||||
|
||||
A list of Sanic extensions created by the community.
|
||||
|
||||
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
|
||||
Allows using redis, memcache or an in memory store.
|
||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
|
||||
**Previous:** [Testing](testing.md)
|
||||
|
||||
**Next:** [Contributing](contributing.md)
|
|
@ -1,25 +1,29 @@
|
|||
# Getting Started
|
||||
|
||||
Make sure you have pip and python 3.5 before starting
|
||||
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
|
||||
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
|
||||
syntax, so earlier versions of python won't work.
|
||||
|
||||
## Benchmarks
|
||||
* Install Sanic
|
||||
* `python3 -m pip install sanic`
|
||||
* Edit main.py to include:
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
1. Install Sanic: `python3 -m pip install sanic`
|
||||
2. Create a file called `main.py` with the following code:
|
||||
|
||||
app = Sanic(__name__)
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({ "hello": "world" })
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
```
|
||||
* Run `python3 main.py`
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return text('Hello world!')
|
||||
|
||||
You now have a working Sanic server! To continue on, check out:
|
||||
* [Request Data](request_data.md)
|
||||
* [Routing](routing.md)
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
```
|
||||
|
||||
3. Run the server: `python3 main.py`
|
||||
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||
the message *Hello world!*.
|
||||
|
||||
You now have a working Sanic server!
|
||||
|
||||
**Next:** [Routing](routing.md)
|
||||
|
|
34
docs/index.rst
Normal file
34
docs/index.rst
Normal file
|
@ -0,0 +1,34 @@
|
|||
.. include:: ../README.rst
|
||||
|
||||
Guides
|
||||
======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
getting_started
|
||||
routing
|
||||
request_data
|
||||
deploying
|
||||
static_files
|
||||
middleware
|
||||
exceptions
|
||||
blueprints
|
||||
class_based_views
|
||||
config
|
||||
cookies
|
||||
custom_protocol
|
||||
testing
|
||||
extensions
|
||||
contributing
|
||||
|
||||
|
||||
Module Documentation
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
|
||||
Module Reference <_api/sanic>
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
|
@ -1,36 +1,32 @@
|
|||
# Middleware
|
||||
|
||||
Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware returns a response object, the request will stop processing and a response will be returned.
|
||||
Middleware are functions which are executed before or after requests to the
|
||||
server. They can be used to modify the *request to* or *response from*
|
||||
user-defined handler functions.
|
||||
|
||||
Middleware is registered via the middleware decorator, and can either be added as 'request' or 'response' middleware, based on the argument provided in the decorator. Response middleware receives both the request and the response as arguments.
|
||||
There are two types of middleware: request and response. Both are declared
|
||||
using the `@app.middleware` decorator, with the decorator's parameter being a
|
||||
string representing its type: `'request'` or `'response'`. Response middleware
|
||||
receives both the request and the response as arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
The simplest middleware doesn't modify the request or response at all:
|
||||
|
||||
```python
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.middleware
|
||||
async def halt_request(request):
|
||||
print("I am a spy")
|
||||
|
||||
@app.middleware('request')
|
||||
async def halt_request(request):
|
||||
return text('I halted the request')
|
||||
async def print_on_request(request):
|
||||
print("I print when a request is received by the server")
|
||||
|
||||
@app.middleware('response')
|
||||
async def halt_response(request, response):
|
||||
return text('I halted the response')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return text('I would like to speak now please')
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
async def print_on_response(request, response):
|
||||
print("I print when a response is returned by the server")
|
||||
```
|
||||
|
||||
## Middleware chain
|
||||
## Modifying the request or response
|
||||
|
||||
If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware:
|
||||
Middleware can modify the request or response parameter it is given, *as long
|
||||
as it does not return it*. The following example shows a practical use-case for
|
||||
this.
|
||||
|
||||
```python
|
||||
app = Sanic(__name__)
|
||||
|
@ -46,4 +42,29 @@ async def prevent_xss(request, response):
|
|||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks.
|
||||
The above code will apply the two middleware in order. First, the middleware
|
||||
**custom_banner** will change the HTTP response header *Server* to
|
||||
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
|
||||
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
|
||||
are invoked *after* a user function returns a response.
|
||||
|
||||
## Responding early
|
||||
|
||||
If middleware returns a `HTTPResponse` object, the request will stop processing
|
||||
and the response will be returned. If this occurs to a request before the
|
||||
relevant user route handler is reached, the handler will never be called.
|
||||
Returning a response will also prevent any further middleware from running.
|
||||
|
||||
```python
|
||||
@app.middleware('request')
|
||||
async def halt_request(request):
|
||||
return text('I halted the request')
|
||||
|
||||
@app.middleware('response')
|
||||
async def halt_response(request, response):
|
||||
return text('I halted the response')
|
||||
```
|
||||
|
||||
**Previous:** [Static Files](static_files.md)
|
||||
|
||||
**Next:** [Exceptions](exceptions.md)
|
||||
|
|
|
@ -1,33 +1,40 @@
|
|||
# Request Data
|
||||
|
||||
## Properties
|
||||
When an endpoint receives a HTTP request, the route function is passed a
|
||||
`Request` object.
|
||||
|
||||
The following request variables are accessible as properties:
|
||||
The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
`request.files` (dictionary of File objects) - List of files that have a name, body, and type
|
||||
`request.json` (any) - JSON body
|
||||
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
|
||||
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
|
||||
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
|
||||
- `json` (any) - JSON body
|
||||
|
||||
See request.py for more information
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "message": request.json })
|
||||
```
|
||||
|
||||
@app.route("/form")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
||||
- `args` (dict) - Query string variables. A query string is the section of a
|
||||
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
||||
the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
||||
The request's `query_string` variable holds the unparsed string value.
|
||||
|
||||
@app.route("/files")
|
||||
def post_json(request):
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||
```
|
||||
|
||||
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/files")
|
||||
def post_json(request):
|
||||
test_file = request.files.get('test')
|
||||
|
||||
file_parameters = {
|
||||
|
@ -37,13 +44,52 @@ def post_json(request):
|
|||
}
|
||||
|
||||
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
||||
```
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||
- `form` (dict) - Posted form variables.
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/users", methods=["POST",])
|
||||
def create_user(request):
|
||||
@app.route("/form")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
||||
```
|
||||
|
||||
- `body` (bytes) - Posted raw body. This property allows retrieval of the
|
||||
request's raw data, regardless of content type.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/users", methods=["POST",])
|
||||
def create_user(request):
|
||||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
||||
```
|
||||
|
||||
- `ip` (str) - IP address of the requester.
|
||||
|
||||
## Accessing values using `get` and `getlist`
|
||||
|
||||
The request properties which return a dictionary actually return a subclass of
|
||||
`dict` called `RequestParameters`. The key difference when using this object is
|
||||
the distinction between the `get` and `getlist` methods.
|
||||
|
||||
- `get(key, default=None)` operates as normal, except that when the value of
|
||||
the given key is a list, *only the first item is returned*.
|
||||
- `getlist(key, default=None)` operates as normal, *returning the entire list*.
|
||||
|
||||
```python
|
||||
from sanic.request import RequestParameters
|
||||
|
||||
args = RequestParameters()
|
||||
args['titles'] = ['Post 1', 'Post 2']
|
||||
|
||||
args.get('titles') # => 'Post 1'
|
||||
|
||||
args.getlist('titles') # => ['Post 1', 'Post 2']
|
||||
```
|
||||
|
||||
**Previous:** [Routing](routing.md)
|
||||
|
||||
**Next:** [Deploying](deploying.md)
|
||||
|
|
|
@ -1,17 +1,48 @@
|
|||
# Routing
|
||||
|
||||
Sanic comes with a basic router that supports request parameters. To specify a parameter, surround it with carrots like so: `<PARAM>`. Request parameters will be passed to the request handler functions as keyword arguments. To specify a type, add a :type after the parameter name, in the carrots. If the parameter does not match the type supplied, Sanic will throw a NotFound exception, resulting in a 404 page not found error.
|
||||
Routing allows the user to specify handler functions for different URL endpoints.
|
||||
|
||||
|
||||
## Examples
|
||||
A basic route looks like the following, where `app` is an instance of the
|
||||
`Sanic` class:
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({ "hello": "world" })
|
||||
```
|
||||
|
||||
When the url `http://server.url/` is accessed (the base url of the server), the
|
||||
final `/` is matched by the router to the handler function, `test`, which then
|
||||
returns a JSON object.
|
||||
|
||||
Sanic handler functions must be defined using the `async def` syntax, as they
|
||||
are asynchronous functions.
|
||||
|
||||
## Request parameters
|
||||
|
||||
Sanic comes with a basic router that supports request parameters.
|
||||
|
||||
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
|
||||
Request parameters will be passed to the route handler functions as keyword
|
||||
arguments.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/tag/<tag>')
|
||||
async def tag_handler(request, tag):
|
||||
return text('Tag - {}'.format(tag))
|
||||
```
|
||||
|
||||
To specify a type for the parameter, add a `:type` after the parameter name,
|
||||
inside the quotes. If the parameter does not match the specified type, Sanic
|
||||
will throw a `NotFound` exception, resulting in a `404: Page not found` error
|
||||
on the URL.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/number/<integer_arg:int>')
|
||||
async def integer_handler(request, integer_arg):
|
||||
|
@ -29,16 +60,52 @@ async def person_handler(request, name):
|
|||
async def folder_handler(request, folder_id):
|
||||
return text('Folder - {}'.format(folder_id))
|
||||
|
||||
```
|
||||
|
||||
## HTTP request types
|
||||
|
||||
By default, a route defined on a URL will be used for all requests to that URL.
|
||||
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
||||
which restricts the handler function to the HTTP methods in the given list.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/post')
|
||||
async def post_handler(request, methods=['POST']):
|
||||
return text('POST request - {}'.format(request.json))
|
||||
|
||||
@app.route('/get')
|
||||
async def GET_handler(request, methods=['GET']):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
```
|
||||
|
||||
## The `add_route` method
|
||||
|
||||
As we have seen, routes are often specified using the `@app.route` decorator.
|
||||
However, this decorator is really just a wrapper for the `app.add_route`
|
||||
method, which is used as follows:
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
# Define the handler functions
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
app.add_route(handler1, '/test')
|
||||
|
||||
async def handler2(request, name):
|
||||
return text('Folder - {}'.format(name))
|
||||
app.add_route(handler2, '/folder/<name>')
|
||||
|
||||
async def person_handler2(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
app.add_route(person_handler2, '/person/<name:[A-z]>')
|
||||
|
||||
# Add each handler function as a route
|
||||
app.add_route(handler1, '/test')
|
||||
app.add_route(handler2, '/folder/<name>')
|
||||
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
|
||||
```
|
||||
|
||||
**Previous:** [Getting Started](getting_started.md)
|
||||
|
||||
**Next:** [Request Data](request_data.md)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
# Static Files
|
||||
|
||||
Both directories and files can be served by registering with static
|
||||
|
||||
## Example
|
||||
Static files and directories, such as an image file, are served by Sanic when
|
||||
registered with the `app.static` method. The method takes an endpoint URL and a
|
||||
filename. The file specified will then be accessible via the given endpoint.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
app = Sanic(__name__)
|
||||
|
||||
# Serves files from the static folder to the URL /static
|
||||
|
@ -16,3 +17,7 @@ app.static('/the_best.png', '/home/ubuntu/test.png')
|
|||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
**Previous:** [Deploying](deploying.md)
|
||||
|
||||
**Next:** [Middleware](middleware.md)
|
||||
|
|
|
@ -49,3 +49,7 @@ def test_endpoint_challenge():
|
|||
# Assert that the server responds with the challenge string
|
||||
assert response.text == request_data['challenge']
|
||||
```
|
||||
|
||||
**Previous:** [Custom protocols](custom_protocol.md)
|
||||
|
||||
**Next:** [Sanic extensions](extensions.md)
|
||||
|
|
|
@ -21,7 +21,7 @@ from aiocache.serializers import JsonSerializer
|
|||
app = Sanic(__name__)
|
||||
|
||||
aiocache.settings.set_defaults(
|
||||
cache="aiocache.RedisCache"
|
||||
class_="aiocache.RedisCache"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -9,17 +9,15 @@ and pass in an instance of it when we create our Sanic instance. Inside this
|
|||
class' default handler, we can do anything including sending exceptions to
|
||||
an external service.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
from sanic.exceptions import Handler, SanicException
|
||||
"""
|
||||
Imports and code relevant for our CustomHandler class
|
||||
(Ordinarily this would be in a separate file)
|
||||
"""
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import Handler, SanicException
|
||||
|
||||
|
||||
class CustomHandler(Handler):
|
||||
|
||||
def default(self, request, exception):
|
||||
# Here, we have access to the exception object
|
||||
# and can do anything with it (log, send to external service, etc)
|
||||
|
@ -31,9 +29,7 @@ class CustomHandler(Handler):
|
|||
# Then, we must finish handling the exception by returning
|
||||
# our response to the client
|
||||
# For this we can just call the super class' default handler
|
||||
return super.default(self, request, exception)
|
||||
|
||||
|
||||
return super().default(request, exception)
|
||||
|
||||
|
||||
"""
|
||||
|
@ -49,11 +45,12 @@ app = Sanic(__name__)
|
|||
handler = CustomHandler(sanic=app)
|
||||
app.error_handler = handler
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
# Here, something occurs which causes an unexpected exception
|
||||
# This exception will flow to our custom handler.
|
||||
x = 1 / 0
|
||||
1 / 0
|
||||
return json({"test": True})
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ logging.basicConfig(
|
|||
log = logging.getLogger()
|
||||
|
||||
# Set logger to override default basicConfig
|
||||
sanic = Sanic(logger=True)
|
||||
sanic = Sanic()
|
||||
@sanic.route("/")
|
||||
def test(request):
|
||||
log.info("received request; responding with 'hey'")
|
||||
|
|
|
@ -11,9 +11,16 @@ from sanic.blueprints import Blueprint
|
|||
app = Sanic()
|
||||
bp = Blueprint("bp", host="bp.example.com")
|
||||
|
||||
@app.route('/', host=["example.com",
|
||||
"somethingelse.com",
|
||||
"therestofyourdomains.com"])
|
||||
async def hello(request):
|
||||
return text("Some defaults")
|
||||
|
||||
@app.route('/', host="example.com")
|
||||
async def hello(request):
|
||||
return text("Answer")
|
||||
|
||||
@app.route('/', host="sub.example.com")
|
||||
async def hello(request):
|
||||
return text("42")
|
||||
|
|
|
@ -12,3 +12,6 @@ kyoukai
|
|||
falcon
|
||||
tornado
|
||||
aiofiles
|
||||
sphinx
|
||||
recommonmark
|
||||
beautifulsoup4
|
||||
|
|
|
@ -2,4 +2,3 @@ httptools
|
|||
ujson
|
||||
uvloop
|
||||
aiofiles
|
||||
multidict
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from .sanic import Sanic
|
||||
from .blueprints import Blueprint
|
||||
|
||||
__version__ = '0.1.9'
|
||||
__version__ = '0.2.0'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
|
|
@ -3,6 +3,7 @@ from collections import defaultdict
|
|||
|
||||
class BlueprintSetup:
|
||||
"""
|
||||
Creates a blueprint state like object.
|
||||
"""
|
||||
|
||||
def __init__(self, blueprint, app, options):
|
||||
|
@ -32,13 +33,13 @@ class BlueprintSetup:
|
|||
|
||||
def add_exception(self, handler, *args, **kwargs):
|
||||
"""
|
||||
Registers exceptions to sanic
|
||||
Registers exceptions to sanic.
|
||||
"""
|
||||
self.app.exception(*args, **kwargs)(handler)
|
||||
|
||||
def add_static(self, uri, file_or_directory, *args, **kwargs):
|
||||
"""
|
||||
Registers static files to sanic
|
||||
Registers static files to sanic.
|
||||
"""
|
||||
if self.url_prefix:
|
||||
uri = self.url_prefix + uri
|
||||
|
@ -47,7 +48,7 @@ class BlueprintSetup:
|
|||
|
||||
def add_middleware(self, middleware, *args, **kwargs):
|
||||
"""
|
||||
Registers middleware to sanic
|
||||
Registers middleware to sanic.
|
||||
"""
|
||||
if args or kwargs:
|
||||
self.app.middleware(*args, **kwargs)(middleware)
|
||||
|
@ -77,18 +78,23 @@ class Blueprint:
|
|||
|
||||
def make_setup_state(self, app, options):
|
||||
"""
|
||||
Returns a new BlueprintSetup object
|
||||
"""
|
||||
return BlueprintSetup(self, app, options)
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Registers the blueprint to the sanic app.
|
||||
"""
|
||||
state = self.make_setup_state(app, options)
|
||||
for deferred in self.deferred_functions:
|
||||
deferred(state)
|
||||
|
||||
def route(self, uri, methods=None, host=None):
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""
|
||||
Creates a blueprint route from a decorated function.
|
||||
:param uri: Endpoint at which the route will be accessible.
|
||||
:param methods: List of acceptable HTTP methods.
|
||||
"""
|
||||
def decorator(handler):
|
||||
self.record(lambda s: s.add_route(handler, uri, methods, host))
|
||||
|
@ -97,12 +103,18 @@ class Blueprint:
|
|||
|
||||
def add_route(self, handler, uri, methods=None, host=None):
|
||||
"""
|
||||
Creates a blueprint route from a function.
|
||||
:param handler: Function to handle uri request.
|
||||
:param uri: Endpoint at which the route will be accessible.
|
||||
:param methods: List of acceptable HTTP methods.
|
||||
"""
|
||||
self.record(lambda s: s.add_route(handler, uri, methods, host))
|
||||
return handler
|
||||
|
||||
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)
|
||||
|
@ -111,6 +123,7 @@ class Blueprint:
|
|||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint middleware from a decorated function.
|
||||
"""
|
||||
def register_middleware(middleware):
|
||||
self.record(
|
||||
|
@ -127,6 +140,7 @@ class Blueprint:
|
|||
|
||||
def exception(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint exception from a decorated function.
|
||||
"""
|
||||
def decorator(handler):
|
||||
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
|
||||
|
@ -135,6 +149,9 @@ class Blueprint:
|
|||
|
||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint static route from a decorated function.
|
||||
:param uri: Endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
self.record(
|
||||
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))
|
||||
|
|
|
@ -1,6 +1,104 @@
|
|||
from .response import text
|
||||
from .response import text, html
|
||||
from .log import log
|
||||
from traceback import format_exc
|
||||
from traceback import format_exc, extract_tb
|
||||
import sys
|
||||
|
||||
TRACEBACK_STYLE = '''
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
h3 code {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.frame-line > * {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.frame-line {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.frame-code {
|
||||
font-size: 16px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.tb-wrapper {
|
||||
border: 1px solid #f3f3f3;
|
||||
}
|
||||
|
||||
.tb-header {
|
||||
background-color: #f3f3f3;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.frame-descriptor {
|
||||
background-color: #e2eafb;
|
||||
}
|
||||
|
||||
.frame-descriptor {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
'''
|
||||
|
||||
TRACEBACK_WRAPPER_HTML = '''
|
||||
<html>
|
||||
<head>
|
||||
{style}
|
||||
</head>
|
||||
<body>
|
||||
<h1>{exc_name}</h1>
|
||||
<h3><code>{exc_value}</code></h3>
|
||||
<div class="tb-wrapper">
|
||||
<p class="tb-header">Traceback (most recent call last):</p>
|
||||
{frame_html}
|
||||
<p class="summary">
|
||||
<b>{exc_name}: {exc_value}</b>
|
||||
while handling uri <code>{uri}</code>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
TRACEBACK_LINE_HTML = '''
|
||||
<div class="frame-line">
|
||||
<p class="frame-descriptor">
|
||||
File {0.filename}, line <i>{0.lineno}</i>,
|
||||
in <code><b>{0.name}</b></code>
|
||||
</p>
|
||||
<p class="frame-code"><code>{0.line}</code></p>
|
||||
</div>
|
||||
'''
|
||||
|
||||
INTERNAL_SERVER_ERROR_HTML = '''
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>
|
||||
The server encountered an internal error and cannot complete
|
||||
your request.
|
||||
</p>
|
||||
'''
|
||||
|
||||
|
||||
class SanicException(Exception):
|
||||
|
@ -42,9 +140,24 @@ class PayloadTooLarge(SanicException):
|
|||
class Handler:
|
||||
handlers = None
|
||||
|
||||
def __init__(self, sanic):
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
self.sanic = sanic
|
||||
self.debug = False
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
frames = extract_tb(tb)
|
||||
|
||||
frame_html = []
|
||||
for frame in frames:
|
||||
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
|
||||
|
||||
return TRACEBACK_WRAPPER_HTML.format(
|
||||
style=TRACEBACK_STYLE,
|
||||
exc_name=exc_type.__name__,
|
||||
exc_value=exc_value,
|
||||
frame_html=''.join(frame_html),
|
||||
uri=request.url)
|
||||
|
||||
def add(self, exception, handler):
|
||||
self.handlers[exception] = handler
|
||||
|
@ -52,6 +165,7 @@ class Handler:
|
|||
def response(self, request, exception):
|
||||
"""
|
||||
Fetches and executes an exception handler and returns a response object
|
||||
|
||||
:param request: Request
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
|
@ -60,7 +174,8 @@ class Handler:
|
|||
try:
|
||||
response = handler(request=request, exception=exception)
|
||||
except:
|
||||
if self.sanic.debug:
|
||||
log.error(format_exc())
|
||||
if self.debug:
|
||||
response_message = (
|
||||
'Exception raised in exception handler "{}" '
|
||||
'for uri: "{}"\n{}').format(
|
||||
|
@ -72,16 +187,18 @@ class Handler:
|
|||
return response
|
||||
|
||||
def default(self, request, exception):
|
||||
log.error(format_exc())
|
||||
if issubclass(type(exception), SanicException):
|
||||
return text(
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500))
|
||||
elif self.sanic.debug:
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
response_message = (
|
||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||
request.url, format_exc()))
|
||||
log.error(response_message)
|
||||
return text(response_message, status=500)
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return text(
|
||||
'An error occurred while generating the response', status=500)
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log = logging.getLogger('sanic')
|
||||
|
|
|
@ -21,19 +21,13 @@ class RequestParameters(dict):
|
|||
value of the list and getlist returns the whole shebang
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.super = super()
|
||||
self.super.__init__(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.get(name)
|
||||
|
||||
def get(self, name, default=None):
|
||||
values = self.super.get(name)
|
||||
return values[0] if values else default
|
||||
"""Return the first value, either the default or actual"""
|
||||
return super().get(name, [default])[0]
|
||||
|
||||
def getlist(self, name, default=None):
|
||||
return self.super.get(name, default)
|
||||
"""Return the entire list"""
|
||||
return super().get(name, default)
|
||||
|
||||
|
||||
class Request(dict):
|
||||
|
@ -41,18 +35,20 @@ class Request(dict):
|
|||
Properties of an HTTP request such as URL, headers, etc.
|
||||
"""
|
||||
__slots__ = (
|
||||
'url', 'headers', 'version', 'method', '_cookies',
|
||||
'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'query_string', 'body',
|
||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip',
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method):
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
# TODO: Content-Encoding detection
|
||||
url_parsed = parse_url(url_bytes)
|
||||
self.url = url_parsed.path.decode('utf-8')
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
self.method = method
|
||||
self.transport = transport
|
||||
self.query_string = None
|
||||
if url_parsed.query:
|
||||
self.query_string = url_parsed.query.decode('utf-8')
|
||||
|
@ -139,6 +135,12 @@ class Request(dict):
|
|||
self._cookies = {}
|
||||
return self._cookies
|
||||
|
||||
@property
|
||||
def ip(self):
|
||||
if not hasattr(self, '_ip'):
|
||||
self._ip = self.transport.get_extra_info('peername')
|
||||
return self._ip
|
||||
|
||||
|
||||
File = namedtuple('File', ['type', 'body', 'name'])
|
||||
|
||||
|
@ -146,6 +148,7 @@ File = namedtuple('File', ['type', 'body', 'name'])
|
|||
def parse_multipart_form(body, boundary):
|
||||
"""
|
||||
Parses a request body and returns fields and files
|
||||
|
||||
:param body: Bytes request body
|
||||
:param boundary: Bytes multipart boundary
|
||||
:return: fields (RequestParameters), files (RequestParameters)
|
||||
|
|
|
@ -83,10 +83,10 @@ class HTTPResponse:
|
|||
if body is not None:
|
||||
try:
|
||||
# Try to encode it regularly
|
||||
self.body = body.encode('utf-8')
|
||||
self.body = body.encode()
|
||||
except AttributeError:
|
||||
# Convert it to a str if you can't
|
||||
self.body = str(body).encode('utf-8')
|
||||
self.body = str(body).encode()
|
||||
else:
|
||||
self.body = body_bytes
|
||||
|
||||
|
@ -142,22 +142,47 @@ class HTTPResponse:
|
|||
return self._cookies
|
||||
|
||||
|
||||
def json(body, status=200, headers=None):
|
||||
return HTTPResponse(json_dumps(body), headers=headers, status=status,
|
||||
content_type="application/json")
|
||||
def json(body, status=200, headers=None, **kwargs):
|
||||
"""
|
||||
Returns response object with body in json format.
|
||||
:param body: Response data to be serialized.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param \**kwargs: Remaining arguments that are passed to the json encoder.
|
||||
"""
|
||||
return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
|
||||
status=status, content_type="application/json")
|
||||
|
||||
|
||||
def text(body, status=200, headers=None):
|
||||
"""
|
||||
Returns response object with body in text format.
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/plain; charset=utf-8")
|
||||
|
||||
|
||||
def html(body, status=200, headers=None):
|
||||
"""
|
||||
Returns response object with body in html format.
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
async def file(location, mime_type=None, headers=None):
|
||||
"""
|
||||
Returns response object with file data.
|
||||
:param location: Location of file on system.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
|
@ -169,3 +194,26 @@ async def file(location, mime_type=None, headers=None):
|
|||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream)
|
||||
|
||||
|
||||
def redirect(to, headers=None, status=302,
|
||||
content_type="text/html; charset=utf-8"):
|
||||
"""
|
||||
Aborts execution and causes a 302 redirect (by default).
|
||||
|
||||
:param to: path or fully qualified URL to redirect to
|
||||
: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 content_type:
|
||||
the content type (string) of the response
|
||||
:returns: the redirecting Response
|
||||
"""
|
||||
headers = headers or {}
|
||||
|
||||
# According to RFC 7231, a relative URI is now permitted.
|
||||
headers['Location'] = to
|
||||
|
||||
return HTTPResponse(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type)
|
||||
|
|
|
@ -2,6 +2,7 @@ import re
|
|||
from collections import defaultdict, namedtuple
|
||||
from functools import lru_cache
|
||||
from .exceptions import NotFound, InvalidUsage
|
||||
from .views import CompositionView
|
||||
|
||||
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
|
@ -31,12 +32,20 @@ class RouteDoesNotExist(Exception):
|
|||
class Router:
|
||||
"""
|
||||
Router supports basic routing with parameters and method checks
|
||||
|
||||
Usage:
|
||||
@app.route('/my_url/<my_param>', methods=['GET', 'POST', ...])
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@sanic.route('/my/url/<my_param>', methods=['GET', 'POST', ...])
|
||||
def my_route(request, my_param):
|
||||
do stuff...
|
||||
|
||||
or
|
||||
@app.route('/my_url/<my_param:my_type>', methods=['GET', 'POST', ...])
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@sanic.route('/my/url/<my_param:my_type>', methods['GET', 'POST', ...])
|
||||
def my_route_with_type(request, my_param: my_type):
|
||||
do stuff...
|
||||
|
||||
|
@ -61,6 +70,7 @@ class Router:
|
|||
def add(self, uri, methods, handler, host=None):
|
||||
"""
|
||||
Adds a handler to the route list
|
||||
|
||||
:param uri: Path to match
|
||||
:param methods: Array of accepted method names.
|
||||
If none are provided, any method is allowed
|
||||
|
@ -76,11 +86,15 @@ class Router:
|
|||
if self.hosts is None:
|
||||
self.hosts = set(host)
|
||||
else:
|
||||
if isinstance(host, list):
|
||||
host = set(host)
|
||||
self.hosts.add(host)
|
||||
if isinstance(host, str):
|
||||
uri = host + uri
|
||||
|
||||
if uri in self.routes_all:
|
||||
raise RouteExists("Route already registered: {}".format(uri))
|
||||
else:
|
||||
for h in host:
|
||||
self.add(uri, methods, handler, h)
|
||||
return
|
||||
|
||||
# Dict for faster lookups of if method allowed
|
||||
if methods:
|
||||
|
@ -114,6 +128,32 @@ class Router:
|
|||
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
|
||||
pattern = re.compile(r'^{}$'.format(pattern_string))
|
||||
|
||||
def merge_route(route, methods, handler):
|
||||
# merge to the existing route when possible.
|
||||
if not route.methods or not methods:
|
||||
# method-unspecified routes are not mergeable.
|
||||
raise RouteExists(
|
||||
"Route already registered: {}".format(uri))
|
||||
elif route.methods.intersection(methods):
|
||||
# already existing method is not overloadable.
|
||||
duplicated = methods.intersection(route.methods)
|
||||
raise RouteExists(
|
||||
"Route already registered: {} [{}]".format(
|
||||
uri, ','.join(list(duplicated))))
|
||||
if isinstance(route.handler, CompositionView):
|
||||
view = route.handler
|
||||
else:
|
||||
view = CompositionView()
|
||||
view.add(route.methods, route.handler)
|
||||
view.add(methods, handler)
|
||||
route = route._replace(
|
||||
handler=view, methods=methods.union(route.methods))
|
||||
return route
|
||||
|
||||
route = self.routes_all.get(uri)
|
||||
if route:
|
||||
route = merge_route(route, methods, handler)
|
||||
else:
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters)
|
||||
|
@ -149,6 +189,7 @@ class Router:
|
|||
"""
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
error
|
||||
|
||||
:param request: Request object
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
|
|
|
@ -21,18 +21,22 @@ from os import set_inheritable
|
|||
|
||||
class Sanic:
|
||||
def __init__(self, name=None, router=None,
|
||||
error_handler=None, logger=None):
|
||||
if logger is None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s: %(levelname)s: %(message)s"
|
||||
)
|
||||
error_handler=None):
|
||||
# Only set up a default log handler if the
|
||||
# end-user application didn't set anything up.
|
||||
if not logging.root.handlers and log.level == logging.NOTSET:
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s: %(levelname)s: %(message)s")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
log.setLevel(logging.INFO)
|
||||
if name is None:
|
||||
frame_records = stack()[1]
|
||||
name = getmodulename(frame_records[1])
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.error_handler = error_handler or Handler(self)
|
||||
self.error_handler = error_handler or Handler()
|
||||
self.config = Config()
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
|
@ -51,9 +55,10 @@ class Sanic:
|
|||
# -------------------------------------------------------------------- #
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=None, host=None):
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""
|
||||
Decorates a function to be registered as a route
|
||||
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:return: decorated function
|
||||
|
@ -71,11 +76,31 @@ class Sanic:
|
|||
|
||||
return response
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None):
|
||||
return self.route(uri, methods=["GET"], host=host)
|
||||
|
||||
def post(self, uri, host=None):
|
||||
return self.route(uri, methods=["POST"], host=host)
|
||||
|
||||
def put(self, uri, host=None):
|
||||
return self.route(uri, methods=["PUT"], host=host)
|
||||
|
||||
def head(self, uri, host=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host)
|
||||
|
||||
def options(self, uri, host=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host)
|
||||
|
||||
def patch(self, uri, host=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host)
|
||||
|
||||
def add_route(self, handler, uri, methods=None, host=None):
|
||||
"""
|
||||
A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
|
||||
:param handler: function or class instance
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
|
@ -91,7 +116,8 @@ class Sanic:
|
|||
def exception(self, *exceptions):
|
||||
"""
|
||||
Decorates a function to be registered as a handler for exceptions
|
||||
:param *exceptions: exceptions
|
||||
|
||||
:param \*exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
|
@ -137,6 +163,7 @@ class Sanic:
|
|||
def blueprint(self, blueprint, **options):
|
||||
"""
|
||||
Registers a blueprint on the application.
|
||||
|
||||
:param blueprint: Blueprint object
|
||||
:param options: option dictionary with blueprint defaults
|
||||
:return: Nothing
|
||||
|
@ -154,7 +181,8 @@ class Sanic:
|
|||
def register_blueprint(self, *args, **kwargs):
|
||||
# TODO: deprecate 1.0
|
||||
log.warning("Use of register_blueprint will be deprecated in "
|
||||
"version 1.0. Please use the blueprint method instead")
|
||||
"version 1.0. Please use the blueprint method instead",
|
||||
DeprecationWarning)
|
||||
return self.blueprint(*args, **kwargs)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
|
@ -169,6 +197,7 @@ class Sanic:
|
|||
Takes a request from the HTTP Server and returns a response object to
|
||||
be sent back The HTTP Server only expects a response object, so
|
||||
exception handling must be done here
|
||||
|
||||
:param request: HTTP Request object
|
||||
:param response_callback: Response function to be called with the
|
||||
response as the only argument
|
||||
|
@ -236,7 +265,7 @@ class Sanic:
|
|||
e, format_exc()))
|
||||
else:
|
||||
response = HTTPResponse(
|
||||
"An error occured while handling an error")
|
||||
"An error occurred while handling an error")
|
||||
|
||||
response_callback(response)
|
||||
|
||||
|
@ -245,12 +274,13 @@ class Sanic:
|
|||
# -------------------------------------------------------------------- #
|
||||
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, sock=None,
|
||||
workers=1, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
stop_event=None):
|
||||
after_start=None, before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, workers=1, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None, register_sys_signals=True):
|
||||
"""
|
||||
Runs the HTTP Server and listens until keyboard interrupt or term
|
||||
signal. On termination, drains connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param debug: Enables debug output (slows server)
|
||||
|
@ -262,6 +292,7 @@ class Sanic:
|
|||
received before it is respected
|
||||
:param after_stop: Functions to be executed when all requests are
|
||||
complete
|
||||
:param ssl: SSLContext for SSL encryption of worker(s)
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param workers: Number of processes
|
||||
received before it is respected
|
||||
|
@ -269,7 +300,7 @@ class Sanic:
|
|||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
self.error_handler.debug = True
|
||||
self.error_handler.debug = debug
|
||||
self.debug = debug
|
||||
self.loop = loop
|
||||
|
||||
|
@ -278,12 +309,14 @@ class Sanic:
|
|||
'host': host,
|
||||
'port': port,
|
||||
'sock': sock,
|
||||
'ssl': ssl,
|
||||
'debug': debug,
|
||||
'request_handler': self.handle_request,
|
||||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||
'loop': loop,
|
||||
'register_sys_signals': register_sys_signals,
|
||||
'backlog': backlog
|
||||
}
|
||||
|
||||
|
@ -315,7 +348,11 @@ class Sanic:
|
|||
log.debug(self.config.LOGO)
|
||||
|
||||
# Serve
|
||||
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
|
||||
if ssl is None:
|
||||
proto = "http"
|
||||
else:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
try:
|
||||
if workers == 1:
|
||||
|
@ -345,6 +382,7 @@ class Sanic:
|
|||
"""
|
||||
Starts multiple server processes simultaneously. Stops on interrupt
|
||||
and terminate signals, and drains connections when complete.
|
||||
|
||||
:param server_settings: kw arguments to be passed to the serve function
|
||||
:param workers: number of workers to launch
|
||||
:param stop_event: if provided, is used as a stop signal
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import asyncio
|
||||
import traceback
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from multidict import CIMultiDict
|
||||
from signal import SIGINT, SIGTERM
|
||||
from time import time
|
||||
from httptools import HttpRequestParser
|
||||
|
@ -18,11 +18,30 @@ from .request import Request
|
|||
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
||||
|
||||
|
||||
current_time = None
|
||||
|
||||
|
||||
class Signal:
|
||||
stopped = False
|
||||
|
||||
|
||||
current_time = None
|
||||
class CIDict(dict):
|
||||
"""
|
||||
Case Insensitive dict where all keys are converted to lowercase
|
||||
This does not maintain the inputted case when calling items() or keys()
|
||||
in favor of speed, since headers are case insensitive
|
||||
"""
|
||||
def get(self, key, default=None):
|
||||
return super().get(key.casefold(), default)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return super().__getitem__(key.casefold())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
return super().__setitem__(key.casefold(), value)
|
||||
|
||||
def __contains__(self, key):
|
||||
return super().__contains__(key.casefold())
|
||||
|
||||
|
||||
class HttpProtocol(asyncio.Protocol):
|
||||
|
@ -70,15 +89,14 @@ class HttpProtocol(asyncio.Protocol):
|
|||
def connection_lost(self, exc):
|
||||
self.connections.discard(self)
|
||||
self._timeout_handler.cancel()
|
||||
self.cleanup()
|
||||
|
||||
def connection_timeout(self):
|
||||
# Check if
|
||||
time_elapsed = current_time - self._last_request_time
|
||||
if time_elapsed < self.request_timeout:
|
||||
time_left = self.request_timeout - time_elapsed
|
||||
self._timeout_handler = \
|
||||
self.loop.call_later(time_left, self.connection_timeout)
|
||||
self._timeout_handler = (
|
||||
self.loop.call_later(time_left, self.connection_timeout))
|
||||
else:
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
|
@ -118,18 +136,15 @@ class HttpProtocol(asyncio.Protocol):
|
|||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
|
||||
self.headers.append((name.decode(), value.decode('utf-8')))
|
||||
self.headers.append((name.decode().casefold(), value.decode()))
|
||||
|
||||
def on_headers_complete(self):
|
||||
remote_addr = self.transport.get_extra_info('peername')
|
||||
if remote_addr:
|
||||
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
|
||||
|
||||
self.request = Request(
|
||||
url_bytes=self.url,
|
||||
headers=CIMultiDict(self.headers),
|
||||
headers=CIDict(self.headers),
|
||||
version=self.parser.get_http_version(),
|
||||
method=self.parser.get_method().decode()
|
||||
method=self.parser.get_method().decode(),
|
||||
transport=self.transport
|
||||
)
|
||||
|
||||
def on_body(self, body):
|
||||
|
@ -148,32 +163,51 @@ class HttpProtocol(asyncio.Protocol):
|
|||
|
||||
def write_response(self, response):
|
||||
try:
|
||||
keep_alive = self.parser.should_keep_alive() \
|
||||
and not self.signal.stopped
|
||||
keep_alive = (
|
||||
self.parser.should_keep_alive() and not self.signal.stopped)
|
||||
self.transport.write(
|
||||
response.output(
|
||||
self.request.version, keep_alive, self.request_timeout))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before response written @ {}'.format(
|
||||
self.request.ip))
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(e))
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
else:
|
||||
# Record that we received data
|
||||
self._last_request_time = current_time
|
||||
self.cleanup()
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(e))
|
||||
|
||||
def write_error(self, exception):
|
||||
try:
|
||||
response = self.error_handler.response(self.request, exception)
|
||||
version = self.request.version if self.request else '1.1'
|
||||
self.transport.write(response.output(version))
|
||||
self.transport.close()
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before error written @ {}'.format(
|
||||
self.request.ip))
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing error failed, connection closed {}".format(e))
|
||||
"Writing error failed, connection closed {}".format(e),
|
||||
from_error=True)
|
||||
finally:
|
||||
self.transport.close()
|
||||
|
||||
def bail_out(self, message):
|
||||
def bail_out(self, message, from_error=False):
|
||||
if from_error and self.transport.is_closing():
|
||||
log.error(
|
||||
("Transport closed @ {} and exception "
|
||||
"experienced during error handling").format(
|
||||
self.transport.get_extra_info('peername')))
|
||||
log.debug(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
else:
|
||||
exception = ServerError(message)
|
||||
self.write_error(exception)
|
||||
log.error(message)
|
||||
|
@ -201,6 +235,7 @@ def update_current_time(loop):
|
|||
"""
|
||||
Caches the current time, since it is needed
|
||||
at the end of every keep-alive request to update the request timeout time
|
||||
|
||||
:param loop:
|
||||
:return:
|
||||
"""
|
||||
|
@ -225,10 +260,12 @@ def trigger_events(events, loop):
|
|||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100):
|
||||
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
register_sys_signals=True):
|
||||
"""
|
||||
Starts asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param request_handler: Sanic request handler with middleware
|
||||
|
@ -238,11 +275,14 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
:param after_start: Function to be executed after the server starts
|
||||
listening. Takes single argument `loop`
|
||||
:param before_stop: Function to be executed when a stop signal is
|
||||
received before it is respected. Takes single argumenet `loop`
|
||||
received before it is respected. Takes single
|
||||
argument `loop`
|
||||
:param after_stop: Function to be executed when a stop signal is
|
||||
received after it is respected. Takes single argumenet `loop`
|
||||
received after it is respected. Takes single
|
||||
argument `loop`
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
:param ssl: SSLContext
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param request_max_size: size in bytes, `None` for no limit
|
||||
:param reuse_port: `True` for multiple workers
|
||||
|
@ -275,6 +315,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
server,
|
||||
host,
|
||||
port,
|
||||
ssl=ssl,
|
||||
reuse_port=reuse_port,
|
||||
sock=sock,
|
||||
backlog=backlog
|
||||
|
@ -293,6 +334,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
trigger_events(after_start, loop)
|
||||
|
||||
# Register signals for graceful termination
|
||||
if register_sys_signals:
|
||||
for _signal in (SIGINT, SIGTERM):
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
|
||||
|
|
|
@ -15,12 +15,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
|
|||
"""
|
||||
Registers a static directory handler with Sanic by adding a route to the
|
||||
router and registering a handler.
|
||||
|
||||
:param app: Sanic
|
||||
:param file_or_directory: File or directory path to serve from
|
||||
:param uri: URL to serve from
|
||||
:param pattern: regular expression used to match files in the URL
|
||||
:param use_modified_since: If true, send file modified time, and return
|
||||
not modified if the browser's matches the server's
|
||||
not modified if the browser's matches the
|
||||
server's
|
||||
"""
|
||||
|
||||
# If we're not trying to match a file directly,
|
||||
|
|
|
@ -7,21 +7,25 @@ class HTTPMethodView:
|
|||
to every HTTP method you want to support.
|
||||
|
||||
For example:
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return text('I am get method')
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
return text('I am put method')
|
||||
|
||||
etc.
|
||||
|
||||
If someone tries to use a non-implemented method, there will be a
|
||||
405 response.
|
||||
|
||||
If you need any url params just mention them in method definition:
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
def get(self, request, my_param_here, *args, **kwargs):
|
||||
return text('I am get method with %s' % my_param_here)
|
||||
|
||||
|
@ -61,3 +65,38 @@ class HTTPMethodView:
|
|||
view.__doc__ = cls.__doc__
|
||||
view.__module__ = cls.__module__
|
||||
return view
|
||||
|
||||
|
||||
class CompositionView:
|
||||
""" Simple method-function mapped view for the sanic.
|
||||
You can add handler functions to methods (get, post, put, patch, delete)
|
||||
for every HTTP method you want to support.
|
||||
|
||||
For example:
|
||||
view = CompositionView()
|
||||
view.add(['GET'], lambda request: text('I am get method'))
|
||||
view.add(['POST', 'PUT'], lambda request: text('I am post/put method'))
|
||||
|
||||
etc.
|
||||
|
||||
If someone tries to use a non-implemented method, there will be a
|
||||
405 response.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
|
||||
def add(self, methods, handler):
|
||||
for method in methods:
|
||||
if method in self.handlers:
|
||||
raise KeyError(
|
||||
'Method {} already is registered.'.format(method))
|
||||
self.handlers[method] = handler
|
||||
|
||||
def __call__(self, request, *args, **kwargs):
|
||||
handler = self.handlers.get(request.method.upper(), None)
|
||||
if handler is None:
|
||||
raise InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
request.method, request.url), status_code=405)
|
||||
return handler(request, *args, **kwargs)
|
||||
|
|
1
setup.py
1
setup.py
|
@ -30,7 +30,6 @@ setup(
|
|||
'httptools>=0.0.9',
|
||||
'ujson>=1.35',
|
||||
'aiofiles>=0.3.0',
|
||||
'multidict>=2.0',
|
||||
],
|
||||
classifiers=[
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
@ -75,8 +76,13 @@ def test_handled_unhandled_exception(exception_app):
|
|||
request, response = sanic_endpoint_test(
|
||||
exception_app, uri='/divide_by_zero')
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while generating the response'
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
assert soup.h1.text == 'Internal Server Error'
|
||||
|
||||
message = " ".join(soup.p.text.split())
|
||||
assert message == (
|
||||
"The server encountered an internal error and "
|
||||
"cannot complete your request.")
|
||||
|
||||
def test_exception_in_exception_handler(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
|
@ -84,3 +90,23 @@ def test_exception_in_exception_handler(exception_app):
|
|||
exception_app, uri='/error_in_error_handler_handler')
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app,
|
||||
uri='/error_in_error_handler_handler',
|
||||
debug=False)
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app,
|
||||
uri='/error_in_error_handler_handler',
|
||||
debug=True)
|
||||
assert response.status == 500
|
||||
assert response.body.startswith(b'Exception raised in exception ')
|
||||
|
|
|
@ -2,6 +2,7 @@ from sanic import Sanic
|
|||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
exception_handler_app = Sanic('test_exception_handler')
|
||||
|
||||
|
@ -21,6 +22,12 @@ def handler_3(request):
|
|||
raise NotFound("OK")
|
||||
|
||||
|
||||
@exception_handler_app.route('/4')
|
||||
def handler_4(request):
|
||||
foo = bar
|
||||
return text(foo)
|
||||
|
||||
|
||||
@exception_handler_app.exception(NotFound, ServerError)
|
||||
def handler_exception(request, exception):
|
||||
return text("OK")
|
||||
|
@ -47,3 +54,20 @@ def test_text_exception__handler():
|
|||
exception_handler_app, uri='/random')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_html_traceback_output_in_debug_mode():
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_handler_app, uri='/4', debug=True)
|
||||
assert response.status == 500
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
html = str(soup)
|
||||
|
||||
assert 'response = handler(request, *args, **kwargs)' in html
|
||||
assert 'handler_4' in html
|
||||
assert 'foo = bar' in html
|
||||
|
||||
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||
assert (
|
||||
"NameError: name 'bar' "
|
||||
"is not defined while handling uri /4") == summary_text
|
||||
|
|
|
@ -19,7 +19,7 @@ def test_log():
|
|||
stream=log_stream
|
||||
)
|
||||
log = logging.getLogger()
|
||||
app = Sanic('test_logging', logger=True)
|
||||
app = Sanic('test_logging')
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
log.info('hello world')
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.response import json, text, redirect
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
import pytest
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
|
@ -188,3 +189,73 @@ def test_post_form_multipart_form_data():
|
|||
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
||||
|
||||
assert request.form.get('test') == 'OK'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def redirect_app():
|
||||
app = Sanic('test_redirection')
|
||||
|
||||
@app.route('/redirect_init')
|
||||
async def redirect_init(request):
|
||||
return redirect("/redirect_target")
|
||||
|
||||
@app.route('/redirect_init_with_301')
|
||||
async def redirect_init_with_301(request):
|
||||
return redirect("/redirect_target", status=301)
|
||||
|
||||
@app.route('/redirect_target')
|
||||
async def redirect_target(request):
|
||||
return text('OK')
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_redirect_default_302(redirect_app):
|
||||
"""
|
||||
We expect a 302 default status code and the headers to be set.
|
||||
"""
|
||||
request, response = sanic_endpoint_test(
|
||||
redirect_app, method="get",
|
||||
uri="/redirect_init",
|
||||
allow_redirects=False)
|
||||
|
||||
assert response.status == 302
|
||||
assert response.headers["Location"] == "/redirect_target"
|
||||
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
|
||||
|
||||
|
||||
def test_redirect_headers_none(redirect_app):
|
||||
request, response = sanic_endpoint_test(
|
||||
redirect_app, method="get",
|
||||
uri="/redirect_init",
|
||||
headers=None,
|
||||
allow_redirects=False)
|
||||
|
||||
assert response.status == 302
|
||||
assert response.headers["Location"] == "/redirect_target"
|
||||
|
||||
|
||||
def test_redirect_with_301(redirect_app):
|
||||
"""
|
||||
Test redirection with a different status code.
|
||||
"""
|
||||
request, response = sanic_endpoint_test(
|
||||
redirect_app, method="get",
|
||||
uri="/redirect_init_with_301",
|
||||
allow_redirects=False)
|
||||
|
||||
assert response.status == 301
|
||||
assert response.headers["Location"] == "/redirect_target"
|
||||
|
||||
|
||||
def test_get_then_redirect_follow_redirect(redirect_app):
|
||||
"""
|
||||
With `allow_redirects` we expect a 200.
|
||||
"""
|
||||
response = sanic_endpoint_test(
|
||||
redirect_app, method="get",
|
||||
uri="/redirect_init", gather_request=False,
|
||||
allow_redirects=True)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
|
|
@ -10,6 +10,84 @@ from sanic.utils import sanic_endpoint_test
|
|||
# UTF-8
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_shorthand_routes_get():
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/get', method='get')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/get', method='post')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_post():
|
||||
app = Sanic('test_shorhand_routes_post')
|
||||
|
||||
@app.post('/post')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/post', method='post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/post', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_put():
|
||||
app = Sanic('test_shorhand_routes_put')
|
||||
|
||||
@app.put('/put')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/put', method='put')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/put', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_patch():
|
||||
app = Sanic('test_shorhand_routes_patch')
|
||||
|
||||
@app.patch('/patch')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/patch', method='patch')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/patch', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_head():
|
||||
app = Sanic('test_shorhand_routes_head')
|
||||
|
||||
@app.head('/head')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/head', method='head')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/head', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_options():
|
||||
app = Sanic('test_shorhand_routes_options')
|
||||
|
||||
@app.options('/options')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='options')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_static_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
|
@ -463,3 +541,67 @@ def test_remove_route_without_clean_cache():
|
|||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_overload_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@app.route('/overload', methods=['GET'])
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
|
||||
@app.route('/overload', methods=['POST', 'PUT'])
|
||||
async def handler2(request):
|
||||
return text('OK2')
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'get', uri='/overload')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'post', uri='/overload')
|
||||
assert response.text == 'OK2'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'put', uri='/overload')
|
||||
assert response.text == 'OK2'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'delete', uri='/overload')
|
||||
assert response.status == 405
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/overload', methods=['PUT', 'DELETE'])
|
||||
async def handler3(request):
|
||||
return text('Duplicated')
|
||||
|
||||
|
||||
def test_unmergeable_overload_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@app.route('/overload_whole', methods=None)
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/overload_whole', methods=['POST', 'PUT'])
|
||||
async def handler2(request):
|
||||
return text('Duplicated')
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'get', uri='/overload_whole')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'post', uri='/overload_whole')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
|
||||
@app.route('/overload_part', methods=['GET'])
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/overload_part')
|
||||
async def handler2(request):
|
||||
return text('Duplicated')
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'get', uri='/overload_part')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, 'post', uri='/overload_part')
|
||||
assert response.status == 405
|
||||
|
|
41
tests/test_sanic.py
Normal file
41
tests/test_sanic.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
from sanic import Sanic
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.utils import HOST, PORT
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
|
||||
async def stop(app):
|
||||
await asyncio.sleep(0.2)
|
||||
app.stop()
|
||||
|
||||
|
||||
def test_register_system_signals():
|
||||
"""Test if sanic register system signals"""
|
||||
app = Sanic('test_register_system_signals')
|
||||
|
||||
@app.route('/hello')
|
||||
async def hello_route(request):
|
||||
return HTTPResponse()
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.add_signal_handler = MagicMock()
|
||||
asyncio.ensure_future(stop(app), loop=loop)
|
||||
app.run(HOST, PORT, loop=loop)
|
||||
assert loop.add_signal_handler.called == True
|
||||
|
||||
|
||||
def test_dont_register_system_signals():
|
||||
"""Test if sanic don't register system signals"""
|
||||
app = Sanic('test_register_system_signals')
|
||||
|
||||
@app.route('/hello')
|
||||
async def hello_route(request):
|
||||
return HTTPResponse()
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.add_signal_handler = MagicMock()
|
||||
asyncio.ensure_future(stop(app), loop=loop)
|
||||
app.run(HOST, PORT, loop=loop, register_sys_signals=False)
|
||||
assert loop.add_signal_handler.called == False
|
|
@ -4,7 +4,7 @@ from sanic.utils import sanic_endpoint_test
|
|||
|
||||
|
||||
def test_vhosts():
|
||||
app = Sanic('test_text')
|
||||
app = Sanic('test_vhosts')
|
||||
|
||||
@app.route('/', host="example.com")
|
||||
async def handler(request):
|
||||
|
@ -21,3 +21,19 @@ def test_vhosts():
|
|||
headers = {"Host": "subdomain.example.com"}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
assert response.text == "You're at subdomain.example.com!"
|
||||
|
||||
|
||||
def test_vhosts_with_list():
|
||||
app = Sanic('test_vhosts')
|
||||
|
||||
@app.route('/', host=["hello.com", "world.com"])
|
||||
async def handler(request):
|
||||
return text("Hello, world!")
|
||||
|
||||
headers = {"Host": "hello.com"}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
assert response.text == "Hello, world!"
|
||||
|
||||
headers = {"Host": "world.com"}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
assert response.text == "Hello, world!"
|
||||
|
|
Loading…
Reference in New Issue
Block a user