Merge branch 'master' into add_register_sys_signals_flag
This commit is contained in:
commit
214162adf0
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,11 +1,15 @@
|
||||||
*~
|
*~
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.egg
|
*.egg
|
||||||
|
*.eggs
|
||||||
|
*.pyc
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
coverage
|
coverage
|
||||||
.tox
|
.tox
|
||||||
settings.py
|
settings.py
|
||||||
*.pyc
|
|
||||||
.idea/*
|
.idea/*
|
||||||
.cache/*
|
.cache/*
|
||||||
|
.python-version
|
||||||
|
docs/_build/
|
||||||
|
docs/_api/
|
96
README.md
96
README.md
|
@ -1,96 +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)
|
|
||||||
* [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
|
||||||
|
|
||||||
Blueprints are objects that can be used for sub-routing within an application.
|
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
|
methods for adding routes, which are then registered with the application in a
|
||||||
flexible and pluggable manner.
|
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
|
||||||
Blueprints are especially useful for larger applications, where your application
|
responsibility.
|
||||||
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>`.
|
|
||||||
|
|
||||||
|
|
||||||
## My First Blueprint
|
## My First Blueprint
|
||||||
|
|
||||||
The following shows a very simple blueprint that registers a handler-function at
|
The following shows a very simple blueprint that registers a handler-function at
|
||||||
the root `/` of your application.
|
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.
|
main application later.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -34,7 +29,8 @@ async def bp_root(request):
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Registering Blueprints
|
## Registering blueprints
|
||||||
|
|
||||||
Blueprints must be registered with the application.
|
Blueprints must be registered with the application.
|
||||||
|
|
||||||
```python
|
```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
|
This will add the blueprint to the application and register any routes defined
|
||||||
by that blueprint.
|
by that blueprint. In this example, the registered routes in the `app.router`
|
||||||
In this example, the registered routes in the `app.router` will look like:
|
will look like:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
[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.
|
Using blueprints allows you to also register middleware globally.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -72,30 +73,36 @@ async def halt_response(request, response):
|
||||||
return text('I halted the response')
|
return text('I halted the response')
|
||||||
```
|
```
|
||||||
|
|
||||||
## Exceptions
|
### Exceptions
|
||||||
Exceptions can also be applied exclusively to blueprints globally.
|
|
||||||
|
Exceptions can be applied exclusively to blueprints globally.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@bp.exception(NotFound)
|
@bp.exception(NotFound)
|
||||||
def ignore_404s(request, exception):
|
def ignore_404s(request, exception):
|
||||||
return text("Yep, I totally found the page: {}".format(request.url))
|
return text("Yep, I totally found the page: {}".format(request.url))
|
||||||
|
```
|
||||||
|
|
||||||
## Static files
|
### Static files
|
||||||
Static files can also be served globally, under the blueprint prefix.
|
|
||||||
|
Static files can be served globally, under the blueprint prefix.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
bp.static('/folder/to/serve', '/web/path')
|
bp.static('/folder/to/serve', '/web/path')
|
||||||
```
|
```
|
||||||
|
|
||||||
## Start and Stop
|
## 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
|
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:
|
Available events are:
|
||||||
|
|
||||||
* before_server_start - Executed before the server begins to accept connections
|
- `before_server_start`: Executed before the server begins to accept connections
|
||||||
* after_server_start - Executed after 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
|
- `before_server_stop`: Executed before the server stops accepting connections
|
||||||
* after_server_stop - Executed after the server is stopped and all requests are complete
|
- `after_server_stop`: Executed after the server is stopped and all requests are complete
|
||||||
|
|
||||||
```python
|
```python
|
||||||
bp = Blueprint('my_blueprint')
|
bp = Blueprint('my_blueprint')
|
||||||
|
@ -109,3 +116,49 @@ async def setup_connection():
|
||||||
async def close_connection():
|
async def close_connection():
|
||||||
await database.close()
|
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
|
```python
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView
|
||||||
|
@ -10,7 +27,6 @@ from sanic.response import text
|
||||||
|
|
||||||
app = Sanic('some_name')
|
app = Sanic('some_name')
|
||||||
|
|
||||||
|
|
||||||
class SimpleView(HTTPMethodView):
|
class SimpleView(HTTPMethodView):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
@ -28,11 +44,14 @@ class SimpleView(HTTPMethodView):
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
return text('I am delete method')
|
return text('I am delete method')
|
||||||
|
|
||||||
app.add_route(SimpleView(), '/')
|
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
|
```python
|
||||||
class NameView(HTTPMethodView):
|
class NameView(HTTPMethodView):
|
||||||
|
@ -40,6 +59,24 @@ class NameView(HTTPMethodView):
|
||||||
def get(self, request, name):
|
def get(self, request, name):
|
||||||
return text('Hello {}'.format(name))
|
return text('Hello {}'.format(name))
|
||||||
|
|
||||||
app.add_route(NameView(), '/<name>')
|
app.add_route(NameView.as_view(), '/<name>')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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):
|
||||||
|
decorators = [some_decorator_here]
|
||||||
|
|
||||||
|
def get(self, request, name):
|
||||||
|
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
|
## Running tests
|
||||||
|
|
||||||
* `python -m pip install pytest`
|
* `python -m pip install pytest`
|
||||||
* `python -m pytest tests`
|
* `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
|
## 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
|
# 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
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@app.route("/cookie")
|
@app.route("/cookie")
|
||||||
|
@ -16,28 +16,11 @@ async def test(request):
|
||||||
return text("Test cookie set to: {}".format(test_cookie))
|
return text("Test cookie set to: {}".format(test_cookie))
|
||||||
```
|
```
|
||||||
|
|
||||||
## Response
|
## Writing cookies
|
||||||
|
|
||||||
Response cookies can be set like dictionary values and
|
When returning a response, cookies can be set on the `Response` object.
|
||||||
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
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@app.route("/cookie")
|
@app.route("/cookie")
|
||||||
|
@ -48,3 +31,22 @@ async def test(request):
|
||||||
response.cookies['test']['httponly'] = True
|
response.cookies['test']['httponly'] = True
|
||||||
return response
|
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)
|
||||||
|
|
76
docs/custom_protocol.md
Normal file
76
docs/custom_protocol.md
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
# Custom Protocols
|
||||||
|
|
||||||
|
*Note: this is advanced usage, and most readers will not need such functionality.*
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The constructor of the custom protocol class receives the following keyword
|
||||||
|
arguments from Sanic.
|
||||||
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
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
|
||||||
|
from sanic.server import HttpProtocol
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomHttpProtocol(HttpProtocol):
|
||||||
|
|
||||||
|
def __init__(self, *, loop, request_handler, error_handler,
|
||||||
|
signal, connections, request_timeout, request_max_size):
|
||||||
|
super().__init__(
|
||||||
|
loop=loop, request_handler=request_handler,
|
||||||
|
error_handler=error_handler, signal=signal,
|
||||||
|
connections=connections, request_timeout=request_timeout,
|
||||||
|
request_max_size=request_max_size)
|
||||||
|
|
||||||
|
def write_response(self, response):
|
||||||
|
if isinstance(response, str):
|
||||||
|
response = text(response)
|
||||||
|
self.transport.write(
|
||||||
|
response.output(self.request.version)
|
||||||
|
)
|
||||||
|
self.transport.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def string(request):
|
||||||
|
return 'string'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/1')
|
||||||
|
async def response(request):
|
||||||
|
return text('response')
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Previous:** [Cookies](cookies.md)
|
||||||
|
|
||||||
|
**Next:** [Testing](testing.md)
|
|
@ -1,35 +1,59 @@
|
||||||
# Deploying
|
# Deploying
|
||||||
|
|
||||||
When it comes to deploying Sanic, there's not much to it, but there are
|
Deploying Sanic is made simple by the inbuilt webserver. After defining an
|
||||||
a few things to take note of.
|
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
|
## Workers
|
||||||
|
|
||||||
By default, Sanic listens in the main process using only 1 CPU core.
|
By default, Sanic listens in the main process using only one CPU core. To crank
|
||||||
To crank up the juice, just specify the number of workers in the run
|
up the juice, just specify the number of workers in the `run` arguments.
|
||||||
arguments like so:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||||
```
|
```
|
||||||
|
|
||||||
Sanic will automatically spin up multiple processes and route
|
Sanic will automatically spin up multiple processes and route traffic between
|
||||||
traffic between them. We recommend as many workers as you have
|
them. We recommend as many workers as you have available cores.
|
||||||
available cores.
|
|
||||||
|
|
||||||
## Running via Command
|
## Running via command
|
||||||
|
|
||||||
If you like using command line arguments, you can launch a sanic server
|
If you like using command line arguments, you can launch a Sanic server by
|
||||||
by executing the module. For example, if you initialized sanic as
|
executing the module. For example, if you initialized Sanic as `app` in a file
|
||||||
app in a file named server.py, you could run the server like so:
|
named `server.py`, you could run the server like so:
|
||||||
|
|
||||||
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
|
`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
|
With this way of running sanic, it is not necessary to invoke `app.run` in your
|
||||||
your python file. If you do, just make sure you wrap it in name == main
|
Python file. If you do, make sure you wrap it so that it only executes when
|
||||||
like so:
|
directly run by the interpreter.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
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
|
||||||
|
|
||||||
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
|
## Throwing an exception
|
||||||
|
|
||||||
|
To throw an exception, simply `raise` the relevant exception from the
|
||||||
|
`sanic.exceptions` module.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
|
|
||||||
@app.route('/killme')
|
@app.route('/killme')
|
||||||
def i_am_ready_to_die(request):
|
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
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import NotFound
|
from sanic.exceptions import NotFound
|
||||||
|
|
||||||
|
@ -26,3 +33,17 @@ from sanic.exceptions import NotFound
|
||||||
def ignore_404s(request, exception):
|
def ignore_404s(request, exception):
|
||||||
return text("Yep, I totally found the page: {}".format(request.url))
|
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)
|
||||||
|
|
11
docs/extensions.md
Normal file
11
docs/extensions.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
**Previous:** [Testing](testing.md)
|
||||||
|
|
||||||
|
**Next:** [Contributing](contributing.md)
|
|
@ -1,25 +1,29 @@
|
||||||
# Getting Started
|
# 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.
|
||||||
|
|
||||||
|
1. Install Sanic: `python3 -m pip install sanic`
|
||||||
|
2. Create a file called `main.py` with the following code:
|
||||||
|
|
||||||
## Benchmarks
|
|
||||||
* Install Sanic
|
|
||||||
* `python3 -m pip install sanic`
|
|
||||||
* Edit main.py to include:
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import text
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return json({ "hello": "world" })
|
return text('Hello world!')
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
```
|
```
|
||||||
* Run `python3 main.py`
|
|
||||||
|
|
||||||
You now have a working Sanic server! To continue on, check out:
|
3. Run the server: `python3 main.py`
|
||||||
* [Request Data](request_data.md)
|
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||||
* [Routing](routing.md)
|
the message *Hello world!*.
|
||||||
|
|
||||||
|
You now have a working Sanic server!
|
||||||
|
|
||||||
|
**Next:** [Routing](routing.md)
|
||||||
|
|
33
docs/index.rst
Normal file
33
docs/index.rst
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
.. include:: ../README.rst
|
||||||
|
|
||||||
|
Guides
|
||||||
|
======
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
getting_started
|
||||||
|
routing
|
||||||
|
request_data
|
||||||
|
deploying
|
||||||
|
static_files
|
||||||
|
middleware
|
||||||
|
exceptions
|
||||||
|
blueprints
|
||||||
|
class_based_views
|
||||||
|
cookies
|
||||||
|
custom_protocol
|
||||||
|
testing
|
||||||
|
extensions
|
||||||
|
contributing
|
||||||
|
|
||||||
|
|
||||||
|
Module Documentation
|
||||||
|
====================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
Module Reference <_api/sanic>
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`search`
|
|
@ -1,36 +1,32 @@
|
||||||
# Middleware
|
# 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
|
```python
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
@app.middleware
|
|
||||||
async def halt_request(request):
|
|
||||||
print("I am a spy")
|
|
||||||
|
|
||||||
@app.middleware('request')
|
@app.middleware('request')
|
||||||
async def halt_request(request):
|
async def print_on_request(request):
|
||||||
return text('I halted the request')
|
print("I print when a request is received by the server")
|
||||||
|
|
||||||
@app.middleware('response')
|
@app.middleware('response')
|
||||||
async def halt_response(request, response):
|
async def print_on_response(request, response):
|
||||||
return text('I halted the response')
|
print("I print when a response is returned by the server")
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
async def handler(request):
|
|
||||||
return text('I would like to speak now please')
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
```python
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
@ -46,4 +42,29 @@ async def prevent_xss(request, response):
|
||||||
app.run(host="0.0.0.0", port=8000)
|
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,30 +1,37 @@
|
||||||
# Request Data
|
# 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
|
- `json` (any) - JSON body
|
||||||
`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
|
|
||||||
|
|
||||||
See request.py for more information
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
from sanic.response import json
|
||||||
from sanic.response import json, text
|
|
||||||
|
|
||||||
@app.route("/json")
|
@app.route("/json")
|
||||||
def post_json(request):
|
def post_json(request):
|
||||||
return json({ "received": True, "message": request.json })
|
return json({ "received": True, "message": request.json })
|
||||||
|
```
|
||||||
|
|
||||||
@app.route("/form")
|
- `args` (dict) - Query string variables. A query string is the section of a
|
||||||
def post_json(request):
|
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
||||||
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
||||||
|
The request's `query_string` variable holds the unparsed string value.
|
||||||
|
|
||||||
|
```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")
|
@app.route("/files")
|
||||||
def post_json(request):
|
def post_json(request):
|
||||||
|
@ -37,13 +44,52 @@ def post_json(request):
|
||||||
}
|
}
|
||||||
|
|
||||||
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
||||||
|
```
|
||||||
|
|
||||||
@app.route("/query_string")
|
- `form` (dict) - Posted form variables.
|
||||||
def query_string(request):
|
|
||||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@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",])
|
@app.route("/users", methods=["POST",])
|
||||||
def create_user(request):
|
def create_user(request):
|
||||||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
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
|
# 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.
|
||||||
|
|
||||||
|
A basic route looks like the following, where `app` is an instance of the
|
||||||
## Examples
|
`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
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@app.route('/tag/<tag>')
|
@app.route('/tag/<tag>')
|
||||||
async def tag_handler(request, tag):
|
async def tag_handler(request, tag):
|
||||||
return text('Tag - {}'.format(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>')
|
@app.route('/number/<integer_arg:int>')
|
||||||
async def integer_handler(request, integer_arg):
|
async def integer_handler(request, integer_arg):
|
||||||
|
@ -29,16 +60,52 @@ async def person_handler(request, name):
|
||||||
async def folder_handler(request, folder_id):
|
async def folder_handler(request, folder_id):
|
||||||
return text('Folder - {}'.format(folder_id))
|
return text('Folder - {}'.format(folder_id))
|
||||||
|
|
||||||
async def handler1(request):
|
```
|
||||||
return text('OK')
|
|
||||||
app.add_route(handler1, '/test')
|
|
||||||
|
|
||||||
async def handler(request, name):
|
## HTTP request types
|
||||||
return text('Folder - {}'.format(name))
|
|
||||||
app.add_route(handler, '/folder/<name>')
|
|
||||||
|
|
||||||
async def person_handler(request, name):
|
By default, a route defined on a URL will be used for all requests to that URL.
|
||||||
return text('Person - {}'.format(name))
|
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
||||||
app.add_route(handler, '/person/<name:[A-z]>')
|
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')
|
||||||
|
|
||||||
|
async def handler2(request, name):
|
||||||
|
return text('Folder - {}'.format(name))
|
||||||
|
|
||||||
|
async def person_handler2(request, name):
|
||||||
|
return text('Person - {}'.format(name))
|
||||||
|
|
||||||
|
# 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
|
# Static Files
|
||||||
|
|
||||||
Both directories and files can be served by registering with static
|
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
|
||||||
## Example
|
filename. The file specified will then be accessible via the given endpoint.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
# Serves files from the static folder to the URL /static
|
# 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)
|
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 that the server responds with the challenge string
|
||||||
assert response.text == request_data['challenge']
|
assert response.text == request_data['challenge']
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Previous:** [Custom protocols](custom_protocol.md)
|
||||||
|
|
||||||
|
**Next:** [Sanic extensions](extensions.md)
|
||||||
|
|
|
@ -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
|
class' default handler, we can do anything including sending exceptions to
|
||||||
an external service.
|
an external service.
|
||||||
"""
|
"""
|
||||||
|
from sanic.exceptions import Handler, SanicException
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Imports and code relevant for our CustomHandler class
|
Imports and code relevant for our CustomHandler class
|
||||||
(Ordinarily this would be in a separate file)
|
(Ordinarily this would be in a separate file)
|
||||||
"""
|
"""
|
||||||
from sanic.response import text
|
|
||||||
from sanic.exceptions import Handler, SanicException
|
|
||||||
|
|
||||||
class CustomHandler(Handler):
|
class CustomHandler(Handler):
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
# Here, we have access to the exception object
|
# Here, we have access to the exception object
|
||||||
# and can do anything with it (log, send to external service, etc)
|
# 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
|
# Then, we must finish handling the exception by returning
|
||||||
# our response to the client
|
# our response to the client
|
||||||
# For this we can just call the super class' default handler
|
# 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)
|
handler = CustomHandler(sanic=app)
|
||||||
app.error_handler = handler
|
app.error_handler = handler
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
# Here, something occurs which causes an unexpected exception
|
# Here, something occurs which causes an unexpected exception
|
||||||
# This exception will flow to our custom handler.
|
# This exception will flow to our custom handler.
|
||||||
x = 1 / 0
|
1 / 0
|
||||||
return json({"test": True})
|
return json({"test": True})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ logging.basicConfig(
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
# Set logger to override default basicConfig
|
# Set logger to override default basicConfig
|
||||||
sanic = Sanic(logger=True)
|
sanic = Sanic()
|
||||||
@sanic.route("/")
|
@sanic.route("/")
|
||||||
def test(request):
|
def test(request):
|
||||||
log.info("received request; responding with 'hey'")
|
log.info("received request; responding with 'hey'")
|
||||||
|
|
65
examples/sanic_asyncpg_example.py
Normal file
65
examples/sanic_asyncpg_example.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
""" To run this example you need additional asyncpg package
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import uvloop
|
||||||
|
from asyncpg import create_pool
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
'host': '<host>',
|
||||||
|
'user': '<username>',
|
||||||
|
'password': '<password>',
|
||||||
|
'port': '<port>',
|
||||||
|
'database': '<database>'
|
||||||
|
}
|
||||||
|
|
||||||
|
def jsonify(records):
|
||||||
|
""" Parse asyncpg record response into JSON format
|
||||||
|
|
||||||
|
"""
|
||||||
|
return [{key: value for key, value in
|
||||||
|
zip(r.keys(), r.values())} for r in records]
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
async def make_pool():
|
||||||
|
return await create_pool(**DB_CONFIG)
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
pool = loop.run_until_complete(make_pool())
|
||||||
|
|
||||||
|
async def create_db():
|
||||||
|
""" Create some table and add some data
|
||||||
|
|
||||||
|
"""
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
async with connection.transaction():
|
||||||
|
await connection.execute('DROP TABLE IF EXISTS sanic_post')
|
||||||
|
await connection.execute("""CREATE TABLE sanic_post (
|
||||||
|
id serial primary key,
|
||||||
|
content varchar(50),
|
||||||
|
post_date timestamp
|
||||||
|
);""")
|
||||||
|
for i in range(0, 100):
|
||||||
|
await connection.execute(f"""INSERT INTO sanic_post
|
||||||
|
(id, content, post_date) VALUES ({i}, {i}, now())""")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def handler(request):
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
async with connection.transaction():
|
||||||
|
results = await connection.fetch('SELECT * FROM sanic_post')
|
||||||
|
return json({'posts': jsonify(results)})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
loop.run_until_complete(create_db())
|
||||||
|
app.run(host='0.0.0.0', port=8000, loop=loop)
|
|
@ -64,11 +64,11 @@ def query_string(request):
|
||||||
# Run Server
|
# Run Server
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
def after_start(loop):
|
def after_start(app, loop):
|
||||||
log.info("OH OH OH OH OHHHHHHHH")
|
log.info("OH OH OH OH OHHHHHHHH")
|
||||||
|
|
||||||
|
|
||||||
def before_stop(loop):
|
def before_stop(app, loop):
|
||||||
log.info("TRIED EVERYTHING")
|
log.info("TRIED EVERYTHING")
|
||||||
|
|
||||||
|
|
||||||
|
|
39
examples/vhosts.py
Normal file
39
examples/vhosts.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
# curl -H "Host: example.com" localhost:8000
|
||||||
|
# curl -H "Host: sub.example.com" localhost:8000
|
||||||
|
# curl -H "Host: bp.example.com" localhost:8000/question
|
||||||
|
# curl -H "Host: bp.example.com" localhost:8000/answer
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
@bp.route("/question")
|
||||||
|
async def hello(request):
|
||||||
|
return text("What is the meaning of life?")
|
||||||
|
|
||||||
|
@bp.route("/answer")
|
||||||
|
async def hello(request):
|
||||||
|
return text("42")
|
||||||
|
|
||||||
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
|
@ -12,3 +12,6 @@ kyoukai
|
||||||
falcon
|
falcon
|
||||||
tornado
|
tornado
|
||||||
aiofiles
|
aiofiles
|
||||||
|
sphinx
|
||||||
|
recommonmark
|
||||||
|
beautifulsoup4
|
||||||
|
|
|
@ -2,4 +2,3 @@ httptools
|
||||||
ujson
|
ujson
|
||||||
uvloop
|
uvloop
|
||||||
aiofiles
|
aiofiles
|
||||||
multidict
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from .sanic import Sanic
|
from .sanic import Sanic
|
||||||
from .blueprints import Blueprint
|
from .blueprints import Blueprint
|
||||||
|
|
||||||
__version__ = '0.1.9'
|
__version__ = '0.2.0'
|
||||||
|
|
||||||
__all__ = ['Sanic', 'Blueprint']
|
__all__ = ['Sanic', 'Blueprint']
|
||||||
|
|
|
@ -20,7 +20,7 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
module = import_module(module_name)
|
module = import_module(module_name)
|
||||||
app = getattr(module, app_name, None)
|
app = getattr(module, app_name, None)
|
||||||
if type(app) is not Sanic:
|
if not isinstance(app, Sanic):
|
||||||
raise ValueError("Module is not a Sanic app, it is a {}. "
|
raise ValueError("Module is not a Sanic app, it is a {}. "
|
||||||
"Perhaps you meant {}.app?"
|
"Perhaps you meant {}.app?"
|
||||||
.format(type(app).__name__, args.module))
|
.format(type(app).__name__, args.module))
|
||||||
|
|
|
@ -3,6 +3,7 @@ from collections import defaultdict
|
||||||
|
|
||||||
class BlueprintSetup:
|
class BlueprintSetup:
|
||||||
"""
|
"""
|
||||||
|
Creates a blueprint state like object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, blueprint, app, options):
|
def __init__(self, blueprint, app, options):
|
||||||
|
@ -18,24 +19,27 @@ class BlueprintSetup:
|
||||||
#: blueprint.
|
#: blueprint.
|
||||||
self.url_prefix = url_prefix
|
self.url_prefix = url_prefix
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods):
|
def add_route(self, handler, uri, methods, host=None):
|
||||||
"""
|
"""
|
||||||
A helper method to register a handler to the application url routes.
|
A helper method to register a handler to the application url routes.
|
||||||
"""
|
"""
|
||||||
if self.url_prefix:
|
if self.url_prefix:
|
||||||
uri = self.url_prefix + uri
|
uri = self.url_prefix + uri
|
||||||
|
|
||||||
self.app.route(uri=uri, methods=methods)(handler)
|
if host is None:
|
||||||
|
host = self.blueprint.host
|
||||||
|
|
||||||
|
self.app.route(uri=uri, methods=methods, host=host)(handler)
|
||||||
|
|
||||||
def add_exception(self, handler, *args, **kwargs):
|
def add_exception(self, handler, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Registers exceptions to sanic
|
Registers exceptions to sanic.
|
||||||
"""
|
"""
|
||||||
self.app.exception(*args, **kwargs)(handler)
|
self.app.exception(*args, **kwargs)(handler)
|
||||||
|
|
||||||
def add_static(self, uri, file_or_directory, *args, **kwargs):
|
def add_static(self, uri, file_or_directory, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Registers static files to sanic
|
Registers static files to sanic.
|
||||||
"""
|
"""
|
||||||
if self.url_prefix:
|
if self.url_prefix:
|
||||||
uri = self.url_prefix + uri
|
uri = self.url_prefix + uri
|
||||||
|
@ -44,7 +48,7 @@ class BlueprintSetup:
|
||||||
|
|
||||||
def add_middleware(self, middleware, *args, **kwargs):
|
def add_middleware(self, middleware, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Registers middleware to sanic
|
Registers middleware to sanic.
|
||||||
"""
|
"""
|
||||||
if args or kwargs:
|
if args or kwargs:
|
||||||
self.app.middleware(*args, **kwargs)(middleware)
|
self.app.middleware(*args, **kwargs)(middleware)
|
||||||
|
@ -53,7 +57,7 @@ class BlueprintSetup:
|
||||||
|
|
||||||
|
|
||||||
class Blueprint:
|
class Blueprint:
|
||||||
def __init__(self, name, url_prefix=None):
|
def __init__(self, name, url_prefix=None, host=None):
|
||||||
"""
|
"""
|
||||||
Creates a new blueprint
|
Creates a new blueprint
|
||||||
:param name: Unique name of the blueprint
|
:param name: Unique name of the blueprint
|
||||||
|
@ -63,6 +67,7 @@ class Blueprint:
|
||||||
self.url_prefix = url_prefix
|
self.url_prefix = url_prefix
|
||||||
self.deferred_functions = []
|
self.deferred_functions = []
|
||||||
self.listeners = defaultdict(list)
|
self.listeners = defaultdict(list)
|
||||||
|
self.host = host
|
||||||
|
|
||||||
def record(self, func):
|
def record(self, func):
|
||||||
"""
|
"""
|
||||||
|
@ -73,32 +78,43 @@ class Blueprint:
|
||||||
|
|
||||||
def make_setup_state(self, app, options):
|
def make_setup_state(self, app, options):
|
||||||
"""
|
"""
|
||||||
|
Returns a new BlueprintSetup object
|
||||||
"""
|
"""
|
||||||
return BlueprintSetup(self, app, options)
|
return BlueprintSetup(self, app, options)
|
||||||
|
|
||||||
def register(self, app, options):
|
def register(self, app, options):
|
||||||
"""
|
"""
|
||||||
|
Registers the blueprint to the sanic app.
|
||||||
"""
|
"""
|
||||||
state = self.make_setup_state(app, options)
|
state = self.make_setup_state(app, options)
|
||||||
for deferred in self.deferred_functions:
|
for deferred in self.deferred_functions:
|
||||||
deferred(state)
|
deferred(state)
|
||||||
|
|
||||||
def route(self, uri, methods=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):
|
def decorator(handler):
|
||||||
self.record(lambda s: s.add_route(handler, uri, methods))
|
self.record(lambda s: s.add_route(handler, uri, methods, host))
|
||||||
return handler
|
return handler
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods=None):
|
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))
|
self.record(lambda s: s.add_route(handler, uri, methods, host))
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def listener(self, event):
|
def listener(self, event):
|
||||||
"""
|
"""
|
||||||
|
Create a listener from a decorated function.
|
||||||
|
:param event: Event to listen to.
|
||||||
"""
|
"""
|
||||||
def decorator(listener):
|
def decorator(listener):
|
||||||
self.listeners[event].append(listener)
|
self.listeners[event].append(listener)
|
||||||
|
@ -107,6 +123,7 @@ class Blueprint:
|
||||||
|
|
||||||
def middleware(self, *args, **kwargs):
|
def middleware(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
Creates a blueprint middleware from a decorated function.
|
||||||
"""
|
"""
|
||||||
def register_middleware(middleware):
|
def register_middleware(middleware):
|
||||||
self.record(
|
self.record(
|
||||||
|
@ -123,6 +140,7 @@ class Blueprint:
|
||||||
|
|
||||||
def exception(self, *args, **kwargs):
|
def exception(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
Creates a blueprint exception from a decorated function.
|
||||||
"""
|
"""
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
|
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
|
||||||
|
@ -131,6 +149,9 @@ class Blueprint:
|
||||||
|
|
||||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
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(
|
self.record(
|
||||||
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))
|
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))
|
||||||
|
|
|
@ -1,5 +1,104 @@
|
||||||
from .response import text
|
from .response import text, html
|
||||||
from traceback import format_exc
|
from .log import log
|
||||||
|
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):
|
class SanicException(Exception):
|
||||||
|
@ -45,29 +144,61 @@ class Handler:
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
self.sanic = sanic
|
self.sanic = sanic
|
||||||
|
|
||||||
|
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):
|
def add(self, exception, handler):
|
||||||
self.handlers[exception] = handler
|
self.handlers[exception] = handler
|
||||||
|
|
||||||
def response(self, request, exception):
|
def response(self, request, exception):
|
||||||
"""
|
"""
|
||||||
Fetches and executes an exception handler and returns a response object
|
Fetches and executes an exception handler and returns a response object
|
||||||
|
|
||||||
:param request: Request
|
:param request: Request
|
||||||
:param exception: Exception to handle
|
:param exception: Exception to handle
|
||||||
:return: Response object
|
:return: Response object
|
||||||
"""
|
"""
|
||||||
handler = self.handlers.get(type(exception), self.default)
|
handler = self.handlers.get(type(exception), self.default)
|
||||||
|
try:
|
||||||
response = handler(request=request, exception=exception)
|
response = handler(request=request, exception=exception)
|
||||||
|
except:
|
||||||
|
log.error(format_exc())
|
||||||
|
if self.sanic.debug:
|
||||||
|
response_message = (
|
||||||
|
'Exception raised in exception handler "{}" '
|
||||||
|
'for uri: "{}"\n{}').format(
|
||||||
|
handler.__name__, request.url, format_exc())
|
||||||
|
log.error(response_message)
|
||||||
|
return text(response_message, 500)
|
||||||
|
else:
|
||||||
|
return text('An error occurred while handling an error', 500)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
|
log.error(format_exc())
|
||||||
if issubclass(type(exception), SanicException):
|
if issubclass(type(exception), SanicException):
|
||||||
return text(
|
return text(
|
||||||
"Error: {}".format(exception),
|
'Error: {}'.format(exception),
|
||||||
status=getattr(exception, 'status_code', 500))
|
status=getattr(exception, 'status_code', 500))
|
||||||
elif self.sanic.debug:
|
elif self.sanic.debug:
|
||||||
return text(
|
html_output = self._render_traceback_html(exception, request)
|
||||||
"Error: {}\nException: {}".format(
|
|
||||||
exception, format_exc()), status=500)
|
response_message = (
|
||||||
|
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||||
|
request.url, format_exc()))
|
||||||
|
log.error(response_message)
|
||||||
|
return html(html_output, status=500)
|
||||||
else:
|
else:
|
||||||
return text(
|
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||||
"An error occurred while generating the request", status=500)
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger('sanic')
|
||||||
|
|
|
@ -21,16 +21,13 @@ class RequestParameters(dict):
|
||||||
value of the list and getlist returns the whole shebang
|
value of the list and getlist returns the whole shebang
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.super = super()
|
|
||||||
self.super.__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def get(self, name, default=None):
|
def get(self, name, default=None):
|
||||||
values = self.super.get(name)
|
"""Return the first value, either the default or actual"""
|
||||||
return values[0] if values else default
|
return super().get(name, [default])[0]
|
||||||
|
|
||||||
def getlist(self, name, default=None):
|
def getlist(self, name, default=None):
|
||||||
return self.super.get(name, default)
|
"""Return the entire list"""
|
||||||
|
return super().get(name, default)
|
||||||
|
|
||||||
|
|
||||||
class Request(dict):
|
class Request(dict):
|
||||||
|
@ -38,18 +35,20 @@ class Request(dict):
|
||||||
Properties of an HTTP request such as URL, headers, etc.
|
Properties of an HTTP request such as URL, headers, etc.
|
||||||
"""
|
"""
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'url', 'headers', 'version', 'method', '_cookies',
|
'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||||
'query_string', 'body',
|
'query_string', 'body',
|
||||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
'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
|
# TODO: Content-Encoding detection
|
||||||
url_parsed = parse_url(url_bytes)
|
url_parsed = parse_url(url_bytes)
|
||||||
self.url = url_parsed.path.decode('utf-8')
|
self.url = url_parsed.path.decode('utf-8')
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
self.version = version
|
self.version = version
|
||||||
self.method = method
|
self.method = method
|
||||||
|
self.transport = transport
|
||||||
self.query_string = None
|
self.query_string = None
|
||||||
if url_parsed.query:
|
if url_parsed.query:
|
||||||
self.query_string = url_parsed.query.decode('utf-8')
|
self.query_string = url_parsed.query.decode('utf-8')
|
||||||
|
@ -64,7 +63,7 @@ class Request(dict):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
if not self.parsed_json:
|
if self.parsed_json is None:
|
||||||
try:
|
try:
|
||||||
self.parsed_json = json_loads(self.body)
|
self.parsed_json = json_loads(self.body)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -72,6 +71,17 @@ class Request(dict):
|
||||||
|
|
||||||
return self.parsed_json
|
return self.parsed_json
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self):
|
||||||
|
"""
|
||||||
|
Attempts to return the auth header token.
|
||||||
|
:return: token related to request
|
||||||
|
"""
|
||||||
|
auth_header = self.headers.get('Authorization')
|
||||||
|
if auth_header is not None:
|
||||||
|
return auth_header.split()[1]
|
||||||
|
return auth_header
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self):
|
def form(self):
|
||||||
if self.parsed_form is None:
|
if self.parsed_form is None:
|
||||||
|
@ -125,6 +135,12 @@ class Request(dict):
|
||||||
self._cookies = {}
|
self._cookies = {}
|
||||||
return 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'])
|
File = namedtuple('File', ['type', 'body', 'name'])
|
||||||
|
|
||||||
|
@ -132,6 +148,7 @@ File = namedtuple('File', ['type', 'body', 'name'])
|
||||||
def parse_multipart_form(body, boundary):
|
def parse_multipart_form(body, boundary):
|
||||||
"""
|
"""
|
||||||
Parses a request body and returns fields and files
|
Parses a request body and returns fields and files
|
||||||
|
|
||||||
:param body: Bytes request body
|
:param body: Bytes request body
|
||||||
:param boundary: Bytes multipart boundary
|
:param boundary: Bytes multipart boundary
|
||||||
:return: fields (RequestParameters), files (RequestParameters)
|
:return: fields (RequestParameters), files (RequestParameters)
|
||||||
|
|
|
@ -83,10 +83,10 @@ class HTTPResponse:
|
||||||
if body is not None:
|
if body is not None:
|
||||||
try:
|
try:
|
||||||
# Try to encode it regularly
|
# Try to encode it regularly
|
||||||
self.body = body.encode('utf-8')
|
self.body = body.encode()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Convert it to a str if you can't
|
# Convert it to a str if you can't
|
||||||
self.body = str(body).encode('utf-8')
|
self.body = str(body).encode()
|
||||||
else:
|
else:
|
||||||
self.body = body_bytes
|
self.body = body_bytes
|
||||||
|
|
||||||
|
@ -103,10 +103,14 @@ class HTTPResponse:
|
||||||
|
|
||||||
headers = b''
|
headers = b''
|
||||||
if self.headers:
|
if self.headers:
|
||||||
headers = b''.join(
|
for name, value in self.headers.items():
|
||||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
try:
|
||||||
for name, value in self.headers.items()
|
headers += (
|
||||||
)
|
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
|
||||||
|
except AttributeError:
|
||||||
|
headers += (
|
||||||
|
b'%b: %b\r\n' % (
|
||||||
|
str(name).encode(), str(value).encode('utf-8')))
|
||||||
|
|
||||||
# Try to pull from the common codes first
|
# Try to pull from the common codes first
|
||||||
# Speeds up response rate 6% over pulling from all
|
# Speeds up response rate 6% over pulling from all
|
||||||
|
@ -139,21 +143,45 @@ class HTTPResponse:
|
||||||
|
|
||||||
|
|
||||||
def json(body, status=200, headers=None):
|
def json(body, status=200, headers=None):
|
||||||
|
"""
|
||||||
|
Returns response object with body in json format.
|
||||||
|
:param body: Response data to be serialized.
|
||||||
|
:param status: Response code.
|
||||||
|
:param headers: Custom Headers.
|
||||||
|
"""
|
||||||
return HTTPResponse(json_dumps(body), headers=headers, status=status,
|
return HTTPResponse(json_dumps(body), headers=headers, status=status,
|
||||||
content_type="application/json")
|
content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
def text(body, status=200, headers=None):
|
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,
|
return HTTPResponse(body, status=status, headers=headers,
|
||||||
content_type="text/plain; charset=utf-8")
|
content_type="text/plain; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
def html(body, status=200, headers=None):
|
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,
|
return HTTPResponse(body, status=status, headers=headers,
|
||||||
content_type="text/html; charset=utf-8")
|
content_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
async def file(location, mime_type=None, headers=None):
|
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]
|
filename = path.split(location)[-1]
|
||||||
|
|
||||||
async with open_async(location, mode='rb') as _file:
|
async with open_async(location, mode='rb') as _file:
|
||||||
|
@ -165,3 +193,26 @@ async def file(location, mime_type=None, headers=None):
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=mime_type,
|
content_type=mime_type,
|
||||||
body_bytes=out_stream)
|
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)
|
||||||
|
|
|
@ -3,6 +3,7 @@ from collections import defaultdict, namedtuple
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .exceptions import NotFound, InvalidUsage
|
from .exceptions import NotFound, InvalidUsage
|
||||||
|
from .views import CompositionView
|
||||||
|
|
||||||
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
|
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
|
||||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||||
|
@ -23,16 +24,28 @@ class RouteExists(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RouteDoesNotExist(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Router:
|
class Router:
|
||||||
"""
|
"""
|
||||||
Router supports basic routing with parameters and method checks
|
Router supports basic routing with parameters and method checks
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
|
|
||||||
def my_route(request, my_parameter):
|
.. code-block:: python
|
||||||
|
|
||||||
|
@sanic.route('/my/url/<my_param>', methods=['GET', 'POST', ...])
|
||||||
|
def my_route(request, my_param):
|
||||||
do stuff...
|
do stuff...
|
||||||
|
|
||||||
or
|
or
|
||||||
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...])
|
|
||||||
def my_route_with_type(request, my_parameter):
|
.. 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...
|
do stuff...
|
||||||
|
|
||||||
Parameters will be passed as keyword arguments to the request handling
|
Parameters will be passed as keyword arguments to the request handling
|
||||||
|
@ -51,10 +64,12 @@ class Router:
|
||||||
self.routes_static = {}
|
self.routes_static = {}
|
||||||
self.routes_dynamic = defaultdict(list)
|
self.routes_dynamic = defaultdict(list)
|
||||||
self.routes_always_check = []
|
self.routes_always_check = []
|
||||||
|
self.hosts = None
|
||||||
|
|
||||||
def add(self, uri, methods, handler):
|
def add(self, uri, methods, handler, host=None):
|
||||||
"""
|
"""
|
||||||
Adds a handler to the route list
|
Adds a handler to the route list
|
||||||
|
|
||||||
:param uri: Path to match
|
:param uri: Path to match
|
||||||
:param methods: Array of accepted method names.
|
:param methods: Array of accepted method names.
|
||||||
If none are provided, any method is allowed
|
If none are provided, any method is allowed
|
||||||
|
@ -62,8 +77,23 @@ class Router:
|
||||||
When executed, it should provide a response object.
|
When executed, it should provide a response object.
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if uri in self.routes_all:
|
|
||||||
raise RouteExists("Route already registered: {}".format(uri))
|
if host is not None:
|
||||||
|
# we want to track if there are any
|
||||||
|
# vhosts on the Router instance so that we can
|
||||||
|
# default to the behavior without vhosts
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
for h in host:
|
||||||
|
self.add(uri, methods, handler, h)
|
||||||
|
return
|
||||||
|
|
||||||
# Dict for faster lookups of if method allowed
|
# Dict for faster lookups of if method allowed
|
||||||
if methods:
|
if methods:
|
||||||
|
@ -97,6 +127,32 @@ class Router:
|
||||||
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
|
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
|
||||||
pattern = re.compile(r'^{}$'.format(pattern_string))
|
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(
|
route = Route(
|
||||||
handler=handler, methods=methods, pattern=pattern,
|
handler=handler, methods=methods, pattern=pattern,
|
||||||
parameters=parameters)
|
parameters=parameters)
|
||||||
|
@ -109,17 +165,41 @@ class Router:
|
||||||
else:
|
else:
|
||||||
self.routes_static[uri] = route
|
self.routes_static[uri] = route
|
||||||
|
|
||||||
|
def remove(self, uri, clean_cache=True, host=None):
|
||||||
|
if host is not None:
|
||||||
|
uri = host + uri
|
||||||
|
try:
|
||||||
|
route = self.routes_all.pop(uri)
|
||||||
|
except KeyError:
|
||||||
|
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
|
||||||
|
|
||||||
|
if route in self.routes_always_check:
|
||||||
|
self.routes_always_check.remove(route)
|
||||||
|
elif url_hash(uri) in self.routes_dynamic \
|
||||||
|
and route in self.routes_dynamic[url_hash(uri)]:
|
||||||
|
self.routes_dynamic[url_hash(uri)].remove(route)
|
||||||
|
else:
|
||||||
|
self.routes_static.pop(uri)
|
||||||
|
|
||||||
|
if clean_cache:
|
||||||
|
self._get.cache_clear()
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""
|
"""
|
||||||
Gets a request handler based on the URL of the request, or raises an
|
Gets a request handler based on the URL of the request, or raises an
|
||||||
error
|
error
|
||||||
|
|
||||||
:param request: Request object
|
:param request: Request object
|
||||||
:return: handler, arguments, keyword arguments
|
:return: handler, arguments, keyword arguments
|
||||||
"""
|
"""
|
||||||
return self._get(request.url, request.method)
|
if self.hosts is None:
|
||||||
|
return self._get(request.url, request.method, '')
|
||||||
|
else:
|
||||||
|
return self._get(request.url, request.method,
|
||||||
|
request.headers.get("Host", ''))
|
||||||
|
|
||||||
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
|
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
|
||||||
def _get(self, url, method):
|
def _get(self, url, method, host):
|
||||||
"""
|
"""
|
||||||
Gets a request handler based on the URL of the request, or raises an
|
Gets a request handler based on the URL of the request, or raises an
|
||||||
error. Internal method for caching.
|
error. Internal method for caching.
|
||||||
|
@ -127,6 +207,7 @@ class Router:
|
||||||
:param method: Request method
|
:param method: Request method
|
||||||
:return: handler, arguments, keyword arguments
|
:return: handler, arguments, keyword arguments
|
||||||
"""
|
"""
|
||||||
|
url = host + url
|
||||||
# Check against known static routes
|
# Check against known static routes
|
||||||
route = self.routes_static.get(url)
|
route = self.routes_static.get(url)
|
||||||
if route:
|
if route:
|
||||||
|
|
135
sanic/sanic.py
135
sanic/sanic.py
|
@ -4,7 +4,6 @@ from functools import partial
|
||||||
from inspect import isawaitable, stack, getmodulename
|
from inspect import isawaitable, stack, getmodulename
|
||||||
from multiprocessing import Process, Event
|
from multiprocessing import Process, Event
|
||||||
from signal import signal, SIGTERM, SIGINT
|
from signal import signal, SIGTERM, SIGINT
|
||||||
from time import sleep
|
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -13,19 +12,25 @@ from .exceptions import Handler
|
||||||
from .log import log
|
from .log import log
|
||||||
from .response import HTTPResponse
|
from .response import HTTPResponse
|
||||||
from .router import Router
|
from .router import Router
|
||||||
from .server import serve
|
from .server import serve, HttpProtocol
|
||||||
from .static import register as static_register
|
from .static import register as static_register
|
||||||
from .exceptions import ServerError
|
from .exceptions import ServerError
|
||||||
|
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||||
|
from os import set_inheritable
|
||||||
|
|
||||||
|
|
||||||
class Sanic:
|
class Sanic:
|
||||||
def __init__(self, name=None, router=None,
|
def __init__(self, name=None, router=None,
|
||||||
error_handler=None, logger=None):
|
error_handler=None):
|
||||||
if logger is None:
|
# Only set up a default log handler if the
|
||||||
logging.basicConfig(
|
# end-user application didn't set anything up.
|
||||||
level=logging.INFO,
|
if not logging.root.handlers and log.level == logging.NOTSET:
|
||||||
format="%(asctime)s: %(levelname)s: %(message)s"
|
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:
|
if name is None:
|
||||||
frame_records = stack()[1]
|
frame_records = stack()[1]
|
||||||
name = getmodulename(frame_records[1])
|
name = getmodulename(frame_records[1])
|
||||||
|
@ -39,6 +44,8 @@ class Sanic:
|
||||||
self._blueprint_order = []
|
self._blueprint_order = []
|
||||||
self.loop = None
|
self.loop = None
|
||||||
self.debug = None
|
self.debug = None
|
||||||
|
self.sock = None
|
||||||
|
self.processes = None
|
||||||
|
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
|
@ -48,9 +55,10 @@ class Sanic:
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def route(self, uri, methods=None):
|
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||||
"""
|
"""
|
||||||
Decorates a function to be registered as a route
|
Decorates a function to be registered as a route
|
||||||
|
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
:param methods: list or tuple of methods allowed
|
:param methods: list or tuple of methods allowed
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
|
@ -62,29 +70,54 @@ class Sanic:
|
||||||
uri = '/' + uri
|
uri = '/' + uri
|
||||||
|
|
||||||
def response(handler):
|
def response(handler):
|
||||||
self.router.add(uri=uri, methods=methods, handler=handler)
|
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||||
|
host=host)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def add_route(self, handler, uri, methods=None):
|
# 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
|
A helper method to register class instance or
|
||||||
functions as a handler to the application url
|
functions as a handler to the application url
|
||||||
routes.
|
routes.
|
||||||
|
|
||||||
:param handler: function or class instance
|
:param handler: function or class instance
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
:param methods: list or tuple of methods allowed
|
:param methods: list or tuple of methods allowed
|
||||||
:return: function or class instance
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
self.route(uri=uri, methods=methods)(handler)
|
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
def remove_route(self, uri, clean_cache=True, host=None):
|
||||||
|
self.router.remove(uri, clean_cache, host)
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def exception(self, *exceptions):
|
def exception(self, *exceptions):
|
||||||
"""
|
"""
|
||||||
Decorates a function to be registered as a handler for exceptions
|
Decorates a function to be registered as a handler for exceptions
|
||||||
:param *exceptions: exceptions
|
|
||||||
|
:param \*exceptions: exceptions
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -130,6 +163,7 @@ class Sanic:
|
||||||
def blueprint(self, blueprint, **options):
|
def blueprint(self, blueprint, **options):
|
||||||
"""
|
"""
|
||||||
Registers a blueprint on the application.
|
Registers a blueprint on the application.
|
||||||
|
|
||||||
:param blueprint: Blueprint object
|
:param blueprint: Blueprint object
|
||||||
:param options: option dictionary with blueprint defaults
|
:param options: option dictionary with blueprint defaults
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
|
@ -147,7 +181,8 @@ class Sanic:
|
||||||
def register_blueprint(self, *args, **kwargs):
|
def register_blueprint(self, *args, **kwargs):
|
||||||
# TODO: deprecate 1.0
|
# TODO: deprecate 1.0
|
||||||
log.warning("Use of register_blueprint will be deprecated in "
|
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)
|
return self.blueprint(*args, **kwargs)
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
@ -162,6 +197,7 @@ class Sanic:
|
||||||
Takes a request from the HTTP Server and returns a response object to
|
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
|
be sent back The HTTP Server only expects a response object, so
|
||||||
exception handling must be done here
|
exception handling must be done here
|
||||||
|
|
||||||
:param request: HTTP Request object
|
:param request: HTTP Request object
|
||||||
:param response_callback: Response function to be called with the
|
:param response_callback: Response function to be called with the
|
||||||
response as the only argument
|
response as the only argument
|
||||||
|
@ -229,7 +265,7 @@ class Sanic:
|
||||||
e, format_exc()))
|
e, format_exc()))
|
||||||
else:
|
else:
|
||||||
response = HTTPResponse(
|
response = HTTPResponse(
|
||||||
"An error occured while handling an error")
|
"An error occurred while handling an error")
|
||||||
|
|
||||||
response_callback(response)
|
response_callback(response)
|
||||||
|
|
||||||
|
@ -238,26 +274,30 @@ class Sanic:
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
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,
|
after_start=None, before_stop=None, after_stop=None, ssl=None,
|
||||||
workers=1, loop=None, register_sys_signals=True):
|
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
|
Runs the HTTP Server and listens until keyboard interrupt or term
|
||||||
signal. On termination, drains connections before closing.
|
signal. On termination, drains connections before closing.
|
||||||
|
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
:param port: Port to host on
|
:param port: Port to host on
|
||||||
:param debug: Enables debug output (slows server)
|
:param debug: Enables debug output (slows server)
|
||||||
:param before_start: Function to be executed before the server starts
|
:param before_start: Functions to be executed before the server starts
|
||||||
accepting connections
|
accepting connections
|
||||||
:param after_start: Function to be executed after the server starts
|
:param after_start: Functions to be executed after the server starts
|
||||||
accepting connections
|
accepting connections
|
||||||
:param before_stop: Function to be executed when a stop signal is
|
:param before_stop: Functions to be executed when a stop signal is
|
||||||
received before it is respected
|
received before it is respected
|
||||||
:param after_stop: Function to be executed when all requests are
|
:param after_stop: Functions to be executed when all requests are
|
||||||
complete
|
complete
|
||||||
|
:param ssl: SSLContext for SSL encryption of worker(s)
|
||||||
:param sock: Socket for the server to accept connections from
|
:param sock: Socket for the server to accept connections from
|
||||||
:param workers: Number of processes
|
:param workers: Number of processes
|
||||||
received before it is respected
|
received before it is respected
|
||||||
:param loop: asyncio compatible event loop
|
:param loop: asyncio compatible event loop
|
||||||
|
:param protocol: Subclass of asyncio protocol class
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
self.error_handler.debug = True
|
self.error_handler.debug = True
|
||||||
|
@ -265,16 +305,19 @@ class Sanic:
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
|
|
||||||
server_settings = {
|
server_settings = {
|
||||||
|
'protocol': protocol,
|
||||||
'host': host,
|
'host': host,
|
||||||
'port': port,
|
'port': port,
|
||||||
'sock': sock,
|
'sock': sock,
|
||||||
|
'ssl': ssl,
|
||||||
'debug': debug,
|
'debug': debug,
|
||||||
'request_handler': self.handle_request,
|
'request_handler': self.handle_request,
|
||||||
'error_handler': self.error_handler,
|
'error_handler': self.error_handler,
|
||||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||||
'loop': loop,
|
'loop': loop,
|
||||||
'register_sys_signals': register_sys_signals
|
'register_sys_signals': register_sys_signals,
|
||||||
|
'backlog': backlog
|
||||||
}
|
}
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
@ -291,7 +334,7 @@ class Sanic:
|
||||||
for blueprint in self.blueprints.values():
|
for blueprint in self.blueprints.values():
|
||||||
listeners += blueprint.listeners[event_name]
|
listeners += blueprint.listeners[event_name]
|
||||||
if args:
|
if args:
|
||||||
if type(args) is not list:
|
if callable(args):
|
||||||
args = [args]
|
args = [args]
|
||||||
listeners += args
|
listeners += args
|
||||||
if reverse:
|
if reverse:
|
||||||
|
@ -305,7 +348,11 @@ class Sanic:
|
||||||
log.debug(self.config.LOGO)
|
log.debug(self.config.LOGO)
|
||||||
|
|
||||||
# Serve
|
# 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:
|
try:
|
||||||
if workers == 1:
|
if workers == 1:
|
||||||
|
@ -313,7 +360,7 @@ class Sanic:
|
||||||
else:
|
else:
|
||||||
log.info('Spinning up {} workers...'.format(workers))
|
log.info('Spinning up {} workers...'.format(workers))
|
||||||
|
|
||||||
self.serve_multiple(server_settings, workers)
|
self.serve_multiple(server_settings, workers, stop_event)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(
|
log.exception(
|
||||||
|
@ -325,13 +372,17 @@ class Sanic:
|
||||||
"""
|
"""
|
||||||
This kills the Sanic
|
This kills the Sanic
|
||||||
"""
|
"""
|
||||||
|
if self.processes is not None:
|
||||||
|
for process in self.processes:
|
||||||
|
process.terminate()
|
||||||
|
self.sock.close()
|
||||||
get_event_loop().stop()
|
get_event_loop().stop()
|
||||||
|
|
||||||
@staticmethod
|
def serve_multiple(self, server_settings, workers, stop_event=None):
|
||||||
def serve_multiple(server_settings, workers, stop_event=None):
|
|
||||||
"""
|
"""
|
||||||
Starts multiple server processes simultaneously. Stops on interrupt
|
Starts multiple server processes simultaneously. Stops on interrupt
|
||||||
and terminate signals, and drains connections when complete.
|
and terminate signals, and drains connections when complete.
|
||||||
|
|
||||||
:param server_settings: kw arguments to be passed to the serve function
|
:param server_settings: kw arguments to be passed to the serve function
|
||||||
:param workers: number of workers to launch
|
:param workers: number of workers to launch
|
||||||
:param stop_event: if provided, is used as a stop signal
|
:param stop_event: if provided, is used as a stop signal
|
||||||
|
@ -340,26 +391,28 @@ class Sanic:
|
||||||
server_settings['reuse_port'] = True
|
server_settings['reuse_port'] = True
|
||||||
|
|
||||||
# Create a stop event to be triggered by a signal
|
# Create a stop event to be triggered by a signal
|
||||||
if not stop_event:
|
if stop_event is None:
|
||||||
stop_event = Event()
|
stop_event = Event()
|
||||||
signal(SIGINT, lambda s, f: stop_event.set())
|
signal(SIGINT, lambda s, f: stop_event.set())
|
||||||
signal(SIGTERM, lambda s, f: stop_event.set())
|
signal(SIGTERM, lambda s, f: stop_event.set())
|
||||||
|
|
||||||
processes = []
|
self.sock = socket()
|
||||||
|
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||||
|
self.sock.bind((server_settings['host'], server_settings['port']))
|
||||||
|
set_inheritable(self.sock.fileno(), True)
|
||||||
|
server_settings['sock'] = self.sock
|
||||||
|
server_settings['host'] = None
|
||||||
|
server_settings['port'] = None
|
||||||
|
|
||||||
|
self.processes = []
|
||||||
for _ in range(workers):
|
for _ in range(workers):
|
||||||
process = Process(target=serve, kwargs=server_settings)
|
process = Process(target=serve, kwargs=server_settings)
|
||||||
|
process.daemon = True
|
||||||
process.start()
|
process.start()
|
||||||
processes.append(process)
|
self.processes.append(process)
|
||||||
|
|
||||||
# Infinitely wait for the stop event
|
for process in self.processes:
|
||||||
try:
|
|
||||||
while not stop_event.is_set():
|
|
||||||
sleep(0.3)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
log.info('Spinning down workers...')
|
|
||||||
for process in processes:
|
|
||||||
process.terminate()
|
|
||||||
for process in processes:
|
|
||||||
process.join()
|
process.join()
|
||||||
|
|
||||||
|
# the above processes will block this until they're stopped
|
||||||
|
self.stop()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import traceback
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from multidict import CIMultiDict
|
|
||||||
from signal import SIGINT, SIGTERM
|
from signal import SIGINT, SIGTERM
|
||||||
from time import time
|
from time import time
|
||||||
from httptools import HttpRequestParser
|
from httptools import HttpRequestParser
|
||||||
|
@ -18,11 +18,30 @@ from .request import Request
|
||||||
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
||||||
|
|
||||||
|
|
||||||
|
current_time = None
|
||||||
|
|
||||||
|
|
||||||
class Signal:
|
class Signal:
|
||||||
stopped = False
|
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):
|
class HttpProtocol(asyncio.Protocol):
|
||||||
|
@ -70,15 +89,14 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
self.connections.discard(self)
|
self.connections.discard(self)
|
||||||
self._timeout_handler.cancel()
|
self._timeout_handler.cancel()
|
||||||
self.cleanup()
|
|
||||||
|
|
||||||
def connection_timeout(self):
|
def connection_timeout(self):
|
||||||
# Check if
|
# Check if
|
||||||
time_elapsed = current_time - self._last_request_time
|
time_elapsed = current_time - self._last_request_time
|
||||||
if time_elapsed < self.request_timeout:
|
if time_elapsed < self.request_timeout:
|
||||||
time_left = self.request_timeout - time_elapsed
|
time_left = self.request_timeout - time_elapsed
|
||||||
self._timeout_handler = \
|
self._timeout_handler = (
|
||||||
self.loop.call_later(time_left, self.connection_timeout)
|
self.loop.call_later(time_left, self.connection_timeout))
|
||||||
else:
|
else:
|
||||||
if self._request_handler_task:
|
if self._request_handler_task:
|
||||||
self._request_handler_task.cancel()
|
self._request_handler_task.cancel()
|
||||||
|
@ -118,18 +136,15 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
exception = PayloadTooLarge('Payload Too Large')
|
exception = PayloadTooLarge('Payload Too Large')
|
||||||
self.write_error(exception)
|
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):
|
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(
|
self.request = Request(
|
||||||
url_bytes=self.url,
|
url_bytes=self.url,
|
||||||
headers=CIMultiDict(self.headers),
|
headers=CIDict(self.headers),
|
||||||
version=self.parser.get_http_version(),
|
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):
|
def on_body(self, body):
|
||||||
|
@ -148,32 +163,51 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
|
|
||||||
def write_response(self, response):
|
def write_response(self, response):
|
||||||
try:
|
try:
|
||||||
keep_alive = self.parser.should_keep_alive() \
|
keep_alive = (
|
||||||
and not self.signal.stopped
|
self.parser.should_keep_alive() and not self.signal.stopped)
|
||||||
self.transport.write(
|
self.transport.write(
|
||||||
response.output(
|
response.output(
|
||||||
self.request.version, keep_alive, self.request_timeout))
|
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:
|
if not keep_alive:
|
||||||
self.transport.close()
|
self.transport.close()
|
||||||
else:
|
else:
|
||||||
# Record that we received data
|
# Record that we received data
|
||||||
self._last_request_time = current_time
|
self._last_request_time = current_time
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
except Exception as e:
|
|
||||||
self.bail_out(
|
|
||||||
"Writing response failed, connection closed {}".format(e))
|
|
||||||
|
|
||||||
def write_error(self, exception):
|
def write_error(self, exception):
|
||||||
try:
|
try:
|
||||||
response = self.error_handler.response(self.request, exception)
|
response = self.error_handler.response(self.request, exception)
|
||||||
version = self.request.version if self.request else '1.1'
|
version = self.request.version if self.request else '1.1'
|
||||||
self.transport.write(response.output(version))
|
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:
|
except Exception as e:
|
||||||
self.bail_out(
|
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)
|
exception = ServerError(message)
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
log.error(message)
|
log.error(message)
|
||||||
|
@ -201,6 +235,7 @@ def update_current_time(loop):
|
||||||
"""
|
"""
|
||||||
Caches the current time, since it is needed
|
Caches the current time, since it is needed
|
||||||
at the end of every keep-alive request to update the request timeout time
|
at the end of every keep-alive request to update the request timeout time
|
||||||
|
|
||||||
:param loop:
|
:param loop:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
@ -224,25 +259,35 @@ def trigger_events(events, loop):
|
||||||
|
|
||||||
|
|
||||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
after_start=None, before_stop=None, after_stop=None,
|
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||||
debug=False, request_timeout=60, sock=None,
|
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
||||||
request_max_size=None, reuse_port=False, loop=None,
|
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
||||||
register_sys_signals=True):
|
register_sys_signals=True):
|
||||||
"""
|
"""
|
||||||
Starts asynchronous HTTP Server on an individual process.
|
Starts asynchronous HTTP Server on an individual process.
|
||||||
|
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
:param port: Port to host on
|
:param port: Port to host on
|
||||||
:param request_handler: Sanic request handler with middleware
|
:param request_handler: Sanic request handler with middleware
|
||||||
|
:param error_handler: Sanic error handler with middleware
|
||||||
|
:param before_start: Function to be executed before the server starts
|
||||||
|
listening. Takes single argument `loop`
|
||||||
:param after_start: Function to be executed after the server starts
|
:param after_start: Function to be executed after the server starts
|
||||||
listening. Takes single argument `loop`
|
listening. Takes single argument `loop`
|
||||||
:param before_stop: Function to be executed when a stop signal is
|
:param before_stop: Function to be executed when a stop signal is
|
||||||
received before it is respected. Takes 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
|
||||||
|
argument `loop`
|
||||||
:param debug: Enables debug output (slows server)
|
:param debug: Enables debug output (slows server)
|
||||||
:param request_timeout: time in seconds
|
:param request_timeout: time in seconds
|
||||||
|
:param ssl: SSLContext
|
||||||
:param sock: Socket for the server to accept connections from
|
:param sock: Socket for the server to accept connections from
|
||||||
:param request_max_size: size in bytes, `None` for no limit
|
:param request_max_size: size in bytes, `None` for no limit
|
||||||
:param reuse_port: `True` for multiple workers
|
:param reuse_port: `True` for multiple workers
|
||||||
:param loop: asyncio compatible event loop
|
:param loop: asyncio compatible event loop
|
||||||
|
:param protocol: Subclass of asyncio protocol class
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
loop = loop or async_loop.new_event_loop()
|
loop = loop or async_loop.new_event_loop()
|
||||||
|
@ -256,7 +301,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
connections = set()
|
connections = set()
|
||||||
signal = Signal()
|
signal = Signal()
|
||||||
server = partial(
|
server = partial(
|
||||||
HttpProtocol,
|
protocol,
|
||||||
loop=loop,
|
loop=loop,
|
||||||
connections=connections,
|
connections=connections,
|
||||||
signal=signal,
|
signal=signal,
|
||||||
|
@ -270,8 +315,10 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
server,
|
server,
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
|
ssl=ssl,
|
||||||
reuse_port=reuse_port,
|
reuse_port=reuse_port,
|
||||||
sock=sock
|
sock=sock,
|
||||||
|
backlog=backlog
|
||||||
)
|
)
|
||||||
|
|
||||||
# Instead of pulling time at the end of every request,
|
# Instead of pulling time at the end of every request,
|
||||||
|
|
|
@ -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
|
Registers a static directory handler with Sanic by adding a route to the
|
||||||
router and registering a handler.
|
router and registering a handler.
|
||||||
|
|
||||||
:param app: Sanic
|
:param app: Sanic
|
||||||
:param file_or_directory: File or directory path to serve from
|
:param file_or_directory: File or directory path to serve from
|
||||||
:param uri: URL to serve from
|
:param uri: URL to serve from
|
||||||
:param pattern: regular expression used to match files in the URL
|
:param pattern: regular expression used to match files in the URL
|
||||||
:param use_modified_since: If true, send file modified time, and return
|
: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,
|
# If we're not trying to match a file directly,
|
||||||
|
|
|
@ -16,15 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
loop=None, debug=False, *request_args,
|
loop=None, debug=False, server_kwargs={},
|
||||||
**request_kwargs):
|
*request_args, **request_kwargs):
|
||||||
results = []
|
results = []
|
||||||
exceptions = []
|
exceptions = []
|
||||||
|
|
||||||
if gather_request:
|
if gather_request:
|
||||||
@app.middleware
|
|
||||||
def _collect_request(request):
|
def _collect_request(request):
|
||||||
results.append(request)
|
results.append(request)
|
||||||
|
app.request_middleware.appendleft(_collect_request)
|
||||||
|
|
||||||
async def _collect_response(sanic, loop):
|
async def _collect_response(sanic, loop):
|
||||||
try:
|
try:
|
||||||
|
@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
exceptions.append(e)
|
exceptions.append(e)
|
||||||
app.stop()
|
app.stop()
|
||||||
|
|
||||||
app.run(host=HOST, debug=debug, port=42101,
|
app.run(host=HOST, debug=debug, port=PORT,
|
||||||
after_start=_collect_response, loop=loop)
|
after_start=_collect_response, loop=loop, **server_kwargs)
|
||||||
|
|
||||||
if exceptions:
|
if exceptions:
|
||||||
raise ValueError("Exception during request: {}".format(exceptions))
|
raise ValueError("Exception during request: {}".format(exceptions))
|
||||||
|
|
|
@ -7,33 +7,96 @@ class HTTPMethodView:
|
||||||
to every HTTP method you want to support.
|
to every HTTP method you want to support.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
class DummyView(View):
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
def put(self, request, *args, **kwargs):
|
def put(self, request, *args, **kwargs):
|
||||||
return text('I am put method')
|
return text('I am put method')
|
||||||
|
|
||||||
etc.
|
etc.
|
||||||
|
|
||||||
If someone tries to use a non-implemented method, there will be a
|
If someone tries to use a non-implemented method, there will be a
|
||||||
405 response.
|
405 response.
|
||||||
|
|
||||||
If you need any url params just mention them in method definition:
|
If you need any url params just mention them in method definition:
|
||||||
class DummyView(View):
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
def get(self, request, my_param_here, *args, **kwargs):
|
def get(self, request, my_param_here, *args, **kwargs):
|
||||||
return text('I am get method with %s' % my_param_here)
|
return text('I am get method with %s' % my_param_here)
|
||||||
|
|
||||||
To add the view into the routing you could use
|
To add the view into the routing you could use
|
||||||
1) app.add_route(DummyView(), '/')
|
1) app.add_route(DummyView.as_view(), '/')
|
||||||
2) app.route('/')(DummyView())
|
2) app.route('/')(DummyView.as_view())
|
||||||
|
|
||||||
|
To add any decorator you could set it into decorators variable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __call__(self, request, *args, **kwargs):
|
decorators = []
|
||||||
|
|
||||||
|
def dispatch_request(self, request, *args, **kwargs):
|
||||||
handler = getattr(self, request.method.lower(), None)
|
handler = getattr(self, request.method.lower(), None)
|
||||||
if handler:
|
if handler:
|
||||||
return handler(request, *args, **kwargs)
|
return handler(request, *args, **kwargs)
|
||||||
raise InvalidUsage(
|
raise InvalidUsage(
|
||||||
'Method {} not allowed for URL {}'.format(
|
'Method {} not allowed for URL {}'.format(
|
||||||
request.method, request.url), status_code=405)
|
request.method, request.url), status_code=405)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_view(cls, *class_args, **class_kwargs):
|
||||||
|
""" Converts the class into an actual view function that can be used
|
||||||
|
with the routing system.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def view(*args, **kwargs):
|
||||||
|
self = view.view_class(*class_args, **class_kwargs)
|
||||||
|
return self.dispatch_request(*args, **kwargs)
|
||||||
|
|
||||||
|
if cls.decorators:
|
||||||
|
view.__module__ = cls.__module__
|
||||||
|
for decorator in cls.decorators:
|
||||||
|
view = decorator(view)
|
||||||
|
|
||||||
|
view.view_class = cls
|
||||||
|
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',
|
'httptools>=0.0.9',
|
||||||
'ujson>=1.35',
|
'ujson>=1.35',
|
||||||
'aiofiles>=0.3.0',
|
'aiofiles>=0.3.0',
|
||||||
'multidict>=2.0',
|
|
||||||
],
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 2 - Pre-Alpha',
|
'Development Status :: 2 - Pre-Alpha',
|
||||||
|
|
|
@ -59,6 +59,71 @@ def test_several_bp_with_url_prefix():
|
||||||
request, response = sanic_endpoint_test(app, uri='/test2/')
|
request, response = sanic_endpoint_test(app, uri='/test2/')
|
||||||
assert response.text == 'Hello2'
|
assert response.text == 'Hello2'
|
||||||
|
|
||||||
|
def test_bp_with_host():
|
||||||
|
app = Sanic('test_bp_host')
|
||||||
|
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return text('Hello')
|
||||||
|
|
||||||
|
@bp.route('/', host="sub.example.com")
|
||||||
|
def handler(request):
|
||||||
|
return text('Hello subdomain!')
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
headers = {"Host": "example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test1/',
|
||||||
|
headers=headers)
|
||||||
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
headers = {"Host": "sub.example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test1/',
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
assert response.text == 'Hello subdomain!'
|
||||||
|
|
||||||
|
|
||||||
|
def test_several_bp_with_host():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
bp = Blueprint('test_text',
|
||||||
|
url_prefix='/test',
|
||||||
|
host="example.com")
|
||||||
|
bp2 = Blueprint('test_text2',
|
||||||
|
url_prefix='/test',
|
||||||
|
host="sub.example.com")
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return text('Hello')
|
||||||
|
|
||||||
|
@bp2.route('/')
|
||||||
|
def handler2(request):
|
||||||
|
return text('Hello2')
|
||||||
|
|
||||||
|
@bp2.route('/other/')
|
||||||
|
def handler2(request):
|
||||||
|
return text('Hello3')
|
||||||
|
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
app.blueprint(bp2)
|
||||||
|
|
||||||
|
assert bp.host == "example.com"
|
||||||
|
headers = {"Host": "example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test/',
|
||||||
|
headers=headers)
|
||||||
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
assert bp2.host == "sub.example.com"
|
||||||
|
headers = {"Host": "sub.example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test/',
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
assert response.text == 'Hello2'
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test/other/',
|
||||||
|
headers=headers)
|
||||||
|
assert response.text == 'Hello3'
|
||||||
|
|
||||||
def test_bp_middleware():
|
def test_bp_middleware():
|
||||||
app = Sanic('test_middleware')
|
app = Sanic('test_middleware')
|
||||||
|
|
32
tests/test_custom_protocol.py
Normal file
32
tests/test_custom_protocol.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.server import HttpProtocol
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
app = Sanic('test_custom_porotocol')
|
||||||
|
|
||||||
|
|
||||||
|
class CustomHttpProtocol(HttpProtocol):
|
||||||
|
|
||||||
|
def write_response(self, response):
|
||||||
|
if isinstance(response, str):
|
||||||
|
response = text(response)
|
||||||
|
self.transport.write(
|
||||||
|
response.output(self.request.version)
|
||||||
|
)
|
||||||
|
self.transport.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/1')
|
||||||
|
async def handler_1(request):
|
||||||
|
return 'OK'
|
||||||
|
|
||||||
|
|
||||||
|
def test_use_custom_protocol():
|
||||||
|
server_kwargs = {
|
||||||
|
'protocol': CustomHttpProtocol
|
||||||
|
}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/1',
|
||||||
|
server_kwargs=server_kwargs)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == 'OK'
|
|
@ -1,51 +1,93 @@
|
||||||
|
import pytest
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
|
||||||
# GET
|
|
||||||
# ------------------------------------------------------------ #
|
|
||||||
|
|
||||||
exception_app = Sanic('test_exceptions')
|
class SanicExceptionTestException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@exception_app.route('/')
|
@pytest.fixture(scope='module')
|
||||||
|
def exception_app():
|
||||||
|
app = Sanic('test_exceptions')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
def handler(request):
|
def handler(request):
|
||||||
return text('OK')
|
return text('OK')
|
||||||
|
|
||||||
|
@app.route('/error')
|
||||||
@exception_app.route('/error')
|
|
||||||
def handler_error(request):
|
def handler_error(request):
|
||||||
raise ServerError("OK")
|
raise ServerError("OK")
|
||||||
|
|
||||||
|
@app.route('/404')
|
||||||
@exception_app.route('/404')
|
|
||||||
def handler_404(request):
|
def handler_404(request):
|
||||||
raise NotFound("OK")
|
raise NotFound("OK")
|
||||||
|
|
||||||
|
@app.route('/invalid')
|
||||||
@exception_app.route('/invalid')
|
|
||||||
def handler_invalid(request):
|
def handler_invalid(request):
|
||||||
raise InvalidUsage("OK")
|
raise InvalidUsage("OK")
|
||||||
|
|
||||||
|
@app.route('/divide_by_zero')
|
||||||
|
def handle_unhandled_exception(request):
|
||||||
|
1 / 0
|
||||||
|
|
||||||
def test_no_exception():
|
@app.route('/error_in_error_handler_handler')
|
||||||
|
def custom_error_handler(request):
|
||||||
|
raise SanicExceptionTestException('Dummy message!')
|
||||||
|
|
||||||
|
@app.exception(SanicExceptionTestException)
|
||||||
|
def error_in_error_handler_handler(request, exception):
|
||||||
|
1 / 0
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_exception(exception_app):
|
||||||
|
"""Test that a route works without an exception"""
|
||||||
request, response = sanic_endpoint_test(exception_app)
|
request, response = sanic_endpoint_test(exception_app)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == 'OK'
|
assert response.text == 'OK'
|
||||||
|
|
||||||
|
|
||||||
def test_server_error_exception():
|
def test_server_error_exception(exception_app):
|
||||||
|
"""Test the built-in ServerError exception works"""
|
||||||
request, response = sanic_endpoint_test(exception_app, uri='/error')
|
request, response = sanic_endpoint_test(exception_app, uri='/error')
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_usage_exception():
|
def test_invalid_usage_exception(exception_app):
|
||||||
|
"""Test the built-in InvalidUsage exception works"""
|
||||||
request, response = sanic_endpoint_test(exception_app, uri='/invalid')
|
request, response = sanic_endpoint_test(exception_app, uri='/invalid')
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
|
||||||
|
|
||||||
def test_not_found_exception():
|
def test_not_found_exception(exception_app):
|
||||||
|
"""Test the built-in NotFound exception works"""
|
||||||
request, response = sanic_endpoint_test(exception_app, uri='/404')
|
request, response = sanic_endpoint_test(exception_app, uri='/404')
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_handled_unhandled_exception(exception_app):
|
||||||
|
"""Test that an exception not built into sanic is handled"""
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
exception_app, uri='/divide_by_zero')
|
||||||
|
assert response.status == 500
|
||||||
|
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"""
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
exception_app, uri='/error_in_error_handler_handler')
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.body == b'An error occurred while handling an error'
|
||||||
|
|
|
@ -2,6 +2,7 @@ from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
exception_handler_app = Sanic('test_exception_handler')
|
exception_handler_app = Sanic('test_exception_handler')
|
||||||
|
|
||||||
|
@ -21,6 +22,12 @@ def handler_3(request):
|
||||||
raise NotFound("OK")
|
raise NotFound("OK")
|
||||||
|
|
||||||
|
|
||||||
|
@exception_handler_app.route('/4')
|
||||||
|
def handler_4(request):
|
||||||
|
foo = bar
|
||||||
|
return text(foo)
|
||||||
|
|
||||||
|
|
||||||
@exception_handler_app.exception(NotFound, ServerError)
|
@exception_handler_app.exception(NotFound, ServerError)
|
||||||
def handler_exception(request, exception):
|
def handler_exception(request, exception):
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
@ -47,3 +54,20 @@ def test_text_exception__handler():
|
||||||
exception_handler_app, uri='/random')
|
exception_handler_app, uri='/random')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == 'OK'
|
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
|
stream=log_stream
|
||||||
)
|
)
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
app = Sanic('test_logging', logger=True)
|
app = Sanic('test_logging')
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def handler(request):
|
def handler(request):
|
||||||
log.info('hello world')
|
log.info('hello world')
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
from multiprocessing import Array, Event, Process
|
from multiprocessing import Array, Event, Process
|
||||||
from time import sleep
|
from time import sleep, time
|
||||||
from ujson import loads as json_loads
|
from ujson import loads as json_loads
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
from sanic.utils import local_request, HOST, PORT
|
from sanic.utils import local_request, HOST, PORT
|
||||||
|
@ -13,8 +15,9 @@ from sanic.utils import local_request, HOST, PORT
|
||||||
|
|
||||||
# TODO: Figure out why this freezes on pytest but not when
|
# TODO: Figure out why this freezes on pytest but not when
|
||||||
# executed via interpreter
|
# executed via interpreter
|
||||||
|
@pytest.mark.skip(
|
||||||
def skip_test_multiprocessing():
|
reason="Freezes with pytest not on interpreter")
|
||||||
|
def test_multiprocessing():
|
||||||
app = Sanic('test_json')
|
app = Sanic('test_json')
|
||||||
|
|
||||||
response = Array('c', 50)
|
response = Array('c', 50)
|
||||||
|
@ -51,3 +54,28 @@ def skip_test_multiprocessing():
|
||||||
raise ValueError("Expected JSON response but got '{}'".format(response))
|
raise ValueError("Expected JSON response but got '{}'".format(response))
|
||||||
|
|
||||||
assert results.get('test') == True
|
assert results.get('test') == True
|
||||||
|
|
||||||
|
@pytest.mark.skip(
|
||||||
|
reason="Freezes with pytest not on interpreter")
|
||||||
|
def test_drain_connections():
|
||||||
|
app = Sanic('test_json')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return json({"test": True})
|
||||||
|
|
||||||
|
stop_event = Event()
|
||||||
|
async def after_start(*args, **kwargs):
|
||||||
|
http_response = await local_request('get', '/')
|
||||||
|
stop_event.set()
|
||||||
|
|
||||||
|
start = time()
|
||||||
|
app.serve_multiple({
|
||||||
|
'host': HOST,
|
||||||
|
'port': PORT,
|
||||||
|
'after_start': after_start,
|
||||||
|
'request_handler': app.handle_request,
|
||||||
|
}, workers=2, stop_event=stop_event)
|
||||||
|
end = time()
|
||||||
|
|
||||||
|
assert end - start < 0.05
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from json import loads as json_loads, dumps as json_dumps
|
from json import loads as json_loads, dumps as json_dumps
|
||||||
from sanic import Sanic
|
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.utils import sanic_endpoint_test
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# GET
|
# GET
|
||||||
|
@ -33,6 +34,31 @@ def test_text():
|
||||||
assert response.text == 'Hello'
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
|
||||||
|
def test_headers():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
headers = {"spam": "great"}
|
||||||
|
return text('Hello', headers=headers)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.headers.get('spam') == 'great'
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_str_headers():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
headers = {"answer": 42}
|
||||||
|
return text('Hello', headers=headers)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.headers.get('answer') == '42'
|
||||||
|
|
||||||
def test_invalid_response():
|
def test_invalid_response():
|
||||||
app = Sanic('test_invalid_response')
|
app = Sanic('test_invalid_response')
|
||||||
|
|
||||||
|
@ -92,6 +118,24 @@ def test_query_string():
|
||||||
assert request.args.get('test2') == 'false'
|
assert request.args.get('test2') == 'false'
|
||||||
|
|
||||||
|
|
||||||
|
def test_token():
|
||||||
|
app = Sanic('test_post_token')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
# uuid4 generated token.
|
||||||
|
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'Authorization': 'Token {}'.format(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, headers=headers)
|
||||||
|
|
||||||
|
assert request.token == token
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# POST
|
# POST
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
@ -145,3 +189,73 @@ def test_post_form_multipart_form_data():
|
||||||
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
||||||
|
|
||||||
assert request.form.get('test') == 'OK'
|
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'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.router import RouteExists
|
from sanic.router import RouteExists, RouteDoesNotExist
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,6 +10,84 @@ from sanic.utils import sanic_endpoint_test
|
||||||
# UTF-8
|
# 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():
|
def test_static_routes():
|
||||||
app = Sanic('test_dynamic_route')
|
app = Sanic('test_dynamic_route')
|
||||||
|
|
||||||
|
@ -356,3 +434,174 @@ def test_add_route_method_not_allowed():
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
||||||
assert response.status == 405
|
assert response.status == 405
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_static_route():
|
||||||
|
app = Sanic('test_remove_static_route')
|
||||||
|
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK1')
|
||||||
|
|
||||||
|
async def handler2(request):
|
||||||
|
return text('OK2')
|
||||||
|
|
||||||
|
app.add_route(handler1, '/test')
|
||||||
|
app.add_route(handler2, '/test2')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/test')
|
||||||
|
app.remove_route('/test2')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_dynamic_route():
|
||||||
|
app = Sanic('test_remove_dynamic_route')
|
||||||
|
|
||||||
|
async def handler(request, name):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<name>')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/folder/<name>')
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_inexistent_route():
|
||||||
|
app = Sanic('test_remove_inexistent_route')
|
||||||
|
|
||||||
|
with pytest.raises(RouteDoesNotExist):
|
||||||
|
app.remove_route('/test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_unhashable_route():
|
||||||
|
app = Sanic('test_remove_unhashable_route')
|
||||||
|
|
||||||
|
async def handler(request, unhashable):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_route_without_clean_cache():
|
||||||
|
app = Sanic('test_remove_static_route')
|
||||||
|
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/test')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/test', clean_cache=True)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
app.add_route(handler, '/test')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/test', clean_cache=False)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
59
tests/test_server_events.py
Normal file
59
tests/test_server_events.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
from io import StringIO
|
||||||
|
from random import choice
|
||||||
|
from string import ascii_letters
|
||||||
|
import signal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
AVAILABLE_LISTENERS = [
|
||||||
|
'before_start',
|
||||||
|
'after_start',
|
||||||
|
'before_stop',
|
||||||
|
'after_stop'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def create_listener(listener_name, in_list):
|
||||||
|
async def _listener(app, loop):
|
||||||
|
print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name))
|
||||||
|
in_list.insert(0, app.name + listener_name)
|
||||||
|
return _listener
|
||||||
|
|
||||||
|
|
||||||
|
def start_stop_app(random_name_app, **run_kwargs):
|
||||||
|
|
||||||
|
def stop_on_alarm(signum, frame):
|
||||||
|
raise KeyboardInterrupt('SIGINT for sanic to stop gracefully')
|
||||||
|
|
||||||
|
signal.signal(signal.SIGALRM, stop_on_alarm)
|
||||||
|
signal.alarm(1)
|
||||||
|
try:
|
||||||
|
random_name_app.run(**run_kwargs)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
|
||||||
|
def test_single_listener(listener_name):
|
||||||
|
"""Test that listeners on their own work"""
|
||||||
|
random_name_app = Sanic(''.join(
|
||||||
|
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||||
|
output = list()
|
||||||
|
start_stop_app(
|
||||||
|
random_name_app,
|
||||||
|
**{listener_name: create_listener(listener_name, output)})
|
||||||
|
assert random_name_app.name + listener_name == output.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_listeners():
|
||||||
|
random_name_app = Sanic(''.join(
|
||||||
|
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||||
|
output = list()
|
||||||
|
start_stop_app(
|
||||||
|
random_name_app,
|
||||||
|
**{listener_name: create_listener(listener_name, output)
|
||||||
|
for listener_name in AVAILABLE_LISTENERS})
|
||||||
|
for listener_name in AVAILABLE_LISTENERS:
|
||||||
|
assert random_name_app.name + listener_name == output.pop()
|
39
tests/test_vhosts.py
Normal file
39
tests/test_vhosts.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json, text
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
|
||||||
|
def test_vhosts():
|
||||||
|
app = Sanic('test_vhosts')
|
||||||
|
|
||||||
|
@app.route('/', host="example.com")
|
||||||
|
async def handler(request):
|
||||||
|
return text("You're at example.com!")
|
||||||
|
|
||||||
|
@app.route('/', host="subdomain.example.com")
|
||||||
|
async def handler(request):
|
||||||
|
return text("You're at subdomain.example.com!")
|
||||||
|
|
||||||
|
headers = {"Host": "example.com"}
|
||||||
|
request, response = sanic_endpoint_test(app, headers=headers)
|
||||||
|
assert response.text == "You're at example.com!"
|
||||||
|
|
||||||
|
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!"
|
|
@ -26,7 +26,7 @@ def test_methods():
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
return text('I am delete method')
|
return text('I am delete method')
|
||||||
|
|
||||||
app.add_route(DummyView(), '/')
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, method="get")
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
assert response.text == 'I am get method'
|
assert response.text == 'I am get method'
|
||||||
|
@ -48,7 +48,7 @@ def test_unexisting_methods():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
app.add_route(DummyView(), '/')
|
app.add_route(DummyView.as_view(), '/')
|
||||||
request, response = sanic_endpoint_test(app, method="get")
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
assert response.text == 'I am get method'
|
assert response.text == 'I am get method'
|
||||||
request, response = sanic_endpoint_test(app, method="post")
|
request, response = sanic_endpoint_test(app, method="post")
|
||||||
|
@ -63,7 +63,7 @@ def test_argument_methods():
|
||||||
def get(self, request, my_param_here):
|
def get(self, request, my_param_here):
|
||||||
return text('I am get method with %s' % my_param_here)
|
return text('I am get method with %s' % my_param_here)
|
||||||
|
|
||||||
app.add_route(DummyView(), '/<my_param_here>')
|
app.add_route(DummyView.as_view(), '/<my_param_here>')
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, uri='/test123')
|
request, response = sanic_endpoint_test(app, uri='/test123')
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ def test_with_bp():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
bp.add_route(DummyView(), '/')
|
bp.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
app.blueprint(bp)
|
app.blueprint(bp)
|
||||||
request, response = sanic_endpoint_test(app)
|
request, response = sanic_endpoint_test(app)
|
||||||
|
@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
bp.add_route(DummyView(), '/')
|
bp.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
app.blueprint(bp)
|
app.blueprint(bp)
|
||||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||||
|
@ -112,7 +112,7 @@ def test_with_middleware():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
app.add_route(DummyView(), '/')
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ def test_with_middleware_response():
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return text('I am get method')
|
return text('I am get method')
|
||||||
|
|
||||||
app.add_route(DummyView(), '/')
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app)
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
@ -153,3 +153,44 @@ def test_with_middleware_response():
|
||||||
assert type(results[0]) is Request
|
assert type(results[0]) is Request
|
||||||
assert type(results[1]) is Request
|
assert type(results[1]) is Request
|
||||||
assert issubclass(type(results[2]), HTTPResponse)
|
assert issubclass(type(results[2]), HTTPResponse)
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_custom_class_methods():
|
||||||
|
app = Sanic('test_with_custom_class_methods')
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
global_var = 0
|
||||||
|
|
||||||
|
def _iternal_method(self):
|
||||||
|
self.global_var += 10
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
self._iternal_method()
|
||||||
|
return text('I am get method and global var is {}'.format(self.global_var))
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
|
assert response.text == 'I am get method and global var is 10'
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_decorator():
|
||||||
|
app = Sanic('test_with_decorator')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def stupid_decorator(view):
|
||||||
|
def decorator(*args, **kwargs):
|
||||||
|
results.append(1)
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
decorators = [stupid_decorator]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
|
assert response.text == 'I am get method'
|
||||||
|
assert results[0] == 1
|
||||||
|
|
Loading…
Reference in New Issue
Block a user