Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d790a71a0 | ||
|
|
cfc32d940a | ||
|
|
59242df7d6 | ||
|
|
fad9fbca6f | ||
|
|
c81c92967c | ||
|
|
fa36dcbe09 | ||
|
|
c72bcc136c | ||
|
|
d52f5f0b09 | ||
|
|
4f6650fce2 | ||
|
|
2774414f9e | ||
|
|
c1f1dca22c | ||
|
|
965fbc917d | ||
|
|
2a1b63c93c | ||
|
|
011be51232 | ||
|
|
d1c4f3172f | ||
|
|
14697c7ea9 | ||
|
|
54ca8c787b | ||
|
|
88c3bffe20 | ||
|
|
a4f85cd02e | ||
|
|
fd118a41fd | ||
|
|
b0f71c5304 | ||
|
|
a162f2ce34 | ||
|
|
0f64702d72 | ||
|
|
3c355f19eb | ||
|
|
fb9f2171a9 | ||
|
|
4efcb6d5ad | ||
|
|
5ddc166b12 | ||
|
|
ecd7e5bdd3 | ||
|
|
d0a121ad06 | ||
|
|
85a28d3c30 | ||
|
|
0ad31a471b | ||
|
|
28f7abd1f8 | ||
|
|
049a5a7493 | ||
|
|
a9ee01c7ec | ||
|
|
1f89b15792 | ||
|
|
afe390d407 | ||
|
|
6f4f2758d6 | ||
|
|
592ee5f839 | ||
|
|
a811c84e99 | ||
|
|
214162adf0 | ||
|
|
28396c8620 | ||
|
|
72fba62e09 | ||
|
|
5669cafc59 | ||
|
|
0c7275da1a | ||
|
|
e5cbf25cbd | ||
|
|
c3cf618e84 | ||
|
|
7780a8c187 | ||
|
|
40f1e14bb7 | ||
|
|
a7cd4ccd09 | ||
|
|
6fd69b6284 | ||
|
|
96424b6b0a | ||
|
|
bef34d66f5 | ||
|
|
c6049be688 | ||
|
|
5ea653244e | ||
|
|
596bb54ee3 | ||
|
|
7554e87374 | ||
|
|
6176964bdf | ||
|
|
f77bb81def | ||
|
|
ed4752bbc0 | ||
|
|
30862c0a3e | ||
|
|
7c4ffa8866 | ||
|
|
a79f073077 | ||
|
|
4682d7b4b8 | ||
|
|
d2217b5c5f | ||
|
|
0a160c4a0b | ||
|
|
aed8255a8b | ||
|
|
ba60659894 | ||
|
|
1ad7b95437 | ||
|
|
cc43ee3b3d | ||
|
|
5a7b70c054 | ||
|
|
e9bfa30c1d | ||
|
|
bb83a25a52 | ||
|
|
99c8d779dc | ||
|
|
2c1ff5bf5d | ||
|
|
e218a59911 | ||
|
|
11f3c79a77 | ||
|
|
5cfd8b9aa8 | ||
|
|
ebce7b01c7 | ||
|
|
9d4b104d2d | ||
|
|
5a6fb679c9 | ||
|
|
9102a9cd6e | ||
|
|
573d1da0ef | ||
|
|
2848dce968 | ||
|
|
ba1e006585 | ||
|
|
9108a4c69f | ||
|
|
f42ceb1baa | ||
|
|
5903dd2939 | ||
|
|
55b39a3f15 | ||
|
|
9bc69f7de9 | ||
|
|
2aa380c5a3 | ||
|
|
638fbcb619 | ||
|
|
ccbbce0036 | ||
|
|
41918eaf0a | ||
|
|
5c344f7efa | ||
|
|
213580ea78 | ||
|
|
d5fd269fda | ||
|
|
2cf4baddfb | ||
|
|
48d496936a | ||
|
|
e410c06a68 | ||
|
|
f6c2d0bcaf | ||
|
|
e2a16f96a8 | ||
|
|
49fdc6563f | ||
|
|
b5bbef09c5 | ||
|
|
7a1e089725 | ||
|
|
7de3f7aa78 | ||
|
|
02b9a0a297 | ||
|
|
a86cc32dff | ||
|
|
974c857a97 | ||
|
|
6e53fa03f5 | ||
|
|
8c5e214131 | ||
|
|
cf60ebd988 | ||
|
|
9dd954bccd | ||
|
|
a02eb8e7cb | ||
|
|
a93ca9b8f1 | ||
|
|
15e4ec7ffb | ||
|
|
62df50e22b | ||
|
|
055430d4b8 | ||
|
|
57f27c41e0 | ||
|
|
385328ffd5 | ||
|
|
34d1e5e67e | ||
|
|
4f832ac9af | ||
|
|
b0bf989056 | ||
|
|
fd0e8624c4 | ||
|
|
5566668a5f | ||
|
|
f8e6becb9e | ||
|
|
dd28d70680 | ||
|
|
ed8e3f237c | ||
|
|
77c04c4cf9 | ||
|
|
ebc8d7168a | ||
|
|
434fa74e67 | ||
|
|
47a4f34cdf | ||
|
|
1317b1799c | ||
|
|
fcae4a9f0a | ||
|
|
d733c5bb7c | ||
|
|
baf8254907 | ||
|
|
802e7d4654 | ||
|
|
616e20d467 | ||
|
|
5c7c2cf85e | ||
|
|
1942644434 | ||
|
|
b67482de9b | ||
|
|
35bb71f952 | ||
|
|
9c91b09ab1 | ||
|
|
06911a8d2e | ||
|
|
e7922c1b54 | ||
|
|
16959caa34 | ||
|
|
0675f388b9 | ||
|
|
b4ad9459da | ||
|
|
8a9b1fee14 | ||
|
|
e6eb697bb2 | ||
|
|
4ccc782e29 | ||
|
|
cfdd9f66d1 | ||
|
|
035cbf84ae | ||
|
|
31e92a8b4f | ||
|
|
87c24e5a7c | ||
|
|
552ff9d736 | ||
|
|
738396c2e2 | ||
|
|
9f18b5a096 | ||
|
|
15c965c08c | ||
|
|
7a8fd6b0df | ||
|
|
3ada6f358c | ||
|
|
f1c2854358 | ||
|
|
87559a34f8 | ||
|
|
9586351f37 | ||
|
|
0f6ed642da | ||
|
|
6d1d4ade19 | ||
|
|
73a57e1105 | ||
|
|
e7314d1775 | ||
|
|
ee8f8c2930 | ||
|
|
64e0e2d19f | ||
|
|
6bb4dae5e0 | ||
|
|
83e9d08853 | ||
|
|
7dfa62516c | ||
|
|
15e7d8ab2e | ||
|
|
a4f77984b7 | ||
|
|
39b279f0f2 | ||
|
|
ac44900fc4 | ||
|
|
548458c3e0 | ||
|
|
986b0aa106 | ||
|
|
01b42fb399 | ||
|
|
f74d44152a | ||
|
|
1557854755 | ||
|
|
56d6c2a929 | ||
|
|
5402e6d3a6 | ||
|
|
52c59e7133 | ||
|
|
7654c2f902 | ||
|
|
be9eca2d63 | ||
|
|
d5ad5e46da | ||
|
|
a486fb99a9 | ||
|
|
2b10860c32 | ||
|
|
a03f216f42 | ||
|
|
7d7cbaacf1 | ||
|
|
00b5a496dd | ||
|
|
7e6c92dc52 | ||
|
|
6cf3754051 | ||
|
|
cf7616ebe5 | ||
|
|
5b7964f8b6 | ||
|
|
f1f38c24da | ||
|
|
67a50becb0 | ||
|
|
d7e94473f3 | ||
|
|
184c896f41 | ||
|
|
8be849cc40 | ||
|
|
275851a755 | ||
|
|
16182472fa | ||
|
|
29f3c22fed | ||
|
|
a116666d55 | ||
|
|
c2622511ce | ||
|
|
50243037eb | ||
|
|
74f305cfb7 | ||
|
|
75990fbaf4 | ||
|
|
cc982c5a61 | ||
|
|
2d05243c4a | ||
|
|
2f0a582aa7 | ||
|
|
665881471d | ||
|
|
39211f8fbd | ||
|
|
32ea45d403 | ||
|
|
cd17a42234 | ||
|
|
8e19b5938c | ||
|
|
9e208ab744 | ||
|
|
94bd9702e5 | ||
|
|
3add40625d | ||
|
|
5afae986a0 | ||
|
|
f091d82bad | ||
|
|
5c1ef2c1cf | ||
|
|
8411255700 | ||
|
|
c657c531b4 | ||
|
|
ef9d8710f5 | ||
|
|
75fc9f91b9 | ||
|
|
545d9eb59b | ||
|
|
a9b67c3028 | ||
|
|
35e79f3985 | ||
|
|
435d5585e9 | ||
|
|
ddfb7f2861 | ||
|
|
6c806549ae | ||
|
|
8957e4ec25 | ||
|
|
2003eceba1 | ||
|
|
8fc1462d11 | ||
|
|
93b45e9598 | ||
|
|
a3a14cdab2 | ||
|
|
9ba2f99ea2 | ||
|
|
f9db796a6e | ||
|
|
94c7aaf7f8 | ||
|
|
6ef6d9a905 | ||
|
|
b44e9baaec | ||
|
|
f9176bfdea | ||
|
|
721044b378 | ||
|
|
154f8570f0 | ||
|
|
0464d31a9c | ||
|
|
e3453553e1 | ||
|
|
6abaa78f9e | ||
|
|
457507d8dc | ||
|
|
3ea1a80496 | ||
|
|
fac4bca4f4 | ||
|
|
662e0c9965 | ||
|
|
80af9e6d76 | ||
|
|
9b466db5c9 | ||
|
|
c34427690a | ||
|
|
d8a974bb4f | ||
|
|
70c56b7db3 | ||
|
|
209b763302 | ||
|
|
190b7a6076 | ||
|
|
0c215685f2 | ||
|
|
d86ac5e3e0 | ||
|
|
47927608b2 | ||
|
|
13808bf282 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,11 +1,15 @@
|
||||
*~
|
||||
*.egg-info
|
||||
*.egg
|
||||
*.eggs
|
||||
*.pyc
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage
|
||||
.tox
|
||||
settings.py
|
||||
*.pyc
|
||||
.idea/*
|
||||
.cache/*
|
||||
.python-version
|
||||
docs/_build/
|
||||
docs/_api/
|
||||
12
.travis.yml
12
.travis.yml
@@ -1,14 +1,10 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- '3.5'
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r requirements-dev.txt
|
||||
- python setup.py install
|
||||
- pip install flake8
|
||||
- pip install pytest
|
||||
before_script: flake8 sanic
|
||||
script: py.test -v tests
|
||||
- '3.6'
|
||||
install: pip install tox-travis
|
||||
script: tox
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
|
||||
91
README.md
91
README.md
@@ -1,91 +0,0 @@
|
||||
# Sanic
|
||||
|
||||
[](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
[](https://travis-ci.org/channelcat/sanic)
|
||||
[](https://pypi.python.org/pypi/sanic/)
|
||||
[](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"})
|
||||
|
||||
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)
|
||||
* [Deploying](docs/deploying.md)
|
||||
* [Contributing](docs/contributing.md)
|
||||
* [License](LICENSE)
|
||||
|
||||
## TODO:
|
||||
* Streamed file processing
|
||||
* File output
|
||||
* Examples of integrations with 3rd-party modules
|
||||
* RESTful router
|
||||
|
||||
## Limitations:
|
||||
* No wheels for uvloop and httptools on Windows :(
|
||||
|
||||
## Final Thoughts:
|
||||
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
127
README.rst
Normal file
127
README.rst
Normal file
@@ -0,0 +1,127 @@
|
||||
Sanic
|
||||
=================================
|
||||
|
||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
|
||||
|
||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
|
||||
|
||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
|
||||
Benchmarks
|
||||
----------
|
||||
|
||||
All tests were run on an AWS medium instance running ubuntu, using 1
|
||||
process. Each script delivered a small JSON response and was tested with
|
||||
wrk using 100 connections. Pypy was tested for Falcon and Flask but did
|
||||
not speed up requests.
|
||||
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Server | Implementation | Requests/sec | Avg Latency |
|
||||
+===========+=======================+================+===============+
|
||||
| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Tornado | Python 3.5 | 2,138 | 46.66ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
|
||||
Hello World Example
|
||||
-------------------
|
||||
|
||||
.. code:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
SSL Example
|
||||
-----------
|
||||
|
||||
Optionally pass in an SSLContext:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import ssl
|
||||
certificate = "/path/to/certificate"
|
||||
keyfile = "/path/to/keyfile"
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(certificate, keyfile=keyfile)
|
||||
|
||||
app.run(host="0.0.0.0", port=8443, ssl=context)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
- ``python -m pip install sanic``
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation can be found in the ``docs`` directory.
|
||||
|
||||
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
|
||||
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master
|
||||
:target: https://travis-ci.org/channelcat/sanic
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
|
||||
TODO
|
||||
----
|
||||
* Streamed file processing
|
||||
* File output
|
||||
* Examples of integrations with 3rd-party modules
|
||||
* RESTful router
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
* No wheels for uvloop and httptools on Windows :(
|
||||
|
||||
Final Thoughts
|
||||
--------------
|
||||
|
||||
::
|
||||
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
@@ -1,25 +1,20 @@
|
||||
# Blueprints
|
||||
|
||||
Blueprints are objects that can be used for sub-routing within an application.
|
||||
Instead of adding routes to the application object, blueprints define similar
|
||||
Instead of adding routes to the application instance, blueprints define similar
|
||||
methods for adding routes, which are then registered with the application in a
|
||||
flexible and pluggable manner.
|
||||
|
||||
## Why?
|
||||
|
||||
Blueprints are especially useful for larger applications, where your application
|
||||
logic can be broken down into several groups or areas of responsibility.
|
||||
|
||||
It is also useful for API versioning, where one blueprint may point at
|
||||
`/v1/<routes>`, and another pointing at `/v2/<routes>`.
|
||||
|
||||
Blueprints are especially useful for larger applications, where your
|
||||
application logic can be broken down into several groups or areas of
|
||||
responsibility.
|
||||
|
||||
## My First Blueprint
|
||||
|
||||
The following shows a very simple blueprint that registers a handler-function at
|
||||
the root `/` of your application.
|
||||
|
||||
Suppose you save this file as `my_blueprint.py`, this can be imported in your
|
||||
Suppose you save this file as `my_blueprint.py`, which can be imported into your
|
||||
main application later.
|
||||
|
||||
```python
|
||||
@@ -34,7 +29,8 @@ async def bp_root(request):
|
||||
|
||||
```
|
||||
|
||||
## Registering Blueprints
|
||||
## Registering blueprints
|
||||
|
||||
Blueprints must be registered with the application.
|
||||
|
||||
```python
|
||||
@@ -48,14 +44,19 @@ app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
```
|
||||
|
||||
This will add the blueprint to the application and register any routes defined
|
||||
by that blueprint.
|
||||
In this example, the registered routes in the `app.router` will look like:
|
||||
by that blueprint. In this example, the registered routes in the `app.router`
|
||||
will look like:
|
||||
|
||||
```python
|
||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
||||
```
|
||||
|
||||
## Middleware
|
||||
## Using blueprints
|
||||
|
||||
Blueprints have much the same functionality as an application instance.
|
||||
|
||||
### Middleware
|
||||
|
||||
Using blueprints allows you to also register middleware globally.
|
||||
|
||||
```python
|
||||
@@ -72,40 +73,92 @@ async def halt_response(request, response):
|
||||
return text('I halted the response')
|
||||
```
|
||||
|
||||
## Exceptions
|
||||
Exceptions can also be applied exclusively to blueprints globally.
|
||||
### Exceptions
|
||||
|
||||
Exceptions can be applied exclusively to blueprints globally.
|
||||
|
||||
```python
|
||||
@bp.exception(NotFound)
|
||||
def ignore_404s(request, exception):
|
||||
return text("Yep, I totally found the page: {}".format(request.url))
|
||||
```
|
||||
|
||||
## Static files
|
||||
Static files can also be served globally, under the blueprint prefix.
|
||||
### Static files
|
||||
|
||||
Static files can be served globally, under the blueprint prefix.
|
||||
|
||||
```python
|
||||
bp.static('/folder/to/serve', '/web/path')
|
||||
```
|
||||
|
||||
## Start and Stop
|
||||
Blueprints and run functions during the start and stop process of the server.
|
||||
If running in multiprocessor mode (more than 1 worker), these are triggered after the workers fork
|
||||
## Start and stop
|
||||
|
||||
Blueprints can run functions during the start and stop process of the server.
|
||||
If running in multiprocessor mode (more than 1 worker), these are triggered
|
||||
after the workers fork.
|
||||
|
||||
Available events are:
|
||||
|
||||
* before_server_start - Executed before the server begins to accept connections
|
||||
* after_server_start - Executed after the server begins to accept connections
|
||||
* before_server_stop - Executed before the server stops accepting connections
|
||||
* after_server_stop - Executed after the server is stopped and all requests are complete
|
||||
- `before_server_start`: Executed before the server begins to accept connections
|
||||
- `after_server_start`: Executed after the server begins to accept connections
|
||||
- `before_server_stop`: Executed before the server stops accepting connections
|
||||
- `after_server_stop`: Executed after the server is stopped and all requests are complete
|
||||
|
||||
```python
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
||||
@bp.listen('before_server_start')
|
||||
async def setup_connection():
|
||||
@bp.listener('before_server_start')
|
||||
async def setup_connection(app, loop):
|
||||
global database
|
||||
database = mysql.connect(host='127.0.0.1'...)
|
||||
|
||||
@bp.listen('after_server_stop')
|
||||
async def close_connection():
|
||||
@bp.listener('after_server_stop')
|
||||
async def close_connection(app, loop):
|
||||
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,15 +1,32 @@
|
||||
# Class based views
|
||||
# Class-Based Views
|
||||
|
||||
Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response.
|
||||
Class-based views are simply classes which implement response behaviour to
|
||||
requests. They provide a way to compartmentalise handling of different HTTP
|
||||
request types at the same endpoint. Rather than defining and decorating three
|
||||
different handler functions, one for each of an endpoint's supported request
|
||||
type, the endpoint can be assigned a class-based view.
|
||||
|
||||
## Defining views
|
||||
|
||||
A class-based view should subclass `HTTPMethodView`. You can then implement
|
||||
class methods for every HTTP request type you want to support. If a request is
|
||||
received that has no defined method, a `405: Method not allowed` response will
|
||||
be generated.
|
||||
|
||||
To register a class-based view on an endpoint, the `app.add_route` method is
|
||||
used. The first argument should be the defined class with the method `as_view`
|
||||
invoked, and the second should be the URL endpoint.
|
||||
|
||||
The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class
|
||||
using all these methods would look like the following.
|
||||
|
||||
## Examples
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('some_name')
|
||||
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
@@ -27,11 +44,14 @@ class SimpleView(HTTPMethodView):
|
||||
def delete(self, request):
|
||||
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
|
||||
class NameView(HTTPMethodView):
|
||||
@@ -39,6 +59,54 @@ class NameView(HTTPMethodView):
|
||||
def get(self, request, 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')
|
||||
```
|
||||
|
||||
## Using CompositionView
|
||||
|
||||
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
|
||||
move handler functions outside of the view class.
|
||||
|
||||
Handler functions for each supported HTTP method are defined elsewhere in the
|
||||
source, and then added to the view using the `CompositionView.add` method. The
|
||||
first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`),
|
||||
and the second is the handler function. The following example shows
|
||||
`CompositionView` usage with both an external handler function and an inline
|
||||
lambda:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import CompositionView
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
def get_handler(request):
|
||||
return text('I am a get method')
|
||||
|
||||
view = CompositionView()
|
||||
view.add(['GET'], get_handler)
|
||||
view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
|
||||
|
||||
# Use the new view to handle requests to the base URL
|
||||
app.add_route(view, '/')
|
||||
```
|
||||
|
||||
**Previous:** [Blueprints](blueprints.md)
|
||||
|
||||
**Next:** [Cookies](cookies.md)
|
||||
|
||||
155
docs/conf.py
Normal file
155
docs/conf.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Sanic documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Dec 25 18:07:21 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add support for Markdown documentation using Recommonmark
|
||||
from recommonmark.parser import CommonMarkParser
|
||||
|
||||
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
|
||||
# autogenerate documentation from docstrings
|
||||
root_directory = os.path.dirname(os.getcwd())
|
||||
sys.path.insert(0, root_directory)
|
||||
|
||||
import sanic
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.githubpages']
|
||||
|
||||
templates_path = ['_templates']
|
||||
|
||||
# Enable support for both Restructured Text and Markdown
|
||||
source_parsers = {'.md': CommonMarkParser}
|
||||
source_suffix = ['.rst', '.md']
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Sanic'
|
||||
copyright = '2016, Sanic contributors'
|
||||
author = 'Sanic contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = sanic.__version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = sanic.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
#
|
||||
# modules.rst is generated by sphinx-apidoc but is unused. This suppresses
|
||||
# a warning about it.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'modules.rst']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Sanicdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Sanic.tex', 'Sanic Documentation',
|
||||
'Sanic contributors', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'sanic', 'Sanic Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Sanic', 'Sanic Documentation',
|
||||
author, 'Sanic', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
# -- Options for Epub output ----------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
epub_author = author
|
||||
epub_publisher = author
|
||||
epub_copyright = copyright
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
@@ -1,10 +1,35 @@
|
||||
# How to contribute to Sanic
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest!
|
||||
Thank you for your interest! Sanic is always looking for contributors. If you
|
||||
don't feel comfortable contributing code, adding docstrings to the source files
|
||||
is very appreciated.
|
||||
|
||||
## Running tests
|
||||
|
||||
* `python -m pip install pytest`
|
||||
* `python -m pytest tests`
|
||||
|
||||
## Documentation
|
||||
|
||||
Sanic's documentation is built
|
||||
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
|
||||
Markdown and can be found in the `docs` folder, while the module reference is
|
||||
automatically generated using `sphinx-apidoc`.
|
||||
|
||||
To generate the documentation from scratch:
|
||||
|
||||
```bash
|
||||
sphinx-apidoc -fo docs/_api/ sanic
|
||||
sphinx-build -b html docs docs/_build
|
||||
```
|
||||
|
||||
The HTML documentation will be created in the `docs/_build` folder.
|
||||
|
||||
## Warning
|
||||
One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged.
|
||||
|
||||
One of the main goals of Sanic is speed. Code that lowers the performance of
|
||||
Sanic without significant gains in usability, security, or features may not be
|
||||
merged. Please don't let this intimidate you! If you have any concerns about an
|
||||
idea, open an issue for discussion and help.
|
||||
|
||||
**Previous:** [Sanic extensions](extensions.md)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Cookies
|
||||
|
||||
## Request
|
||||
Cookies are pieces of data which persist inside a user's browser. Sanic can
|
||||
both read and write cookies, which are stored as key-value pairs.
|
||||
|
||||
Request cookies can be accessed via the request.cookie dictionary
|
||||
## Reading cookies
|
||||
|
||||
### Example
|
||||
A user's cookies can be accessed `Request` object's `cookie` dictionary.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
@@ -16,28 +16,11 @@ async def test(request):
|
||||
return text("Test cookie set to: {}".format(test_cookie))
|
||||
```
|
||||
|
||||
## Response
|
||||
## Writing cookies
|
||||
|
||||
Response cookies can be set like dictionary values and
|
||||
have the following parameters available:
|
||||
|
||||
* expires - datetime - Time for cookie to expire on the client's browser
|
||||
* path - string - The Path attribute specifies the subset of URLs to
|
||||
which this cookie applies
|
||||
* comment - string - Cookie comment (metadata)
|
||||
* domain - string - Specifies the domain for which the
|
||||
cookie is valid. An explicitly specified domain must always
|
||||
start with a dot.
|
||||
* max-age - number - Number of seconds the cookie should live for
|
||||
* secure - boolean - Specifies whether the cookie will only be sent via
|
||||
HTTPS
|
||||
* httponly - boolean - Specifies whether the cookie cannot be read
|
||||
by javascript
|
||||
|
||||
### Example
|
||||
When returning a response, cookies can be set on the `Response` object.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
@@ -47,4 +30,50 @@ async def test(request):
|
||||
response.cookies['test']['domain'] = '.gotta-go-fast.com'
|
||||
response.cookies['test']['httponly'] = True
|
||||
return response
|
||||
```
|
||||
```
|
||||
|
||||
## Deleting cookies
|
||||
|
||||
Cookies can be removed semantically or explicitly.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
response = text("Time to eat some cookies muahaha")
|
||||
|
||||
# This cookie will be set to expire in 0 seconds
|
||||
del response.cookies['kill_me']
|
||||
|
||||
# This cookie will self destruct in 5 seconds
|
||||
response.cookies['short_life'] = 'Glad to be here'
|
||||
response.cookies['short_life']['max-age'] = 5
|
||||
del response.cookies['favorite_color']
|
||||
|
||||
# This cookie will remain unchanged
|
||||
response.cookies['favorite_color'] = 'blue'
|
||||
response.cookies['favorite_color'] = 'pink'
|
||||
del response.cookies['favorite_color']
|
||||
|
||||
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. Defaults to /.
|
||||
- `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
|
||||
|
||||
When it comes to deploying Sanic, there's not much to it, but there are
|
||||
a few things to take note of.
|
||||
Deploying Sanic is made simple by the inbuilt webserver. After defining an
|
||||
instance of `sanic.Sanic`, we can call the `run` method with the following
|
||||
keyword arguments:
|
||||
|
||||
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
||||
- `port` *(default `8000`)*: Port to host the server on.
|
||||
- `debug` *(default `False`)*: Enables debug output (slows server).
|
||||
- `before_start` *(default `None`)*: Function or list of functions to be executed
|
||||
before the server starts accepting connections.
|
||||
- `after_start` *(default `None`)*: Function or list of functions to be executed
|
||||
after the server starts accepting connections.
|
||||
- `before_stop` *(default `None`)*: Function or list of functions to be
|
||||
executed when a stop signal is received before it is
|
||||
respected.
|
||||
- `after_stop` *(default `None`)*: Function or list of functions to be executed
|
||||
when all requests are complete.
|
||||
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
|
||||
- `sock` *(default `None`)*: Socket for the server to accept connections from.
|
||||
- `workers` *(default `1`)*: Number of worker processes to spawn.
|
||||
- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is
|
||||
specified, Sanic creates its own event loop.
|
||||
- `protocol` *(default `HttpProtocol`)*: Subclass
|
||||
of
|
||||
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
||||
|
||||
## Workers
|
||||
|
||||
By default, Sanic listens in the main process using only 1 CPU core.
|
||||
To crank up the juice, just specify the number of workers in the run
|
||||
arguments like so:
|
||||
By default, Sanic listens in the main process using only one CPU core. To crank
|
||||
up the juice, just specify the number of workers in the `run` arguments.
|
||||
|
||||
```python
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
|
||||
Sanic will automatically spin up multiple processes and route
|
||||
traffic between them. We recommend as many workers as you have
|
||||
available cores.
|
||||
Sanic will automatically spin up multiple processes and route traffic between
|
||||
them. We recommend as many workers as you have available cores.
|
||||
|
||||
## Running via Command
|
||||
## Running via command
|
||||
|
||||
If you like using command line arguments, you can launch a sanic server
|
||||
by executing the module. For example, if you initialized sanic as
|
||||
app in a file named server.py, you could run the server like so:
|
||||
If you like using command line arguments, you can launch a Sanic server by
|
||||
executing the module. For example, if you initialized Sanic as `app` in a file
|
||||
named `server.py`, you could run the server like so:
|
||||
|
||||
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
|
||||
|
||||
With this way of running sanic, it is not necessary to run app.run in
|
||||
your python file. If you do, just make sure you wrap it in name == main
|
||||
like so:
|
||||
With this way of running sanic, it is not necessary to invoke `app.run` in your
|
||||
Python file. If you do, make sure you wrap it so that it only executes when
|
||||
directly run by the interpreter.
|
||||
|
||||
```python
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
```
|
||||
|
||||
**Previous:** [Request Data](request_data.md)
|
||||
|
||||
**Next:** [Static Files](static_files.md)
|
||||
|
||||
@@ -1,28 +1,49 @@
|
||||
# Exceptions
|
||||
|
||||
Exceptions can be thrown from within request handlers and will automatically be handled by Sanic. Exceptions take a message as their first argument, and can also take a status_code to be passed back in the HTTP response. Check sanic.exceptions for the full list of exceptions to throw.
|
||||
Exceptions can be thrown from within request handlers and will automatically be
|
||||
handled by Sanic. Exceptions take a message as their first argument, and can
|
||||
also take a status code to be passed back in the HTTP response.
|
||||
|
||||
## Throwing an exception
|
||||
|
||||
To throw an exception, simply `raise` the relevant exception from the
|
||||
`sanic.exceptions` module.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
@app.route('/killme')
|
||||
def i_am_ready_to_die(request):
|
||||
raise ServerError("Something bad happened")
|
||||
raise ServerError("Something bad happened", status_code=500)
|
||||
```
|
||||
|
||||
## Handling Exceptions
|
||||
## Handling exceptions
|
||||
|
||||
Just use the @exception decorator. The decorator expects a list of exceptions to handle as arguments. You can pass SanicException to catch them all! The exception handler must expect a request and exception object as arguments.
|
||||
To override Sanic's default handling of an exception, the `@app.exception`
|
||||
decorator is used. The decorator expects a list of exceptions to handle as
|
||||
arguments. You can pass `SanicException` to catch them all! The decorated
|
||||
exception handler function must take a `Request` and `Exception` object as
|
||||
arguments.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import NotFound
|
||||
|
||||
@app.exception(NotFound)
|
||||
def ignore_404s(request, exception):
|
||||
return text("Yep, I totally found the page: {}".format(request.url))
|
||||
```
|
||||
```
|
||||
|
||||
## Useful exceptions
|
||||
|
||||
Some of the most useful exceptions are presented below:
|
||||
|
||||
- `NotFound`: called when a suitable route for the request isn't found.
|
||||
- `ServerError`: called when something goes wrong inside the server. This
|
||||
usually occurs if there is an exception raised in user code.
|
||||
|
||||
See the `sanic.exceptions` module for the full list of exceptions to throw.
|
||||
|
||||
**Previous:** [Middleware](middleware.md)
|
||||
|
||||
**Next:** [Blueprints](blueprints.md)
|
||||
|
||||
12
docs/extensions.md
Normal file
12
docs/extensions.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Sanic Extensions
|
||||
|
||||
A list of Sanic extensions created by the community.
|
||||
|
||||
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
|
||||
Allows using redis, memcache or an in memory store.
|
||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
|
||||
**Previous:** [Testing](testing.md)
|
||||
|
||||
**Next:** [Contributing](contributing.md)
|
||||
@@ -1,25 +1,29 @@
|
||||
# Getting Started
|
||||
|
||||
Make sure you have pip and python 3.5 before starting
|
||||
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
|
||||
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
|
||||
syntax, so earlier versions of python won't work.
|
||||
|
||||
## Benchmarks
|
||||
* Install Sanic
|
||||
* `python3 -m pip install sanic`
|
||||
* Edit main.py to include:
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
1. Install Sanic: `python3 -m pip install sanic`
|
||||
2. Create a file called `main.py` with the following code:
|
||||
|
||||
app = Sanic(__name__)
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({ "hello": "world" })
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
```
|
||||
* Run `python3 main.py`
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return text('Hello world!')
|
||||
|
||||
You now have a working Sanic server! To continue on, check out:
|
||||
* [Request Data](request_data.md)
|
||||
* [Routing](routing.md)
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
```
|
||||
|
||||
3. Run the server: `python3 main.py`
|
||||
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||
the message *Hello world!*.
|
||||
|
||||
You now have a working Sanic server!
|
||||
|
||||
**Next:** [Routing](routing.md)
|
||||
|
||||
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,18 +1,61 @@
|
||||
# Middleware
|
||||
|
||||
Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware returns a response object, the request will stop processing and a response will be returned.
|
||||
Middleware are functions which are executed before or after requests to the
|
||||
server. They can be used to modify the *request to* or *response from*
|
||||
user-defined handler functions.
|
||||
|
||||
Middleware is registered via the middleware decorator, and can either be added as 'request' or 'response' middleware, based on the argument provided in the decorator. Response middleware receives both the request and the response as arguments.
|
||||
There are two types of middleware: request and response. Both are declared
|
||||
using the `@app.middleware` decorator, with the decorator's parameter being a
|
||||
string representing its type: `'request'` or `'response'`. Response middleware
|
||||
receives both the request and the response as arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
The simplest middleware doesn't modify the request or response at all:
|
||||
|
||||
```python
|
||||
@app.middleware('request')
|
||||
async def print_on_request(request):
|
||||
print("I print when a request is received by the server")
|
||||
|
||||
@app.middleware('response')
|
||||
async def print_on_response(request, response):
|
||||
print("I print when a response is returned by the server")
|
||||
```
|
||||
|
||||
## Modifying the request or response
|
||||
|
||||
Middleware can modify the request or response parameter it is given, *as long
|
||||
as it does not return it*. The following example shows a practical use-case for
|
||||
this.
|
||||
|
||||
```python
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.middleware
|
||||
async def halt_request(request):
|
||||
print("I am a spy")
|
||||
@app.middleware('response')
|
||||
async def custom_banner(request, response):
|
||||
response.headers["Server"] = "Fake-Server"
|
||||
|
||||
@app.middleware('response')
|
||||
async def prevent_xss(request, response):
|
||||
response.headers["x-xss-protection"] = "1; mode=block"
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
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')
|
||||
@@ -20,10 +63,8 @@ async def halt_request(request):
|
||||
@app.middleware('response')
|
||||
async def halt_response(request, response):
|
||||
return text('I halted the response')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return text('I would like to speak now please')
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
**Previous:** [Static Files](static_files.md)
|
||||
|
||||
**Next:** [Exceptions](exceptions.md)
|
||||
|
||||
@@ -1,49 +1,95 @@
|
||||
# Request Data
|
||||
|
||||
## Properties
|
||||
When an endpoint receives a HTTP request, the route function is passed a
|
||||
`Request` object.
|
||||
|
||||
The following request variables are accessible as properties:
|
||||
The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
`request.files` (dictionary of File objects) - List of files that have a name, body, and type
|
||||
`request.json` (any) - JSON body
|
||||
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
|
||||
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
|
||||
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
|
||||
- `json` (any) - JSON body
|
||||
|
||||
See request.py for more information
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "message": request.json })
|
||||
```
|
||||
|
||||
- `args` (dict) - Query string variables. A query string is the section of a
|
||||
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
||||
the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
||||
The request's `query_string` variable holds the unparsed string value.
|
||||
|
||||
## Examples
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||
```
|
||||
|
||||
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/files")
|
||||
def post_json(request):
|
||||
test_file = request.files.get('test')
|
||||
|
||||
file_parameters = {
|
||||
'body': test_file.body,
|
||||
'name': test_file.name,
|
||||
'type': test_file.type,
|
||||
}
|
||||
|
||||
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
||||
```
|
||||
|
||||
- `form` (dict) - Posted form variables.
|
||||
|
||||
```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",])
|
||||
def create_user(request):
|
||||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
||||
```
|
||||
|
||||
- `ip` (str) - IP address of the requester.
|
||||
|
||||
## Accessing values using `get` and `getlist`
|
||||
|
||||
The request properties which return a dictionary actually return a subclass of
|
||||
`dict` called `RequestParameters`. The key difference when using this object is
|
||||
the distinction between the `get` and `getlist` methods.
|
||||
|
||||
- `get(key, default=None)` operates as normal, except that when the value of
|
||||
the given key is a list, *only the first item is returned*.
|
||||
- `getlist(key, default=None)` operates as normal, *returning the entire list*.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.request import RequestParameters
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "message": request.json })
|
||||
args = RequestParameters()
|
||||
args['titles'] = ['Post 1', 'Post 2']
|
||||
|
||||
@app.route("/form")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
||||
args.get('titles') # => 'Post 1'
|
||||
|
||||
@app.route("/files")
|
||||
def post_json(request):
|
||||
test_file = request.files.get('test')
|
||||
|
||||
file_parameters = {
|
||||
'body': test_file.body,
|
||||
'name': test_file.name,
|
||||
'type': test_file.type,
|
||||
}
|
||||
|
||||
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||
|
||||
|
||||
@app.route("/users", methods=["POST",])
|
||||
def create_user(request):
|
||||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
||||
args.getlist('titles') # => ['Post 1', 'Post 2']
|
||||
```
|
||||
|
||||
**Previous:** [Routing](routing.md)
|
||||
|
||||
**Next:** [Deploying](deploying.md)
|
||||
|
||||
@@ -1,17 +1,48 @@
|
||||
# Routing
|
||||
|
||||
Sanic comes with a basic router that supports request parameters. To specify a parameter, surround it with carrots like so: `<PARAM>`. Request parameters will be passed to the request handler functions as keyword arguments. To specify a type, add a :type after the parameter name, in the carrots. If the parameter does not match the type supplied, Sanic will throw a NotFound exception, resulting in a 404 page not found error.
|
||||
Routing allows the user to specify handler functions for different URL endpoints.
|
||||
|
||||
|
||||
## Examples
|
||||
A basic route looks like the following, where `app` is an instance of the
|
||||
`Sanic` class:
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({ "hello": "world" })
|
||||
```
|
||||
|
||||
When the url `http://server.url/` is accessed (the base url of the server), the
|
||||
final `/` is matched by the router to the handler function, `test`, which then
|
||||
returns a JSON object.
|
||||
|
||||
Sanic handler functions must be defined using the `async def` syntax, as they
|
||||
are asynchronous functions.
|
||||
|
||||
## Request parameters
|
||||
|
||||
Sanic comes with a basic router that supports request parameters.
|
||||
|
||||
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
|
||||
Request parameters will be passed to the route handler functions as keyword
|
||||
arguments.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/tag/<tag>')
|
||||
async def tag_handler(request, tag):
|
||||
return text('Tag - {}'.format(tag))
|
||||
```
|
||||
|
||||
To specify a type for the parameter, add a `:type` after the parameter name,
|
||||
inside the quotes. If the parameter does not match the specified type, Sanic
|
||||
will throw a `NotFound` exception, resulting in a `404: Page not found` error
|
||||
on the URL.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/number/<integer_arg:int>')
|
||||
async def integer_handler(request, integer_arg):
|
||||
@@ -29,16 +60,52 @@ async def person_handler(request, name):
|
||||
async def folder_handler(request, folder_id):
|
||||
return text('Folder - {}'.format(folder_id))
|
||||
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
app.add_route(handler1, '/test')
|
||||
```
|
||||
|
||||
async def handler(request, name):
|
||||
return text('Folder - {}'.format(name))
|
||||
app.add_route(handler, '/folder/<name>')
|
||||
## HTTP request types
|
||||
|
||||
async def person_handler(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
app.add_route(handler, '/person/<name:[A-z]>')
|
||||
By default, a route defined on a URL will be used for all requests to that URL.
|
||||
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
||||
which restricts the handler function to the HTTP methods in the given list.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/post')
|
||||
async def post_handler(request, methods=['POST']):
|
||||
return text('POST request - {}'.format(request.json))
|
||||
|
||||
@app.route('/get')
|
||||
async def GET_handler(request, methods=['GET']):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
```
|
||||
|
||||
## The `add_route` method
|
||||
|
||||
As we have seen, routes are often specified using the `@app.route` decorator.
|
||||
However, this decorator is really just a wrapper for the `app.add_route`
|
||||
method, which is used as follows:
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
# Define the handler functions
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
|
||||
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
|
||||
|
||||
Both directories and files can be served by registering with static
|
||||
|
||||
## Example
|
||||
Static files and directories, such as an image file, are served by Sanic when
|
||||
registered with the `app.static` method. The method takes an endpoint URL and a
|
||||
filename. The file specified will then be accessible via the given endpoint.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
app = Sanic(__name__)
|
||||
|
||||
# Serves files from the static folder to the URL /static
|
||||
@@ -16,3 +17,7 @@ app.static('/the_best.png', '/home/ubuntu/test.png')
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
**Previous:** [Deploying](deploying.md)
|
||||
|
||||
**Next:** [Middleware](middleware.md)
|
||||
|
||||
55
docs/testing.md
Normal file
55
docs/testing.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Testing
|
||||
|
||||
Sanic endpoints can be tested locally using the `sanic.utils` module, which
|
||||
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
||||
library. The `sanic_endpoint_test` function runs a local server, issues a
|
||||
configurable request to an endpoint, and returns the result. It takes the
|
||||
following arguments:
|
||||
|
||||
- `app` An instance of a Sanic app.
|
||||
- `method` *(default `'get'`)* A string representing the HTTP method to use.
|
||||
- `uri` *(default `'/'`)* A string representing the endpoint to test.
|
||||
- `gather_request` *(default `True`)* A boolean which determines whether the
|
||||
original request will be returned by the function. If set to `True`, the
|
||||
return value is a tuple of `(request, response)`, if `False` only the
|
||||
response is returned.
|
||||
- `loop` *(default `None`)* The event loop to use.
|
||||
- `debug` *(default `False`)* A boolean which determines whether to run the
|
||||
server in debug mode.
|
||||
|
||||
The function further takes the `*request_args` and `**request_kwargs`, which
|
||||
are passed directly to the aiohttp ClientSession request. For example, to
|
||||
supply data with a GET request, `method` would be `get` and the keyword
|
||||
argument `params={'value', 'key'}` would be supplied. More information about
|
||||
the available arguments to aiohttp can be found
|
||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||
|
||||
Below is a complete example of an endpoint test,
|
||||
using [pytest](http://doc.pytest.org/en/latest/). The test checks that the
|
||||
`/challenge` endpoint responds to a GET request with a supplied challenge
|
||||
string.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import aiohttp
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
# Import the Sanic app, usually created with Sanic(__name__)
|
||||
from external_server import app
|
||||
|
||||
def test_endpoint_challenge():
|
||||
# Create the challenge data
|
||||
request_data = {'challenge': 'dummy_challenge'}
|
||||
|
||||
# Send the request to the endpoint, using the default `get` method
|
||||
request, response = sanic_endpoint_test(app,
|
||||
uri='/challenge',
|
||||
params=request_data)
|
||||
|
||||
# Assert that the server responds with the challenge string
|
||||
assert response.text == request_data['challenge']
|
||||
```
|
||||
|
||||
**Previous:** [Custom protocols](custom_protocol.md)
|
||||
|
||||
**Next:** [Sanic extensions](extensions.md)
|
||||
@@ -1,12 +1,8 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
import uvloop
|
||||
import aiohttp
|
||||
|
||||
#Create an event loop manually so that we can use it for both sanic & aiohttp
|
||||
loop = uvloop.new_event_loop()
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
async def fetch(session, url):
|
||||
@@ -24,10 +20,9 @@ async def test(request):
|
||||
"""
|
||||
url = "https://api.github.com/repos/channelcat/sanic"
|
||||
|
||||
async with aiohttp.ClientSession(loop=loop) as session:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
response = await fetch(session, url)
|
||||
return json(response)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, loop=loop)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, workers=2)
|
||||
|
||||
@@ -21,7 +21,7 @@ from aiocache.serializers import JsonSerializer
|
||||
app = Sanic(__name__)
|
||||
|
||||
aiocache.settings.set_defaults(
|
||||
cache="aiocache.RedisCache"
|
||||
class_="aiocache.RedisCache"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,31 +9,27 @@ and pass in an instance of it when we create our Sanic instance. Inside this
|
||||
class' default handler, we can do anything including sending exceptions to
|
||||
an external service.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
from sanic.exceptions import Handler, SanicException
|
||||
"""
|
||||
Imports and code relevant for our CustomHandler class
|
||||
(Ordinarily this would be in a separate file)
|
||||
"""
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import Handler, SanicException
|
||||
|
||||
|
||||
class CustomHandler(Handler):
|
||||
|
||||
def default(self, request, exception):
|
||||
# Here, we have access to the exception object
|
||||
# and can do anything with it (log, send to external service, etc)
|
||||
|
||||
# Some exceptions are trivial and built into Sanic (404s, etc)
|
||||
if not issubclass(type(exception), SanicException):
|
||||
if not isinstance(exception, SanicException):
|
||||
print(exception)
|
||||
|
||||
# Then, we must finish handling the exception by returning
|
||||
# our response to the client
|
||||
# For this we can just call the super class' default handler
|
||||
return super.default(self, request, exception)
|
||||
|
||||
|
||||
return super().default(request, exception)
|
||||
|
||||
|
||||
"""
|
||||
@@ -49,11 +45,12 @@ app = Sanic(__name__)
|
||||
handler = CustomHandler(sanic=app)
|
||||
app.error_handler = handler
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
# Here, something occurs which causes an unexpected exception
|
||||
# This exception will flow to our custom handler.
|
||||
x = 1 / 0
|
||||
1 / 0
|
||||
return json({"test": True})
|
||||
|
||||
|
||||
|
||||
18
examples/jinja_example.py
Normal file
18
examples/jinja_example.py
Normal file
@@ -0,0 +1,18 @@
|
||||
## To use this example:
|
||||
# curl -d '{"name": "John Doe"}' localhost:8000
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import html
|
||||
from jinja2 import Template
|
||||
|
||||
template = Template('Hello {{ name }}!')
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
data = request.json
|
||||
return html(template.render(**data))
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
36
examples/limit_concurrency.py
Normal file
36
examples/limit_concurrency.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
sem = None
|
||||
|
||||
def init(sanic, loop):
|
||||
global sem
|
||||
CONCURRENCY_PER_WORKER = 4
|
||||
sem = asyncio.Semaphore(CONCURRENCY_PER_WORKER, loop=loop)
|
||||
|
||||
async def bounded_fetch(session, url):
|
||||
"""
|
||||
Use session object to perform 'get' request on url
|
||||
"""
|
||||
async with sem, session.get(url) as response:
|
||||
return await response.json()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
"""
|
||||
Download and serve example JSON
|
||||
"""
|
||||
url = "https://api.github.com/repos/channelcat/sanic"
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
response = await bounded_fetch(session, url)
|
||||
return json(response)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, workers=2, before_start=init)
|
||||
23
examples/override_logging.py
Normal file
23
examples/override_logging.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
import json
|
||||
import logging
|
||||
|
||||
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
|
||||
logging_format += "%(message)s"
|
||||
|
||||
logging.basicConfig(
|
||||
format=logging_format,
|
||||
level=logging.DEBUG
|
||||
)
|
||||
log = logging.getLogger()
|
||||
|
||||
# Set logger to override default basicConfig
|
||||
sanic = Sanic()
|
||||
@sanic.route("/")
|
||||
def test(request):
|
||||
log.info("received request; responding with 'hey'")
|
||||
return text("hey")
|
||||
|
||||
sanic.run(host="0.0.0.0", port=8000)
|
||||
20
examples/run_async.py
Normal file
20
examples/run_async.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from multiprocessing import Event
|
||||
from signal import signal, SIGINT
|
||||
import asyncio
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"answer": "42"})
|
||||
|
||||
server = app.create_server(host="0.0.0.0", port=8001)
|
||||
loop = asyncio.get_event_loop()
|
||||
task = asyncio.ensure_future(server)
|
||||
signal(SIGINT, lambda s, f: loop.close())
|
||||
try:
|
||||
loop.run_forever()
|
||||
except:
|
||||
loop.stop()
|
||||
65
examples/sanic_aiopg_example.py
Normal file
65
examples/sanic_aiopg_example.py
Normal file
@@ -0,0 +1,65 @@
|
||||
""" To run this example you need additional aiopg package
|
||||
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
import aiopg
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
database_name = os.environ['DATABASE_NAME']
|
||||
database_host = os.environ['DATABASE_HOST']
|
||||
database_user = os.environ['DATABASE_USER']
|
||||
database_password = os.environ['DATABASE_PASSWORD']
|
||||
|
||||
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
|
||||
database_password,
|
||||
database_host,
|
||||
database_name)
|
||||
|
||||
|
||||
async def get_pool():
|
||||
return await aiopg.create_pool(connection)
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
|
||||
async def prepare_db(app, loop):
|
||||
"""
|
||||
Let's create some table and add some data
|
||||
"""
|
||||
async with aiopg.create_pool(connection) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute('DROP TABLE IF EXISTS sanic_polls')
|
||||
await cur.execute("""CREATE TABLE sanic_polls (
|
||||
id serial primary key,
|
||||
question varchar(50),
|
||||
pub_date timestamp
|
||||
);""")
|
||||
for i in range(0, 100):
|
||||
await cur.execute("""INSERT INTO sanic_polls
|
||||
(id, question, pub_date) VALUES ({}, {}, now())
|
||||
""".format(i, i))
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def handle(request):
|
||||
result = []
|
||||
async def test_select():
|
||||
async with aiopg.create_pool(connection) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("SELECT question, pub_date FROM sanic_polls")
|
||||
async for row in cur:
|
||||
result.append({"question": row[0], "pub_date": row[1]})
|
||||
res = await test_select()
|
||||
return json({'polls': result})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0',
|
||||
port=8000,
|
||||
debug=True,
|
||||
before_start=prepare_db)
|
||||
66
examples/sanic_aiopg_sqlalchemy_example.py
Normal file
66
examples/sanic_aiopg_sqlalchemy_example.py
Normal file
@@ -0,0 +1,66 @@
|
||||
""" To run this example you need additional aiopg package
|
||||
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
import uvloop
|
||||
from aiopg.sa import create_engine
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
database_name = os.environ['DATABASE_NAME']
|
||||
database_host = os.environ['DATABASE_HOST']
|
||||
database_user = os.environ['DATABASE_USER']
|
||||
database_password = os.environ['DATABASE_PASSWORD']
|
||||
|
||||
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
|
||||
database_password,
|
||||
database_host,
|
||||
database_name)
|
||||
|
||||
metadata = sa.MetaData()
|
||||
|
||||
polls = sa.Table('sanic_polls', metadata,
|
||||
sa.Column('id', sa.Integer, primary_key=True),
|
||||
sa.Column('question', sa.String(50)),
|
||||
sa.Column("pub_date", sa.DateTime))
|
||||
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
|
||||
|
||||
async def prepare_db(app, loop):
|
||||
""" Let's add some data
|
||||
|
||||
"""
|
||||
async with create_engine(connection) as engine:
|
||||
async with engine.acquire() as conn:
|
||||
await conn.execute('DROP TABLE IF EXISTS sanic_polls')
|
||||
await conn.execute("""CREATE TABLE sanic_polls (
|
||||
id serial primary key,
|
||||
question varchar(50),
|
||||
pub_date timestamp
|
||||
);""")
|
||||
for i in range(0, 100):
|
||||
await conn.execute(
|
||||
polls.insert().values(question=i,
|
||||
pub_date=datetime.datetime.now())
|
||||
)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def handle(request):
|
||||
async with create_engine(connection) as engine:
|
||||
async with engine.acquire() as conn:
|
||||
result = []
|
||||
async for row in conn.execute(polls.select()):
|
||||
result.append({"question": row.question, "pub_date": row.pub_date})
|
||||
return json({"polls": result})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, before_start=prepare_db)
|
||||
58
examples/sanic_asyncpg_example.py
Normal file
58
examples/sanic_asyncpg_example.py
Normal file
@@ -0,0 +1,58 @@
|
||||
""" 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
|
||||
|
||||
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]
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
async def create_db(app, loop):
|
||||
"""
|
||||
Create some table and add some data
|
||||
"""
|
||||
async with create_pool(**DB_CONFIG) as pool:
|
||||
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 create_pool(**DB_CONFIG) as pool:
|
||||
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__':
|
||||
app.run(host='0.0.0.0', port=8000, before_start=create_db)
|
||||
@@ -9,19 +9,18 @@ from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
## peewee_async related imports
|
||||
import uvloop
|
||||
import peewee
|
||||
from peewee_async import Manager, PostgresqlDatabase
|
||||
|
||||
# we instantiate a custom loop so we can pass it to our db manager
|
||||
loop = uvloop.new_event_loop()
|
||||
|
||||
database = PostgresqlDatabase(database='test',
|
||||
host='127.0.0.1',
|
||||
user='postgres',
|
||||
password='mysecretpassword')
|
||||
def setup(app, loop):
|
||||
database = PostgresqlDatabase(database='test',
|
||||
host='127.0.0.1',
|
||||
user='postgres',
|
||||
password='mysecretpassword')
|
||||
|
||||
objects = Manager(database, loop=loop)
|
||||
objects = Manager(database, loop=loop)
|
||||
|
||||
## from peewee_async docs:
|
||||
# Also there’s no need to connect and re-connect before executing async queries
|
||||
@@ -76,5 +75,4 @@ async def get(request):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0', port=8000, loop=loop)
|
||||
|
||||
app.run(host='0.0.0.0', port=8000, before_start=setup)
|
||||
|
||||
@@ -64,11 +64,11 @@ def query_string(request):
|
||||
# Run Server
|
||||
# ----------------------------------------------- #
|
||||
|
||||
def after_start(loop):
|
||||
def after_start(app, loop):
|
||||
log.info("OH OH OH OH OHHHHHHHH")
|
||||
|
||||
|
||||
def before_stop(loop):
|
||||
def before_stop(app, loop):
|
||||
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
|
||||
tornado
|
||||
aiofiles
|
||||
sphinx
|
||||
recommonmark
|
||||
beautifulsoup4
|
||||
|
||||
@@ -2,4 +2,3 @@ httptools
|
||||
ujson
|
||||
uvloop
|
||||
aiofiles
|
||||
multidict
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .sanic import Sanic
|
||||
from .blueprints import Blueprint
|
||||
|
||||
__version__ = '0.1.8'
|
||||
__version__ = '0.3.0'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
||||
@@ -20,7 +20,7 @@ if __name__ == "__main__":
|
||||
|
||||
module = import_module(module_name)
|
||||
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 {}. "
|
||||
"Perhaps you meant {}.app?"
|
||||
.format(type(app).__name__, args.module))
|
||||
|
||||
@@ -3,6 +3,7 @@ from collections import defaultdict
|
||||
|
||||
class BlueprintSetup:
|
||||
"""
|
||||
Creates a blueprint state like object.
|
||||
"""
|
||||
|
||||
def __init__(self, blueprint, app, options):
|
||||
@@ -18,24 +19,27 @@ class BlueprintSetup:
|
||||
#: blueprint.
|
||||
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.
|
||||
"""
|
||||
if self.url_prefix:
|
||||
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):
|
||||
"""
|
||||
Registers exceptions to sanic
|
||||
Registers exceptions to sanic.
|
||||
"""
|
||||
self.app.exception(*args, **kwargs)(handler)
|
||||
|
||||
def add_static(self, uri, file_or_directory, *args, **kwargs):
|
||||
"""
|
||||
Registers static files to sanic
|
||||
Registers static files to sanic.
|
||||
"""
|
||||
if self.url_prefix:
|
||||
uri = self.url_prefix + uri
|
||||
@@ -44,7 +48,7 @@ class BlueprintSetup:
|
||||
|
||||
def add_middleware(self, middleware, *args, **kwargs):
|
||||
"""
|
||||
Registers middleware to sanic
|
||||
Registers middleware to sanic.
|
||||
"""
|
||||
if args or kwargs:
|
||||
self.app.middleware(*args, **kwargs)(middleware)
|
||||
@@ -53,7 +57,7 @@ class BlueprintSetup:
|
||||
|
||||
|
||||
class Blueprint:
|
||||
def __init__(self, name, url_prefix=None):
|
||||
def __init__(self, name, url_prefix=None, host=None):
|
||||
"""
|
||||
Creates a new blueprint
|
||||
:param name: Unique name of the blueprint
|
||||
@@ -63,6 +67,7 @@ class Blueprint:
|
||||
self.url_prefix = url_prefix
|
||||
self.deferred_functions = []
|
||||
self.listeners = defaultdict(list)
|
||||
self.host = host
|
||||
|
||||
def record(self, func):
|
||||
"""
|
||||
@@ -73,32 +78,43 @@ class Blueprint:
|
||||
|
||||
def make_setup_state(self, app, options):
|
||||
"""
|
||||
Returns a new BlueprintSetup object
|
||||
"""
|
||||
return BlueprintSetup(self, app, options)
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Registers the blueprint to the sanic app.
|
||||
"""
|
||||
state = self.make_setup_state(app, options)
|
||||
for deferred in self.deferred_functions:
|
||||
deferred(state)
|
||||
|
||||
def route(self, uri, methods=None):
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""
|
||||
Creates a blueprint route from a decorated function.
|
||||
:param uri: Endpoint at which the route will be accessible.
|
||||
:param methods: List of acceptable HTTP methods.
|
||||
"""
|
||||
def decorator(handler):
|
||||
self.record(lambda s: s.add_route(handler, uri, methods))
|
||||
self.record(lambda s: s.add_route(handler, uri, methods, host))
|
||||
return handler
|
||||
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
|
||||
|
||||
def listener(self, event):
|
||||
"""
|
||||
Create a listener from a decorated function.
|
||||
:param event: Event to listen to.
|
||||
"""
|
||||
def decorator(listener):
|
||||
self.listeners[event].append(listener)
|
||||
@@ -107,6 +123,7 @@ class Blueprint:
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint middleware from a decorated function.
|
||||
"""
|
||||
def register_middleware(middleware):
|
||||
self.record(
|
||||
@@ -123,6 +140,7 @@ class Blueprint:
|
||||
|
||||
def exception(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint exception from a decorated function.
|
||||
"""
|
||||
def decorator(handler):
|
||||
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
|
||||
@@ -131,6 +149,9 @@ class Blueprint:
|
||||
|
||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint static route from a decorated function.
|
||||
:param uri: Endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
self.record(
|
||||
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))
|
||||
|
||||
@@ -42,8 +42,9 @@ class CookieJar(dict):
|
||||
"""
|
||||
CookieJar dynamically writes headers as cookies are added and removed
|
||||
It gets around the limitation of one header per name by using the
|
||||
MultiHeader class to provide a unique key that encodes to Set-Cookie
|
||||
MultiHeader class to provide a unique key that encodes to Set-Cookie.
|
||||
"""
|
||||
|
||||
def __init__(self, headers):
|
||||
super().__init__()
|
||||
self.headers = headers
|
||||
@@ -54,6 +55,7 @@ class CookieJar(dict):
|
||||
cookie_header = self.cookie_headers.get(key)
|
||||
if not cookie_header:
|
||||
cookie = Cookie(key, value)
|
||||
cookie['path'] = '/'
|
||||
cookie_header = MultiHeader("Set-Cookie")
|
||||
self.cookie_headers[key] = cookie_header
|
||||
self.headers[cookie_header] = cookie
|
||||
@@ -62,8 +64,14 @@ class CookieJar(dict):
|
||||
self[key].value = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.cookie_headers[key]
|
||||
return super().__delitem__(key)
|
||||
if key not in self.cookie_headers:
|
||||
self[key] = ''
|
||||
self[key]['max-age'] = 0
|
||||
else:
|
||||
cookie_header = self.cookie_headers[key]
|
||||
del self.headers[cookie_header]
|
||||
del self.cookie_headers[key]
|
||||
return super().__delitem__(key)
|
||||
|
||||
|
||||
class Cookie(dict):
|
||||
@@ -107,7 +115,8 @@ class Cookie(dict):
|
||||
value.strftime("%a, %d-%b-%Y %T GMT")
|
||||
))
|
||||
elif key in self._flags:
|
||||
output.append(self._keys[key])
|
||||
if self[key]:
|
||||
output.append(self._keys[key])
|
||||
else:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
|
||||
|
||||
@@ -1,5 +1,104 @@
|
||||
from .response import text
|
||||
from traceback import format_exc
|
||||
from .response import text, html
|
||||
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):
|
||||
@@ -34,12 +133,31 @@ class RequestTimeout(SanicException):
|
||||
status_code = 408
|
||||
|
||||
|
||||
class PayloadTooLarge(SanicException):
|
||||
status_code = 413
|
||||
|
||||
|
||||
class Handler:
|
||||
handlers = None
|
||||
|
||||
def __init__(self, sanic):
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
self.sanic = sanic
|
||||
self.debug = False
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
frames = extract_tb(tb)
|
||||
|
||||
frame_html = []
|
||||
for frame in frames:
|
||||
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
|
||||
|
||||
return TRACEBACK_WRAPPER_HTML.format(
|
||||
style=TRACEBACK_STYLE,
|
||||
exc_name=exc_type.__name__,
|
||||
exc_value=exc_value,
|
||||
frame_html=''.join(frame_html),
|
||||
uri=request.url)
|
||||
|
||||
def add(self, exception, handler):
|
||||
self.handlers[exception] = handler
|
||||
@@ -47,23 +165,40 @@ class Handler:
|
||||
def response(self, request, exception):
|
||||
"""
|
||||
Fetches and executes an exception handler and returns a response object
|
||||
|
||||
:param request: Request
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.handlers.get(type(exception), self.default)
|
||||
response = handler(request=request, exception=exception)
|
||||
try:
|
||||
response = handler(request=request, exception=exception)
|
||||
except:
|
||||
log.error(format_exc())
|
||||
if self.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
|
||||
|
||||
def default(self, request, exception):
|
||||
if issubclass(type(exception), SanicException):
|
||||
log.error(format_exc())
|
||||
if isinstance(exception, SanicException):
|
||||
return text(
|
||||
"Error: {}".format(exception),
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500))
|
||||
elif self.sanic.debug:
|
||||
return text(
|
||||
"Error: {}\nException: {}".format(
|
||||
exception, format_exc()), status=500)
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
response_message = (
|
||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||
request.url, format_exc()))
|
||||
log.error(response_message)
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return text(
|
||||
"An error occurred while generating the request", status=500)
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
|
||||
log = logging.getLogger(__name__)
|
||||
log = logging.getLogger('sanic')
|
||||
|
||||
@@ -4,6 +4,7 @@ from http.cookies import SimpleCookie
|
||||
from httptools import parse_url
|
||||
from urllib.parse import parse_qs
|
||||
from ujson import loads as json_loads
|
||||
from sanic.exceptions import InvalidUsage
|
||||
|
||||
from .log import log
|
||||
|
||||
@@ -20,16 +21,13 @@ class RequestParameters(dict):
|
||||
value of the list and getlist returns the whole shebang
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.super = super()
|
||||
self.super.__init__(*args, **kwargs)
|
||||
|
||||
def get(self, name, default=None):
|
||||
values = self.super.get(name)
|
||||
return values[0] if values else default
|
||||
"""Return the first value, either the default or actual"""
|
||||
return super().get(name, [default])[0]
|
||||
|
||||
def getlist(self, name, default=None):
|
||||
return self.super.get(name, default)
|
||||
"""Return the entire list"""
|
||||
return super().get(name, default)
|
||||
|
||||
|
||||
class Request(dict):
|
||||
@@ -37,24 +35,26 @@ class Request(dict):
|
||||
Properties of an HTTP request such as URL, headers, etc.
|
||||
"""
|
||||
__slots__ = (
|
||||
'url', 'headers', 'version', 'method', '_cookies',
|
||||
'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'query_string', 'body',
|
||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip',
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method):
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
# TODO: Content-Encoding detection
|
||||
url_parsed = parse_url(url_bytes)
|
||||
self.url = url_parsed.path.decode('utf-8')
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
self.method = method
|
||||
self.transport = transport
|
||||
self.query_string = None
|
||||
if url_parsed.query:
|
||||
self.query_string = url_parsed.query.decode('utf-8')
|
||||
|
||||
# Init but do not inhale
|
||||
self.body = None
|
||||
self.body = []
|
||||
self.parsed_json = None
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
@@ -63,14 +63,25 @@ class Request(dict):
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
if not self.parsed_json:
|
||||
if self.parsed_json is None:
|
||||
try:
|
||||
self.parsed_json = json_loads(self.body)
|
||||
except Exception:
|
||||
log.exception("failed when parsing body as json")
|
||||
raise InvalidUsage("Failed when parsing body as 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
|
||||
def form(self):
|
||||
if self.parsed_form is None:
|
||||
@@ -89,7 +100,7 @@ class Request(dict):
|
||||
self.parsed_form, self.parsed_files = (
|
||||
parse_multipart_form(self.body, boundary))
|
||||
except Exception:
|
||||
log.exception("failed when parsing form")
|
||||
log.exception("Failed when parsing form")
|
||||
|
||||
return self.parsed_form
|
||||
|
||||
@@ -114,15 +125,22 @@ class Request(dict):
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
if 'Cookie' in self.headers:
|
||||
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
|
||||
if cookie is not None:
|
||||
cookies = SimpleCookie()
|
||||
cookies.load(self.headers['Cookie'])
|
||||
cookies.load(cookie)
|
||||
self._cookies = {name: cookie.value
|
||||
for name, cookie in cookies.items()}
|
||||
else:
|
||||
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'])
|
||||
|
||||
@@ -130,6 +148,7 @@ File = namedtuple('File', ['type', 'body', 'name'])
|
||||
def parse_multipart_form(body, boundary):
|
||||
"""
|
||||
Parses a request body and returns fields and files
|
||||
|
||||
:param body: Bytes request body
|
||||
:param boundary: Bytes multipart boundary
|
||||
:return: fields (RequestParameters), files (RequestParameters)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from aiofiles import open as open_async
|
||||
from .cookies import CookieJar
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
|
||||
from ujson import dumps as json_dumps
|
||||
|
||||
from .cookies import CookieJar
|
||||
|
||||
COMMON_STATUS_CODES = {
|
||||
200: b'OK',
|
||||
400: b'Bad Request',
|
||||
@@ -79,7 +81,12 @@ class HTTPResponse:
|
||||
self.content_type = content_type
|
||||
|
||||
if body is not None:
|
||||
self.body = body.encode('utf-8')
|
||||
try:
|
||||
# Try to encode it regularly
|
||||
self.body = body.encode()
|
||||
except AttributeError:
|
||||
# Convert it to a str if you can't
|
||||
self.body = str(body).encode()
|
||||
else:
|
||||
self.body = body_bytes
|
||||
|
||||
@@ -96,10 +103,14 @@ class HTTPResponse:
|
||||
|
||||
headers = b''
|
||||
if self.headers:
|
||||
headers = b''.join(
|
||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
||||
for name, value in self.headers.items()
|
||||
)
|
||||
for name, value in self.headers.items():
|
||||
try:
|
||||
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
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
@@ -131,22 +142,47 @@ class HTTPResponse:
|
||||
return self._cookies
|
||||
|
||||
|
||||
def json(body, status=200, headers=None):
|
||||
return HTTPResponse(json_dumps(body), headers=headers, status=status,
|
||||
content_type="application/json")
|
||||
def json(body, status=200, headers=None, **kwargs):
|
||||
"""
|
||||
Returns response object with body in json format.
|
||||
:param body: Response data to be serialized.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param \**kwargs: Remaining arguments that are passed to the json encoder.
|
||||
"""
|
||||
return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
|
||||
status=status, content_type="application/json")
|
||||
|
||||
|
||||
def text(body, status=200, headers=None):
|
||||
"""
|
||||
Returns response object with body in text format.
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/plain; charset=utf-8")
|
||||
|
||||
|
||||
def html(body, status=200, headers=None):
|
||||
"""
|
||||
Returns response object with body in html format.
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
async def file(location, mime_type=None, headers=None):
|
||||
"""
|
||||
Returns response object with file data.
|
||||
:param location: Location of file on system.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
@@ -158,3 +194,26 @@ async def file(location, mime_type=None, headers=None):
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream)
|
||||
|
||||
|
||||
def redirect(to, headers=None, status=302,
|
||||
content_type="text/html; charset=utf-8"):
|
||||
"""
|
||||
Aborts execution and causes a 302 redirect (by default).
|
||||
|
||||
:param to: path or fully qualified URL to redirect to
|
||||
:param headers: optional dict of headers to include in the new request
|
||||
:param status: status code (int) of the new request, defaults to 302
|
||||
:param content_type:
|
||||
the content type (string) of the response
|
||||
:returns: the redirecting Response
|
||||
"""
|
||||
headers = headers or {}
|
||||
|
||||
# According to RFC 7231, a relative URI is now permitted.
|
||||
headers['Location'] = to
|
||||
|
||||
return HTTPResponse(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type)
|
||||
|
||||
117
sanic/router.py
117
sanic/router.py
@@ -3,6 +3,7 @@ from collections import defaultdict, namedtuple
|
||||
from functools import lru_cache
|
||||
from .config import Config
|
||||
from .exceptions import NotFound, InvalidUsage
|
||||
from .views import CompositionView
|
||||
|
||||
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
@@ -23,18 +24,36 @@ class RouteExists(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RouteDoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Router:
|
||||
"""
|
||||
Router supports basic routing with parameters and method checks
|
||||
|
||||
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...
|
||||
|
||||
or
|
||||
|
||||
.. 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...
|
||||
|
||||
Parameters will be passed as keyword arguments to the request handling
|
||||
function provided Parameters can also have a type by appending :type to
|
||||
the <parameter>. If no type is provided, a string is expected. A regular
|
||||
expression can also be passed in as the type
|
||||
function. Provided parameters can also have a type by appending :type to
|
||||
the <parameter>. Given parameter must be able to be type-casted to this.
|
||||
If no type is provided, a string is expected. A regular expression can
|
||||
also be passed in as the type. The argument given to the function will
|
||||
always be a string, independent of the type.
|
||||
"""
|
||||
routes_static = None
|
||||
routes_dynamic = None
|
||||
@@ -45,19 +64,36 @@ class Router:
|
||||
self.routes_static = {}
|
||||
self.routes_dynamic = defaultdict(list)
|
||||
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
|
||||
|
||||
:param uri: Path to match
|
||||
:param methods: Array of accepted method names.
|
||||
If none are provided, any method is allowed
|
||||
If none are provided, any method is allowed
|
||||
:param handler: Request handler function.
|
||||
When executed, it should provide a response object.
|
||||
When executed, it should provide a response object.
|
||||
: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
|
||||
if methods:
|
||||
@@ -91,9 +127,35 @@ class Router:
|
||||
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
|
||||
pattern = re.compile(r'^{}$'.format(pattern_string))
|
||||
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters)
|
||||
def merge_route(route, methods, handler):
|
||||
# merge to the existing route when possible.
|
||||
if not route.methods or not methods:
|
||||
# method-unspecified routes are not mergeable.
|
||||
raise RouteExists(
|
||||
"Route already registered: {}".format(uri))
|
||||
elif route.methods.intersection(methods):
|
||||
# already existing method is not overloadable.
|
||||
duplicated = methods.intersection(route.methods)
|
||||
raise RouteExists(
|
||||
"Route already registered: {} [{}]".format(
|
||||
uri, ','.join(list(duplicated))))
|
||||
if isinstance(route.handler, CompositionView):
|
||||
view = route.handler
|
||||
else:
|
||||
view = CompositionView()
|
||||
view.add(route.methods, route.handler)
|
||||
view.add(methods, handler)
|
||||
route = route._replace(
|
||||
handler=view, methods=methods.union(route.methods))
|
||||
return route
|
||||
|
||||
route = self.routes_all.get(uri)
|
||||
if route:
|
||||
route = merge_route(route, methods, handler)
|
||||
else:
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if properties['unhashable']:
|
||||
@@ -103,17 +165,41 @@ class Router:
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
error
|
||||
|
||||
:param request: Request object
|
||||
: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)
|
||||
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
|
||||
error. Internal method for caching.
|
||||
@@ -121,6 +207,7 @@ class Router:
|
||||
:param method: Request method
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
url = host + url
|
||||
# Check against known static routes
|
||||
route = self.routes_static.get(url)
|
||||
if route:
|
||||
|
||||
230
sanic/sanic.py
230
sanic/sanic.py
@@ -1,37 +1,46 @@
|
||||
import logging
|
||||
from asyncio import get_event_loop
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
from inspect import isawaitable, stack, getmodulename
|
||||
from multiprocessing import Process, Event
|
||||
from signal import signal, SIGTERM, SIGINT
|
||||
from time import sleep
|
||||
from traceback import format_exc
|
||||
|
||||
from .config import Config
|
||||
from .exceptions import Handler
|
||||
from .log import log, logging
|
||||
from .exceptions import ServerError
|
||||
from .log import log
|
||||
from .response import HTTPResponse
|
||||
from .router import Router
|
||||
from .server import serve
|
||||
from .server import serve, serve_multiple, HttpProtocol
|
||||
from .static import register as static_register
|
||||
from .exceptions import ServerError
|
||||
|
||||
|
||||
class Sanic:
|
||||
def __init__(self, name=None, router=None, error_handler=None):
|
||||
def __init__(self, name=None, router=None,
|
||||
error_handler=None):
|
||||
# Only set up a default log handler if the
|
||||
# end-user application didn't set anything up.
|
||||
if not logging.root.handlers and log.level == logging.NOTSET:
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s: %(levelname)s: %(message)s")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
log.setLevel(logging.INFO)
|
||||
if name is None:
|
||||
frame_records = stack()[1]
|
||||
name = getmodulename(frame_records[1])
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.error_handler = error_handler or Handler(self)
|
||||
self.error_handler = error_handler or Handler()
|
||||
self.config = Config()
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
self.blueprints = {}
|
||||
self._blueprint_order = []
|
||||
self.loop = None
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.processes = None
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
@@ -41,9 +50,10 @@ class Sanic:
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
# 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
|
||||
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:return: decorated function
|
||||
@@ -55,29 +65,54 @@ class Sanic:
|
||||
uri = '/' + uri
|
||||
|
||||
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 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
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
|
||||
:param handler: function or class instance
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:return: function or class instance
|
||||
"""
|
||||
self.route(uri=uri, methods=methods)(handler)
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
return handler
|
||||
|
||||
def remove_route(self, uri, clean_cache=True, host=None):
|
||||
self.router.remove(uri, clean_cache, host)
|
||||
|
||||
# Decorator
|
||||
def exception(self, *exceptions):
|
||||
"""
|
||||
Decorates a function to be registered as a handler for exceptions
|
||||
:param *exceptions: exceptions
|
||||
|
||||
:param \*exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
@@ -123,6 +158,7 @@ class Sanic:
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""
|
||||
Registers a blueprint on the application.
|
||||
|
||||
:param blueprint: Blueprint object
|
||||
:param options: option dictionary with blueprint defaults
|
||||
:return: Nothing
|
||||
@@ -140,7 +176,8 @@ class Sanic:
|
||||
def register_blueprint(self, *args, **kwargs):
|
||||
# TODO: deprecate 1.0
|
||||
log.warning("Use of register_blueprint will be deprecated in "
|
||||
"version 1.0. Please use the blueprint method instead")
|
||||
"version 1.0. Please use the blueprint method instead",
|
||||
DeprecationWarning)
|
||||
return self.blueprint(*args, **kwargs)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
@@ -155,9 +192,10 @@ class Sanic:
|
||||
Takes a request from the HTTP Server and returns a response object to
|
||||
be sent back The HTTP Server only expects a response object, so
|
||||
exception handling must be done here
|
||||
|
||||
:param request: HTTP Request object
|
||||
:param response_callback: Response function to be called with the
|
||||
response as the only argument
|
||||
response as the only argument
|
||||
:return: Nothing
|
||||
"""
|
||||
try:
|
||||
@@ -193,18 +231,18 @@ class Sanic:
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Response Middleware
|
||||
# -------------------------------------------- #
|
||||
# -------------------------------------------- #
|
||||
# Response Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
if self.response_middleware:
|
||||
for middleware in self.response_middleware:
|
||||
_response = middleware(request, response)
|
||||
if isawaitable(_response):
|
||||
_response = await _response
|
||||
if _response:
|
||||
response = _response
|
||||
break
|
||||
if self.response_middleware:
|
||||
for middleware in self.response_middleware:
|
||||
_response = middleware(request, response)
|
||||
if isawaitable(_response):
|
||||
_response = await _response
|
||||
if _response:
|
||||
response = _response
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
# -------------------------------------------- #
|
||||
@@ -222,7 +260,7 @@ class Sanic:
|
||||
e, format_exc()))
|
||||
else:
|
||||
response = HTTPResponse(
|
||||
"An error occured while handling an error")
|
||||
"An error occurred while handling an error")
|
||||
|
||||
response_callback(response)
|
||||
|
||||
@@ -231,42 +269,52 @@ class Sanic:
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, sock=None,
|
||||
workers=1, loop=None):
|
||||
after_start=None, before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, workers=1, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None, register_sys_signals=True):
|
||||
"""
|
||||
Runs the HTTP Server and listens until keyboard interrupt or term
|
||||
signal. On termination, drains connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param before_start: Function to be executed before the server starts
|
||||
accepting connections
|
||||
:param after_start: Function to be executed after the server starts
|
||||
accepting connections
|
||||
:param before_stop: Function to be executed when a stop signal is
|
||||
received before it is respected
|
||||
:param after_stop: Function to be executed when all requests are
|
||||
complete
|
||||
:param before_start: Functions to be executed before the server starts
|
||||
accepting connections
|
||||
:param after_start: Functions to be executed after the server starts
|
||||
accepting connections
|
||||
:param before_stop: Functions to be executed when a stop signal is
|
||||
received before it is respected
|
||||
:param after_stop: Functions to be executed when all requests are
|
||||
complete
|
||||
:param ssl: SSLContext for SSL encryption of worker(s)
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param workers: Number of processes
|
||||
received before it is respected
|
||||
:param loop: asyncio compatible event loop
|
||||
received before it is respected
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
self.error_handler.debug = True
|
||||
self.error_handler.debug = debug
|
||||
self.debug = debug
|
||||
if loop is not None:
|
||||
log.warning("Passing a loop will be deprecated in version 0.4.0"
|
||||
" https://github.com/channelcat/sanic/pull/335"
|
||||
" has more information.", DeprecationWarning)
|
||||
self.loop = loop
|
||||
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'sock': sock,
|
||||
'ssl': ssl,
|
||||
'debug': debug,
|
||||
'request_handler': self.handle_request,
|
||||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||
'loop': loop
|
||||
'register_sys_signals': register_sys_signals,
|
||||
'backlog': backlog
|
||||
}
|
||||
|
||||
# -------------------------------------------- #
|
||||
@@ -283,7 +331,7 @@ class Sanic:
|
||||
for blueprint in self.blueprints.values():
|
||||
listeners += blueprint.listeners[event_name]
|
||||
if args:
|
||||
if type(args) is not list:
|
||||
if callable(args):
|
||||
args = [args]
|
||||
listeners += args
|
||||
if reverse:
|
||||
@@ -297,15 +345,16 @@ class Sanic:
|
||||
log.debug(self.config.LOGO)
|
||||
|
||||
# Serve
|
||||
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
try:
|
||||
if workers == 1:
|
||||
serve(**server_settings)
|
||||
else:
|
||||
log.info('Spinning up {} workers...'.format(workers))
|
||||
|
||||
self.serve_multiple(server_settings, workers)
|
||||
serve_multiple(server_settings, workers, stop_event)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
@@ -314,44 +363,65 @@ class Sanic:
|
||||
log.info("Server Stopped")
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
This kills the Sanic
|
||||
"""
|
||||
"""This kills the Sanic"""
|
||||
get_event_loop().stop()
|
||||
|
||||
@staticmethod
|
||||
def serve_multiple(server_settings, workers, stop_event=None):
|
||||
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
|
||||
before_start=None, after_start=None,
|
||||
before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None):
|
||||
"""
|
||||
Starts multiple server processes simultaneously. Stops on interrupt
|
||||
and terminate signals, and drains connections when complete.
|
||||
:param server_settings: kw arguments to be passed to the serve function
|
||||
:param workers: number of workers to launch
|
||||
:param stop_event: if provided, is used as a stop signal
|
||||
:return:
|
||||
Asynchronous version of `run`.
|
||||
"""
|
||||
server_settings['reuse_port'] = True
|
||||
if loop is not None:
|
||||
log.warning("Passing a loop will be deprecated in version 0.4.0"
|
||||
" https://github.com/channelcat/sanic/pull/335"
|
||||
" has more information.", DeprecationWarning)
|
||||
loop = get_event_loop()
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'sock': sock,
|
||||
'ssl': ssl,
|
||||
'debug': debug,
|
||||
'request_handler': self.handle_request,
|
||||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||
'loop': loop,
|
||||
'backlog': backlog
|
||||
}
|
||||
|
||||
# Create a stop event to be triggered by a signal
|
||||
if not stop_event:
|
||||
stop_event = Event()
|
||||
signal(SIGINT, lambda s, f: stop_event.set())
|
||||
signal(SIGTERM, lambda s, f: stop_event.set())
|
||||
# -------------------------------------------- #
|
||||
# Register start/stop events
|
||||
# -------------------------------------------- #
|
||||
|
||||
processes = []
|
||||
for _ in range(workers):
|
||||
process = Process(target=serve, kwargs=server_settings)
|
||||
process.start()
|
||||
processes.append(process)
|
||||
for event_name, settings_name, args, reverse in (
|
||||
("before_server_start", "before_start", before_start, False),
|
||||
("after_server_start", "after_start", after_start, False),
|
||||
("before_server_stop", "before_stop", before_stop, True),
|
||||
("after_server_stop", "after_stop", after_stop, True)):
|
||||
listeners = []
|
||||
for blueprint in self.blueprints.values():
|
||||
listeners += blueprint.listeners[event_name]
|
||||
if args:
|
||||
if callable(args):
|
||||
args = [args]
|
||||
listeners += args
|
||||
if reverse:
|
||||
listeners.reverse()
|
||||
# Prepend sanic to the arguments when listeners are triggered
|
||||
listeners = [partial(listener, self) for listener in listeners]
|
||||
server_settings[settings_name] = listeners
|
||||
|
||||
# Infinitely wait for the stop event
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
sleep(0.3)
|
||||
except:
|
||||
pass
|
||||
server_settings['run_async'] = True
|
||||
|
||||
log.info('Spinning down workers...')
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
for process in processes:
|
||||
process.join()
|
||||
# Serve
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return await serve(**server_settings)
|
||||
|
||||
230
sanic/server.py
230
sanic/server.py
@@ -1,12 +1,20 @@
|
||||
import asyncio
|
||||
import os
|
||||
import traceback
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from multidict import CIMultiDict
|
||||
from signal import SIGINT, SIGTERM
|
||||
from multiprocessing import Process, Event
|
||||
from os import set_inheritable
|
||||
from signal import SIGTERM, SIGINT
|
||||
from signal import signal as signal_func
|
||||
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||
from time import time
|
||||
|
||||
from httptools import HttpRequestParser
|
||||
from httptools.parser.errors import HttpParserError
|
||||
|
||||
from .exceptions import ServerError
|
||||
|
||||
try:
|
||||
import uvloop as async_loop
|
||||
except ImportError:
|
||||
@@ -14,14 +22,33 @@ except ImportError:
|
||||
|
||||
from .log import log
|
||||
from .request import Request
|
||||
from .exceptions import RequestTimeout
|
||||
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
||||
|
||||
current_time = None
|
||||
|
||||
|
||||
class Signal:
|
||||
stopped = False
|
||||
|
||||
|
||||
current_time = None
|
||||
class CIDict(dict):
|
||||
"""
|
||||
Case Insensitive dict where all keys are converted to lowercase
|
||||
This does not maintain the inputted case when calling items() or keys()
|
||||
in favor of speed, since headers are case insensitive
|
||||
"""
|
||||
|
||||
def get(self, key, default=None):
|
||||
return super().get(key.casefold(), default)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return super().__getitem__(key.casefold())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
return super().__setitem__(key.casefold(), value)
|
||||
|
||||
def __contains__(self, key):
|
||||
return super().__contains__(key.casefold())
|
||||
|
||||
|
||||
class HttpProtocol(asyncio.Protocol):
|
||||
@@ -36,7 +63,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
'_total_request_size', '_timeout_handler', '_last_communication_time')
|
||||
|
||||
def __init__(self, *, loop, request_handler, error_handler,
|
||||
signal=Signal(), connections={}, request_timeout=60,
|
||||
signal=Signal(), connections=set(), request_timeout=60,
|
||||
request_max_size=None):
|
||||
self.loop = loop
|
||||
self.transport = None
|
||||
@@ -60,30 +87,28 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# -------------------------------------------- #
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.connections[self] = True
|
||||
self.connections.add(self)
|
||||
self._timeout_handler = self.loop.call_later(
|
||||
self.request_timeout, self.connection_timeout)
|
||||
self.transport = transport
|
||||
self._last_request_time = current_time
|
||||
|
||||
def connection_lost(self, exc):
|
||||
del self.connections[self]
|
||||
self.connections.discard(self)
|
||||
self._timeout_handler.cancel()
|
||||
self.cleanup()
|
||||
|
||||
def connection_timeout(self):
|
||||
# Check if
|
||||
time_elapsed = current_time - self._last_request_time
|
||||
if time_elapsed < self.request_timeout:
|
||||
time_left = self.request_timeout - time_elapsed
|
||||
self._timeout_handler = \
|
||||
self.loop.call_later(time_left, self.connection_timeout)
|
||||
self._timeout_handler = (
|
||||
self.loop.call_later(time_left, self.connection_timeout))
|
||||
else:
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
response = self.error_handler.response(
|
||||
self.request, RequestTimeout('Request Timeout'))
|
||||
self.write_response(response)
|
||||
exception = RequestTimeout('Request Timeout')
|
||||
self.write_error(exception)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Parsing
|
||||
@@ -94,9 +119,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# memory limits
|
||||
self._total_request_size += len(data)
|
||||
if self._total_request_size > self.request_max_size:
|
||||
return self.bail_out(
|
||||
"Request too large ({}), connection closed".format(
|
||||
self._total_request_size))
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
|
||||
# Create parser if this is the first time we're receiving data
|
||||
if self.parser is None:
|
||||
@@ -107,39 +131,35 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# Parse request chunk or close connection
|
||||
try:
|
||||
self.parser.feed_data(data)
|
||||
except HttpParserError as e:
|
||||
self.bail_out(
|
||||
"Invalid request data, connection closed ({})".format(e))
|
||||
except HttpParserError:
|
||||
exception = InvalidUsage('Bad Request')
|
||||
self.write_error(exception)
|
||||
|
||||
def on_url(self, url):
|
||||
self.url = url
|
||||
|
||||
def on_header(self, name, value):
|
||||
if name == b'Content-Length' and int(value) > self.request_max_size:
|
||||
return self.bail_out(
|
||||
"Request body too large ({}), connection closed".format(value))
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
|
||||
self.headers.append((name.decode(), value.decode('utf-8')))
|
||||
self.headers.append((name.decode().casefold(), value.decode()))
|
||||
|
||||
def on_headers_complete(self):
|
||||
remote_addr = self.transport.get_extra_info('peername')
|
||||
if remote_addr:
|
||||
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
|
||||
|
||||
self.request = Request(
|
||||
url_bytes=self.url,
|
||||
headers=CIMultiDict(self.headers),
|
||||
headers=CIDict(self.headers),
|
||||
version=self.parser.get_http_version(),
|
||||
method=self.parser.get_method().decode()
|
||||
method=self.parser.get_method().decode(),
|
||||
transport=self.transport
|
||||
)
|
||||
|
||||
def on_body(self, body):
|
||||
if self.request.body:
|
||||
self.request.body += body
|
||||
else:
|
||||
self.request.body = body
|
||||
self.request.body.append(body)
|
||||
|
||||
def on_message_complete(self):
|
||||
if self.request.body:
|
||||
self.request.body = b''.join(self.request.body)
|
||||
self._request_handler_task = self.loop.create_task(
|
||||
self.request_handler(self.request, self.write_response))
|
||||
|
||||
@@ -149,24 +169,54 @@ class HttpProtocol(asyncio.Protocol):
|
||||
|
||||
def write_response(self, response):
|
||||
try:
|
||||
keep_alive = self.parser.should_keep_alive() \
|
||||
and not self.signal.stopped
|
||||
keep_alive = (
|
||||
self.parser.should_keep_alive() and not self.signal.stopped)
|
||||
self.transport.write(
|
||||
response.output(
|
||||
self.request.version, keep_alive, self.request_timeout))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before response written @ {}'.format(
|
||||
self.request.ip))
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(e))
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
else:
|
||||
# Record that we received data
|
||||
self._last_request_time = current_time
|
||||
self.cleanup()
|
||||
|
||||
def write_error(self, exception):
|
||||
try:
|
||||
response = self.error_handler.response(self.request, exception)
|
||||
version = self.request.version if self.request else '1.1'
|
||||
self.transport.write(response.output(version))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before error written @ {}'.format(
|
||||
self.request.ip))
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(e))
|
||||
"Writing error failed, connection closed {}".format(e),
|
||||
from_error=True)
|
||||
finally:
|
||||
self.transport.close()
|
||||
|
||||
def bail_out(self, message):
|
||||
log.debug(message)
|
||||
self.transport.close()
|
||||
def bail_out(self, message, from_error=False):
|
||||
if from_error and self.transport.is_closing():
|
||||
log.error(
|
||||
("Transport closed @ {} and exception "
|
||||
"experienced during error handling").format(
|
||||
self.transport.get_extra_info('peername')))
|
||||
log.debug(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
else:
|
||||
exception = ServerError(message)
|
||||
self.write_error(exception)
|
||||
log.error(message)
|
||||
|
||||
def cleanup(self):
|
||||
self.parser = None
|
||||
@@ -191,6 +241,7 @@ def update_current_time(loop):
|
||||
"""
|
||||
Caches the current time, since it is needed
|
||||
at the end of every keep-alive request to update the request timeout time
|
||||
|
||||
:param loop:
|
||||
:return:
|
||||
"""
|
||||
@@ -214,27 +265,38 @@ def trigger_events(events, loop):
|
||||
|
||||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None,
|
||||
debug=False, request_timeout=60, sock=None,
|
||||
request_max_size=None, reuse_port=False, loop=None):
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
register_sys_signals=True, run_async=False):
|
||||
"""
|
||||
Starts asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param request_handler: Sanic request handler with middleware
|
||||
:param error_handler: Sanic error handler with middleware
|
||||
:param before_start: Function to be executed before the server starts
|
||||
listening. Takes arguments `app` instance and `loop`
|
||||
:param after_start: Function to be executed after the server starts
|
||||
listening. Takes single argument `loop`
|
||||
listening. Takes arguments `app` instance and `loop`
|
||||
:param before_stop: Function to be executed when a stop signal is
|
||||
received before it is respected. Takes single argumenet `loop`
|
||||
received before it is respected. Takes arguments
|
||||
`app` instance and `loop`
|
||||
:param after_stop: Function to be executed when a stop signal is
|
||||
received after it is respected. Takes arguments
|
||||
`app` instance and `loop`
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
:param ssl: SSLContext
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param request_max_size: size in bytes, `None` for no limit
|
||||
:param reuse_port: `True` for multiple workers
|
||||
:param loop: asyncio compatible event loop
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
loop = loop or async_loop.new_event_loop()
|
||||
loop = async_loop.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
if debug:
|
||||
@@ -242,9 +304,10 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
|
||||
trigger_events(before_start, loop)
|
||||
|
||||
connections = {}
|
||||
connections = set()
|
||||
signal = Signal()
|
||||
server_coroutine = loop.create_server(lambda: HttpProtocol(
|
||||
server = partial(
|
||||
protocol,
|
||||
loop=loop,
|
||||
connections=connections,
|
||||
signal=signal,
|
||||
@@ -252,28 +315,43 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
error_handler=error_handler,
|
||||
request_timeout=request_timeout,
|
||||
request_max_size=request_max_size,
|
||||
), host, port, reuse_port=reuse_port, sock=sock)
|
||||
)
|
||||
|
||||
server_coroutine = loop.create_server(
|
||||
server,
|
||||
host,
|
||||
port,
|
||||
ssl=ssl,
|
||||
reuse_port=reuse_port,
|
||||
sock=sock,
|
||||
backlog=backlog
|
||||
)
|
||||
# Instead of pulling time at the end of every request,
|
||||
# pull it once per minute
|
||||
loop.call_soon(partial(update_current_time, loop))
|
||||
|
||||
if run_async:
|
||||
return server_coroutine
|
||||
|
||||
try:
|
||||
http_server = loop.run_until_complete(server_coroutine)
|
||||
except Exception:
|
||||
except:
|
||||
log.exception("Unable to start server")
|
||||
return
|
||||
|
||||
trigger_events(after_start, loop)
|
||||
|
||||
# Register signals for graceful termination
|
||||
for _signal in (SIGINT, SIGTERM):
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
if register_sys_signals:
|
||||
for _signal in (SIGINT, SIGTERM):
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
|
||||
pid = os.getpid()
|
||||
try:
|
||||
log.info('Starting worker [{}]'.format(pid))
|
||||
loop.run_forever()
|
||||
finally:
|
||||
log.info("Stop requested, draining connections...")
|
||||
log.info("Stopping worker [{}]".format(pid))
|
||||
|
||||
# Run the on_stop function if provided
|
||||
trigger_events(before_stop, loop)
|
||||
@@ -284,7 +362,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
|
||||
# Complete all tasks on the loop
|
||||
signal.stopped = True
|
||||
for connection in connections.keys():
|
||||
for connection in connections:
|
||||
connection.close_if_idle()
|
||||
|
||||
while connections:
|
||||
@@ -293,3 +371,51 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
trigger_events(after_stop, loop)
|
||||
|
||||
loop.close()
|
||||
|
||||
|
||||
def serve_multiple(server_settings, workers, stop_event=None):
|
||||
"""
|
||||
Starts multiple server processes simultaneously. Stops on interrupt
|
||||
and terminate signals, and drains connections when complete.
|
||||
|
||||
:param server_settings: kw arguments to be passed to the serve function
|
||||
:param workers: number of workers to launch
|
||||
:param stop_event: if provided, is used as a stop signal
|
||||
:return:
|
||||
"""
|
||||
if server_settings.get('loop', None) is not None:
|
||||
log.warning("Passing a loop will be deprecated in version 0.4.0"
|
||||
" https://github.com/channelcat/sanic/pull/335"
|
||||
" has more information.", DeprecationWarning)
|
||||
server_settings['reuse_port'] = True
|
||||
|
||||
sock = socket()
|
||||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
sock.bind((server_settings['host'], server_settings['port']))
|
||||
set_inheritable(sock.fileno(), True)
|
||||
server_settings['sock'] = sock
|
||||
server_settings['host'] = None
|
||||
server_settings['port'] = None
|
||||
|
||||
if stop_event is None:
|
||||
stop_event = Event()
|
||||
|
||||
signal_func(SIGINT, lambda s, f: stop_event.set())
|
||||
signal_func(SIGTERM, lambda s, f: stop_event.set())
|
||||
|
||||
processes = []
|
||||
for _ in range(workers):
|
||||
process = Process(target=serve, kwargs=server_settings)
|
||||
process.daemon = True
|
||||
process.start()
|
||||
processes.append(process)
|
||||
|
||||
for process in processes:
|
||||
process.join()
|
||||
|
||||
# the above processes will block this until they're stopped
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
sock.close()
|
||||
|
||||
asyncio.get_event_loop().stop()
|
||||
|
||||
@@ -2,6 +2,7 @@ from aiofiles.os import stat
|
||||
from os import path
|
||||
from re import sub
|
||||
from time import strftime, gmtime
|
||||
from urllib.parse import unquote
|
||||
|
||||
from .exceptions import FileNotFound, InvalidUsage
|
||||
from .response import file, HTTPResponse
|
||||
@@ -14,12 +15,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||
"""
|
||||
Registers a static directory handler with Sanic by adding a route to the
|
||||
router and registering a handler.
|
||||
|
||||
:param app: Sanic
|
||||
:param file_or_directory: File or directory path to serve from
|
||||
:param uri: URL to serve from
|
||||
:param pattern: regular expression used to match files in the URL
|
||||
:param use_modified_since: If true, send file modified time, and return
|
||||
not modified if the browser's matches the server's
|
||||
not modified if the browser's matches the
|
||||
server's
|
||||
"""
|
||||
|
||||
# If we're not trying to match a file directly,
|
||||
@@ -32,12 +35,17 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||
# served. os.path.realpath seems to be very slow
|
||||
if file_uri and '../' in file_uri:
|
||||
raise InvalidUsage("Invalid URL")
|
||||
|
||||
# Merge served directory and requested file if provided
|
||||
# Strip all / that in the beginning of the URL to help prevent python
|
||||
# from herping a derp and treating the uri as an absolute path
|
||||
file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \
|
||||
if file_uri else file_or_directory
|
||||
file_path = file_or_directory
|
||||
if file_uri:
|
||||
file_path = path.join(
|
||||
file_or_directory, sub('^[/]*', '', file_uri))
|
||||
|
||||
# URL decode the path sent by the browser otherwise we won't be able to
|
||||
# match filenames which got encoded (filenames with spaces etc)
|
||||
file_path = unquote(file_path)
|
||||
try:
|
||||
headers = {}
|
||||
# Check if the client has been sent this file before
|
||||
|
||||
@@ -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,
|
||||
loop=None, debug=False, *request_args,
|
||||
**request_kwargs):
|
||||
debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
results = []
|
||||
exceptions = []
|
||||
|
||||
if gather_request:
|
||||
@app.middleware
|
||||
def _collect_request(request):
|
||||
results.append(request)
|
||||
app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
@@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||
exceptions.append(e)
|
||||
app.stop()
|
||||
|
||||
app.run(host=HOST, debug=debug, port=42101,
|
||||
after_start=_collect_response, loop=loop)
|
||||
app.run(host=HOST, debug=debug, port=PORT,
|
||||
after_start=_collect_response, **server_kwargs)
|
||||
|
||||
if exceptions:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
@@ -47,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||
return request, response
|
||||
except:
|
||||
raise ValueError(
|
||||
"request and response object expected, got ({})".format(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[0]
|
||||
except:
|
||||
raise ValueError(
|
||||
"request object expected, got ({})".format(results))
|
||||
"Request object expected, got ({})".format(results))
|
||||
|
||||
@@ -3,34 +3,100 @@ from .exceptions import InvalidUsage
|
||||
|
||||
class HTTPMethodView:
|
||||
""" Simple class based implementation of view for the sanic.
|
||||
You should implement methods(get, post, put, patch, delete) for the class
|
||||
You should implement methods (get, post, put, patch, delete) for the class
|
||||
to every HTTP method you want to support.
|
||||
For example:
|
||||
class DummyView(View):
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return text('I am get method')
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
return text('I am put method')
|
||||
|
||||
etc.
|
||||
If someone try use not implemented method, there will be 405 response
|
||||
|
||||
If you need any url params just mention them in method definition like:
|
||||
class DummyView(View):
|
||||
If someone tries to use a non-implemented method, there will be a
|
||||
405 response.
|
||||
|
||||
If you need any url params just mention them in method definition:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
def get(self, request, my_param_here, *args, **kwargs):
|
||||
return text('I am get method with %s' % my_param_here)
|
||||
|
||||
To add the view into the routing you could use
|
||||
1) app.add_route(DummyView(), '/')
|
||||
2) app.route('/')(DummyView())
|
||||
1) app.add_route(DummyView.as_view(), '/')
|
||||
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)
|
||||
if handler:
|
||||
return handler(request, *args, **kwargs)
|
||||
raise InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
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',
|
||||
'ujson>=1.35',
|
||||
'aiofiles>=0.3.0',
|
||||
'multidict>=2.0',
|
||||
],
|
||||
classifiers=[
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type TestJSONResponse struct {
|
||||
Test bool
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
|
||||
response := TestJSONResponse{true}
|
||||
|
||||
js, err := json.Marshal(response)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", handler)
|
||||
http.ListenAndServe(":" + os.Args[1], nil)
|
||||
http.HandleFunc("/", handler)
|
||||
http.ListenAndServe(":"+os.Args[1], nil)
|
||||
}
|
||||
|
||||
1
tests/static/decode me.txt
Normal file
1
tests/static/decode me.txt
Normal file
@@ -0,0 +1 @@
|
||||
I need to be decoded as a uri
|
||||
1
tests/static/test.file
Normal file
1
tests/static/test.file
Normal file
@@ -0,0 +1 @@
|
||||
I am just a regular static file
|
||||
20
tests/test_bad_request.py
Normal file
20
tests/test_bad_request.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import asyncio
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def test_bad_request_response():
|
||||
app = Sanic('test_bad_request_response')
|
||||
lines = []
|
||||
async def _request(sanic, loop):
|
||||
connect = asyncio.open_connection('127.0.0.1', 42101)
|
||||
reader, writer = await connect
|
||||
writer.write(b'not http')
|
||||
while True:
|
||||
line = await reader.readline()
|
||||
if not line:
|
||||
break
|
||||
lines.append(line)
|
||||
app.stop()
|
||||
app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request)
|
||||
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
|
||||
assert lines[-1] == b'Error: Bad Request'
|
||||
@@ -59,6 +59,71 @@ def test_several_bp_with_url_prefix():
|
||||
request, response = sanic_endpoint_test(app, uri='/test2/')
|
||||
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():
|
||||
app = Sanic('test_middleware')
|
||||
@@ -162,4 +227,4 @@ def test_bp_static():
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
assert response.body == current_file_contents
|
||||
|
||||
@@ -3,6 +3,7 @@ from http.cookies import SimpleCookie
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
import pytest
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -25,6 +26,39 @@ def test_cookies():
|
||||
assert response.text == 'Cookies are: working!'
|
||||
assert response_cookies['right_back'].value == 'at you'
|
||||
|
||||
@pytest.mark.parametrize("httponly,expected", [
|
||||
(False, False),
|
||||
(True, True),
|
||||
])
|
||||
def test_false_cookies(httponly, expected):
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
response = text('Cookies are: {}'.format(request.cookies['test']))
|
||||
response.cookies['right_back'] = 'at you'
|
||||
response.cookies['right_back']['httponly'] = httponly
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
'HttpOnly' in response_cookies == expected
|
||||
|
||||
def test_http2_cookies():
|
||||
app = Sanic('test_http2_cookies')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
response = text('Cookies are: {}'.format(request.cookies['test']))
|
||||
return response
|
||||
|
||||
headers = {'cookie': 'test=working!'}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
|
||||
assert response.text == 'Cookies are: working!'
|
||||
|
||||
def test_cookie_options():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@@ -41,4 +75,23 @@ def test_cookie_options():
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
assert response_cookies['test'].value == 'at you'
|
||||
assert response_cookies['test']['httponly'] == True
|
||||
assert response_cookies['test']['httponly'] == True
|
||||
|
||||
def test_cookie_deletion():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
response = text("OK")
|
||||
del response.cookies['i_want_to_die']
|
||||
response.cookies['i_never_existed'] = 'testing'
|
||||
del response.cookies['i_never_existed']
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
assert int(response_cookies['i_want_to_die']['max-age']) == 0
|
||||
with pytest.raises(KeyError):
|
||||
hold_my_beer = response.cookies['i_never_existed']
|
||||
|
||||
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,112 @@
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
exception_app = Sanic('test_exceptions')
|
||||
class SanicExceptionTestException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@exception_app.route('/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
@pytest.fixture(scope='module')
|
||||
def exception_app():
|
||||
app = Sanic('test_exceptions')
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@app.route('/error')
|
||||
def handler_error(request):
|
||||
raise ServerError("OK")
|
||||
|
||||
@app.route('/404')
|
||||
def handler_404(request):
|
||||
raise NotFound("OK")
|
||||
|
||||
@app.route('/invalid')
|
||||
def handler_invalid(request):
|
||||
raise InvalidUsage("OK")
|
||||
|
||||
@app.route('/divide_by_zero')
|
||||
def handle_unhandled_exception(request):
|
||||
1 / 0
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@exception_app.route('/error')
|
||||
def handler_error(request):
|
||||
raise ServerError("OK")
|
||||
|
||||
|
||||
@exception_app.route('/404')
|
||||
def handler_404(request):
|
||||
raise NotFound("OK")
|
||||
|
||||
|
||||
@exception_app.route('/invalid')
|
||||
def handler_invalid(request):
|
||||
raise InvalidUsage("OK")
|
||||
|
||||
|
||||
def test_no_exception():
|
||||
def test_no_exception(exception_app):
|
||||
"""Test that a route works without an exception"""
|
||||
request, response = sanic_endpoint_test(exception_app)
|
||||
assert response.status == 200
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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'
|
||||
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app,
|
||||
uri='/error_in_error_handler_handler',
|
||||
debug=False)
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app,
|
||||
uri='/error_in_error_handler_handler',
|
||||
debug=True)
|
||||
assert response.status == 500
|
||||
assert response.body.startswith(b'Exception raised in exception ')
|
||||
|
||||
@@ -2,6 +2,7 @@ from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
exception_handler_app = Sanic('test_exception_handler')
|
||||
|
||||
@@ -21,6 +22,12 @@ def handler_3(request):
|
||||
raise NotFound("OK")
|
||||
|
||||
|
||||
@exception_handler_app.route('/4')
|
||||
def handler_4(request):
|
||||
foo = bar
|
||||
return text(foo)
|
||||
|
||||
|
||||
@exception_handler_app.exception(NotFound, ServerError)
|
||||
def handler_exception(request, exception):
|
||||
return text("OK")
|
||||
@@ -47,3 +54,20 @@ def test_text_exception__handler():
|
||||
exception_handler_app, uri='/random')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_html_traceback_output_in_debug_mode():
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_handler_app, uri='/4', debug=True)
|
||||
assert response.status == 500
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
html = str(soup)
|
||||
|
||||
assert 'response = handler(request, *args, **kwargs)' in html
|
||||
assert 'handler_4' in html
|
||||
assert 'foo = bar' in html
|
||||
|
||||
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||
assert (
|
||||
"NameError: name 'bar' "
|
||||
"is not defined while handling uri /4") == summary_text
|
||||
|
||||
37
tests/test_logging.py
Normal file
37
tests/test_logging.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from sanic.response import text
|
||||
from sanic import Sanic
|
||||
from io import StringIO
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
import logging
|
||||
|
||||
logging_format = '''module: %(module)s; \
|
||||
function: %(funcName)s(); \
|
||||
message: %(message)s'''
|
||||
|
||||
|
||||
def test_log():
|
||||
log_stream = StringIO()
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
logging.basicConfig(
|
||||
format=logging_format,
|
||||
level=logging.DEBUG,
|
||||
stream=log_stream
|
||||
)
|
||||
log = logging.getLogger()
|
||||
app = Sanic('test_logging')
|
||||
rand_string = str(uuid.uuid4())
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
log.info(rand_string)
|
||||
return text('hello')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
log_text = log_stream.getvalue()
|
||||
assert rand_string in log_text
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_log()
|
||||
@@ -51,7 +51,7 @@ def test_middleware_response():
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is Request
|
||||
assert type(results[1]) is Request
|
||||
assert issubclass(type(results[2]), HTTPResponse)
|
||||
assert isinstance(results[2], HTTPResponse)
|
||||
|
||||
|
||||
def test_middleware_override_request():
|
||||
|
||||
@@ -1,53 +1,26 @@
|
||||
from multiprocessing import Array, Event, Process
|
||||
from time import sleep
|
||||
from ujson import loads as json_loads
|
||||
import multiprocessing
|
||||
import random
|
||||
import signal
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.utils import local_request, HOST, PORT
|
||||
from sanic.utils import HOST, PORT
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
def test_multiprocessing():
|
||||
"""Tests that the number of children we produce is correct"""
|
||||
# Selects a number at random so we can spot check
|
||||
num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1))
|
||||
app = Sanic('test_multiprocessing')
|
||||
process_list = set()
|
||||
|
||||
# TODO: Figure out why this freezes on pytest but not when
|
||||
# executed via interpreter
|
||||
def stop_on_alarm(*args):
|
||||
for process in multiprocessing.active_children():
|
||||
process_list.add(process.pid)
|
||||
process.terminate()
|
||||
|
||||
def skip_test_multiprocessing():
|
||||
app = Sanic('test_json')
|
||||
signal.signal(signal.SIGALRM, stop_on_alarm)
|
||||
signal.alarm(1)
|
||||
app.run(HOST, PORT, workers=num_workers)
|
||||
|
||||
response = Array('c', 50)
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return json({"test": True})
|
||||
assert len(process_list) == num_workers
|
||||
|
||||
stop_event = Event()
|
||||
async def after_start(*args, **kwargs):
|
||||
http_response = await local_request('get', '/')
|
||||
response.value = http_response.text.encode()
|
||||
stop_event.set()
|
||||
|
||||
def rescue_crew():
|
||||
sleep(5)
|
||||
stop_event.set()
|
||||
|
||||
rescue_process = Process(target=rescue_crew)
|
||||
rescue_process.start()
|
||||
|
||||
app.serve_multiple({
|
||||
'host': HOST,
|
||||
'port': PORT,
|
||||
'after_start': after_start,
|
||||
'request_handler': app.handle_request,
|
||||
'request_max_size': 100000,
|
||||
}, workers=2, stop_event=stop_event)
|
||||
|
||||
rescue_process.terminate()
|
||||
|
||||
try:
|
||||
results = json_loads(response.value)
|
||||
except:
|
||||
raise ValueError("Expected JSON response but got '{}'".format(response))
|
||||
|
||||
assert results.get('test') == True
|
||||
|
||||
54
tests/test_payload_too_large.py
Normal file
54
tests/test_payload_too_large.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import PayloadTooLarge
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
data_received_app = Sanic('data_received')
|
||||
data_received_app.config.REQUEST_MAX_SIZE = 1
|
||||
data_received_default_app = Sanic('data_received_default')
|
||||
data_received_default_app.config.REQUEST_MAX_SIZE = 1
|
||||
on_header_default_app = Sanic('on_header')
|
||||
on_header_default_app.config.REQUEST_MAX_SIZE = 500
|
||||
|
||||
|
||||
@data_received_app.route('/1')
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
@data_received_app.exception(PayloadTooLarge)
|
||||
def handler_exception(request, exception):
|
||||
return text('Payload Too Large from error_handler.', 413)
|
||||
|
||||
|
||||
def test_payload_too_large_from_error_handler():
|
||||
response = sanic_endpoint_test(
|
||||
data_received_app, uri='/1', gather_request=False)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Payload Too Large from error_handler.'
|
||||
|
||||
|
||||
@data_received_default_app.route('/1')
|
||||
async def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
def test_payload_too_large_at_data_received_default():
|
||||
response = sanic_endpoint_test(
|
||||
data_received_default_app, uri='/1', gather_request=False)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Error: Payload Too Large'
|
||||
|
||||
|
||||
@on_header_default_app.route('/1')
|
||||
async def handler3(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
def test_payload_too_large_at_on_header_default():
|
||||
data = 'a' * 1000
|
||||
response = sanic_endpoint_test(
|
||||
on_header_default_app, method='post', uri='/1',
|
||||
gather_request=False, data=data)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Error: Payload Too Large'
|
||||
@@ -12,7 +12,7 @@ request_timeout_default_app = Sanic('test_request_timeout_default')
|
||||
|
||||
@request_timeout_app.route('/1')
|
||||
async def handler_1(request):
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(2)
|
||||
return text('OK')
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_server_error_request_timeout():
|
||||
|
||||
@request_timeout_default_app.route('/1')
|
||||
async def handler_2(request):
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(2)
|
||||
return text('OK')
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.response import json, text, redirect
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
import pytest
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
@@ -32,6 +34,47 @@ def test_text():
|
||||
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():
|
||||
app = Sanic('test_invalid_response')
|
||||
|
||||
@app.exception(ServerError)
|
||||
def handler_exception(request, exception):
|
||||
return text('Internal Server Error.', 500)
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return 'This should fail'
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
assert response.status == 500
|
||||
assert response.text == "Internal Server Error."
|
||||
|
||||
|
||||
def test_json():
|
||||
app = Sanic('test_json')
|
||||
|
||||
@@ -49,6 +92,19 @@ def test_json():
|
||||
assert results.get('test') == True
|
||||
|
||||
|
||||
def test_invalid_json():
|
||||
app = Sanic('test_json')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return json(request.json())
|
||||
|
||||
data = "I am not json"
|
||||
request, response = sanic_endpoint_test(app, data=data)
|
||||
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
def test_query_string():
|
||||
app = Sanic('test_query_string')
|
||||
|
||||
@@ -62,6 +118,24 @@ def test_query_string():
|
||||
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
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -115,3 +189,73 @@ def test_post_form_multipart_form_data():
|
||||
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
||||
|
||||
assert request.form.get('test') == 'OK'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def redirect_app():
|
||||
app = Sanic('test_redirection')
|
||||
|
||||
@app.route('/redirect_init')
|
||||
async def redirect_init(request):
|
||||
return redirect("/redirect_target")
|
||||
|
||||
@app.route('/redirect_init_with_301')
|
||||
async def redirect_init_with_301(request):
|
||||
return redirect("/redirect_target", status=301)
|
||||
|
||||
@app.route('/redirect_target')
|
||||
async def redirect_target(request):
|
||||
return text('OK')
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_redirect_default_302(redirect_app):
|
||||
"""
|
||||
We expect a 302 default status code and the headers to be set.
|
||||
"""
|
||||
request, response = sanic_endpoint_test(
|
||||
redirect_app, method="get",
|
||||
uri="/redirect_init",
|
||||
allow_redirects=False)
|
||||
|
||||
assert response.status == 302
|
||||
assert response.headers["Location"] == "/redirect_target"
|
||||
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
|
||||
|
||||
|
||||
def test_redirect_headers_none(redirect_app):
|
||||
request, response = sanic_endpoint_test(
|
||||
redirect_app, method="get",
|
||||
uri="/redirect_init",
|
||||
headers=None,
|
||||
allow_redirects=False)
|
||||
|
||||
assert response.status == 302
|
||||
assert response.headers["Location"] == "/redirect_target"
|
||||
|
||||
|
||||
def test_redirect_with_301(redirect_app):
|
||||
"""
|
||||
Test redirection with a different status code.
|
||||
"""
|
||||
request, response = sanic_endpoint_test(
|
||||
redirect_app, method="get",
|
||||
uri="/redirect_init_with_301",
|
||||
allow_redirects=False)
|
||||
|
||||
assert response.status == 301
|
||||
assert response.headers["Location"] == "/redirect_target"
|
||||
|
||||
|
||||
def test_get_then_redirect_follow_redirect(redirect_app):
|
||||
"""
|
||||
With `allow_redirects` we expect a 200.
|
||||
"""
|
||||
response = sanic_endpoint_test(
|
||||
redirect_app, method="get",
|
||||
uri="/redirect_init", gather_request=False,
|
||||
allow_redirects=True)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
18
tests/test_response.py
Normal file
18
tests/test_response.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from random import choice
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
def test_response_body_not_a_string():
|
||||
"""Test when a response body sent from the application is not a string"""
|
||||
app = Sanic('response_body_not_a_string')
|
||||
random_num = choice(range(1000))
|
||||
|
||||
@app.route('/hello')
|
||||
async def hello_route(request):
|
||||
return HTTPResponse(body=random_num)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/hello')
|
||||
assert response.text == str(random_num)
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.router import RouteExists
|
||||
from sanic.router import RouteExists, RouteDoesNotExist
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
@@ -10,6 +10,84 @@ from sanic.utils import sanic_endpoint_test
|
||||
# UTF-8
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_shorthand_routes_get():
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/get', method='get')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/get', method='post')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_post():
|
||||
app = Sanic('test_shorhand_routes_post')
|
||||
|
||||
@app.post('/post')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/post', method='post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/post', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_put():
|
||||
app = Sanic('test_shorhand_routes_put')
|
||||
|
||||
@app.put('/put')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/put', method='put')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/put', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_patch():
|
||||
app = Sanic('test_shorhand_routes_patch')
|
||||
|
||||
@app.patch('/patch')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/patch', method='patch')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/patch', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_head():
|
||||
app = Sanic('test_shorhand_routes_head')
|
||||
|
||||
@app.head('/head')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/head', method='head')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/head', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_options():
|
||||
app = Sanic('test_shorhand_routes_options')
|
||||
|
||||
@app.options('/options')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='options')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_static_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@@ -356,3 +434,174 @@ def test_add_route_method_not_allowed():
|
||||
|
||||
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
||||
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()
|
||||
50
tests/test_signal_handlers.py
Normal file
50
tests/test_signal_handlers.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.utils import HOST, PORT
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
import asyncio
|
||||
from queue import Queue
|
||||
|
||||
|
||||
async def stop(app, loop):
|
||||
await asyncio.sleep(0.1)
|
||||
app.stop()
|
||||
|
||||
calledq = Queue()
|
||||
|
||||
def set_loop(app, loop):
|
||||
loop.add_signal_handler = MagicMock()
|
||||
|
||||
def after(app, loop):
|
||||
calledq.put(loop.add_signal_handler.called)
|
||||
|
||||
def test_register_system_signals():
|
||||
"""Test if sanic register system signals"""
|
||||
app = Sanic('test_register_system_signals')
|
||||
|
||||
@app.route('/hello')
|
||||
async def hello_route(request):
|
||||
return HTTPResponse()
|
||||
|
||||
app.run(HOST, PORT,
|
||||
before_start=set_loop,
|
||||
after_start=stop,
|
||||
after_stop=after)
|
||||
assert calledq.get() == True
|
||||
|
||||
|
||||
def test_dont_register_system_signals():
|
||||
"""Test if sanic don't register system signals"""
|
||||
app = Sanic('test_register_system_signals')
|
||||
|
||||
@app.route('/hello')
|
||||
async def hello_route(request):
|
||||
return HTTPResponse()
|
||||
|
||||
app.run(HOST, PORT,
|
||||
before_start=set_loop,
|
||||
after_start=stop,
|
||||
after_stop=after,
|
||||
register_sys_signals=False)
|
||||
assert calledq.get() == False
|
||||
@@ -1,30 +1,62 @@
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
def test_static_file():
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
with open(current_file, 'rb') as file:
|
||||
current_file_contents = file.read()
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_directory():
|
||||
"""The static directory to serve"""
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
current_directory = os.path.dirname(os.path.abspath(current_file))
|
||||
static_directory = os.path.join(current_directory, 'static')
|
||||
return static_directory
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_path(static_file_directory):
|
||||
"""The path to the static file that we want to serve"""
|
||||
return os.path.join(static_file_directory, 'test.file')
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def static_file_content(static_file_path):
|
||||
"""The content of the static file to check"""
|
||||
with open(static_file_path, 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
def test_static_file(static_file_path, static_file_content):
|
||||
app = Sanic('test_static')
|
||||
app.static('/testing.file', current_file)
|
||||
app.static('/testing.file', static_file_path)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
assert response.body == static_file_content
|
||||
|
||||
def test_static_directory():
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
current_directory = os.path.dirname(os.path.abspath(current_file))
|
||||
with open(current_file, 'rb') as file:
|
||||
current_file_contents = file.read()
|
||||
|
||||
def test_static_directory(
|
||||
static_file_directory, static_file_path, static_file_content):
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/dir', current_directory)
|
||||
app.static('/dir', static_file_directory)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/test_static.py')
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/test.file')
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
assert response.body == static_file_content
|
||||
|
||||
|
||||
def test_static_url_decode_file(static_file_directory):
|
||||
decode_me_path = os.path.join(static_file_directory, 'decode me.txt')
|
||||
with open(decode_me_path, 'rb') as file:
|
||||
decode_me_contents = file.read()
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/dir', static_file_directory)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt')
|
||||
assert response.status == 200
|
||||
assert response.body == decode_me_contents
|
||||
|
||||
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):
|
||||
return text('I am delete method')
|
||||
|
||||
app.add_route(DummyView(), '/')
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
request, response = sanic_endpoint_test(app, method="get")
|
||||
assert response.text == 'I am get method'
|
||||
@@ -48,7 +48,7 @@ def test_unexisting_methods():
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView(), '/')
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
request, response = sanic_endpoint_test(app, method="get")
|
||||
assert response.text == 'I am get method'
|
||||
request, response = sanic_endpoint_test(app, method="post")
|
||||
@@ -63,7 +63,7 @@ def test_argument_methods():
|
||||
def get(self, request, 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')
|
||||
|
||||
@@ -79,7 +79,7 @@ def test_with_bp():
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
bp.add_route(DummyView(), '/')
|
||||
bp.add_route(DummyView.as_view(), '/')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app)
|
||||
@@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix():
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
bp.add_route(DummyView(), '/')
|
||||
bp.add_route(DummyView.as_view(), '/')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
@@ -112,7 +112,7 @@ def test_with_middleware():
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView(), '/')
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
results = []
|
||||
|
||||
@@ -145,11 +145,52 @@ def test_with_middleware_response():
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView(), '/')
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
assert response.text == 'I am get method'
|
||||
assert type(results[0]) is Request
|
||||
assert type(results[1]) is Request
|
||||
assert issubclass(type(results[2]), HTTPResponse)
|
||||
assert isinstance(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
|
||||
|
||||
34
tox.ini
34
tox.ini
@@ -1,34 +1,26 @@
|
||||
[tox]
|
||||
|
||||
envlist = py35, report
|
||||
envlist = py35, py36, flake8
|
||||
|
||||
[travis]
|
||||
|
||||
python =
|
||||
3.5: py35, flake8
|
||||
3.6: py36, flake8
|
||||
|
||||
[testenv]
|
||||
|
||||
deps =
|
||||
aiohttp
|
||||
pytest
|
||||
# pytest-cov
|
||||
coverage
|
||||
beautifulsoup4
|
||||
|
||||
commands =
|
||||
coverage run -m pytest tests {posargs}
|
||||
mv .coverage .coverage.{envname}
|
||||
pytest tests {posargs}
|
||||
|
||||
basepython:
|
||||
py35: python3.5
|
||||
|
||||
whitelist_externals =
|
||||
coverage
|
||||
mv
|
||||
echo
|
||||
|
||||
[testenv:report]
|
||||
[testenv:flake8]
|
||||
deps =
|
||||
flake8
|
||||
|
||||
commands =
|
||||
coverage combine
|
||||
coverage report
|
||||
coverage html
|
||||
echo "Open file://{toxinidir}/coverage/index.html"
|
||||
|
||||
basepython =
|
||||
python3.5
|
||||
flake8 sanic
|
||||
|
||||
Reference in New Issue
Block a user