Compare commits
328 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6c2d0bcaf | ||
|
|
e2a16f96a8 | ||
|
|
cf60ebd988 | ||
|
|
9dd954bccd | ||
|
|
a02eb8e7cb | ||
|
|
a93ca9b8f1 | ||
|
|
15e4ec7ffb | ||
|
|
62df50e22b | ||
|
|
055430d4b8 | ||
|
|
57f27c41e0 | ||
|
|
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 | ||
|
|
73a57e1105 | ||
|
|
e7314d1775 | ||
|
|
ee8f8c2930 | ||
|
|
64e0e2d19f | ||
|
|
6bb4dae5e0 | ||
|
|
83e9d08853 | ||
|
|
7dfa62516c | ||
|
|
15e7d8ab2e | ||
|
|
a4f77984b7 | ||
|
|
39b279f0f2 | ||
|
|
ac44900fc4 | ||
|
|
548458c3e0 | ||
|
|
986b0aa106 | ||
|
|
01b42fb399 | ||
|
|
f74d44152a | ||
|
|
1557854755 | ||
|
|
56d6c2a929 | ||
|
|
5402e6d3a6 | ||
|
|
be9eca2d63 | ||
|
|
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 | ||
|
|
98b08676e2 | ||
|
|
39f3a63ced | ||
|
|
89e2084489 | ||
|
|
70c56b7db3 | ||
|
|
209b763302 | ||
|
|
190b7a6076 | ||
|
|
cce47a633a | ||
|
|
ec2330c42b | ||
|
|
0c215685f2 | ||
|
|
d86ac5e3e0 | ||
|
|
ee89b6ad03 | ||
|
|
a5e6d6d2e8 | ||
|
|
1eea1f5485 | ||
|
|
da4567eea5 | ||
|
|
9010a6573f | ||
|
|
d8e480ab48 | ||
|
|
0bd61f6a57 | ||
|
|
c01cbb3a8c | ||
|
|
0ca5c4eeff | ||
|
|
47927608b2 | ||
|
|
13808bf282 | ||
|
|
c3c7964e2e | ||
|
|
fca0221d91 | ||
|
|
9f2d73e2f1 | ||
|
|
fc19f2ea34 | ||
|
|
aa0f15fbb2 | ||
|
|
93f50b8ef7 | ||
|
|
7b85843363 | ||
|
|
f7f578ed44 | ||
|
|
de92603ccf | ||
|
|
d02fffb6b8 | ||
|
|
922c96e3c1 | ||
|
|
993627ec44 | ||
|
|
01681599ff | ||
|
|
3ce6434532 | ||
|
|
a97e554f8f | ||
|
|
fd5a79a685 | ||
|
|
635921adc7 | ||
|
|
9eb4cecbc1 | ||
|
|
879b9a4a15 | ||
|
|
8be4dc8fb5 | ||
|
|
f16ea20de5 | ||
|
|
c51b14856e | ||
|
|
88ee71c425 | ||
|
|
edb12da154 | ||
|
|
d9f6846c76 | ||
|
|
9e0747db15 | ||
|
|
ae3d33ad58 | ||
|
|
edb25f799d | ||
|
|
0822674f70 | ||
|
|
49d004736a | ||
|
|
695f8733bb | ||
|
|
b51af7f4bf | ||
|
|
28ce2447ef | ||
|
|
42e3a50274 | ||
|
|
8ebc92c236 | ||
|
|
b92e46df40 | ||
|
|
be5588d5d8 | ||
|
|
0d9fb2f927 | ||
|
|
0e9819fba1 | ||
|
|
aaee40aabd | ||
|
|
5efe51b661 | ||
|
|
50f63142db | ||
|
|
1b65b2e0c6 | ||
|
|
ce8742c605 | ||
|
|
01a013b48a | ||
|
|
3a2eeb9709 | ||
|
|
1271c7d958 | ||
|
|
0032f525ce | ||
|
|
df2f91b82f | ||
|
|
3a1ef6bef2 | ||
|
|
28488075b9 | ||
|
|
3cd3b2d9b7 | ||
|
|
3d88818841 | ||
|
|
b74cf65eca | ||
|
|
80fcacaf8b | ||
|
|
96fcd8443f | ||
|
|
707c55fbe7 | ||
|
|
c44b5551bc | ||
|
|
bd28da0abc | ||
|
|
410299f5a1 | ||
|
|
f3fc958a0c | ||
|
|
47b417db28 | ||
|
|
5171cdd305 | ||
|
|
65950250d9 | ||
|
|
74ae0007d3 | ||
|
|
977081f4af | ||
|
|
ee70f1e55e | ||
|
|
9c16f6dbea | ||
|
|
c50aa34dd9 | ||
|
|
0e479d53da | ||
|
|
984c086296 | ||
|
|
53e00b2b4c | ||
|
|
bb1cb29edd | ||
|
|
bf6879e46f | ||
|
|
12e900e8f9 | ||
|
|
d7fff12b71 | ||
|
|
9051e985a0 | ||
|
|
5361c6f243 | ||
|
|
963aef19e0 | ||
|
|
201e232a0d | ||
|
|
6a71ea50bd | ||
|
|
47ec026536 | ||
|
|
e70263d012 | ||
|
|
658ced9188 | ||
|
|
23290b8627 | ||
|
|
41ea40fc35 | ||
|
|
3802141007 | ||
|
|
50ae2048cc | ||
|
|
b21ab3db12 | ||
|
|
c80abb8cad | ||
|
|
a3bd1eaeab | ||
|
|
be0739614d | ||
|
|
b048f1bad3 | ||
|
|
c3628407eb | ||
|
|
96c13fe23c | ||
|
|
ac9770dd89 | ||
|
|
0e2c092ce3 | ||
|
|
22876b31b1 | ||
|
|
113047d450 | ||
|
|
268a87e3b4 | ||
|
|
452764a8eb | ||
|
|
f540f1e7c4 | ||
|
|
9b561e83e3 | ||
|
|
77c69e3810 | ||
|
|
a5614f6880 | ||
|
|
b74d312c57 | ||
|
|
2312a176fe | ||
|
|
e060dbfec8 | ||
|
|
8f6e5a1263 | ||
|
|
c256825de6 | ||
|
|
cab43503d0 | ||
|
|
d4e2d94816 | ||
|
|
f510550888 | ||
|
|
fc4c192237 | ||
|
|
f4b45deb7f | ||
|
|
d1beabfc8f | ||
|
|
baf1ce95b1 | ||
|
|
e25e1c0e4b | ||
|
|
04a6cc9416 | ||
|
|
50e4dd167e | ||
|
|
f2cc404d7f | ||
|
|
f6a8dbf486 | ||
|
|
7dcdc6208d | ||
|
|
f5569f1723 | ||
|
|
0327e6efba | ||
|
|
138b947b95 | ||
|
|
3d00ca09b9 | ||
|
|
69345272cd | ||
|
|
b6a06afdc0 | ||
|
|
2903e7ee7c | ||
|
|
d5e4355a1c | ||
|
|
6d2d9d3afc | ||
|
|
71a783e7e1 | ||
|
|
a6fa496c30 | ||
|
|
f34fa40ed2 | ||
|
|
c58741fe7a | ||
|
|
7b0f524fb3 | ||
|
|
5e459cb69d | ||
|
|
cbb1f99ccb | ||
|
|
3c05382e07 | ||
|
|
7c3faea0dd | ||
|
|
452438dc07 | ||
|
|
d961e9d1a2 | ||
|
|
8142121c90 | ||
|
|
a904a57fa2 | ||
|
|
4ecb4d2cce | ||
|
|
0a26408c9d | ||
|
|
c539933e38 | ||
|
|
6f105a647e | ||
|
|
18aa937f29 | ||
|
|
f95fe4192b | ||
|
|
625af9a21d | ||
|
|
0e0d4dd3bc | ||
|
|
0c28cdbaf4 | ||
|
|
73ef816d89 | ||
|
|
031a95e4d9 | ||
|
|
2ee4c0fc6a | ||
|
|
3e8b8fb46f | ||
|
|
40d8602270 | ||
|
|
3c7a8a5f45 | ||
|
|
bfee7afd0c | ||
|
|
ea0a037248 | ||
|
|
0148d65dd2 | ||
|
|
8449527ecd | ||
|
|
7ceba1ae9d | ||
|
|
4f102a9cf2 | ||
|
|
d008c25eff |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,11 +1,13 @@
|
||||
*~
|
||||
*.egg-info
|
||||
*.egg
|
||||
*.eggs
|
||||
*.pyc
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage
|
||||
.tox
|
||||
settings.py
|
||||
*.pyc
|
||||
.idea/*
|
||||
.cache/*
|
||||
.python-version
|
||||
|
||||
14
.travis.yml
14
.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 --max-line-length=120 sanic
|
||||
script: py.test -v tests
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
install: pip install tox-travis
|
||||
script: tox
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
|
||||
22
CHANGELOG.md
Normal file
22
CHANGELOG.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Version 0.1
|
||||
-----------
|
||||
- 0.1.7
|
||||
- Reversed static url and directory arguments to meet spec
|
||||
- 0.1.6
|
||||
- Static files
|
||||
- Lazy Cookie Loading
|
||||
- 0.1.5
|
||||
- Cookies
|
||||
- Blueprint listeners and ordering
|
||||
- Faster Router
|
||||
- Fix: Incomplete file reads on medium+ sized post requests
|
||||
- Breaking: after_start and before_stop now pass sanic as their first argument
|
||||
- 0.1.4
|
||||
- Multiprocessing
|
||||
- 0.1.3
|
||||
- Blueprint support
|
||||
- Faster Response processing
|
||||
- 0.1.1 - 0.1.2
|
||||
- Struggling to update pypi via CI
|
||||
- 0.1.0
|
||||
- Released to public
|
||||
39
README.md
39
README.md
@@ -1,24 +1,31 @@
|
||||

|
||||
|
||||
# Sanic
|
||||
|
||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based off the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/.
|
||||
[](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
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.
|
||||
[](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 a 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.
|
||||
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 | 30,601 | 3.23ms |
|
||||
| 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
|
||||
|
||||
@@ -26,17 +33,21 @@ All tests were run on a AWS medium instance running ubuntu, using 1 process. Ea
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({ "hello": "world" })
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
## Installation
|
||||
* `python -m pip install git+https://github.com/channelcat/sanic/`
|
||||
* `python -m pip install sanic`
|
||||
|
||||
## Documentation
|
||||
* [Getting started](docs/getting_started.md)
|
||||
@@ -45,6 +56,12 @@ app.run(host="0.0.0.0", port=8000)
|
||||
* [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)
|
||||
* [Custom Protocol](docs/custom_protocol.md)
|
||||
* [Testing](docs/testing.md)
|
||||
* [Deploying](docs/deploying.md)
|
||||
* [Contributing](docs/contributing.md)
|
||||
* [License](LICENSE)
|
||||
|
||||
@@ -62,7 +79,7 @@ app.run(host="0.0.0.0", port=8000)
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Blueprints are objects that can be used for sub-routing within an application.
|
||||
Instead of adding routes to the application object, blueprints define similar
|
||||
methods for adding routes, which are then registered with the application in a
|
||||
flexible and plugable manner.
|
||||
flexible and pluggable manner.
|
||||
|
||||
## Why?
|
||||
|
||||
@@ -29,7 +29,7 @@ from sanic import Blueprint
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
||||
@bp.route('/')
|
||||
async def bp_root():
|
||||
async def bp_root(request):
|
||||
return json({'my': 'blueprint'})
|
||||
|
||||
```
|
||||
@@ -42,7 +42,7 @@ from sanic import Sanic
|
||||
from my_blueprint import bp
|
||||
|
||||
app = Sanic(__name__)
|
||||
app.register_blueprint(bp)
|
||||
app.blueprint(bp)
|
||||
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
```
|
||||
@@ -56,9 +56,7 @@ In this example, the registered routes in the `app.router` will look like:
|
||||
```
|
||||
|
||||
## Middleware
|
||||
Using blueprints allows you to also register middleware exclusively for that
|
||||
blueprint, without interfering with other blueprints or routes registered
|
||||
directly on the application object.
|
||||
Using blueprints allows you to also register middleware globally.
|
||||
|
||||
```python
|
||||
@bp.middleware
|
||||
@@ -75,11 +73,39 @@ async def halt_response(request, response):
|
||||
```
|
||||
|
||||
## Exceptions
|
||||
Exceptions can also be applied exclusively to blueprints without interfering
|
||||
with other blueprints or routes registered on the application object.
|
||||
Exceptions can also 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.
|
||||
|
||||
```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
|
||||
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
|
||||
|
||||
```python
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
||||
@bp.listen('before_server_start')
|
||||
async def setup_connection():
|
||||
global database
|
||||
database = mysql.connect(host='127.0.0.1'...)
|
||||
|
||||
@bp.listen('after_server_stop')
|
||||
async def close_connection():
|
||||
await database.close()
|
||||
```
|
||||
|
||||
58
docs/class_based_views.md
Normal file
58
docs/class_based_views.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 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.
|
||||
|
||||
## 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):
|
||||
return text('I am get method')
|
||||
|
||||
def post(self, request):
|
||||
return text('I am post method')
|
||||
|
||||
def put(self, request):
|
||||
return text('I am put method')
|
||||
|
||||
def patch(self, request):
|
||||
return text('I am patch method')
|
||||
|
||||
def delete(self, request):
|
||||
return text('I am delete method')
|
||||
|
||||
app.add_route(SimpleView.as_view(), '/')
|
||||
|
||||
```
|
||||
|
||||
If you need any url params just mention them in method definition:
|
||||
|
||||
```python
|
||||
class NameView(HTTPMethodView):
|
||||
|
||||
def get(self, request, name):
|
||||
return text('Hello {}'.format(name))
|
||||
|
||||
app.add_route(NameView.as_view(), '/<name>')
|
||||
|
||||
```
|
||||
|
||||
If you want to add decorator for class, you could set decorators variable
|
||||
|
||||
```
|
||||
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')
|
||||
|
||||
```
|
||||
50
docs/cookies.md
Normal file
50
docs/cookies.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Cookies
|
||||
|
||||
## Request
|
||||
|
||||
Request cookies can be accessed via the request.cookie dictionary
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
test_cookie = request.cookies.get('test')
|
||||
return text("Test cookie set to: {}".format(test_cookie))
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
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
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
response = text("There's a cookie up in this response")
|
||||
response.cookies['test'] = 'It worked!'
|
||||
response.cookies['test']['domain'] = '.gotta-go-fast.com'
|
||||
response.cookies['test']['httponly'] = True
|
||||
return response
|
||||
```
|
||||
70
docs/custom_protocol.md
Normal file
70
docs/custom_protocol.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Custom Protocol
|
||||
|
||||
You can change the behavior of protocol by using custom protocol.
|
||||
If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic.
|
||||
|
||||
* loop
|
||||
`loop` is an asyncio compatible event loop.
|
||||
|
||||
* connections
|
||||
`connections` is a `set object` to store protocol objects.
|
||||
When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections.
|
||||
|
||||
* signal
|
||||
`signal` is a `sanic.server.Signal object` with `stopped attribute`.
|
||||
When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`.
|
||||
|
||||
* request_handler
|
||||
`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments.
|
||||
|
||||
* error_handler
|
||||
`error_handler` is a `sanic.exceptions.Handler` object.
|
||||
|
||||
* request_timeout
|
||||
`request_timeout` is seconds for timeout.
|
||||
|
||||
* request_max_size
|
||||
`request_max_size` is bytes of max request size.
|
||||
|
||||
## Example
|
||||
By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.
|
||||
In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`.
|
||||
|
||||
```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)
|
||||
```
|
||||
35
docs/deploying.md
Normal file
35
docs/deploying.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Deploying
|
||||
|
||||
When it comes to deploying Sanic, there's not much to it, but there are
|
||||
a few things to take note of.
|
||||
|
||||
## 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:
|
||||
|
||||
```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.
|
||||
|
||||
## 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:
|
||||
|
||||
`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:
|
||||
|
||||
```python
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
@@ -4,7 +4,7 @@ Make sure you have pip and python 3.5 before starting
|
||||
|
||||
## Benchmarks
|
||||
* Install Sanic
|
||||
* `python3 -m pip install git+https://github.com/channelcat/sanic/`
|
||||
* `python3 -m pip install sanic`
|
||||
* Edit main.py to include:
|
||||
```python
|
||||
from sanic import Sanic
|
||||
@@ -20,6 +20,6 @@ app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
```
|
||||
* Run `python3 main.py`
|
||||
|
||||
You now have a working sanic server! To continue on, check out:
|
||||
You now have a working Sanic server! To continue on, check out:
|
||||
* [Request Data](request_data.md)
|
||||
* [Routing](routing.md)
|
||||
@@ -1,6 +1,6 @@
|
||||
# Middleware
|
||||
|
||||
Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware return a response object, the request will stop processing and a response will be returned.
|
||||
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 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.
|
||||
|
||||
@@ -27,3 +27,23 @@ async def handler(request):
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
## Middleware chain
|
||||
|
||||
If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware:
|
||||
|
||||
```python
|
||||
app = Sanic(__name__)
|
||||
|
||||
@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 middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks.
|
||||
|
||||
@@ -8,6 +8,7 @@ The following request variables are accessible as properties:
|
||||
`request.json` (any) - JSON body
|
||||
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
|
||||
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
|
||||
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
|
||||
|
||||
See request.py for more information
|
||||
|
||||
@@ -15,7 +16,7 @@ See request.py for more information
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.response import json, text
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
@@ -40,4 +41,9 @@ def post_json(request):
|
||||
@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)
|
||||
```
|
||||
|
||||
@@ -10,16 +10,16 @@ from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/tag/<tag>')
|
||||
async def person_handler(request, tag):
|
||||
async def tag_handler(request, tag):
|
||||
return text('Tag - {}'.format(tag))
|
||||
|
||||
@app.route('/number/<integer_arg:int>')
|
||||
async def person_handler(request, integer_arg):
|
||||
async def integer_handler(request, integer_arg):
|
||||
return text('Integer - {}'.format(integer_arg))
|
||||
|
||||
@app.route('/number/<number_arg:number>')
|
||||
async def person_handler(request, number_arg):
|
||||
return text('Number - {}'.format(number))
|
||||
async def number_handler(request, number_arg):
|
||||
return text('Number - {}'.format(number_arg))
|
||||
|
||||
@app.route('/person/<name:[A-z]>')
|
||||
async def person_handler(request, name):
|
||||
@@ -29,4 +29,16 @@ 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 handler2(request, name):
|
||||
return text('Folder - {}'.format(name))
|
||||
app.add_route(handler2, '/folder/<name>')
|
||||
|
||||
async def person_handler2(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
app.add_route(person_handler2, '/person/<name:[A-z]>')
|
||||
|
||||
```
|
||||
|
||||
18
docs/static_files.md
Normal file
18
docs/static_files.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Static Files
|
||||
|
||||
Both directories and files can be served by registering with static
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
app = Sanic(__name__)
|
||||
|
||||
# Serves files from the static folder to the URL /static
|
||||
app.static('/static', './static')
|
||||
|
||||
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
|
||||
# is requested
|
||||
app.static('/the_best.png', '/home/ubuntu/test.png')
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
51
docs/testing.md
Normal file
51
docs/testing.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 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']
|
||||
```
|
||||
33
examples/aiohttp_example.py
Normal file
33
examples/aiohttp_example.py
Normal file
@@ -0,0 +1,33 @@
|
||||
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):
|
||||
"""
|
||||
Use session object to perform 'get' request on url
|
||||
"""
|
||||
async with 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(loop=loop) as session:
|
||||
response = await fetch(session, url)
|
||||
return json(response)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, loop=loop)
|
||||
|
||||
41
examples/cache_example.py
Normal file
41
examples/cache_example.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Example of caching using aiocache package. To run it you will need a Redis
|
||||
instance running in localhost:6379.
|
||||
|
||||
Running this example you will see that the first call lasts 3 seconds and
|
||||
the rest are instant because the value is retrieved from the Redis.
|
||||
|
||||
If you want more info about the package check
|
||||
https://github.com/argaen/aiocache
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiocache
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.log import log
|
||||
from aiocache import cached
|
||||
from aiocache.serializers import JsonSerializer
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
aiocache.settings.set_defaults(
|
||||
cache="aiocache.RedisCache"
|
||||
)
|
||||
|
||||
|
||||
@cached(key="my_custom_key", serializer=JsonSerializer())
|
||||
async def expensive_call():
|
||||
log.info("Expensive has been called")
|
||||
await asyncio.sleep(3)
|
||||
return {"test": True}
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
log.info("Received GET /")
|
||||
return json(await expensive_call())
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop())
|
||||
60
examples/exception_monitoring.py
Normal file
60
examples/exception_monitoring.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Example intercepting uncaught exceptions using Sanic's error handler framework.
|
||||
|
||||
This may be useful for developers wishing to use Sentry, Airbrake, etc.
|
||||
or a custom system to log and monitor unexpected errors in production.
|
||||
|
||||
First we create our own class inheriting from Handler in sanic.exceptions,
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
"""
|
||||
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):
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
This is an ordinary Sanic server, with the exception that we set the
|
||||
server's error_handler to an instance of our CustomHandler
|
||||
"""
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
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
|
||||
return json({"test": True})
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=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)
|
||||
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(logger=True)
|
||||
@sanic.route("/")
|
||||
def test(request):
|
||||
log.info("received request; responding with 'hey'")
|
||||
return text("hey")
|
||||
|
||||
sanic.run(host="0.0.0.0", port=8000)
|
||||
21
examples/request_timeout.py
Normal file
21
examples/request_timeout.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import RequestTimeout
|
||||
|
||||
Config.REQUEST_TIMEOUT = 1
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
await asyncio.sleep(3)
|
||||
return text('Hello, world!')
|
||||
|
||||
|
||||
@app.exception(RequestTimeout)
|
||||
def timeout(request, exception):
|
||||
return text('RequestTimeout from error_handler.', 408)
|
||||
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
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
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
|
||||
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)
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
async def get_pool():
|
||||
return await aiopg.create_pool(connection)
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
pool = loop.run_until_complete(get_pool())
|
||||
|
||||
|
||||
async def prepare_db():
|
||||
""" Let's create some table and add some data
|
||||
|
||||
"""
|
||||
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):
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
result = []
|
||||
await cur.execute("SELECT question, pub_date FROM sanic_polls")
|
||||
async for row in cur:
|
||||
result.append({"question": row[0], "pub_date": row[1]})
|
||||
return json({"polls": result})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
loop.run_until_complete(prepare_db())
|
||||
app.run(host='0.0.0.0', port=8000, loop=loop)
|
||||
73
examples/sanic_aiopg_sqlalchemy_example.py
Normal file
73
examples/sanic_aiopg_sqlalchemy_example.py
Normal file
@@ -0,0 +1,73 @@
|
||||
""" 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
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
|
||||
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)
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
async def get_engine():
|
||||
return await create_engine(connection)
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
engine = loop.run_until_complete(get_engine())
|
||||
|
||||
|
||||
async def prepare_db():
|
||||
""" Let's add some data
|
||||
|
||||
"""
|
||||
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 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__':
|
||||
loop.run_until_complete(prepare_db())
|
||||
app.run(host='0.0.0.0', port=8000, loop=loop)
|
||||
65
examples/sanic_asyncpg_example.py
Normal file
65
examples/sanic_asyncpg_example.py
Normal file
@@ -0,0 +1,65 @@
|
||||
""" To run this example you need additional asyncpg package
|
||||
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
from asyncpg import create_pool
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
|
||||
DB_CONFIG = {
|
||||
'host': '<host>',
|
||||
'user': '<username>',
|
||||
'password': '<password>',
|
||||
'port': '<port>',
|
||||
'database': '<database>'
|
||||
}
|
||||
|
||||
def jsonify(records):
|
||||
""" Parse asyncpg record response into JSON format
|
||||
|
||||
"""
|
||||
return [{key: value for key, value in
|
||||
zip(r.keys(), r.values())} for r in records]
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
async def make_pool():
|
||||
return await create_pool(**DB_CONFIG)
|
||||
|
||||
app = Sanic(__name__)
|
||||
pool = loop.run_until_complete(make_pool())
|
||||
|
||||
async def create_db():
|
||||
""" Create some table and add some data
|
||||
|
||||
"""
|
||||
async with pool.acquire() as connection:
|
||||
async with connection.transaction():
|
||||
await connection.execute('DROP TABLE IF EXISTS sanic_post')
|
||||
await connection.execute("""CREATE TABLE sanic_post (
|
||||
id serial primary key,
|
||||
content varchar(50),
|
||||
post_date timestamp
|
||||
);""")
|
||||
for i in range(0, 100):
|
||||
await connection.execute(f"""INSERT INTO sanic_post
|
||||
(id, content, post_date) VALUES ({i}, {i}, now())""")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def handler(request):
|
||||
async with pool.acquire() as connection:
|
||||
async with connection.transaction():
|
||||
results = await connection.fetch('SELECT * FROM sanic_post')
|
||||
return json({'posts': jsonify(results)})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
loop.run_until_complete(create_db())
|
||||
app.run(host='0.0.0.0', port=8000, loop=loop)
|
||||
80
examples/sanic_peewee.py
Normal file
80
examples/sanic_peewee.py
Normal file
@@ -0,0 +1,80 @@
|
||||
## You need the following additional packages for this example
|
||||
# aiopg
|
||||
# peewee_async
|
||||
# peewee
|
||||
|
||||
|
||||
## sanic imports
|
||||
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')
|
||||
|
||||
objects = Manager(database, loop=loop)
|
||||
|
||||
## from peewee_async docs:
|
||||
# Also there’s no need to connect and re-connect before executing async queries
|
||||
# with manager! It’s all automatic. But you can run Manager.connect() or
|
||||
# Manager.close() when you need it.
|
||||
|
||||
|
||||
# let's create a simple key value store:
|
||||
class KeyValue(peewee.Model):
|
||||
key = peewee.CharField(max_length=40, unique=True)
|
||||
text = peewee.TextField(default='')
|
||||
|
||||
class Meta:
|
||||
database = database
|
||||
|
||||
# create table synchronously
|
||||
KeyValue.create_table(True)
|
||||
|
||||
# OPTIONAL: close synchronous connection
|
||||
database.close()
|
||||
|
||||
# OPTIONAL: disable any future syncronous calls
|
||||
objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
|
||||
|
||||
|
||||
app = Sanic('peewee_example')
|
||||
|
||||
@app.route('/post/<key>/<value>')
|
||||
async def post(request, key, value):
|
||||
"""
|
||||
Save get parameters to database
|
||||
"""
|
||||
obj = await objects.create(KeyValue, key=key, text=value)
|
||||
return json({'object_id': obj.id})
|
||||
|
||||
|
||||
@app.route('/get')
|
||||
async def get(request):
|
||||
"""
|
||||
Load all objects from database
|
||||
"""
|
||||
all_objects = await objects.execute(KeyValue.select())
|
||||
serialized_obj = []
|
||||
for obj in all_objects:
|
||||
serialized_obj.append({
|
||||
'id': obj.id,
|
||||
'key': obj.key,
|
||||
'value': obj.text}
|
||||
)
|
||||
|
||||
return json({'objects': serialized_obj})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0', port=8000, loop=loop)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
32
examples/vhosts.py
Normal file
32
examples/vhosts.py
Normal file
@@ -0,0 +1,32 @@
|
||||
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")
|
||||
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)
|
||||
@@ -2,9 +2,13 @@ httptools
|
||||
ujson
|
||||
uvloop
|
||||
aiohttp
|
||||
aiocache
|
||||
pytest
|
||||
coverage
|
||||
tox
|
||||
gunicorn
|
||||
bottle
|
||||
kyoukai
|
||||
falcon
|
||||
tornado
|
||||
aiofiles
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
httptools
|
||||
ujson
|
||||
uvloop
|
||||
uvloop
|
||||
aiofiles
|
||||
multidict
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .sanic import Sanic
|
||||
from .blueprints import Blueprint
|
||||
|
||||
__version__ = '0.2.0'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
||||
36
sanic/__main__.py
Normal file
36
sanic/__main__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from argparse import ArgumentParser
|
||||
from importlib import import_module
|
||||
|
||||
from .log import log
|
||||
from .sanic import Sanic
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser(prog='sanic')
|
||||
parser.add_argument('--host', dest='host', type=str, default='127.0.0.1')
|
||||
parser.add_argument('--port', dest='port', type=int, default=8000)
|
||||
parser.add_argument('--workers', dest='workers', type=int, default=1, )
|
||||
parser.add_argument('--debug', dest='debug', action="store_true")
|
||||
parser.add_argument('module')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
module_parts = args.module.split(".")
|
||||
module_name = ".".join(module_parts[:-1])
|
||||
app_name = module_parts[-1]
|
||||
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
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))
|
||||
|
||||
app.run(host=args.host, port=args.port,
|
||||
workers=args.workers, debug=args.debug)
|
||||
except ImportError:
|
||||
log.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(module_name))
|
||||
except ValueError as e:
|
||||
log.error("{}".format(e))
|
||||
@@ -1,3 +1,6 @@
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class BlueprintSetup:
|
||||
"""
|
||||
"""
|
||||
@@ -15,14 +18,17 @@ 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.router.add(uri, 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):
|
||||
"""
|
||||
@@ -30,6 +36,15 @@ class BlueprintSetup:
|
||||
"""
|
||||
self.app.exception(*args, **kwargs)(handler)
|
||||
|
||||
def add_static(self, uri, file_or_directory, *args, **kwargs):
|
||||
"""
|
||||
Registers static files to sanic
|
||||
"""
|
||||
if self.url_prefix:
|
||||
uri = self.url_prefix + uri
|
||||
|
||||
self.app.static(uri, file_or_directory, *args, **kwargs)
|
||||
|
||||
def add_middleware(self, middleware, *args, **kwargs):
|
||||
"""
|
||||
Registers middleware to sanic
|
||||
@@ -41,10 +56,17 @@ 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
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
"""
|
||||
self.name = name
|
||||
self.url_prefix = url_prefix
|
||||
self.deferred_functions = []
|
||||
self.listeners = defaultdict(list)
|
||||
self.host = host
|
||||
|
||||
def record(self, func):
|
||||
"""
|
||||
@@ -65,26 +87,41 @@ class Blueprint:
|
||||
for deferred in self.deferred_functions:
|
||||
deferred(state)
|
||||
|
||||
def route(self, uri, methods=None):
|
||||
def route(self, uri, methods=None, host=None):
|
||||
"""
|
||||
"""
|
||||
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, host=None):
|
||||
"""
|
||||
"""
|
||||
self.record(lambda s: s.add_route(handler, uri, methods, host))
|
||||
return handler
|
||||
|
||||
def listener(self, event):
|
||||
"""
|
||||
"""
|
||||
def decorator(listener):
|
||||
self.listeners[event].append(listener)
|
||||
return listener
|
||||
return decorator
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
"""
|
||||
|
||||
def register_middleware(middleware):
|
||||
self.record(lambda s: s.add_middleware(middleware, *args, **kwargs))
|
||||
self.record(
|
||||
lambda s: s.add_middleware(middleware, *args, **kwargs))
|
||||
return middleware
|
||||
|
||||
# Detect which way this was called, @middleware or @middleware('AT')
|
||||
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
||||
middleware = args[0]
|
||||
args = []
|
||||
return register_middleware(args[0])
|
||||
return register_middleware(middleware)
|
||||
else:
|
||||
return register_middleware
|
||||
|
||||
@@ -95,3 +132,9 @@ class Blueprint:
|
||||
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||
"""
|
||||
"""
|
||||
self.record(
|
||||
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))
|
||||
|
||||
@@ -22,3 +22,4 @@ class Config:
|
||||
"""
|
||||
REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
||||
REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
|
||||
130
sanic/cookies.py
Normal file
130
sanic/cookies.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from datetime import datetime
|
||||
import re
|
||||
import string
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# SimpleCookie
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
# Straight up copied this section of dark magic from SimpleCookie
|
||||
|
||||
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
|
||||
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
|
||||
|
||||
_Translator = {n: '\\%03o' % n
|
||||
for n in set(range(256)) - set(map(ord, _UnescapedChars))}
|
||||
_Translator.update({
|
||||
ord('"'): '\\"',
|
||||
ord('\\'): '\\\\',
|
||||
})
|
||||
|
||||
|
||||
def _quote(str):
|
||||
r"""Quote a string for use in a cookie header.
|
||||
If the string does not need to be double-quoted, then just return the
|
||||
string. Otherwise, surround the string in doublequotes and quote
|
||||
(with a \) special characters.
|
||||
"""
|
||||
if str is None or _is_legal_key(str):
|
||||
return str
|
||||
else:
|
||||
return '"' + str.translate(_Translator) + '"'
|
||||
|
||||
|
||||
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# Custom SimpleCookie
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
def __init__(self, headers):
|
||||
super().__init__()
|
||||
self.headers = headers
|
||||
self.cookie_headers = {}
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# If this cookie doesn't exist, add it to the header keys
|
||||
cookie_header = self.cookie_headers.get(key)
|
||||
if not cookie_header:
|
||||
cookie = Cookie(key, value)
|
||||
cookie_header = MultiHeader("Set-Cookie")
|
||||
self.cookie_headers[key] = cookie_header
|
||||
self.headers[cookie_header] = cookie
|
||||
return super().__setitem__(key, cookie)
|
||||
else:
|
||||
self[key].value = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.cookie_headers[key]
|
||||
return super().__delitem__(key)
|
||||
|
||||
|
||||
class Cookie(dict):
|
||||
"""
|
||||
This is a stripped down version of Morsel from SimpleCookie #gottagofast
|
||||
"""
|
||||
_keys = {
|
||||
"expires": "expires",
|
||||
"path": "Path",
|
||||
"comment": "Comment",
|
||||
"domain": "Domain",
|
||||
"max-age": "Max-Age",
|
||||
"secure": "Secure",
|
||||
"httponly": "HttpOnly",
|
||||
"version": "Version",
|
||||
}
|
||||
_flags = {'secure', 'httponly'}
|
||||
|
||||
def __init__(self, key, value):
|
||||
if key in self._keys:
|
||||
raise KeyError("Cookie name is a reserved word")
|
||||
if not _is_legal_key(key):
|
||||
raise KeyError("Cookie key contains illegal characters")
|
||||
self.key = key
|
||||
self.value = value
|
||||
super().__init__()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self._keys:
|
||||
raise KeyError("Unknown cookie property")
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def encode(self, encoding):
|
||||
output = ['%s=%s' % (self.key, _quote(self.value))]
|
||||
for key, value in self.items():
|
||||
if key == 'max-age' and isinstance(value, int):
|
||||
output.append('%s=%d' % (self._keys[key], value))
|
||||
elif key == 'expires' and isinstance(value, datetime):
|
||||
output.append('%s=%s' % (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT")
|
||||
))
|
||||
elif key in self._flags:
|
||||
output.append(self._keys[key])
|
||||
else:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
|
||||
return "; ".join(output).encode(encoding)
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# Header Trickery
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
|
||||
class MultiHeader:
|
||||
"""
|
||||
Allows us to set a header within response that has a unique key,
|
||||
but may contain duplicate header names
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def encode(self):
|
||||
return self.name.encode()
|
||||
@@ -1,4 +1,5 @@
|
||||
from .response import text
|
||||
from .log import log
|
||||
from traceback import format_exc
|
||||
|
||||
|
||||
@@ -21,6 +22,23 @@ class ServerError(SanicException):
|
||||
status_code = 500
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
status_code = 404
|
||||
|
||||
def __init__(self, message, path, relative_url):
|
||||
super().__init__(message)
|
||||
self.path = path
|
||||
self.relative_url = relative_url
|
||||
|
||||
|
||||
class RequestTimeout(SanicException):
|
||||
status_code = 408
|
||||
|
||||
|
||||
class PayloadTooLarge(SanicException):
|
||||
status_code = 413
|
||||
|
||||
|
||||
class Handler:
|
||||
handlers = None
|
||||
|
||||
@@ -39,13 +57,31 @@ class Handler:
|
||||
: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:
|
||||
if self.sanic.debug:
|
||||
response_message = (
|
||||
'Exception raised in exception handler "{}" '
|
||||
'for uri: "{}"\n{}').format(
|
||||
handler.__name__, request.url, format_exc())
|
||||
log.error(response_message)
|
||||
return text(response_message, 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return response
|
||||
|
||||
def default(self, request, exception):
|
||||
if issubclass(type(exception), SanicException):
|
||||
return text("Error: {}".format(exception), status=getattr(exception, 'status_code', 500))
|
||||
return text(
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500))
|
||||
elif self.sanic.debug:
|
||||
return text("Error: {}\nException: {}".format(exception, format_exc()), status=500)
|
||||
response_message = (
|
||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||
request.url, format_exc()))
|
||||
log.error(response_message)
|
||||
return text(response_message, status=500)
|
||||
else:
|
||||
return text("An error occurred while generating the request", status=500)
|
||||
return text(
|
||||
'An error occurred while generating the response', status=500)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
from cgi import parse_header
|
||||
from collections import namedtuple
|
||||
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
|
||||
|
||||
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
||||
# > If the media type remains unknown, the recipient SHOULD treat it
|
||||
# > as type "application/octet-stream"
|
||||
|
||||
|
||||
class RequestParameters(dict):
|
||||
"""
|
||||
Hosts a dict with lists as values where get returns the first
|
||||
@@ -17,6 +25,9 @@ class RequestParameters(dict):
|
||||
self.super = super()
|
||||
self.super.__init__(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.get(name)
|
||||
|
||||
def get(self, name, default=None):
|
||||
values = self.super.get(name)
|
||||
return values[0] if values else default
|
||||
@@ -25,9 +36,12 @@ class RequestParameters(dict):
|
||||
return self.super.get(name, default)
|
||||
|
||||
|
||||
class Request:
|
||||
class Request(dict):
|
||||
"""
|
||||
Properties of an HTTP request such as URL, headers, etc.
|
||||
"""
|
||||
__slots__ = (
|
||||
'url', 'headers', 'version', 'method',
|
||||
'url', 'headers', 'version', 'method', '_cookies',
|
||||
'query_string', 'body',
|
||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
)
|
||||
@@ -39,7 +53,9 @@ class Request:
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
self.method = method
|
||||
self.query_string = url_parsed.query.decode('utf-8') if url_parsed.query else None
|
||||
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
|
||||
@@ -47,33 +63,48 @@ class Request:
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
self.parsed_args = None
|
||||
self._cookies = None
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
if not self.parsed_json:
|
||||
if self.parsed_json is None:
|
||||
try:
|
||||
self.parsed_json = json_loads(self.body)
|
||||
except:
|
||||
pass
|
||||
except Exception:
|
||||
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:
|
||||
self.parsed_form = {}
|
||||
self.parsed_files = {}
|
||||
content_type, parameters = parse_header(self.headers.get('Content-Type'))
|
||||
self.parsed_form = RequestParameters()
|
||||
self.parsed_files = RequestParameters()
|
||||
content_type = self.headers.get(
|
||||
'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
|
||||
content_type, parameters = parse_header(content_type)
|
||||
try:
|
||||
if content_type is None or content_type == 'application/x-www-form-urlencoded':
|
||||
self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8')))
|
||||
if content_type == 'application/x-www-form-urlencoded':
|
||||
self.parsed_form = RequestParameters(
|
||||
parse_qs(self.body.decode('utf-8')))
|
||||
elif content_type == 'multipart/form-data':
|
||||
# TODO: Stream this instead of reading to/from memory
|
||||
boundary = parameters['boundary'].encode('utf-8')
|
||||
self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
pass
|
||||
self.parsed_form, self.parsed_files = (
|
||||
parse_multipart_form(self.body, boundary))
|
||||
except Exception:
|
||||
log.exception("Failed when parsing form")
|
||||
|
||||
return self.parsed_form
|
||||
|
||||
@@ -88,12 +119,26 @@ class Request:
|
||||
def args(self):
|
||||
if self.parsed_args is None:
|
||||
if self.query_string:
|
||||
self.parsed_args = RequestParameters(parse_qs(self.query_string))
|
||||
self.parsed_args = RequestParameters(
|
||||
parse_qs(self.query_string))
|
||||
else:
|
||||
self.parsed_args = {}
|
||||
|
||||
return self.parsed_args
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
|
||||
if cookie is not None:
|
||||
cookies = SimpleCookie()
|
||||
cookies.load(cookie)
|
||||
self._cookies = {name: cookie.value
|
||||
for name, cookie in cookies.items()}
|
||||
else:
|
||||
self._cookies = {}
|
||||
return self._cookies
|
||||
|
||||
|
||||
File = namedtuple('File', ['type', 'body', 'name'])
|
||||
|
||||
@@ -103,10 +148,10 @@ 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 (dict), files (dict)
|
||||
:return: fields (RequestParameters), files (RequestParameters)
|
||||
"""
|
||||
files = {}
|
||||
fields = {}
|
||||
files = RequestParameters()
|
||||
fields = RequestParameters()
|
||||
|
||||
form_parts = body.split(boundary)
|
||||
for form_part in form_parts[1:-1]:
|
||||
@@ -125,7 +170,8 @@ def parse_multipart_form(body, boundary):
|
||||
|
||||
colon_index = form_line.index(':')
|
||||
form_header_field = form_line[0:colon_index]
|
||||
form_header_value, form_parameters = parse_header(form_line[colon_index + 2:])
|
||||
form_header_value, form_parameters = parse_header(
|
||||
form_line[colon_index + 2:])
|
||||
|
||||
if form_header_field == 'Content-Disposition':
|
||||
if 'filename' in form_parameters:
|
||||
@@ -136,8 +182,16 @@ def parse_multipart_form(body, boundary):
|
||||
|
||||
post_data = form_part[line_index:-4]
|
||||
if file_name or file_type:
|
||||
files[field_name] = File(type=file_type, name=file_name, body=post_data)
|
||||
file = File(type=file_type, name=file_name, body=post_data)
|
||||
if field_name in files:
|
||||
files[field_name].append(file)
|
||||
else:
|
||||
files[field_name] = [file]
|
||||
else:
|
||||
fields[field_name] = post_data.decode('utf-8')
|
||||
value = post_data.decode('utf-8')
|
||||
if field_name in fields:
|
||||
fields[field_name].append(value)
|
||||
else:
|
||||
fields[field_name] = [value]
|
||||
|
||||
return fields, files
|
||||
|
||||
@@ -1,34 +1,98 @@
|
||||
import ujson
|
||||
from aiofiles import open as open_async
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
|
||||
STATUS_CODES = {
|
||||
from ujson import dumps as json_dumps
|
||||
|
||||
from .cookies import CookieJar
|
||||
|
||||
COMMON_STATUS_CODES = {
|
||||
200: b'OK',
|
||||
400: b'Bad Request',
|
||||
404: b'Not Found',
|
||||
500: b'Internal Server Error',
|
||||
}
|
||||
ALL_STATUS_CODES = {
|
||||
100: b'Continue',
|
||||
101: b'Switching Protocols',
|
||||
102: b'Processing',
|
||||
200: b'OK',
|
||||
201: b'Created',
|
||||
202: b'Accepted',
|
||||
203: b'Non-Authoritative Information',
|
||||
204: b'No Content',
|
||||
205: b'Reset Content',
|
||||
206: b'Partial Content',
|
||||
207: b'Multi-Status',
|
||||
208: b'Already Reported',
|
||||
226: b'IM Used',
|
||||
300: b'Multiple Choices',
|
||||
301: b'Moved Permanently',
|
||||
302: b'Found',
|
||||
303: b'See Other',
|
||||
304: b'Not Modified',
|
||||
305: b'Use Proxy',
|
||||
307: b'Temporary Redirect',
|
||||
308: b'Permanent Redirect',
|
||||
400: b'Bad Request',
|
||||
401: b'Unauthorized',
|
||||
402: b'Payment Required',
|
||||
403: b'Forbidden',
|
||||
404: b'Not Found',
|
||||
405: b'Method Not Allowed',
|
||||
406: b'Not Acceptable',
|
||||
407: b'Proxy Authentication Required',
|
||||
408: b'Request Timeout',
|
||||
409: b'Conflict',
|
||||
410: b'Gone',
|
||||
411: b'Length Required',
|
||||
412: b'Precondition Failed',
|
||||
413: b'Request Entity Too Large',
|
||||
414: b'Request-URI Too Long',
|
||||
415: b'Unsupported Media Type',
|
||||
416: b'Requested Range Not Satisfiable',
|
||||
417: b'Expectation Failed',
|
||||
422: b'Unprocessable Entity',
|
||||
423: b'Locked',
|
||||
424: b'Failed Dependency',
|
||||
426: b'Upgrade Required',
|
||||
428: b'Precondition Required',
|
||||
429: b'Too Many Requests',
|
||||
431: b'Request Header Fields Too Large',
|
||||
500: b'Internal Server Error',
|
||||
501: b'Not Implemented',
|
||||
502: b'Bad Gateway',
|
||||
503: b'Service Unavailable',
|
||||
504: b'Gateway Timeout',
|
||||
505: b'HTTP Version Not Supported',
|
||||
506: b'Variant Also Negotiates',
|
||||
507: b'Insufficient Storage',
|
||||
508: b'Loop Detected',
|
||||
510: b'Not Extended',
|
||||
511: b'Network Authentication Required'
|
||||
}
|
||||
|
||||
|
||||
class HTTPResponse:
|
||||
__slots__ = ('body', 'status', 'content_type', 'headers')
|
||||
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||
|
||||
def __init__(self, body=None, status=200, headers=None, content_type='text/plain', body_bytes=b''):
|
||||
def __init__(self, body=None, status=200, headers=None,
|
||||
content_type='text/plain', body_bytes=b''):
|
||||
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('utf-8')
|
||||
except AttributeError:
|
||||
# Convert it to a str if you can't
|
||||
self.body = str(body).encode('utf-8')
|
||||
else:
|
||||
self.body = body_bytes
|
||||
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self._cookies = None
|
||||
|
||||
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
# This is all returned in a kind-of funky way
|
||||
@@ -39,14 +103,30 @@ 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()
|
||||
)
|
||||
return b'HTTP/%b %d %b\r\nContent-Type: %b\r\nContent-Length: %d\r\nConnection: %b\r\n%b%b\r\n%b' % (
|
||||
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
|
||||
status = COMMON_STATUS_CODES.get(self.status)
|
||||
if not status:
|
||||
status = ALL_STATUS_CODES.get(self.status)
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'Content-Type: %b\r\n'
|
||||
b'Content-Length: %d\r\n'
|
||||
b'Connection: %b\r\n'
|
||||
b'%b%b\r\n'
|
||||
b'%b') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
STATUS_CODES.get(self.status, b'FAIL'),
|
||||
status,
|
||||
self.content_type.encode(),
|
||||
len(self.body),
|
||||
b'keep-alive' if keep_alive else b'close',
|
||||
@@ -55,15 +135,37 @@ class HTTPResponse:
|
||||
self.body
|
||||
)
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
self._cookies = CookieJar(self.headers)
|
||||
return self._cookies
|
||||
|
||||
|
||||
def json(body, status=200, headers=None):
|
||||
return HTTPResponse(ujson.dumps(body), headers=headers, status=status,
|
||||
content_type="application/json; charset=utf-8")
|
||||
return HTTPResponse(json_dumps(body), headers=headers, status=status,
|
||||
content_type="application/json")
|
||||
|
||||
|
||||
def text(body, status=200, headers=None):
|
||||
return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8")
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/plain; charset=utf-8")
|
||||
|
||||
|
||||
def html(body, status=200, headers=None):
|
||||
return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8")
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
async def file(location, mime_type=None, headers=None):
|
||||
filename = path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
out_stream = await _file.read()
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
|
||||
return HTTPResponse(status=200,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream)
|
||||
|
||||
239
sanic/router.py
239
sanic/router.py
@@ -1,128 +1,197 @@
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from collections import defaultdict, namedtuple
|
||||
from functools import lru_cache
|
||||
from .config import Config
|
||||
from .exceptions import NotFound, InvalidUsage
|
||||
|
||||
Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters'])
|
||||
Parameter = namedtuple("Parameter", ['name', 'cast'])
|
||||
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
|
||||
REGEX_TYPES = {
|
||||
'string': (str, r'[^/]+'),
|
||||
'int': (int, r'\d+'),
|
||||
'number': (float, r'[0-9\\.]+'),
|
||||
'alpha': (str, r'[A-Za-z]+'),
|
||||
}
|
||||
|
||||
|
||||
def url_hash(url):
|
||||
return url.count('/')
|
||||
|
||||
|
||||
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):
|
||||
@app.route('/my_url/<my_param>', methods=['GET', 'POST', ...])
|
||||
def my_route(request, my_param):
|
||||
do stuff...
|
||||
or
|
||||
@app.route('/my_url/<my_param:my_type>', methods=['GET', 'POST', ...])
|
||||
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
|
||||
|
||||
TODO:
|
||||
This probably needs optimization for larger sets of routes,
|
||||
since it checks every route until it finds a match which is bad and I should feel bad
|
||||
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>. 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 = None
|
||||
regex_types = {
|
||||
"string": (None, "\w+"),
|
||||
"int": (int, "\d+"),
|
||||
"number": (float, "[0-9\\.]+"),
|
||||
"alpha": (None, "[A-Za-z]+"),
|
||||
}
|
||||
routes_static = None
|
||||
routes_dynamic = None
|
||||
routes_always_check = None
|
||||
|
||||
def __init__(self):
|
||||
self.routes = []
|
||||
self.routes_all = {}
|
||||
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
|
||||
:param handler: Request handler function. When executed, it should provide a response object.
|
||||
:param methods: Array of accepted method names.
|
||||
If none are provided, any method is allowed
|
||||
:param handler: Request handler function.
|
||||
When executed, it should provide a response object.
|
||||
:return: Nothing
|
||||
"""
|
||||
|
||||
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:
|
||||
self.hosts.add(host)
|
||||
uri = host + uri
|
||||
|
||||
if uri in self.routes_all:
|
||||
raise RouteExists("Route already registered: {}".format(uri))
|
||||
|
||||
# Dict for faster lookups of if method allowed
|
||||
methods_dict = {method: True for method in methods} if methods else None
|
||||
if methods:
|
||||
methods = frozenset(methods)
|
||||
|
||||
parameters = []
|
||||
properties = {"unhashable": None}
|
||||
|
||||
def add_parameter(match):
|
||||
# We could receive NAME or NAME:PATTERN
|
||||
parts = match.group(1).split(':')
|
||||
if len(parts) == 2:
|
||||
parameter_name, parameter_pattern = parts
|
||||
else:
|
||||
parameter_name = parts[0]
|
||||
parameter_pattern = 'string'
|
||||
name = match.group(1)
|
||||
pattern = 'string'
|
||||
if ':' in name:
|
||||
name, pattern = name.split(':', 1)
|
||||
|
||||
default = (str, pattern)
|
||||
# Pull from pre-configured types
|
||||
parameter_regex = self.regex_types.get(parameter_pattern)
|
||||
if parameter_regex:
|
||||
parameter_type, parameter_pattern = parameter_regex
|
||||
else:
|
||||
parameter_type = None
|
||||
|
||||
parameter = Parameter(name=parameter_name, cast=parameter_type)
|
||||
_type, pattern = REGEX_TYPES.get(pattern, default)
|
||||
parameter = Parameter(name=name, cast=_type)
|
||||
parameters.append(parameter)
|
||||
|
||||
return "({})".format(parameter_pattern)
|
||||
# Mark the whole route as unhashable if it has the hash key in it
|
||||
if re.search('(^|[^^]){1}/', pattern):
|
||||
properties['unhashable'] = True
|
||||
# Mark the route as unhashable if it matches the hash key
|
||||
elif re.search(pattern, '/'):
|
||||
properties['unhashable'] = True
|
||||
|
||||
pattern_string = re.sub("<(.+?)>", add_parameter, uri)
|
||||
pattern = re.compile("^{}$".format(pattern_string))
|
||||
return '({})'.format(pattern)
|
||||
|
||||
route = Route(handler=handler, methods=methods_dict, pattern=pattern, parameters=parameters)
|
||||
self.routes.append(route)
|
||||
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)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if properties['unhashable']:
|
||||
self.routes_always_check.append(route)
|
||||
elif parameters:
|
||||
self.routes_dynamic[url_hash(uri)].append(route)
|
||||
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
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
error
|
||||
:param request: Request object
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
|
||||
route = None
|
||||
args = []
|
||||
kwargs = {}
|
||||
for _route in self.routes:
|
||||
match = _route.pattern.match(request.url)
|
||||
if match:
|
||||
for index, parameter in enumerate(_route.parameters, start=1):
|
||||
value = match.group(index)
|
||||
kwargs[parameter.name] = parameter.cast(value) if parameter.cast is not None else value
|
||||
route = _route
|
||||
break
|
||||
|
||||
if route:
|
||||
if route.methods and request.method not in route.methods:
|
||||
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
|
||||
status_code=405)
|
||||
return route.handler, args, kwargs
|
||||
if self.hosts is None:
|
||||
return self._get(request.url, request.method, '')
|
||||
else:
|
||||
raise NotFound("Requested URL {} not found".format(request.url))
|
||||
return self._get(request.url, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
|
||||
|
||||
class SimpleRouter:
|
||||
"""
|
||||
Simple router records and reads all routes from a dictionary
|
||||
It does not support parameters in routes, but is very fast
|
||||
"""
|
||||
routes = None
|
||||
|
||||
def __init__(self):
|
||||
self.routes = {}
|
||||
|
||||
def add(self, uri, methods, handler):
|
||||
# Dict for faster lookups of method allowed
|
||||
methods_dict = {method: True for method in methods} if methods else None
|
||||
self.routes[uri] = Route(handler=handler, methods=methods_dict, pattern=uri, parameters=None)
|
||||
|
||||
def get(self, request):
|
||||
route = self.routes.get(request.url)
|
||||
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
|
||||
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.
|
||||
:param url: Request URL
|
||||
: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:
|
||||
if route.methods and request.method not in route.methods:
|
||||
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
|
||||
status_code=405)
|
||||
return route.handler, [], {}
|
||||
match = route.pattern.match(url)
|
||||
else:
|
||||
raise NotFound("Requested URL {} not found".format(request.url))
|
||||
# Move on to testing all regex routes
|
||||
for route in self.routes_dynamic[url_hash(url)]:
|
||||
match = route.pattern.match(url)
|
||||
if match:
|
||||
break
|
||||
else:
|
||||
# Lastly, check against all regex routes that cannot be hashed
|
||||
for route in self.routes_always_check:
|
||||
match = route.pattern.match(url)
|
||||
if match:
|
||||
break
|
||||
else:
|
||||
raise NotFound('Requested URL {} not found'.format(url))
|
||||
|
||||
if route.methods and method not in route.methods:
|
||||
raise InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
method, url), status_code=405)
|
||||
|
||||
kwargs = {p.name: p.cast(value)
|
||||
for value, p
|
||||
in zip(match.groups(1), route.parameters)}
|
||||
return route.handler, [], kwargs
|
||||
|
||||
275
sanic/sanic.py
275
sanic/sanic.py
@@ -1,33 +1,57 @@
|
||||
import asyncio
|
||||
from inspect import isawaitable
|
||||
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 traceback import format_exc
|
||||
import logging
|
||||
|
||||
from .config import Config
|
||||
from .exceptions import Handler
|
||||
from .log import log, logging
|
||||
from .log import log
|
||||
from .response import HTTPResponse
|
||||
from .router import Router
|
||||
from .server import serve
|
||||
from .server import serve, HttpProtocol
|
||||
from .static import register as static_register
|
||||
from .exceptions import ServerError
|
||||
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||
from os import set_inheritable
|
||||
|
||||
|
||||
class Sanic:
|
||||
def __init__(self, name, router=None, error_handler=None):
|
||||
def __init__(self, name=None, router=None,
|
||||
error_handler=None, logger=None):
|
||||
if logger is None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s: %(levelname)s: %(message)s"
|
||||
)
|
||||
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.config = Config()
|
||||
self.request_middleware = []
|
||||
self.response_middleware = []
|
||||
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
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Registration
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=None):
|
||||
def route(self, uri, methods=None, host=None):
|
||||
"""
|
||||
Decorates a function to be registered as a route
|
||||
:param uri: path of the URL
|
||||
@@ -35,18 +59,39 @@ class Sanic:
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
# Fix case where the user did not prefix the URL with a /
|
||||
# and will probably get confused as to why it's not working
|
||||
if not uri.startswith('/'):
|
||||
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, 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, 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 route
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
Decorates a function to be registered as a handler for exceptions
|
||||
:param *exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
@@ -69,7 +114,7 @@ class Sanic:
|
||||
if attach_to == 'request':
|
||||
self.request_middleware.append(middleware)
|
||||
if attach_to == 'response':
|
||||
self.response_middleware.append(middleware)
|
||||
self.response_middleware.appendleft(middleware)
|
||||
return middleware
|
||||
|
||||
# Detect which way this was called, @middleware or @middleware('AT')
|
||||
@@ -79,7 +124,17 @@ class Sanic:
|
||||
attach_to = args[0]
|
||||
return register_middleware
|
||||
|
||||
def register_blueprint(self, blueprint, **options):
|
||||
# Static Files
|
||||
def static(self, uri, file_or_directory, pattern='.+',
|
||||
use_modified_since=True):
|
||||
"""
|
||||
Registers a root to serve files from. The input can either be a file
|
||||
or a directory. See
|
||||
"""
|
||||
static_register(self, uri, file_or_directory, pattern,
|
||||
use_modified_since)
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""
|
||||
Registers a blueprint on the application.
|
||||
:param blueprint: Blueprint object
|
||||
@@ -96,20 +151,34 @@ class Sanic:
|
||||
self._blueprint_order.append(blueprint)
|
||||
blueprint.register(self, options)
|
||||
|
||||
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")
|
||||
return self.blueprint(*args, **kwargs)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Request Handling
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def converted_response_type(self, response):
|
||||
pass
|
||||
|
||||
async def handle_request(self, request, response_callback):
|
||||
"""
|
||||
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
|
||||
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
|
||||
:param response_callback: Response function to be called with the
|
||||
response as the only argument
|
||||
:return: Nothing
|
||||
"""
|
||||
try:
|
||||
# Middleware process_request
|
||||
# -------------------------------------------- #
|
||||
# Request Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
response = False
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
@@ -122,36 +191,52 @@ class Sanic:
|
||||
|
||||
# No middleware results
|
||||
if not response:
|
||||
# -------------------------------------------- #
|
||||
# Execute Handler
|
||||
# -------------------------------------------- #
|
||||
|
||||
# Fetch handler from router
|
||||
handler, args, kwargs = self.router.get(request)
|
||||
if handler is None:
|
||||
raise ServerError("'None' was returned while requesting a handler from the router")
|
||||
raise ServerError(
|
||||
("'None' was returned while requesting a "
|
||||
"handler from the router"))
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, *args, **kwargs)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
|
||||
# Middleware process_response
|
||||
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
|
||||
# -------------------------------------------- #
|
||||
# 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
|
||||
|
||||
except Exception as e:
|
||||
# -------------------------------------------- #
|
||||
# Response Generation Failed
|
||||
# -------------------------------------------- #
|
||||
|
||||
try:
|
||||
response = self.error_handler.response(request, e)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
except Exception as e:
|
||||
if self.debug:
|
||||
response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc()))
|
||||
response = HTTPResponse(
|
||||
"Error while handling error: {}\nStack: {}".format(
|
||||
e, format_exc()))
|
||||
else:
|
||||
response = HTTPResponse("An error occured while handling an error")
|
||||
response = HTTPResponse(
|
||||
"An error occured while handling an error")
|
||||
|
||||
response_callback(response)
|
||||
|
||||
@@ -159,19 +244,71 @@ class Sanic:
|
||||
# Execution
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, before_stop=None):
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, sock=None,
|
||||
workers=1, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
stop_event=None):
|
||||
"""
|
||||
Runs the HTTP Server and listens until keyboard interrupt or term signal.
|
||||
On termination, drains connections before closing.
|
||||
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 after_start: Function to be executed after the server starts listening
|
||||
:param before_stop: Function to be executed when a stop signal is received before it is respected
|
||||
: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 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
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
self.error_handler.debug = True
|
||||
self.debug = debug
|
||||
self.loop = loop
|
||||
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'sock': sock,
|
||||
'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
|
||||
}
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Register start/stop events
|
||||
# -------------------------------------------- #
|
||||
|
||||
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
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
@@ -181,21 +318,63 @@ class Sanic:
|
||||
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
|
||||
|
||||
try:
|
||||
serve(
|
||||
host=host,
|
||||
port=port,
|
||||
debug=debug,
|
||||
after_start=after_start,
|
||||
before_stop=before_stop,
|
||||
request_handler=self.handle_request,
|
||||
request_timeout=self.config.REQUEST_TIMEOUT,
|
||||
request_max_size=self.config.REQUEST_MAX_SIZE,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
if workers == 1:
|
||||
serve(**server_settings)
|
||||
else:
|
||||
log.info('Spinning up {} workers...'.format(workers))
|
||||
|
||||
self.serve_multiple(server_settings, workers, stop_event)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
|
||||
log.info("Server Stopped")
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
This kills the Sanic
|
||||
"""
|
||||
asyncio.get_event_loop().stop()
|
||||
if self.processes is not None:
|
||||
for process in self.processes:
|
||||
process.terminate()
|
||||
self.sock.close()
|
||||
get_event_loop().stop()
|
||||
|
||||
def serve_multiple(self, 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:
|
||||
"""
|
||||
server_settings['reuse_port'] = True
|
||||
|
||||
# Create a stop event to be triggered by a signal
|
||||
if stop_event is None:
|
||||
stop_event = Event()
|
||||
signal(SIGINT, lambda s, f: stop_event.set())
|
||||
signal(SIGTERM, lambda s, f: stop_event.set())
|
||||
|
||||
self.sock = socket()
|
||||
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
self.sock.bind((server_settings['host'], server_settings['port']))
|
||||
set_inheritable(self.sock.fileno(), True)
|
||||
server_settings['sock'] = self.sock
|
||||
server_settings['host'] = None
|
||||
server_settings['port'] = None
|
||||
|
||||
self.processes = []
|
||||
for _ in range(workers):
|
||||
process = Process(target=serve, kwargs=server_settings)
|
||||
process.daemon = True
|
||||
process.start()
|
||||
self.processes.append(process)
|
||||
|
||||
for process in self.processes:
|
||||
process.join()
|
||||
|
||||
# the above processes will block this until they're stopped
|
||||
self.stop()
|
||||
|
||||
228
sanic/server.py
228
sanic/server.py
@@ -1,29 +1,43 @@
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from multidict import CIMultiDict
|
||||
from signal import SIGINT, SIGTERM
|
||||
|
||||
import httptools
|
||||
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:
|
||||
except ImportError:
|
||||
async_loop = asyncio
|
||||
|
||||
from .log import log
|
||||
from .request import Request
|
||||
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
||||
|
||||
|
||||
class Signal:
|
||||
stopped = False
|
||||
|
||||
|
||||
class HttpProtocol(asyncio.Protocol):
|
||||
__slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection
|
||||
'parser', 'request', 'url', 'headers', # request params
|
||||
'request_handler', 'request_timeout', 'request_max_size', # request config
|
||||
'_total_request_size', '_timeout_handler') # connection management
|
||||
current_time = None
|
||||
|
||||
def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60,
|
||||
|
||||
class HttpProtocol(asyncio.Protocol):
|
||||
__slots__ = (
|
||||
# event loop, connection
|
||||
'loop', 'transport', 'connections', 'signal',
|
||||
# request params
|
||||
'parser', 'request', 'url', 'headers',
|
||||
# request config
|
||||
'request_handler', 'request_timeout', 'request_max_size',
|
||||
# connection management
|
||||
'_total_request_size', '_timeout_handler', '_last_communication_time')
|
||||
|
||||
def __init__(self, *, loop, request_handler, error_handler,
|
||||
signal=Signal(), connections={}, request_timeout=60,
|
||||
request_max_size=None):
|
||||
self.loop = loop
|
||||
self.transport = None
|
||||
@@ -34,74 +48,99 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.signal = signal
|
||||
self.connections = connections
|
||||
self.request_handler = request_handler
|
||||
self.error_handler = error_handler
|
||||
self.request_timeout = request_timeout
|
||||
self.request_max_size = request_max_size
|
||||
self._total_request_size = 0
|
||||
self._timeout_handler = None
|
||||
self._last_request_time = None
|
||||
self._request_handler_task = None
|
||||
|
||||
# -------------------------------------------- #
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Connection
|
||||
# -------------------------------------------- #
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.connections[self] = True
|
||||
self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout)
|
||||
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):
|
||||
self.bail_out("Request timed out, connection closed")
|
||||
|
||||
# -------------------------------------------- #
|
||||
# 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)
|
||||
else:
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
exception = RequestTimeout('Request Timeout')
|
||||
self.write_error(exception)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Parsing
|
||||
# -------------------------------------------- #
|
||||
|
||||
def data_received(self, data):
|
||||
# Check for the request itself getting too large and exceeding memory limits
|
||||
# Check for the request itself getting too large and exceeding
|
||||
# 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:
|
||||
assert self.request is None
|
||||
self.headers = []
|
||||
self.parser = httptools.HttpRequestParser(self)
|
||||
self.parser = HttpRequestParser(self)
|
||||
|
||||
# Parse request chunk or close connection
|
||||
try:
|
||||
self.parser.feed_data(data)
|
||||
except httptools.parser.errors.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')))
|
||||
|
||||
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=dict(self.headers),
|
||||
headers=CIMultiDict(self.headers),
|
||||
version=self.parser.get_http_version(),
|
||||
method=self.parser.get_method().decode()
|
||||
)
|
||||
|
||||
def on_body(self, body):
|
||||
self.request.body = body
|
||||
if self.request.body:
|
||||
self.request.body += body
|
||||
else:
|
||||
self.request.body = body
|
||||
|
||||
def on_message_complete(self):
|
||||
self.loop.create_task(self.request_handler(self.request, self.write_response))
|
||||
self._request_handler_task = self.loop.create_task(
|
||||
self.request_handler(self.request, self.write_response))
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Responding
|
||||
@@ -109,24 +148,42 @@ class HttpProtocol(asyncio.Protocol):
|
||||
|
||||
def write_response(self, response):
|
||||
try:
|
||||
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))
|
||||
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))
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
else:
|
||||
# Record that we received data
|
||||
self._last_request_time = current_time
|
||||
self.cleanup()
|
||||
except Exception as e:
|
||||
self.bail_out("Writing request failed, connection closed {}".format(e))
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(e))
|
||||
|
||||
def write_error(self, exception):
|
||||
try:
|
||||
response = self.error_handler.response(self.request, exception)
|
||||
version = self.request.version if self.request else '1.1'
|
||||
self.transport.write(response.output(version))
|
||||
self.transport.close()
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing error failed, connection closed {}".format(e))
|
||||
|
||||
def bail_out(self, message):
|
||||
exception = ServerError(message)
|
||||
self.write_error(exception)
|
||||
log.error(message)
|
||||
self.transport.close()
|
||||
|
||||
def cleanup(self):
|
||||
self.parser = None
|
||||
self.request = None
|
||||
self.url = None
|
||||
self.headers = None
|
||||
self._request_handler_task = None
|
||||
self._total_request_size = 0
|
||||
|
||||
def close_if_idle(self):
|
||||
@@ -140,39 +197,100 @@ class HttpProtocol(asyncio.Protocol):
|
||||
return False
|
||||
|
||||
|
||||
def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60,
|
||||
request_max_size=None):
|
||||
# Create Event Loop
|
||||
loop = async_loop.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
# I don't think we take advantage of this
|
||||
# And it slows everything waaayyy down
|
||||
# loop.set_debug(debug)
|
||||
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:
|
||||
"""
|
||||
global current_time
|
||||
current_time = time()
|
||||
loop.call_later(1, partial(update_current_time, loop))
|
||||
|
||||
connections = {}
|
||||
|
||||
def trigger_events(events, loop):
|
||||
"""
|
||||
:param events: one or more sync or async functions to execute
|
||||
:param loop: event loop
|
||||
"""
|
||||
if events:
|
||||
if not isinstance(events, list):
|
||||
events = [events]
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
|
||||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100):
|
||||
"""
|
||||
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 single argument `loop`
|
||||
:param after_start: Function to be executed after the server starts
|
||||
listening. Takes single argument `loop`
|
||||
:param before_stop: Function to be executed when a stop signal is
|
||||
received before it is respected. Takes single argumenet `loop`
|
||||
:param after_stop: Function to be executed when a stop signal is
|
||||
received after it is respected. Takes single argumenet `loop`
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
: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()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
if debug:
|
||||
loop.set_debug(debug)
|
||||
|
||||
trigger_events(before_start, loop)
|
||||
|
||||
connections = set()
|
||||
signal = Signal()
|
||||
server_coroutine = loop.create_server(lambda: HttpProtocol(
|
||||
server = partial(
|
||||
protocol,
|
||||
loop=loop,
|
||||
connections=connections,
|
||||
signal=signal,
|
||||
request_handler=request_handler,
|
||||
error_handler=error_handler,
|
||||
request_timeout=request_timeout,
|
||||
request_max_size=request_max_size,
|
||||
), host, port)
|
||||
)
|
||||
|
||||
server_coroutine = loop.create_server(
|
||||
server,
|
||||
host,
|
||||
port,
|
||||
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))
|
||||
|
||||
try:
|
||||
http_server = loop.run_until_complete(server_coroutine)
|
||||
except OSError as e:
|
||||
log.error("Unable to start server: {}".format(e))
|
||||
return
|
||||
except:
|
||||
except Exception:
|
||||
log.exception("Unable to start server")
|
||||
return
|
||||
|
||||
# Run the on_start function if provided
|
||||
if after_start:
|
||||
result = after_start(loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
trigger_events(after_start, loop)
|
||||
|
||||
# Register signals for graceful termination
|
||||
for _signal in (SIGINT, SIGTERM):
|
||||
@@ -184,10 +302,7 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug
|
||||
log.info("Stop requested, draining connections...")
|
||||
|
||||
# Run the on_stop function if provided
|
||||
if before_stop:
|
||||
result = before_stop(loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
trigger_events(before_stop, loop)
|
||||
|
||||
# Wait for event loop to finish and all connections to drain
|
||||
http_server.close()
|
||||
@@ -195,11 +310,12 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug
|
||||
|
||||
# Complete all tasks on the loop
|
||||
signal.stopped = True
|
||||
for connection in connections.keys():
|
||||
for connection in connections:
|
||||
connection.close_if_idle()
|
||||
|
||||
while connections:
|
||||
loop.run_until_complete(asyncio.sleep(0.1))
|
||||
|
||||
trigger_events(after_stop, loop)
|
||||
|
||||
loop.close()
|
||||
log.info("Server Stopped")
|
||||
|
||||
65
sanic/static.py
Normal file
65
sanic/static.py
Normal file
@@ -0,0 +1,65 @@
|
||||
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
|
||||
|
||||
|
||||
def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||
# TODO: Though sanic is not a file server, I feel like we should atleast
|
||||
# make a good effort here. Modified-since is nice, but we could
|
||||
# also look into etags, expires, and caching
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
if not path.isfile(file_or_directory):
|
||||
uri += '<file_uri:' + pattern + '>'
|
||||
|
||||
async def _handler(request, file_uri=None):
|
||||
# Using this to determine if the URL is trying to break out of the path
|
||||
# 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 = 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
|
||||
# and it has not been modified since
|
||||
if use_modified_since:
|
||||
stats = await stat(file_path)
|
||||
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT',
|
||||
gmtime(stats.st_mtime))
|
||||
if request.headers.get('If-Modified-Since') == modified_since:
|
||||
return HTTPResponse(status=304)
|
||||
headers['Last-Modified'] = modified_since
|
||||
|
||||
return await file(file_path, headers=headers)
|
||||
except:
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
|
||||
app.route(uri, methods=['GET'])(_handler)
|
||||
@@ -5,26 +5,28 @@ HOST = '127.0.0.1'
|
||||
PORT = 42101
|
||||
|
||||
|
||||
async def local_request(method, uri, *args, **kwargs):
|
||||
async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
|
||||
log.info(url)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with aiohttp.ClientSession(cookies=cookies) as session:
|
||||
async with getattr(session, method)(url, *args, **kwargs) as response:
|
||||
response.text = await response.text()
|
||||
response.body = await response.read()
|
||||
return response
|
||||
|
||||
|
||||
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||
loop=None, 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(loop):
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
response = await local_request(method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
@@ -33,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||
exceptions.append(e)
|
||||
app.stop()
|
||||
|
||||
app.run(host=HOST, port=42101, after_start=_collect_response)
|
||||
app.run(host=HOST, debug=debug, port=PORT,
|
||||
after_start=_collect_response, loop=loop, **server_kwargs)
|
||||
|
||||
if exceptions:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
@@ -44,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))
|
||||
|
||||
63
sanic/views.py
Normal file
63
sanic/views.py
Normal file
@@ -0,0 +1,63 @@
|
||||
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
|
||||
to every HTTP method you want to support.
|
||||
|
||||
For example:
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return text('I am get method')
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
return text('I am put method')
|
||||
etc.
|
||||
|
||||
If someone tries to use a non-implemented method, there will be a
|
||||
405 response.
|
||||
|
||||
If you need any url params just mention them in method definition:
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
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.as_view(), '/')
|
||||
2) app.route('/')(DummyView.as_view())
|
||||
|
||||
To add any decorator you could set it into decorators variable
|
||||
"""
|
||||
|
||||
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
|
||||
18
setup.py
18
setup.py
@@ -1,11 +1,23 @@
|
||||
"""
|
||||
Sanic
|
||||
"""
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
|
||||
__file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp:
|
||||
try:
|
||||
version = re.findall(r"^__version__ = '([^']+)'\r?$",
|
||||
fp.read(), re.M)[0]
|
||||
except IndexError:
|
||||
raise RuntimeError('Unable to determine version.')
|
||||
|
||||
setup(
|
||||
name='Sanic',
|
||||
version="0.1.2",
|
||||
version=version,
|
||||
url='http://github.com/channelcat/sanic/',
|
||||
license='MIT',
|
||||
author='Channel Cat',
|
||||
@@ -17,9 +29,11 @@ setup(
|
||||
'uvloop>=0.5.3',
|
||||
'httptools>=0.0.9',
|
||||
'ujson>=1.35',
|
||||
'aiofiles>=0.3.0',
|
||||
'multidict>=2.0',
|
||||
],
|
||||
classifiers=[
|
||||
'Development Status :: 1 - Alpha',
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Environment :: Web Environment',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
|
||||
@@ -15,4 +15,4 @@ async def handle(request):
|
||||
app = web.Application(loop=loop)
|
||||
app.router.add_route('GET', '/', handle)
|
||||
|
||||
web.run_app(app, port=sys.argv[1])
|
||||
web.run_app(app, port=sys.argv[1], access_log=None)
|
||||
|
||||
11
tests/performance/falcon/simple_server.py
Normal file
11
tests/performance/falcon/simple_server.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker falc:app
|
||||
|
||||
import falcon
|
||||
import ujson as json
|
||||
|
||||
class TestResource:
|
||||
def on_get(self, req, resp):
|
||||
resp.body = json.dumps({"test": True})
|
||||
|
||||
app = falcon.API()
|
||||
app.add_route('/', TestResource())
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ app = Sanic("test")
|
||||
async def test(request):
|
||||
return json({"test": True})
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=sys.argv[1])
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=sys.argv[1])
|
||||
|
||||
19
tests/performance/tornado/simple_server.py
Normal file
19
tests/performance/tornado/simple_server.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Run with: python simple_server.py
|
||||
import ujson
|
||||
from tornado import ioloop, web
|
||||
|
||||
|
||||
class MainHandler(web.RequestHandler):
|
||||
def get(self):
|
||||
self.write(ujson.dumps({'test': True}))
|
||||
|
||||
|
||||
app = web.Application([
|
||||
(r'/', MainHandler)
|
||||
], debug=False,
|
||||
compress_response=False,
|
||||
static_hash_cache=True
|
||||
)
|
||||
|
||||
app.listen(8000)
|
||||
ioloop.IOLoop.current().start()
|
||||
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'
|
||||
@@ -1,3 +1,5 @@
|
||||
import inspect
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import json, text
|
||||
@@ -17,7 +19,7 @@ def test_bp():
|
||||
def handler(request):
|
||||
return text('Hello')
|
||||
|
||||
app.register_blueprint(bp)
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
assert response.text == 'Hello'
|
||||
@@ -30,7 +32,7 @@ def test_bp_with_url_prefix():
|
||||
def handler(request):
|
||||
return text('Hello')
|
||||
|
||||
app.register_blueprint(bp)
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
|
||||
assert response.text == 'Hello'
|
||||
@@ -49,14 +51,79 @@ def test_several_bp_with_url_prefix():
|
||||
def handler2(request):
|
||||
return text('Hello2')
|
||||
|
||||
app.register_blueprint(bp)
|
||||
app.register_blueprint(bp2)
|
||||
app.blueprint(bp)
|
||||
app.blueprint(bp2)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
assert response.text == 'Hello'
|
||||
|
||||
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')
|
||||
@@ -70,7 +137,7 @@ def test_bp_middleware():
|
||||
async def handler(request):
|
||||
return text('FAIL')
|
||||
|
||||
app.register_blueprint(blueprint)
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
@@ -97,7 +164,7 @@ def test_bp_exception_handler():
|
||||
def handler_exception(request, exception):
|
||||
return text("OK")
|
||||
|
||||
app.register_blueprint(blueprint)
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/1')
|
||||
assert response.status == 400
|
||||
@@ -108,4 +175,56 @@ def test_bp_exception_handler():
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/3')
|
||||
assert response.status == 200
|
||||
assert response.status == 200
|
||||
|
||||
def test_bp_listeners():
|
||||
app = Sanic('test_middleware')
|
||||
blueprint = Blueprint('test_middleware')
|
||||
|
||||
order = []
|
||||
|
||||
@blueprint.listener('before_server_start')
|
||||
def handler_1(sanic, loop):
|
||||
order.append(1)
|
||||
|
||||
@blueprint.listener('after_server_start')
|
||||
def handler_2(sanic, loop):
|
||||
order.append(2)
|
||||
|
||||
@blueprint.listener('after_server_start')
|
||||
def handler_3(sanic, loop):
|
||||
order.append(3)
|
||||
|
||||
@blueprint.listener('before_server_stop')
|
||||
def handler_4(sanic, loop):
|
||||
order.append(5)
|
||||
|
||||
@blueprint.listener('before_server_stop')
|
||||
def handler_5(sanic, loop):
|
||||
order.append(4)
|
||||
|
||||
@blueprint.listener('after_server_stop')
|
||||
def handler_6(sanic, loop):
|
||||
order.append(6)
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/')
|
||||
|
||||
assert order == [1,2,3,4,5,6]
|
||||
|
||||
def test_bp_static():
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
with open(current_file, 'rb') as file:
|
||||
current_file_contents = file.read()
|
||||
|
||||
app = Sanic('test_static')
|
||||
blueprint = Blueprint('test_static')
|
||||
|
||||
blueprint.static('/testing.file', current_file)
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
|
||||
57
tests/test_cookies.py
Normal file
57
tests/test_cookies.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from datetime import datetime, timedelta
|
||||
from http.cookies import SimpleCookie
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_cookies():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
response = text('Cookies are: {}'.format(request.cookies['test']))
|
||||
response.cookies['right_back'] = 'at you'
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app, cookies={"test": "working!"})
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
assert response.text == 'Cookies are: working!'
|
||||
assert response_cookies['right_back'].value == 'at you'
|
||||
|
||||
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')
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
response = text("OK")
|
||||
response.cookies['test'] = 'at you'
|
||||
response.cookies['test']['httponly'] = True
|
||||
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
assert response_cookies['test'].value == 'at you'
|
||||
assert response_cookies['test']['httponly'] == True
|
||||
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,86 @@
|
||||
import pytest
|
||||
|
||||
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
|
||||
assert response.body == b'An error occurred while generating the response'
|
||||
|
||||
|
||||
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'
|
||||
|
||||
33
tests/test_logging.py
Normal file
33
tests/test_logging.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import asyncio
|
||||
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', logger=True)
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
log.info('hello world')
|
||||
return text('hello')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
log_text = log_stream.getvalue().strip().split('\n')[-3]
|
||||
assert log_text == "module: test_logging; function: handler(); message: hello world"
|
||||
|
||||
if __name__ =="__main__":
|
||||
test_log()
|
||||
@@ -86,3 +86,43 @@ def test_middleware_override_response():
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
|
||||
def test_middleware_order():
|
||||
app = Sanic('test_middleware_order')
|
||||
|
||||
order = []
|
||||
|
||||
@app.middleware('request')
|
||||
async def request1(request):
|
||||
order.append(1)
|
||||
|
||||
@app.middleware('request')
|
||||
async def request2(request):
|
||||
order.append(2)
|
||||
|
||||
@app.middleware('request')
|
||||
async def request3(request):
|
||||
order.append(3)
|
||||
|
||||
@app.middleware('response')
|
||||
async def response1(request, response):
|
||||
order.append(6)
|
||||
|
||||
@app.middleware('response')
|
||||
async def response2(request, response):
|
||||
order.append(5)
|
||||
|
||||
@app.middleware('response')
|
||||
async def response3(request, response):
|
||||
order.append(4)
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
assert response.status == 200
|
||||
assert order == [1,2,3,4,5,6]
|
||||
|
||||
81
tests/test_multiprocessing.py
Normal file
81
tests/test_multiprocessing.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from multiprocessing import Array, Event, Process
|
||||
from time import sleep, time
|
||||
from ujson import loads as json_loads
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.utils import local_request, HOST, PORT
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
# TODO: Figure out why this freezes on pytest but not when
|
||||
# executed via interpreter
|
||||
@pytest.mark.skip(
|
||||
reason="Freezes with pytest not on interpreter")
|
||||
def test_multiprocessing():
|
||||
app = Sanic('test_json')
|
||||
|
||||
response = Array('c', 50)
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return json({"test": True})
|
||||
|
||||
stop_event = Event()
|
||||
async def after_start(*args, **kwargs):
|
||||
http_response = await local_request('get', '/')
|
||||
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
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="Freezes with pytest not on interpreter")
|
||||
def test_drain_connections():
|
||||
app = Sanic('test_json')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return json({"test": True})
|
||||
|
||||
stop_event = Event()
|
||||
async def after_start(*args, **kwargs):
|
||||
http_response = await local_request('get', '/')
|
||||
stop_event.set()
|
||||
|
||||
start = time()
|
||||
app.serve_multiple({
|
||||
'host': HOST,
|
||||
'port': PORT,
|
||||
'after_start': after_start,
|
||||
'request_handler': app.handle_request,
|
||||
}, workers=2, stop_event=stop_event)
|
||||
end = time()
|
||||
|
||||
assert end - start < 0.05
|
||||
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'
|
||||
24
tests/test_request_data.py
Normal file
24
tests/test_request_data.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from ujson import loads
|
||||
|
||||
|
||||
def test_storage():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.middleware('request')
|
||||
def store(request):
|
||||
request['user'] = 'sanic'
|
||||
request['sidekick'] = 'tails'
|
||||
del request['sidekick']
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') })
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
response_json = loads(response.text)
|
||||
assert response_json['user'] == 'sanic'
|
||||
assert response_json.get('sidekick') is None
|
||||
40
tests/test_request_timeout.py
Normal file
40
tests/test_request_timeout.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import RequestTimeout
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.config import Config
|
||||
|
||||
Config.REQUEST_TIMEOUT = 1
|
||||
request_timeout_app = Sanic('test_request_timeout')
|
||||
request_timeout_default_app = Sanic('test_request_timeout_default')
|
||||
|
||||
|
||||
@request_timeout_app.route('/1')
|
||||
async def handler_1(request):
|
||||
await asyncio.sleep(2)
|
||||
return text('OK')
|
||||
|
||||
|
||||
@request_timeout_app.exception(RequestTimeout)
|
||||
def handler_exception(request, exception):
|
||||
return text('Request Timeout from error_handler.', 408)
|
||||
|
||||
|
||||
def test_server_error_request_timeout():
|
||||
request, response = sanic_endpoint_test(request_timeout_app, uri='/1')
|
||||
assert response.status == 408
|
||||
assert response.text == 'Request Timeout from error_handler.'
|
||||
|
||||
|
||||
@request_timeout_default_app.route('/1')
|
||||
async def handler_2(request):
|
||||
await asyncio.sleep(2)
|
||||
return text('OK')
|
||||
|
||||
|
||||
def test_default_server_error_request_timeout():
|
||||
request, response = sanic_endpoint_test(
|
||||
request_timeout_default_app, uri='/1')
|
||||
assert response.status == 408
|
||||
assert response.text == 'Error: Request Timeout'
|
||||
@@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -32,6 +33,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 +91,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')
|
||||
|
||||
@@ -56,12 +111,30 @@ def test_query_string():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")])
|
||||
request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")])
|
||||
|
||||
assert request.args.get('test1') == '1'
|
||||
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
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -80,3 +153,38 @@ def test_post_json():
|
||||
|
||||
assert request.json.get('test') == 'OK'
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_post_form_urlencoded():
|
||||
app = Sanic('test_post_form_urlencoded')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
payload = 'test=OK'
|
||||
headers = {'content-type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
||||
|
||||
assert request.form.get('test') == 'OK'
|
||||
|
||||
|
||||
def test_post_form_multipart_form_data():
|
||||
app = Sanic('test_post_form_multipart_form_data')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
payload = '------sanic\r\n' \
|
||||
'Content-Disposition: form-data; name="test"\r\n' \
|
||||
'\r\n' \
|
||||
'OK\r\n' \
|
||||
'------sanic--\r\n'
|
||||
|
||||
headers = {'content-type': 'multipart/form-data; boundary=----sanic'}
|
||||
|
||||
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
|
||||
|
||||
assert request.form.get('test') == '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)
|
||||
@@ -1,6 +1,8 @@
|
||||
from json import loads as json_loads, dumps as json_dumps
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.response import text
|
||||
from sanic.router import RouteExists, RouteDoesNotExist
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
@@ -8,6 +10,24 @@ from sanic.utils import sanic_endpoint_test
|
||||
# UTF-8
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_static_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@app.route('/test')
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
|
||||
@app.route('/pizazz')
|
||||
async def handler2(request):
|
||||
return text('OK2')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/pizazz')
|
||||
assert response.text == 'OK2'
|
||||
|
||||
|
||||
def test_dynamic_route():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@@ -39,6 +59,11 @@ def test_dynamic_route_string():
|
||||
assert response.text == 'OK'
|
||||
assert results[0] == 'test123'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[1] == 'favicon.ico'
|
||||
|
||||
|
||||
def test_dynamic_route_int():
|
||||
app = Sanic('test_dynamic_route_int')
|
||||
@@ -59,7 +84,7 @@ def test_dynamic_route_int():
|
||||
|
||||
|
||||
def test_dynamic_route_number():
|
||||
app = Sanic('test_dynamic_route_int')
|
||||
app = Sanic('test_dynamic_route_number')
|
||||
|
||||
results = []
|
||||
|
||||
@@ -80,7 +105,7 @@ def test_dynamic_route_number():
|
||||
|
||||
|
||||
def test_dynamic_route_regex():
|
||||
app = Sanic('test_dynamic_route_int')
|
||||
app = Sanic('test_dynamic_route_regex')
|
||||
|
||||
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
|
||||
async def handler(request, folder_id):
|
||||
@@ -97,3 +122,344 @@ def test_dynamic_route_regex():
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_dynamic_route_unhashable():
|
||||
app = Sanic('test_dynamic_route_unhashable')
|
||||
|
||||
@app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||
async def handler(request, unhashable):
|
||||
return text('OK')
|
||||
|
||||
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
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_route_duplicate():
|
||||
app = Sanic('test_route_duplicate')
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/test')
|
||||
async def handler1(request):
|
||||
pass
|
||||
|
||||
@app.route('/test')
|
||||
async def handler2(request):
|
||||
pass
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/test/<dynamic>/')
|
||||
async def handler1(request, dynamic):
|
||||
pass
|
||||
|
||||
@app.route('/test/<dynamic>/')
|
||||
async def handler2(request, dynamic):
|
||||
pass
|
||||
|
||||
|
||||
def test_method_not_allowed():
|
||||
app = Sanic('test_method_not_allowed')
|
||||
|
||||
@app.route('/test', methods=['GET'])
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_static_add_route():
|
||||
app = Sanic('test_static_add_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.text == 'OK1'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||
assert response.text == 'OK2'
|
||||
|
||||
|
||||
def test_dynamic_add_route():
|
||||
app = Sanic('test_dynamic_add_route')
|
||||
|
||||
results = []
|
||||
|
||||
async def handler(request, name):
|
||||
results.append(name)
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, '/folder/<name>')
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[0] == 'test123'
|
||||
|
||||
|
||||
def test_dynamic_add_route_string():
|
||||
app = Sanic('test_dynamic_add_route_string')
|
||||
|
||||
results = []
|
||||
|
||||
async def handler(request, name):
|
||||
results.append(name)
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, '/folder/<name:string>')
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[0] == 'test123'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert results[1] == 'favicon.ico'
|
||||
|
||||
|
||||
def test_dynamic_add_route_int():
|
||||
app = Sanic('test_dynamic_add_route_int')
|
||||
|
||||
results = []
|
||||
|
||||
async def handler(request, folder_id):
|
||||
results.append(folder_id)
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, '/folder/<folder_id:int>')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/12345')
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is int
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/asdf')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_dynamic_add_route_number():
|
||||
app = Sanic('test_dynamic_add_route_number')
|
||||
|
||||
results = []
|
||||
|
||||
async def handler(request, weight):
|
||||
results.append(weight)
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, '/weight/<weight:number>')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/weight/12345')
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is float
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/weight/1234.56')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_dynamic_add_route_regex():
|
||||
app = Sanic('test_dynamic_route_int')
|
||||
|
||||
async def handler(request, folder_id):
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test1')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_dynamic_add_route_unhashable():
|
||||
app = Sanic('test_dynamic_add_route_unhashable')
|
||||
|
||||
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
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_add_route_duplicate():
|
||||
app = Sanic('test_add_route_duplicate')
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
async def handler1(request):
|
||||
pass
|
||||
|
||||
async def handler2(request):
|
||||
pass
|
||||
|
||||
app.add_route(handler1, '/test')
|
||||
app.add_route(handler2, '/test')
|
||||
|
||||
with pytest.raises(RouteExists):
|
||||
async def handler1(request, dynamic):
|
||||
pass
|
||||
|
||||
async def handler2(request, dynamic):
|
||||
pass
|
||||
|
||||
app.add_route(handler1, '/test/<dynamic>/')
|
||||
app.add_route(handler2, '/test/<dynamic>/')
|
||||
|
||||
|
||||
def test_add_route_method_not_allowed():
|
||||
app = Sanic('test_add_route_method_not_allowed')
|
||||
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, '/test', methods=['GET'])
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test')
|
||||
assert response.status == 200
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
62
tests/test_static.py
Normal file
62
tests/test_static.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
@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', static_file_path)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||
assert response.status == 200
|
||||
assert response.body == static_file_content
|
||||
|
||||
|
||||
def test_static_directory(
|
||||
static_file_directory, static_file_path, static_file_content):
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/dir', static_file_directory)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/dir/test.file')
|
||||
assert response.status == 200
|
||||
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
|
||||
23
tests/test_vhosts.py
Normal file
23
tests/test_vhosts.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
def test_vhosts():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@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!"
|
||||
196
tests/test_views.py
Normal file
196
tests/test_views.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text, HTTPResponse
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.request import Request
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
|
||||
def test_methods():
|
||||
app = Sanic('test_methods')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
def post(self, request):
|
||||
return text('I am post method')
|
||||
|
||||
def put(self, request):
|
||||
return text('I am put method')
|
||||
|
||||
def patch(self, request):
|
||||
return text('I am patch method')
|
||||
|
||||
def delete(self, request):
|
||||
return text('I am delete method')
|
||||
|
||||
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")
|
||||
assert response.text == 'I am post method'
|
||||
request, response = sanic_endpoint_test(app, method="put")
|
||||
assert response.text == 'I am put method'
|
||||
request, response = sanic_endpoint_test(app, method="patch")
|
||||
assert response.text == 'I am patch method'
|
||||
request, response = sanic_endpoint_test(app, method="delete")
|
||||
assert response.text == 'I am delete method'
|
||||
|
||||
|
||||
def test_unexisting_methods():
|
||||
app = Sanic('test_unexisting_methods')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
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'
|
||||
request, response = sanic_endpoint_test(app, method="post")
|
||||
assert response.text == 'Error: Method POST not allowed for URL /'
|
||||
|
||||
|
||||
def test_argument_methods():
|
||||
app = Sanic('test_argument_methods')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request, my_param_here):
|
||||
return text('I am get method with %s' % my_param_here)
|
||||
|
||||
app.add_route(DummyView.as_view(), '/<my_param_here>')
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test123')
|
||||
|
||||
assert response.text == 'I am get method with test123'
|
||||
|
||||
|
||||
def test_with_bp():
|
||||
app = Sanic('test_with_bp')
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
bp.add_route(DummyView.as_view(), '/')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
assert response.text == 'I am get method'
|
||||
|
||||
|
||||
def test_with_bp_with_url_prefix():
|
||||
app = Sanic('test_with_bp_with_url_prefix')
|
||||
bp = Blueprint('test_text', url_prefix='/test1')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
bp.add_route(DummyView.as_view(), '/')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
|
||||
assert response.text == 'I am get method'
|
||||
|
||||
|
||||
def test_with_middleware():
|
||||
app = Sanic('test_with_middleware')
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
app.add_route(DummyView.as_view(), '/')
|
||||
|
||||
results = []
|
||||
|
||||
@app.middleware
|
||||
async def handler(request):
|
||||
results.append(request)
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
|
||||
assert response.text == 'I am get method'
|
||||
assert type(results[0]) is Request
|
||||
|
||||
|
||||
def test_with_middleware_response():
|
||||
app = Sanic('test_with_middleware_response')
|
||||
|
||||
results = []
|
||||
|
||||
@app.middleware('request')
|
||||
async def process_response(request):
|
||||
results.append(request)
|
||||
|
||||
@app.middleware('response')
|
||||
async def process_response(request, response):
|
||||
results.append(request)
|
||||
results.append(response)
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
33
tox.ini
33
tox.ini
@@ -1,34 +1,25 @@
|
||||
[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
|
||||
|
||||
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