Compare commits
317 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bece3d2bcf | ||
|
|
307d866bb6 | ||
|
|
95c9514a44 | ||
|
|
768be433d6 | ||
|
|
8d3fd75ec2 | ||
|
|
a42b254c33 | ||
|
|
d24e1ae110 | ||
|
|
2e7badab4e | ||
|
|
7cf3d49f00 | ||
|
|
25037006bf | ||
|
|
f611eb2c2b | ||
|
|
e12c10b087 | ||
|
|
9527e5ded8 | ||
|
|
23b4b20b4f | ||
|
|
7f9ecd659c | ||
|
|
bb31d465f2 | ||
|
|
834468e8e7 | ||
|
|
4720513672 | ||
|
|
a480110d43 | ||
|
|
0a2c95cc10 | ||
|
|
9d2e32902d | ||
|
|
77b6413526 | ||
|
|
9e502099e0 | ||
|
|
c6a7e44ae7 | ||
|
|
1bf06312b8 | ||
|
|
c35721abbd | ||
|
|
7f3c417078 | ||
|
|
69511c2783 | ||
|
|
158a94d34c | ||
|
|
db58bd68f5 | ||
|
|
e65f08a2c8 | ||
|
|
ab8f616385 | ||
|
|
6dc6f9bbb5 | ||
|
|
7754bb995b | ||
|
|
d1fefce61c | ||
|
|
c3abdab9c4 | ||
|
|
8b13e103fd | ||
|
|
5f94f65f4f | ||
|
|
fc0d69616c | ||
|
|
656f5b93d6 | ||
|
|
436d37c079 | ||
|
|
ed0081fcf7 | ||
|
|
140062f8a3 | ||
|
|
75f5fa7c06 | ||
|
|
9152a1a266 | ||
|
|
ade89ab795 | ||
|
|
95cfdee8b8 | ||
|
|
63a27cc5e2 | ||
|
|
472face796 | ||
|
|
8d537a6d0b | ||
|
|
b3101d339e | ||
|
|
85acddddba | ||
|
|
c9d747d97f | ||
|
|
0bba267808 | ||
|
|
1036242064 | ||
|
|
5fd62098bd | ||
|
|
74cc7be922 | ||
|
|
b3814ca89a | ||
|
|
b75a321e4a | ||
|
|
9caa4fec4a | ||
|
|
a0cba1aee1 | ||
|
|
97018ad62f | ||
|
|
a7d17fae44 | ||
|
|
6ce0050979 | ||
|
|
bc035fca78 | ||
|
|
df914a92e4 | ||
|
|
1b939a6823 | ||
|
|
81b6d988ec | ||
|
|
7e9b65feca | ||
|
|
6f098b3d21 | ||
|
|
5ddb0488f2 | ||
|
|
3e87314adf | ||
|
|
f6d4a06661 | ||
|
|
ff17fc95e6 | ||
|
|
c5a46f1cea | ||
|
|
0b072189c4 | ||
|
|
5b22d1486a | ||
|
|
9eb48c2b0d | ||
|
|
ff0632001c | ||
|
|
28bd09a2ea | ||
|
|
c6aaa9b09c | ||
|
|
20d9ec1fd2 | ||
|
|
2c45c2d3c0 | ||
|
|
18829e648a | ||
|
|
a64c636a33 | ||
|
|
5796f211c1 | ||
|
|
ae09dec05e | ||
|
|
afd51e0823 | ||
|
|
0bbf826b21 | ||
|
|
02d1900e2f | ||
|
|
73da11b04c | ||
|
|
4af07e3731 | ||
|
|
7f60f85cd4 | ||
|
|
4c66cb1854 | ||
|
|
35b92e1511 | ||
|
|
e5d3fe52c5 | ||
|
|
63fe7c0a86 | ||
|
|
c5f137c715 | ||
|
|
66923bc0e3 | ||
|
|
8bf7b5a323 | ||
|
|
36d4d85849 | ||
|
|
5f0e05f3bf | ||
|
|
235e5511eb | ||
|
|
6fb60ae0b1 | ||
|
|
6b2883074b | ||
|
|
7fe418d1b7 | ||
|
|
f872ceb0d9 | ||
|
|
0f10a36b40 | ||
|
|
3c45c9170f | ||
|
|
a0730aeb44 | ||
|
|
e5fdc7fdd0 | ||
|
|
015c87b5e1 | ||
|
|
d20a49e500 | ||
|
|
adb7331670 | ||
|
|
084f0d27a3 | ||
|
|
522a0beec0 | ||
|
|
bf46bcf376 | ||
|
|
f330c3f8c5 | ||
|
|
77a51c1e05 | ||
|
|
144f215705 | ||
|
|
51b01b6b44 | ||
|
|
09885534c6 | ||
|
|
b9dfec38c2 | ||
|
|
2ef8120073 | ||
|
|
52ff2e0e63 | ||
|
|
8cf7dce33f | ||
|
|
9d3bb4a37a | ||
|
|
c30437448b | ||
|
|
7e3496f8aa | ||
|
|
46ac79f4dc | ||
|
|
833b14e353 | ||
|
|
e9eca25792 | ||
|
|
1854ad133c | ||
|
|
2b5e723ea5 | ||
|
|
9a18906edd | ||
|
|
93cb7582c2 | ||
|
|
b4529639f6 | ||
|
|
f0a59fccf8 | ||
|
|
46dbaf95a6 | ||
|
|
d418b03708 | ||
|
|
765e90ecfa | ||
|
|
ff1e88dde6 | ||
|
|
62ebcba647 | ||
|
|
429e90183b | ||
|
|
875790e862 | ||
|
|
25edbe6805 | ||
|
|
e148b50d6a | ||
|
|
06d46d56cd | ||
|
|
edd8770c67 | ||
|
|
daedda8547 | ||
|
|
df9d897e75 | ||
|
|
fcd8e5e5ad | ||
|
|
6c003f71f4 | ||
|
|
5b704478d9 | ||
|
|
60eb528d68 | ||
|
|
1cf730d957 | ||
|
|
171110b445 | ||
|
|
22699db855 | ||
|
|
18405b3908 | ||
|
|
f0a55b5cbb | ||
|
|
04a0774ee5 | ||
|
|
3a8cfb1f45 | ||
|
|
dcc19d17d4 | ||
|
|
1ef69adc6f | ||
|
|
75a4df0f32 | ||
|
|
8ba1b5fc35 | ||
|
|
a09471ac6c | ||
|
|
a916eea684 | ||
|
|
511998d8e1 | ||
|
|
e3cf50f791 | ||
|
|
42ba5298a7 | ||
|
|
ee79750a22 | ||
|
|
1787f8617f | ||
|
|
748ca28185 | ||
|
|
9c68d713ba | ||
|
|
fc69678206 | ||
|
|
aebd717039 | ||
|
|
1ddb01ac44 | ||
|
|
3e279cd670 | ||
|
|
724c03630a | ||
|
|
b00b2561e5 | ||
|
|
c5b50fe3cf | ||
|
|
df9884de3c | ||
|
|
65ae7669f9 | ||
|
|
179606feb1 | ||
|
|
536140340e | ||
|
|
5d293df64b | ||
|
|
6188891a53 | ||
|
|
9774661cfe | ||
|
|
563bc34fb5 | ||
|
|
9c95ab3a28 | ||
|
|
b776c37b36 | ||
|
|
5b3f92b70f | ||
|
|
1562b81522 | ||
|
|
be1016ace6 | ||
|
|
ee27c689e1 | ||
|
|
fdbf452ced | ||
|
|
3d9927dee0 | ||
|
|
1456b128d2 | ||
|
|
166f77cb86 | ||
|
|
5577838905 | ||
|
|
9c15982299 | ||
|
|
63c24122db | ||
|
|
1396ca903d | ||
|
|
d1fb5bdc30 | ||
|
|
e27812bf3e | ||
|
|
11a3cf9b99 | ||
|
|
a90d70feae | ||
|
|
466b34735c | ||
|
|
7ca9116e37 | ||
|
|
decd3e737c | ||
|
|
f35442ad1b | ||
|
|
2b296435b3 | ||
|
|
19ee1dfecc | ||
|
|
7da4596ef8 | ||
|
|
a379ef6781 | ||
|
|
7beb065be3 | ||
|
|
38b9091513 | ||
|
|
96db3c9601 | ||
|
|
43c4fc8e33 | ||
|
|
986ff101ce | ||
|
|
94c83c445f | ||
|
|
625865412f | ||
|
|
46677e69ce | ||
|
|
5fbca5b823 | ||
|
|
879fab120f | ||
|
|
391b24bc17 | ||
|
|
d713533d26 | ||
|
|
24f745a334 | ||
|
|
86f3101861 | ||
|
|
fd823c63ab | ||
|
|
fa69892f70 | ||
|
|
cfc53d0d26 | ||
|
|
97c2056e4a | ||
|
|
0ad0164171 | ||
|
|
df0e285b6f | ||
|
|
e92f1b8c28 | ||
|
|
410f86c960 | ||
|
|
85f27320e7 | ||
|
|
9a3fac90e1 | ||
|
|
6984f6eec4 | ||
|
|
d05f502fc8 | ||
|
|
ba41ab8f67 | ||
|
|
250bb7e29d | ||
|
|
48a26fd5df | ||
|
|
3af26540ec | ||
|
|
7d9de068d9 | ||
|
|
d174917a07 | ||
|
|
af398fc4c4 | ||
|
|
878ef446a2 | ||
|
|
668f6477bb | ||
|
|
01a770cbca | ||
|
|
23a1174aa2 | ||
|
|
414020e75b | ||
|
|
ed74bccad6 | ||
|
|
0eedde445c | ||
|
|
88bf78213f | ||
|
|
d342461a51 | ||
|
|
dffaaf8751 | ||
|
|
313edadf47 | ||
|
|
c9ce33dfe6 | ||
|
|
0f50ac7205 | ||
|
|
e807c08275 | ||
|
|
893977365c | ||
|
|
0cac45809f | ||
|
|
489ca3c207 | ||
|
|
313535c599 | ||
|
|
7f1e0557c9 | ||
|
|
0860f84a39 | ||
|
|
2fe9e78b6d | ||
|
|
2ba30f2022 | ||
|
|
b3b27cab34 | ||
|
|
694207a86d | ||
|
|
90138c4bae | ||
|
|
58a833e987 | ||
|
|
86c5a569d5 | ||
|
|
19592e8eea | ||
|
|
8e6678d526 | ||
|
|
e792a1e030 | ||
|
|
f0e818a28c | ||
|
|
69bd63b742 | ||
|
|
b40f30f2e5 | ||
|
|
1fbde87ec2 | ||
|
|
f9dc34c8fa | ||
|
|
f7186f5331 | ||
|
|
6a680e4db0 | ||
|
|
f6b69f412f | ||
|
|
5aed18862d | ||
|
|
62bf213a6e | ||
|
|
e5c32e9b48 | ||
|
|
b87dc37fbb | ||
|
|
002d4cb37c | ||
|
|
ff321fc355 | ||
|
|
c3386dec84 | ||
|
|
927d2761f7 | ||
|
|
5e5f513088 | ||
|
|
9fcf725061 | ||
|
|
601e015f00 | ||
|
|
3289e8403a | ||
|
|
104a7c7d05 | ||
|
|
7560660ec7 | ||
|
|
40ccb4a0dd | ||
|
|
f90288f5dc | ||
|
|
3bf79898d9 | ||
|
|
1d6e11ca10 | ||
|
|
6e903ee7d5 | ||
|
|
21fb1dff7e | ||
|
|
da924a359c | ||
|
|
a5066f15dc | ||
|
|
2dca53a696 | ||
|
|
d8a6d7e02f | ||
|
|
1a8961587c | ||
|
|
fa13ad8849 | ||
|
|
8b23dec322 | ||
|
|
4e8aac4b41 | ||
|
|
e6a828572a | ||
|
|
6ea43d8e6d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,3 +14,5 @@ settings.py
|
||||
docs/_build/
|
||||
docs/_api/
|
||||
build/*
|
||||
access.log
|
||||
error.log
|
||||
|
||||
@@ -12,3 +12,4 @@ deploy:
|
||||
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
|
||||
on:
|
||||
tags: true
|
||||
distributions: "sdist bdist_wheel"
|
||||
|
||||
62
CONTRIBUTING.md
Normal file
62
CONTRIBUTING.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest! Sanic is always looking for contributors. If you
|
||||
don't feel comfortable contributing code, adding docstrings to the source files
|
||||
is very appreciated.
|
||||
|
||||
## Installation
|
||||
|
||||
To develop on sanic (and mainly to just run the tests) it is highly recommend to
|
||||
install from sources.
|
||||
|
||||
So assume you have already cloned the repo and are in the working directory with
|
||||
a virtual environment already set up, then run:
|
||||
|
||||
```bash
|
||||
python setup.py develop && pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
To run the tests for sanic it is recommended to use tox like so:
|
||||
|
||||
```bash
|
||||
tox
|
||||
```
|
||||
|
||||
See it's that simple!
|
||||
|
||||
## Pull requests!
|
||||
|
||||
So the pull request approval rules are pretty simple:
|
||||
1. All pull requests must pass unit tests
|
||||
2. All pull requests must be reviewed and approved by at least
|
||||
one current collaborator on the project
|
||||
3. All pull requests must pass flake8 checks
|
||||
4. If you decide to remove/change anything from any common interface
|
||||
a deprecation message should accompany it.
|
||||
5. If you implement a new feature you should have at least one unit
|
||||
test to accompany it.
|
||||
|
||||
## Documentation
|
||||
|
||||
Sanic's documentation is built
|
||||
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
|
||||
Markdown and can be found in the `docs` folder, while the module reference is
|
||||
automatically generated using `sphinx-apidoc`.
|
||||
|
||||
To generate the documentation from scratch:
|
||||
|
||||
```bash
|
||||
sphinx-apidoc -fo docs/_api/ sanic
|
||||
sphinx-build -b html docs docs/_build
|
||||
```
|
||||
|
||||
The HTML documentation will be created in the `docs/_build` folder.
|
||||
|
||||
## Warning
|
||||
|
||||
One of the main goals of Sanic is speed. Code that lowers the performance of
|
||||
Sanic without significant gains in usability, security, or features may not be
|
||||
merged. Please don't let this intimidate you! If you have any concerns about an
|
||||
idea, open an issue for discussion and help.
|
||||
6
Dockerfile
Normal file
6
Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM python:3.6
|
||||
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install tox
|
||||
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
@@ -0,0 +1,4 @@
|
||||
include README.rst
|
||||
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
4
Makefile
Normal file
4
Makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
test:
|
||||
find . -name "*.pyc" -delete
|
||||
docker build -t sanic/test-image .
|
||||
docker run -t sanic/test-image tox
|
||||
16
README.rst
16
README.rst
@@ -1,5 +1,5 @@
|
||||
Sanic
|
||||
=================================
|
||||
=====
|
||||
|
||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
|
||||
|
||||
@@ -9,6 +9,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
|
||||
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/channelcat/sanic/issues/396>`_ that we use to track those projects!
|
||||
|
||||
Benchmarks
|
||||
----------
|
||||
|
||||
@@ -59,6 +61,13 @@ Installation
|
||||
|
||||
- ``python -m pip install sanic``
|
||||
|
||||
To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables
|
||||
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
|
||||
installation.
|
||||
|
||||
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic``
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
@@ -77,8 +86,9 @@ Documentation
|
||||
|
||||
TODO
|
||||
----
|
||||
* Streamed file processing
|
||||
* http2
|
||||
* Streamed file processing
|
||||
* http2
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
* No wheels for uvloop and httptools on Windows :(
|
||||
|
||||
27
docs/conf.py
27
docs/conf.py
@@ -22,7 +22,7 @@ import sanic
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
extensions = []
|
||||
extensions = ['sphinx.ext.autodoc']
|
||||
|
||||
templates_path = ['_templates']
|
||||
|
||||
@@ -68,7 +68,6 @@ pygments_style = 'sphinx'
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
@@ -80,13 +79,11 @@ html_theme = 'sphinx_rtd_theme'
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Sanicdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
@@ -110,21 +107,14 @@ latex_elements = {
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Sanic.tex', 'Sanic Documentation',
|
||||
'Sanic contributors', 'manual'),
|
||||
]
|
||||
|
||||
latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation',
|
||||
'Sanic contributors', 'manual'), ]
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'sanic', 'Sanic Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
@@ -132,13 +122,10 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Sanic', 'Sanic Documentation',
|
||||
author, 'Sanic', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic',
|
||||
'One line description of project.', 'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
# -- Options for Epub output ----------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
@@ -150,8 +137,6 @@ epub_copyright = copyright
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
|
||||
# -- Custom Settings -------------------------------------------------------
|
||||
|
||||
suppress_warnings = ['image.nonlocal_uri']
|
||||
|
||||
@@ -9,15 +9,18 @@ Guides
|
||||
sanic/getting_started
|
||||
sanic/routing
|
||||
sanic/request_data
|
||||
sanic/response
|
||||
sanic/static_files
|
||||
sanic/exceptions
|
||||
sanic/middleware
|
||||
sanic/blueprints
|
||||
sanic/config
|
||||
sanic/cookies
|
||||
sanic/streaming
|
||||
sanic/class_based_views
|
||||
sanic/custom_protocol
|
||||
sanic/ssl
|
||||
sanic/logging
|
||||
sanic/testing
|
||||
sanic/deploying
|
||||
sanic/extensions
|
||||
|
||||
@@ -55,13 +55,18 @@ will look like:
|
||||
|
||||
Blueprints have much the same functionality as an application instance.
|
||||
|
||||
### WebSocket routes
|
||||
|
||||
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
|
||||
decorator or `bp.add_websocket_route` method.
|
||||
|
||||
### Middleware
|
||||
|
||||
Using blueprints allows you to also register middleware globally.
|
||||
|
||||
```python
|
||||
@bp.middleware
|
||||
async def halt_request(request):
|
||||
async def print_on_request(request):
|
||||
print("I am a spy")
|
||||
|
||||
@bp.middleware('request')
|
||||
@@ -111,7 +116,7 @@ bp = Blueprint('my_blueprint')
|
||||
async def setup_connection(app, loop):
|
||||
global database
|
||||
database = mysql.connect(host='127.0.0.1'...)
|
||||
|
||||
|
||||
@bp.listener('after_server_stop')
|
||||
async def close_connection(app, loop):
|
||||
await database.close()
|
||||
@@ -137,7 +142,7 @@ blueprint_v2 = Blueprint('v2', url_prefix='/v2')
|
||||
@blueprint_v1.route('/')
|
||||
async def api_v1_root(request):
|
||||
return text('Welcome to version 1 of our documentation')
|
||||
|
||||
|
||||
@blueprint_v2.route('/')
|
||||
async def api_v2_root(request):
|
||||
return text('Welcome to version 2 of our documentation')
|
||||
|
||||
@@ -48,6 +48,24 @@ app.add_route(SimpleView.as_view(), '/')
|
||||
|
||||
```
|
||||
|
||||
You can also use `async` syntax.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('some_name')
|
||||
|
||||
class SimpleAsyncView(HTTPMethodView):
|
||||
|
||||
async def get(self, request):
|
||||
return text('I am async get method')
|
||||
|
||||
app.add_route(SimpleAsyncView.as_view(), '/')
|
||||
|
||||
```
|
||||
|
||||
## URL parameters
|
||||
|
||||
If you need any URL parameters, as discussed in the routing guide, include them
|
||||
@@ -128,4 +146,4 @@ view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
|
||||
app.add_route(view, '/')
|
||||
```
|
||||
|
||||
Note: currently you cannot build a URL for a CompositionView using `url_for`.
|
||||
Note: currently you cannot build a URL for a CompositionView using `url_for`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Configuration
|
||||
|
||||
Any reasonably complex application will need configuration that is not baked into the acutal code. Settings might be different for different environments or installations.
|
||||
Any reasonably complex application will need configuration that is not baked into the actual code. Settings might be different for different environments or installations.
|
||||
|
||||
## Basics
|
||||
|
||||
@@ -29,6 +29,14 @@ In general the convention is to only have UPPERCASE configuration parameters. Th
|
||||
|
||||
There are several ways how to load configuration.
|
||||
|
||||
### From environment variables.
|
||||
|
||||
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_vars` boolean to the Sanic constructor to override that:
|
||||
|
||||
```python
|
||||
app = Sanic(load_vars=False)
|
||||
```
|
||||
|
||||
### From an Object
|
||||
|
||||
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
|
||||
@@ -71,8 +79,8 @@ DB_USER = 'appuser'
|
||||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------- | --------- | --------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------- | --------- | --------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
@@ -4,10 +4,39 @@ Thank you for your interest! Sanic is always looking for contributors. If you
|
||||
don't feel comfortable contributing code, adding docstrings to the source files
|
||||
is very appreciated.
|
||||
|
||||
## Installation
|
||||
|
||||
To develop on sanic (and mainly to just run the tests) it is highly recommend to
|
||||
install from sources.
|
||||
|
||||
So assume you have already cloned the repo and are in the working directory with
|
||||
a virtual environment already set up, then run:
|
||||
|
||||
```bash
|
||||
python setup.py develop && pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
* `python -m pip install pytest`
|
||||
* `python -m pytest tests`
|
||||
To run the tests for sanic it is recommended to use tox like so:
|
||||
|
||||
```bash
|
||||
tox
|
||||
```
|
||||
|
||||
See it's that simple!
|
||||
|
||||
## Pull requests!
|
||||
|
||||
So the pull request approval rules are pretty simple:
|
||||
1. All pull requests must pass unit tests
|
||||
* All pull requests must be reviewed and approved by at least
|
||||
one current collaborator on the project
|
||||
* All pull requests must pass flake8 checks
|
||||
* If you decide to remove/change anything from any common interface
|
||||
a deprecation message should accompany it.
|
||||
* If you implement a new feature you should have at least one unit
|
||||
test to accompany it.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ both read and write cookies, which are stored as key-value pairs.
|
||||
|
||||
## Reading cookies
|
||||
|
||||
A user's cookies can be accessed `Request` object's `cookie` dictionary.
|
||||
A user's cookies can be accessed via the `Request` object's `cookies` dictionary.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
@@ -42,20 +42,20 @@ from sanic.response import text
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
response = text("Time to eat some cookies muahaha")
|
||||
|
||||
|
||||
# This cookie will be set to expire in 0 seconds
|
||||
del response.cookies['kill_me']
|
||||
|
||||
|
||||
# This cookie will self destruct in 5 seconds
|
||||
response.cookies['short_life'] = 'Glad to be here'
|
||||
response.cookies['short_life']['max-age'] = 5
|
||||
del response.cookies['favorite_color']
|
||||
|
||||
|
||||
# This cookie will remain unchanged
|
||||
response.cookies['favorite_color'] = 'blue'
|
||||
response.cookies['favorite_color'] = 'pink'
|
||||
del response.cookies['favorite_color']
|
||||
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
|
||||
39
docs/sanic/decorators.md
Normal file
39
docs/sanic/decorators.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Handler Decorators
|
||||
|
||||
Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.
|
||||
|
||||
## Authorization Decorator
|
||||
|
||||
Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response.
|
||||
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
from sanic.response import json
|
||||
|
||||
def authorized():
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
async def decorated_function(request, *args, **kwargs):
|
||||
# run some method that checks the request
|
||||
# for the client's authorization status
|
||||
is_authorized = check_request_for_authorization_status(request)
|
||||
|
||||
if is_authorized:
|
||||
# the user is authorized.
|
||||
# run the handler method and return the response
|
||||
response = await f(request, *args, **kwargs)
|
||||
return response
|
||||
else:
|
||||
# the user is not authorized.
|
||||
return json({'status': 'not_authorized'}, 403)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@authorized()
|
||||
async def test(request):
|
||||
return json({status: 'authorized'})
|
||||
```
|
||||
|
||||
@@ -44,3 +44,29 @@ directly run by the interpreter.
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
|
||||
## Running via Gunicorn
|
||||
|
||||
[Gunicorn](http://gunicorn.org/) ‘Green Unicorn’ is a WSGI HTTP Server for UNIX.
|
||||
It’s a pre-fork worker model ported from Ruby’s Unicorn project.
|
||||
|
||||
In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker`
|
||||
for Gunicorn `worker-class` argument:
|
||||
|
||||
```
|
||||
gunicorn --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
|
||||
```
|
||||
|
||||
## Asynchronous support
|
||||
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
|
||||
However be advised that this method does not support using multiple processes, and is not the preferred way
|
||||
to run the app in general.
|
||||
|
||||
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
|
||||
|
||||
```python
|
||||
server = app.create_server(host="0.0.0.0", port=8000)
|
||||
loop = asyncio.get_event_loop()
|
||||
task = asyncio.ensure_future(server)
|
||||
loop.run_forever()
|
||||
```
|
||||
|
||||
@@ -12,3 +12,13 @@ A list of Sanic extensions created by the community.
|
||||
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
|
||||
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
|
||||
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
|
||||
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
|
||||
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
|
||||
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
|
||||
`Babel` library
|
||||
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
|
||||
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
|
||||
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
|
||||
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
|
||||
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
||||
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
|
||||
|
||||
@@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
|
||||
Sanic aspires to be simple:
|
||||
-------------------
|
||||
Sanic aspires to be simple
|
||||
---------------------------
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
||||
128
docs/sanic/logging.md
Normal file
128
docs/sanic/logging.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Logging
|
||||
|
||||
|
||||
Sanic allows you to do different types of logging (access log, error log) on the requests based on the [python3 logging API](https://docs.python.org/3/howto/logging.html). You should have some basic knowledge on python3 logging if you want do create a new configuration.
|
||||
|
||||
### Quck Start
|
||||
|
||||
A simple example using default setting would be like this:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.config import LOGGING
|
||||
|
||||
# The default logging handlers are ['accessStream', 'errorStream']
|
||||
# but we change it to use other handlers here for demo purpose
|
||||
LOGGING['loggers']['network']['handlers'] = [
|
||||
'accessTimedRotatingFile', 'errorTimedRotationgFile']
|
||||
|
||||
app = Sanic('test')
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
return response.text('Hello World!')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(log_config=LOGGING)
|
||||
```
|
||||
|
||||
After the program starts, it will log down all the information/requests in access.log and error.log in your working directory.
|
||||
|
||||
And to close logging, simply assign log_config=None:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
app.run(log_config=None)
|
||||
```
|
||||
|
||||
This would skip calling logging functions when handling requests.
|
||||
And you could even do further in production to gain extra speed:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
# disable internal messages
|
||||
app.run(debug=False, log_config=None)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
By default, log_config parameter is set to use sanic.config.LOGGING dictionary for configuration. The default configuration provides several predefined `handlers`:
|
||||
|
||||
- internal (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
||||
For internal information console outputs.
|
||||
|
||||
|
||||
- accessStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
||||
For requests information logging in console
|
||||
|
||||
|
||||
- errorStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
||||
For error message and traceback logging in console.
|
||||
|
||||
|
||||
- accessSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
|
||||
For requests information logging to syslog.
|
||||
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
|
||||
Linux (/dev/log) and FreeBSD (/dev/log).<br>
|
||||
You would not be able to access this property if the directory doesn't exist.
|
||||
(Notice that in Docker you have to enable everything by yourself)
|
||||
|
||||
|
||||
- errorSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
|
||||
For error message and traceback logging to syslog.
|
||||
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
|
||||
Linux (/dev/log) and FreeBSD (/dev/log).<br>
|
||||
You would not be able to access this property if the directory doesn't exist.
|
||||
(Notice that in Docker you have to enable everything by yourself)
|
||||
|
||||
|
||||
- accessTimedRotatingFile (using [logging.handlers.TimedRotatingFileHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.TimedRotatingFileHandler))<br>
|
||||
For requests information logging to file with daily rotation support.
|
||||
|
||||
|
||||
- errorTimedRotatingFile (using [logging.handlers.TimedRotatingFileHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.TimedRotatingFileHandler))<br>
|
||||
For error message and traceback logging to file with daily rotation support.
|
||||
|
||||
And `filters`:
|
||||
|
||||
- accessFilter (using sanic.defaultFilter.DefaultFilter)<br>
|
||||
The filter that allows only levels in `DEBUG`, `INFO`, and `NONE(0)`
|
||||
|
||||
|
||||
- errorFilter (using sanic.defaultFilter.DefaultFilter)<br>
|
||||
The filter taht allows only levels in `WARNING`, `ERROR`, and `CRITICAL`
|
||||
|
||||
There are two `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
|
||||
|
||||
- sanic:<br>
|
||||
Used to log internal messages.
|
||||
|
||||
|
||||
- network:<br>
|
||||
Used to log requests from network, and any information from those requests.
|
||||
|
||||
#### Log format:
|
||||
|
||||
In addition to default parameters provided by python (asctime, levelname, message),
|
||||
Sanic provides additional parameters for network logger with accessFilter:
|
||||
|
||||
- host (str)<br>
|
||||
request.ip
|
||||
|
||||
|
||||
- request (str)<br>
|
||||
request.method + " " + request.url
|
||||
|
||||
|
||||
- status (int)<br>
|
||||
response.status
|
||||
|
||||
|
||||
- byte (int)<br>
|
||||
len(response.body)
|
||||
|
||||
|
||||
The default access log format is
|
||||
```python
|
||||
%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d
|
||||
```
|
||||
@@ -1,9 +1,13 @@
|
||||
# Middleware
|
||||
# Middleware And Listeners
|
||||
|
||||
Middleware are functions which are executed before or after requests to the
|
||||
server. They can be used to modify the *request to* or *response from*
|
||||
user-defined handler functions.
|
||||
|
||||
Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle.
|
||||
|
||||
## Middleware
|
||||
|
||||
There are two types of middleware: request and response. Both are declared
|
||||
using the `@app.middleware` decorator, with the decorator's parameter being a
|
||||
string representing its type: `'request'` or `'response'`. Response middleware
|
||||
@@ -64,3 +68,45 @@ async def halt_request(request):
|
||||
async def halt_response(request, response):
|
||||
return text('I halted the response')
|
||||
```
|
||||
|
||||
## Listeners
|
||||
|
||||
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
|
||||
|
||||
- `before_server_start`
|
||||
- `after_server_start`
|
||||
- `before_server_stop`
|
||||
- `after_server_stop`
|
||||
|
||||
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
@app.listener('before_server_start')
|
||||
async def setup_db(app, loop):
|
||||
app.db = await db_setup()
|
||||
|
||||
@app.listener('after_server_start')
|
||||
async def notify_server_started(app, loop):
|
||||
print('Server successfully started!')
|
||||
|
||||
@app.listener('before_server_stop')
|
||||
async def notify_server_stopping(app, loop):
|
||||
print('Server shutting down!')
|
||||
|
||||
@app.listener('after_server_stop')
|
||||
async def close_db(app, loop):
|
||||
await app.db.close()
|
||||
```
|
||||
|
||||
If you want to schedule a background task to run after the loop has started,
|
||||
Sanic provides the `add_task` method to easily do so.
|
||||
|
||||
```python
|
||||
async def notify_server_started_after_five_seconds():
|
||||
await asyncio.sleep(5)
|
||||
print('Server successfully started!')
|
||||
|
||||
app.add_task(notify_server_started_after_five_seconds())
|
||||
```
|
||||
|
||||
@@ -9,30 +9,34 @@ The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "message": request.json })
|
||||
```
|
||||
|
||||
|
||||
- `args` (dict) - Query string variables. A query string is the section of a
|
||||
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
||||
the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
||||
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
|
||||
The request's `query_string` variable holds the unparsed string value.
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||
```
|
||||
|
||||
- `raw_args` (dict) - On many cases you would need to access the url arguments in
|
||||
a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
|
||||
`raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
||||
|
||||
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
@app.route("/files")
|
||||
def post_json(request):
|
||||
test_file = request.files.get('test')
|
||||
@@ -50,7 +54,7 @@ The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
@app.route("/form")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
||||
@@ -58,15 +62,15 @@ The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
- `body` (bytes) - Posted raw body. This property allows retrieval of the
|
||||
request's raw data, regardless of content type.
|
||||
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
@app.route("/users", methods=["POST",])
|
||||
def create_user(request):
|
||||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
||||
```
|
||||
|
||||
|
||||
- `ip` (str) - IP address of the requester.
|
||||
|
||||
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
|
||||
@@ -85,6 +89,13 @@ The following variables are accessible as properties on `Request` objects:
|
||||
return json({'status': 'production'})
|
||||
|
||||
```
|
||||
- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar`
|
||||
- `scheme`: The URL scheme associated with the request: `http` or `https`
|
||||
- `host`: The host associated with the request: `localhost:8080`
|
||||
- `path`: The path of the request: `/posts/1/`
|
||||
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
|
||||
- `uri_template`: Template for matching route handler: `/posts/<id>/`
|
||||
|
||||
|
||||
## Accessing values using `get` and `getlist`
|
||||
|
||||
|
||||
102
docs/sanic/response.md
Normal file
102
docs/sanic/response.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Response
|
||||
|
||||
Use functions in `sanic.response` module to create responses.
|
||||
|
||||
## Plain Text
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/text')
|
||||
def handle_request(request):
|
||||
return response.text('Hello world!')
|
||||
```
|
||||
|
||||
## HTML
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/html')
|
||||
def handle_request(request):
|
||||
return response.html('<p>Hello world!</p>')
|
||||
```
|
||||
|
||||
## JSON
|
||||
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/json')
|
||||
def handle_request(request):
|
||||
return response.json({'message': 'Hello world!'})
|
||||
```
|
||||
|
||||
## File
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/file')
|
||||
async def handle_request(request):
|
||||
return await response.file('/srv/www/whatever.png')
|
||||
```
|
||||
|
||||
## Streaming
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
@app.route("/streaming")
|
||||
async def index(request):
|
||||
async def streaming_fn(response):
|
||||
response.write('foo')
|
||||
response.write('bar')
|
||||
return response.stream(streaming_fn, content_type='text/plain')
|
||||
```
|
||||
|
||||
## Redirect
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/redirect')
|
||||
def handle_request(request):
|
||||
return response.redirect('/json')
|
||||
```
|
||||
|
||||
## Raw
|
||||
|
||||
Response without encoding the body
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/raw')
|
||||
def handle_request(request):
|
||||
return response.raw('raw data')
|
||||
```
|
||||
|
||||
## Modify headers or status
|
||||
|
||||
To modify headers or status code, pass the `headers` or `status` argument to those functions:
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/json')
|
||||
def handle_request(request):
|
||||
return response.json(
|
||||
{'message': 'Hello world!'},
|
||||
headers={'X-Served-By': 'sanic'},
|
||||
status=200
|
||||
)
|
||||
```
|
||||
@@ -52,7 +52,7 @@ async def integer_handler(request, integer_arg):
|
||||
async def number_handler(request, number_arg):
|
||||
return text('Number - {}'.format(number_arg))
|
||||
|
||||
@app.route('/person/<name:[A-z]>')
|
||||
@app.route('/person/<name:[A-z]+>')
|
||||
async def person_handler(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
|
||||
@@ -181,3 +181,37 @@ url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2,
|
||||
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
|
||||
```
|
||||
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.
|
||||
|
||||
## WebSocket routes
|
||||
|
||||
Routes for the WebSocket protocol can be defined with the `@app.websocket`
|
||||
decorator:
|
||||
|
||||
```python
|
||||
@app.websocket('/feed')
|
||||
async def feed(request, ws):
|
||||
while True:
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print('Received: ' + data)
|
||||
```
|
||||
|
||||
Alternatively, the `app.add_websocket_route` method can be used instead of the
|
||||
decorator:
|
||||
|
||||
```python
|
||||
async def feed(request, ws):
|
||||
pass
|
||||
|
||||
app.add_websocket_route(my_websocket_handler, '/feed')
|
||||
```
|
||||
|
||||
Handlers for a WebSocket route are passed the request as first argument, and a
|
||||
WebSocket protocol object as second argument. The protocol object has `send`
|
||||
and `recv` methods to send and receive data respectively.
|
||||
|
||||
WebSocket support requires the [websockets](https://github.com/aaugustin/websockets)
|
||||
package by Aymeric Augustin.
|
||||
|
||||
|
||||
@@ -9,4 +9,12 @@ Optionally pass in an SSLContext:
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile")
|
||||
|
||||
app.run(host="0.0.0.0", port=8443, ssl=context)
|
||||
app.run(host="0.0.0.0", port=8443, ssl=context)
|
||||
|
||||
You can also pass in the locations of a certificate and key as a dictionary:
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
ssl = {'cert': "/path/to/cert", 'key': "/path/to/keyfile"}
|
||||
app.run(host="0.0.0.0", port=8443, ssl=ssl)
|
||||
|
||||
32
docs/sanic/streaming.md
Normal file
32
docs/sanic/streaming.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Streaming
|
||||
|
||||
Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import stream
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
async def sample_streaming_fn(response):
|
||||
response.write('foo,')
|
||||
response.write('bar')
|
||||
|
||||
return stream(sample_streaming_fn, content_type='text/csv')
|
||||
```
|
||||
|
||||
This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides:
|
||||
|
||||
```python
|
||||
@app.route("/")
|
||||
async def index(request):
|
||||
async def stream_from_db(response):
|
||||
conn = await asyncpg.connect(database='test')
|
||||
async with conn.transaction():
|
||||
async for record in conn.cursor('SELECT generate_series(0, 10)'):
|
||||
response.write(record[0])
|
||||
|
||||
return stream(stream_from_db)
|
||||
```
|
||||
@@ -57,17 +57,3 @@ def test_post_json_request_includes_data():
|
||||
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).
|
||||
|
||||
|
||||
### Deprecated: `sanic_endpoint_test`
|
||||
|
||||
Prior to version 0.3.2, testing was provided through the `sanic_endpoint_test` method. This method will be deprecated in the next major version after 0.4.0; please use the `test_client` instead.
|
||||
|
||||
```
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
def test_index_returns_200():
|
||||
request, response = sanic_endpoint_test(app)
|
||||
assert response.status == 200
|
||||
```
|
||||
|
||||
|
||||
@@ -15,4 +15,5 @@ dependencies:
|
||||
- httptools>=0.0.9
|
||||
- ujson>=1.35
|
||||
- aiofiles>=0.3.0
|
||||
- websockets>=3.2
|
||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
||||
@@ -1,5 +1,5 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic import response
|
||||
|
||||
import aiohttp
|
||||
|
||||
@@ -9,20 +9,18 @@ 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()
|
||||
async with session.get(url) as result:
|
||||
return await result.json()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
"""
|
||||
Download and serve example JSON
|
||||
"""
|
||||
@app.route('/')
|
||||
async def handle_request(request):
|
||||
url = "https://api.github.com/repos/channelcat/sanic"
|
||||
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
response = await fetch(session, url)
|
||||
return json(response)
|
||||
result = await fetch(session, url)
|
||||
return response.json(result)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, workers=2)
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, workers=2)
|
||||
|
||||
0
examples/asyncorm/__init__.py
Normal file
0
examples/asyncorm/__init__.py
Normal file
140
examples/asyncorm/__main__.py
Normal file
140
examples/asyncorm/__main__.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import NotFound
|
||||
from sanic.response import json
|
||||
from sanic.views import HTTPMethodView
|
||||
|
||||
from asyncorm import configure_orm
|
||||
from asyncorm.exceptions import QuerysetError
|
||||
|
||||
from library.models import Book
|
||||
from library.serializer import BookSerializer
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def orm_configure(sanic, loop):
|
||||
db_config = {'database': 'sanic_example',
|
||||
'host': 'localhost',
|
||||
'user': 'sanicdbuser',
|
||||
'password': 'sanicDbPass',
|
||||
}
|
||||
|
||||
# configure_orm needs a dictionary with:
|
||||
# * the database configuration
|
||||
# * the application/s where the models are defined
|
||||
orm_app = configure_orm({'loop': loop, # always use the sanic loop!
|
||||
'db_config': db_config,
|
||||
'modules': ['library', ], # list of apps
|
||||
})
|
||||
|
||||
# orm_app is the object that orchestrates the whole ORM
|
||||
# sync_db should be run only once, better do that as external command
|
||||
# it creates the tables in the database!!!!
|
||||
# orm_app.sync_db()
|
||||
|
||||
|
||||
# for all the 404 lets handle the exceptions
|
||||
@app.exception(NotFound)
|
||||
def ignore_404s(request, exception):
|
||||
return json({'method': request.method,
|
||||
'status': exception.status_code,
|
||||
'error': exception.args[0],
|
||||
'results': None,
|
||||
})
|
||||
|
||||
|
||||
# now the propper sanic workflow
|
||||
class BooksView(HTTPMethodView):
|
||||
def arg_parser(self, request):
|
||||
parsed_args = {}
|
||||
for k, v in request.args.items():
|
||||
parsed_args[k] = v[0]
|
||||
return parsed_args
|
||||
|
||||
async def get(self, request):
|
||||
filtered_by = self.arg_parser(request)
|
||||
|
||||
if filtered_by:
|
||||
q_books = await Book.objects.filter(**filtered_by)
|
||||
else:
|
||||
q_books = await Book.objects.all()
|
||||
|
||||
books = [BookSerializer.serialize(book) for book in q_books]
|
||||
|
||||
return json({'method': request.method,
|
||||
'status': 200,
|
||||
'results': books or None,
|
||||
'count': len(books),
|
||||
})
|
||||
|
||||
async def post(self, request):
|
||||
# populate the book with the data in the request
|
||||
book = Book(**request.json)
|
||||
|
||||
# and await on save
|
||||
await book.save()
|
||||
|
||||
return json({'method': request.method,
|
||||
'status': 201,
|
||||
'results': BookSerializer.serialize(book),
|
||||
})
|
||||
|
||||
|
||||
class BookView(HTTPMethodView):
|
||||
async def get_object(self, request, book_id):
|
||||
try:
|
||||
# await on database consults
|
||||
book = await Book.objects.get(**{'id': book_id})
|
||||
except QuerysetError as e:
|
||||
raise NotFound(e.args[0])
|
||||
return book
|
||||
|
||||
async def get(self, request, book_id):
|
||||
# await on database consults
|
||||
book = await self.get_object(request, book_id)
|
||||
|
||||
return json({'method': request.method,
|
||||
'status': 200,
|
||||
'results': BookSerializer.serialize(book),
|
||||
})
|
||||
|
||||
async def put(self, request, book_id):
|
||||
# await on database consults
|
||||
book = await self.get_object(request, book_id)
|
||||
# await on save
|
||||
await book.save(**request.json)
|
||||
|
||||
return json({'method': request.method,
|
||||
'status': 200,
|
||||
'results': BookSerializer.serialize(book),
|
||||
})
|
||||
|
||||
async def patch(self, request, book_id):
|
||||
# await on database consults
|
||||
book = await self.get_object(request, book_id)
|
||||
# await on save
|
||||
await book.save(**request.json)
|
||||
|
||||
return json({'method': request.method,
|
||||
'status': 200,
|
||||
'results': BookSerializer.serialize(book),
|
||||
})
|
||||
|
||||
async def delete(self, request, book_id):
|
||||
# await on database consults
|
||||
book = await self.get_object(request, book_id)
|
||||
# await on its deletion
|
||||
await book.delete()
|
||||
|
||||
return json({'method': request.method,
|
||||
'status': 200,
|
||||
'results': None
|
||||
})
|
||||
|
||||
|
||||
app.add_route(BooksView.as_view(), '/books/')
|
||||
app.add_route(BookView.as_view(), '/books/<book_id:int>/')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
0
examples/asyncorm/library/__init__.py
Normal file
0
examples/asyncorm/library/__init__.py
Normal file
21
examples/asyncorm/library/models.py
Normal file
21
examples/asyncorm/library/models.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from asyncorm.model import Model
|
||||
from asyncorm.fields import CharField, IntegerField, DateField
|
||||
|
||||
|
||||
BOOK_CHOICES = (
|
||||
('hard cover', 'hard cover book'),
|
||||
('paperback', 'paperback book')
|
||||
)
|
||||
|
||||
|
||||
# This is a simple model definition
|
||||
class Book(Model):
|
||||
name = CharField(max_length=50)
|
||||
synopsis = CharField(max_length=255)
|
||||
book_type = CharField(max_length=15, null=True, choices=BOOK_CHOICES)
|
||||
pages = IntegerField(null=True)
|
||||
date_created = DateField(auto_now=True)
|
||||
|
||||
class Meta():
|
||||
ordering = ['name', ]
|
||||
unique_together = ['name', 'synopsis']
|
||||
15
examples/asyncorm/library/serializer.py
Normal file
15
examples/asyncorm/library/serializer.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from asyncorm.model import ModelSerializer, SerializerMethod
|
||||
from library.models import Book
|
||||
|
||||
|
||||
class BookSerializer(ModelSerializer):
|
||||
book_type = SerializerMethod()
|
||||
|
||||
def get_book_type(self, instance):
|
||||
return instance.book_type_display()
|
||||
|
||||
class Meta():
|
||||
model = Book
|
||||
fields = [
|
||||
'id', 'name', 'synopsis', 'book_type', 'pages', 'date_created'
|
||||
]
|
||||
2
examples/asyncorm/requirements.txt
Normal file
2
examples/asyncorm/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
asyncorm==0.0.7
|
||||
sanic==0.4.1
|
||||
@@ -6,6 +6,7 @@ from sanic.response import json, text
|
||||
app = Sanic(__name__)
|
||||
blueprint = Blueprint('name', url_prefix='/my_blueprint')
|
||||
blueprint2 = Blueprint('name', url_prefix='/my_blueprint2')
|
||||
blueprint3 = Blueprint('name', url_prefix='/my_blueprint3')
|
||||
|
||||
|
||||
@blueprint.route('/foo')
|
||||
@@ -17,8 +18,17 @@ async def foo(request):
|
||||
async def foo2(request):
|
||||
return json({'msg': 'hi from blueprint2'})
|
||||
|
||||
@blueprint3.websocket('/foo')
|
||||
async def foo3(request, ws):
|
||||
while True:
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print('Received: ' + data)
|
||||
|
||||
app.register_blueprint(blueprint)
|
||||
app.register_blueprint(blueprint2)
|
||||
app.blueprint(blueprint)
|
||||
app.blueprint(blueprint2)
|
||||
app.blueprint(blueprint3)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
41
examples/dask_distributed.py
Normal file
41
examples/dask_distributed.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
from tornado.platform.asyncio import BaseAsyncIOLoop, to_asyncio_future
|
||||
from distributed import LocalCluster, Client
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
def square(x):
|
||||
return x**2
|
||||
|
||||
|
||||
@app.listener('after_server_start')
|
||||
async def setup(app, loop):
|
||||
# configure tornado use asyncio's loop
|
||||
ioloop = BaseAsyncIOLoop(loop)
|
||||
|
||||
# init distributed client
|
||||
app.client = Client('tcp://localhost:8786', loop=ioloop, start=False)
|
||||
await to_asyncio_future(app.client._start())
|
||||
|
||||
|
||||
@app.listener('before_server_stop')
|
||||
async def stop(app, loop):
|
||||
await to_asyncio_future(app.client._shutdown())
|
||||
|
||||
|
||||
@app.route('/<value:int>')
|
||||
async def test(request, value):
|
||||
future = app.client.submit(square, value)
|
||||
result = await to_asyncio_future(future._result())
|
||||
return response.text(f'The square of {value} is {result}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Distributed cluster should run somewhere else
|
||||
with LocalCluster(scheduler_port=8786, nanny=False, n_workers=2,
|
||||
threads_per_worker=1) as cluster:
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
136
examples/detailed_example.py
Normal file
136
examples/detailed_example.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# This demo requires aioredis and environmental variables established in ENV_VARS
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import aioredis
|
||||
|
||||
import sanic
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
ENV_VARS = ["REDIS_HOST", "REDIS_PORT",
|
||||
"REDIS_MINPOOL", "REDIS_MAXPOOL",
|
||||
"REDIS_PASS", "APP_LOGFILE"]
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
|
||||
logger = None
|
||||
|
||||
|
||||
@app.middleware("request")
|
||||
async def log_uri(request):
|
||||
# Simple middleware to log the URI endpoint that was called
|
||||
logger.info("URI called: {0}".format(request.url))
|
||||
|
||||
|
||||
@app.listener('before_server_start')
|
||||
async def before_server_start(app, loop):
|
||||
logger.info("Starting redis pool")
|
||||
app.redis_pool = await aioredis.create_pool(
|
||||
(app.config.REDIS_HOST, int(app.config.REDIS_PORT)),
|
||||
minsize=int(app.config.REDIS_MINPOOL),
|
||||
maxsize=int(app.config.REDIS_MAXPOOL),
|
||||
password=app.config.REDIS_PASS)
|
||||
|
||||
|
||||
@app.listener('after_server_stop')
|
||||
async def after_server_stop(app, loop):
|
||||
logger.info("Closing redis pool")
|
||||
app.redis_pool.close()
|
||||
await app.redis_pool.wait_closed()
|
||||
|
||||
|
||||
@app.middleware("request")
|
||||
async def attach_db_connectors(request):
|
||||
# Just put the db objects in the request for easier access
|
||||
logger.info("Passing redis pool to request object")
|
||||
request["redis"] = request.app.redis_pool
|
||||
|
||||
|
||||
@app.route("/state/<user_id>", methods=["GET"])
|
||||
async def access_state(request, user_id):
|
||||
try:
|
||||
# Check to see if the value is in cache, if so lets return that
|
||||
with await request["redis"] as redis_conn:
|
||||
state = await redis_conn.get(user_id, encoding="utf-8")
|
||||
if state:
|
||||
return sanic.response.json({"msg": "Success",
|
||||
"status": 200,
|
||||
"success": True,
|
||||
"data": json.loads(state),
|
||||
"finished_at": datetime.now().isoformat()})
|
||||
# Then state object is not in redis
|
||||
logger.critical("Unable to find user_data in cache.")
|
||||
return sanic.response.HTTPResponse({"msg": "User state not found",
|
||||
"success": False,
|
||||
"status": 404,
|
||||
"finished_at": datetime.now().isoformat()}, status=404)
|
||||
except aioredis.ProtocolError:
|
||||
logger.critical("Unable to connect to state cache")
|
||||
return sanic.response.HTTPResponse({"msg": "Internal Server Error",
|
||||
"status": 500,
|
||||
"success": False,
|
||||
"finished_at": datetime.now().isoformat()}, status=500)
|
||||
|
||||
|
||||
@app.route("/state/<user_id>/push", methods=["POST"])
|
||||
async def set_state(request, user_id):
|
||||
try:
|
||||
# Pull a connection from the pool
|
||||
with await request["redis"] as redis_conn:
|
||||
# Set the value in cache to your new value
|
||||
await redis_conn.set(user_id, json.dumps(request.json), expire=1800)
|
||||
logger.info("Successfully pushed state to cache")
|
||||
return sanic.response.HTTPResponse({"msg": "Successfully pushed state to cache",
|
||||
"success": True,
|
||||
"status": 200,
|
||||
"finished_at": datetime.now().isoformat()})
|
||||
except aioredis.ProtocolError:
|
||||
logger.critical("Unable to connect to state cache")
|
||||
return sanic.response.HTTPResponse({"msg": "Internal Server Error",
|
||||
"status": 500,
|
||||
"success": False,
|
||||
"finished_at": datetime.now().isoformat()}, status=500)
|
||||
|
||||
|
||||
def configure():
|
||||
# Setup environment variables
|
||||
env_vars = [os.environ.get(v, None) for v in ENV_VARS]
|
||||
if not all(env_vars):
|
||||
# Send back environment variables that were not set
|
||||
return False, ", ".join([ENV_VARS[i] for i, flag in env_vars if not flag])
|
||||
else:
|
||||
# Add all the env vars to our app config
|
||||
app.config.update({k: v for k, v in zip(ENV_VARS, env_vars)})
|
||||
setup_logging()
|
||||
return True, None
|
||||
|
||||
|
||||
def setup_logging():
|
||||
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
|
||||
logging_format += "%(message)s"
|
||||
|
||||
logging.basicConfig(
|
||||
filename=app.config.APP_LOGFILE,
|
||||
format=logging_format,
|
||||
level=logging.DEBUG)
|
||||
|
||||
|
||||
def main(result, missing):
|
||||
if result:
|
||||
try:
|
||||
app.run(host="0.0.0.0", port=8080, debug=True)
|
||||
except:
|
||||
logging.critical("User killed server. Closing")
|
||||
else:
|
||||
logging.critical("Unable to start. Missing environment variables [{0}]".format(missing))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result, missing = configure()
|
||||
logger = logging.getLogger()
|
||||
main(result, missing)
|
||||
@@ -1,22 +1,21 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
from sanic.exceptions import Handler, SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.exceptions import SanicException
|
||||
"""
|
||||
Imports and code relevant for our CustomHandler class
|
||||
(Ordinarily this would be in a separate file)
|
||||
"""
|
||||
|
||||
|
||||
class CustomHandler(Handler):
|
||||
class CustomHandler(ErrorHandler):
|
||||
|
||||
def default(self, request, exception):
|
||||
# Here, we have access to the exception object
|
||||
@@ -38,11 +37,11 @@ server's error_handler to an instance of our CustomHandler
|
||||
"""
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
handler = CustomHandler(sanic=app)
|
||||
handler = CustomHandler()
|
||||
app.error_handler = handler
|
||||
|
||||
|
||||
@@ -51,7 +50,7 @@ async def test(request):
|
||||
# Here, something occurs which causes an unexpected exception
|
||||
# This exception will flow to our custom handler.
|
||||
1 / 0
|
||||
return json({"test": True})
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
@@ -1,18 +0,0 @@
|
||||
## 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)
|
||||
28
examples/jinja_example/jinja_example.py
Normal file
28
examples/jinja_example/jinja_example.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Render templates in a Flask like way from a "template" directory in
|
||||
# the project
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
# Load the template environment with async support
|
||||
template_env = Environment(
|
||||
loader=PackageLoader('jinja_example', 'templates'),
|
||||
autoescape=select_autoescape(['html', 'xml']),
|
||||
enable_async=True
|
||||
)
|
||||
|
||||
# Load the template from file
|
||||
template = template_env.get_template("example_template.html")
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
rendered_template = await template.render_async(
|
||||
knights='that say nih; asynchronously')
|
||||
return response.html(rendered_template)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8080, debug=True)
|
||||
8
examples/jinja_example/requirements.txt
Normal file
8
examples/jinja_example/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
aiofiles==0.3.1
|
||||
httptools==0.0.9
|
||||
Jinja2==2.9.6
|
||||
MarkupSafe==1.0
|
||||
sanic==0.5.2
|
||||
ujson==1.35
|
||||
uvloop==0.8.0
|
||||
websockets==3.3
|
||||
10
examples/jinja_example/templates/example_template.html
Normal file
10
examples/jinja_example/templates/example_template.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>My Webpage</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<p>knights - {{ knights }}</p>
|
||||
</body>
|
||||
</html>
|
||||
26
examples/modify_header_example.py
Normal file
26
examples/modify_header_example.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Modify header or status in response
|
||||
"""
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.route('/')
|
||||
def handle_request(request):
|
||||
return response.json(
|
||||
{'message': 'Hello world!'},
|
||||
headers={'X-Served-By': 'sanic'},
|
||||
status=200
|
||||
)
|
||||
|
||||
@app.route('/unauthorized')
|
||||
def handle_request(request):
|
||||
return response.json(
|
||||
{'message': 'You are not authorized'},
|
||||
headers={'X-Served-By': 'sanic'},
|
||||
status=404
|
||||
)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
import json
|
||||
from sanic import response
|
||||
import logging
|
||||
|
||||
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||
@@ -18,6 +17,6 @@ sanic = Sanic()
|
||||
@sanic.route("/")
|
||||
def test(request):
|
||||
log.info("received request; responding with 'hey'")
|
||||
return text("hey")
|
||||
return response.text("hey")
|
||||
|
||||
sanic.run(host="0.0.0.0", port=8000)
|
||||
|
||||
85
examples/plotly_example/plotlyjs_example.py
Normal file
85
examples/plotly_example/plotlyjs_example.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from sanic import Sanic
|
||||
|
||||
from sanic_session import InMemorySessionInterface
|
||||
from sanic_jinja2 import SanicJinja2
|
||||
|
||||
import json
|
||||
import plotly
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
jinja = SanicJinja2(app)
|
||||
session = InMemorySessionInterface(cookie_name=app.name, prefix=app.name)
|
||||
|
||||
@app.middleware('request')
|
||||
async def print_on_request(request):
|
||||
print(request.headers)
|
||||
await session.open(request)
|
||||
|
||||
@app.middleware('response')
|
||||
async def print_on_response(request, response):
|
||||
await session.save(request, response)
|
||||
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
rng = pd.date_range('1/1/2011', periods=7500, freq='H')
|
||||
ts = pd.Series(np.random.randn(len(rng)), index=rng)
|
||||
|
||||
graphs = [
|
||||
dict(
|
||||
data=[
|
||||
dict(
|
||||
x=[1, 2, 3],
|
||||
y=[10, 20, 30],
|
||||
type='scatter'
|
||||
),
|
||||
],
|
||||
layout=dict(
|
||||
title='first graph'
|
||||
)
|
||||
),
|
||||
|
||||
dict(
|
||||
data=[
|
||||
dict(
|
||||
x=[1, 3, 5],
|
||||
y=[10, 50, 30],
|
||||
type='bar'
|
||||
),
|
||||
],
|
||||
layout=dict(
|
||||
title='second graph'
|
||||
)
|
||||
),
|
||||
|
||||
dict(
|
||||
data=[
|
||||
dict(
|
||||
x=ts.index, # Can use the pandas data structures directly
|
||||
y=ts
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
# Add "ids" to each of the graphs to pass up to the client
|
||||
# for templating
|
||||
ids = ['graph-{}'.format(i) for i, _ in enumerate(graphs)]
|
||||
|
||||
# Convert the figures to JSON
|
||||
# PlotlyJSONEncoder appropriately converts pandas, datetime, etc
|
||||
# objects to their JSON equivalents
|
||||
graphJSON = json.dumps(graphs, cls=plotly.utils.PlotlyJSONEncoder)
|
||||
|
||||
return jinja.render('index.html', request,
|
||||
ids=ids,
|
||||
graphJSON=graphJSON)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
5
examples/plotly_example/requirements.txt
Normal file
5
examples/plotly_example/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
pandas==0.19.2
|
||||
plotly==2.0.7
|
||||
sanic==0.5.0
|
||||
sanic-jinja2==0.5.1
|
||||
sanic-session==0.1.3
|
||||
0
examples/plotly_example/templates/index.html
Normal file
0
examples/plotly_example/templates/index.html
Normal file
17
examples/redirect_example.py
Normal file
17
examples/redirect_example.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def handle_request(request):
|
||||
return response.redirect('/redirect')
|
||||
|
||||
@app.route('/redirect')
|
||||
async def test(request):
|
||||
return response.json({"Redirected": True})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
@@ -1,6 +1,6 @@
|
||||
from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import RequestTimeout
|
||||
|
||||
@@ -11,11 +11,11 @@ app = Sanic(__name__)
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
await asyncio.sleep(3)
|
||||
return text('Hello, world!')
|
||||
return response.text('Hello, world!')
|
||||
|
||||
|
||||
@app.exception(RequestTimeout)
|
||||
def timeout(request, exception):
|
||||
return text('RequestTimeout from error_handler.', 408)
|
||||
return response.text('RequestTimeout from error_handler.', 408)
|
||||
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
@@ -1,5 +1,5 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic import response
|
||||
from multiprocessing import Event
|
||||
from signal import signal, SIGINT
|
||||
import asyncio
|
||||
@@ -9,10 +9,10 @@ app = Sanic(__name__)
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"answer": "42"})
|
||||
return response.json({"answer": "42"})
|
||||
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
server = app.create_server(host="0.0.0.0", port=8001)
|
||||
server = app.create_server(host="0.0.0.0", port=8000)
|
||||
loop = asyncio.get_event_loop()
|
||||
task = asyncio.ensure_future(server)
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
|
||||
62
examples/sanic_aiomysql_with_global_pool.py
Normal file
62
examples/sanic_aiomysql_with_global_pool.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# encoding: utf-8
|
||||
"""
|
||||
You need the aiomysql
|
||||
"""
|
||||
import os
|
||||
|
||||
import aiomysql
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
database_name = os.environ['DATABASE_NAME']
|
||||
database_host = os.environ['DATABASE_HOST']
|
||||
database_user = os.environ['DATABASE_USER']
|
||||
database_password = os.environ['DATABASE_PASSWORD']
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.listener("before_server_start")
|
||||
async def get_pool(app, loop):
|
||||
"""
|
||||
the first param is the global instance ,
|
||||
so we can store our connection pool in it .
|
||||
and it can be used by different request
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
app.pool = {
|
||||
"aiomysql": await aiomysql.create_pool(host=database_host, user=database_user, password=database_password,
|
||||
db=database_name,
|
||||
maxsize=5)}
|
||||
async with app.pool['aiomysql'].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 test():
|
||||
result = []
|
||||
data = {}
|
||||
async with app.pool['aiomysql'].acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("SELECT question, pub_date FROM sanic_polls")
|
||||
async for row in cur:
|
||||
result.append({"question": row[0], "pub_date": row[1]})
|
||||
if result or len(result) > 0:
|
||||
data['data'] = res
|
||||
return json(data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="127.0.0.1", workers=4, port=12000)
|
||||
61
examples/sanic_aiopeewee.py
Normal file
61
examples/sanic_aiopeewee.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
from aiopeewee import AioModel, AioMySQLDatabase, model_to_dict
|
||||
from peewee import CharField, TextField, DateTimeField
|
||||
from peewee import ForeignKeyField, PrimaryKeyField
|
||||
|
||||
|
||||
db = AioMySQLDatabase('test', user='root', password='',
|
||||
host='127.0.0.1', port=3306)
|
||||
|
||||
|
||||
class User(AioModel):
|
||||
username = CharField()
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
|
||||
class Blog(AioModel):
|
||||
user = ForeignKeyField(User)
|
||||
title = CharField(max_length=25)
|
||||
content = TextField(default='')
|
||||
pub_date = DateTimeField(null=True)
|
||||
pk = PrimaryKeyField()
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.listener('before_server_start')
|
||||
async def setup(app, loop):
|
||||
# create connection pool
|
||||
await db.connect(loop)
|
||||
# create table if not exists
|
||||
await db.create_tables([User, Blog], safe=True)
|
||||
|
||||
|
||||
@app.listener('before_server_stop')
|
||||
async def stop(app, loop):
|
||||
# close connection pool
|
||||
await db.close()
|
||||
|
||||
|
||||
@app.post('/users')
|
||||
async def add_user(request):
|
||||
user = await User.create(**request.json)
|
||||
return json(await model_to_dict(user))
|
||||
|
||||
|
||||
@app.get('/users/count')
|
||||
async def user_count(request):
|
||||
count = await User.select().count()
|
||||
return json({'count': count})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
34
examples/sanic_aioredis_example.py
Normal file
34
examples/sanic_aioredis_example.py
Normal file
@@ -0,0 +1,34 @@
|
||||
""" To run this example you need additional aioredis package
|
||||
"""
|
||||
from sanic import Sanic, response
|
||||
import aioredis
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def handle(request):
|
||||
async with request.app.redis_pool.get() as redis:
|
||||
await redis.set('test-my-key', 'value')
|
||||
val = await redis.get('test-my-key')
|
||||
return response.text(val.decode('utf-8'))
|
||||
|
||||
|
||||
@app.listener('before_server_start')
|
||||
async def before_server_start(app, loop):
|
||||
app.redis_pool = await aioredis.create_pool(
|
||||
('localhost', 6379),
|
||||
minsize=5,
|
||||
maxsize=10,
|
||||
loop=loop
|
||||
)
|
||||
|
||||
|
||||
@app.listener('after_server_stop')
|
||||
async def after_server_stop(app, loop):
|
||||
app.redis_pool.close()
|
||||
await app.redis_pool.wait_closed()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
@@ -1,59 +1,51 @@
|
||||
""" To run this example you need additional asyncpg package
|
||||
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
from asyncpg import create_pool
|
||||
from asyncpg import connect, create_pool
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
DB_CONFIG = {
|
||||
'host': '<host>',
|
||||
'user': '<username>',
|
||||
'user': '<user>',
|
||||
'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]
|
||||
return [dict(r.items()) for r in records]
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.listener('before_server_start')
|
||||
async def create_db(app, loop):
|
||||
"""
|
||||
Create some table and add some data
|
||||
"""
|
||||
async with create_pool(**DB_CONFIG) as pool:
|
||||
async with pool.acquire() as connection:
|
||||
async with connection.transaction():
|
||||
await connection.execute('DROP TABLE IF EXISTS sanic_post')
|
||||
await connection.execute("""CREATE TABLE sanic_post (
|
||||
id serial primary key,
|
||||
content varchar(50),
|
||||
post_date timestamp
|
||||
);""")
|
||||
for i in range(0, 100):
|
||||
await connection.execute(f"""INSERT INTO sanic_post
|
||||
(id, content, post_date) VALUES ({i}, {i}, now())""")
|
||||
async def register_db(app, loop):
|
||||
app.pool = await create_pool(**DB_CONFIG, loop=loop, max_size=100)
|
||||
async with app.pool.acquire() as connection:
|
||||
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, 1000):
|
||||
await connection.execute(f"""INSERT INTO sanic_post
|
||||
(id, content, post_date) VALUES ({i}, {i}, now())""")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def handler(request):
|
||||
async with create_pool(**DB_CONFIG) as pool:
|
||||
async with pool.acquire() as connection:
|
||||
async with connection.transaction():
|
||||
results = await connection.fetch('SELECT * FROM sanic_post')
|
||||
return json({'posts': jsonify(results)})
|
||||
|
||||
@app.get('/')
|
||||
async def root_get(request):
|
||||
async with app.pool.acquire() as connection:
|
||||
results = await connection.fetch('SELECT * FROM sanic_post')
|
||||
return json({'posts': jsonify(results)})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
app.run(host='127.0.0.1', port=8080)
|
||||
|
||||
@@ -5,7 +5,7 @@ motor==1.1
|
||||
sanic==0.2.0
|
||||
"""
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic import response
|
||||
|
||||
|
||||
app = Sanic('motor_mongodb')
|
||||
@@ -25,7 +25,7 @@ async def get(request):
|
||||
for doc in docs:
|
||||
doc['id'] = str(doc['_id'])
|
||||
del doc['_id']
|
||||
return json(docs)
|
||||
return response.json(docs)
|
||||
|
||||
|
||||
@app.route('/post', methods=['POST'])
|
||||
@@ -34,8 +34,8 @@ async def new(request):
|
||||
print(doc)
|
||||
db = get_db()
|
||||
object_id = await db.test_col.save(doc)
|
||||
return json({'object_id': str(object_id)})
|
||||
return response.json({'object_id': str(object_id)})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='127.0.0.1', port=8000)
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
## You need the following additional packages for this example
|
||||
# aiopg
|
||||
# peewee_async
|
||||
@@ -10,8 +11,9 @@ from sanic.response import json
|
||||
|
||||
## peewee_async related imports
|
||||
import peewee
|
||||
from peewee_async import Manager, PostgresqlDatabase
|
||||
|
||||
from peewee import Model, BaseModel
|
||||
from peewee_async import Manager, PostgresqlDatabase, execute
|
||||
from functools import partial
|
||||
# we instantiate a custom loop so we can pass it to our db manager
|
||||
|
||||
## from peewee_async docs:
|
||||
@@ -19,42 +21,77 @@ from peewee_async import Manager, PostgresqlDatabase
|
||||
# with manager! It’s all automatic. But you can run Manager.connect() or
|
||||
# Manager.close() when you need it.
|
||||
|
||||
class AsyncManager(Manager):
|
||||
"""Inherit the peewee_async manager with our own object
|
||||
configuration
|
||||
|
||||
# let's create a simple key value store:
|
||||
class KeyValue(peewee.Model):
|
||||
key = peewee.CharField(max_length=40, unique=True)
|
||||
text = peewee.TextField(default='')
|
||||
database.allow_sync = False
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
database = database
|
||||
def __init__(self, _model_class, *args, **kwargs):
|
||||
super(AsyncManager, self).__init__(*args, **kwargs)
|
||||
self._model_class = _model_class
|
||||
self.database.allow_sync = False
|
||||
|
||||
# create table synchronously
|
||||
KeyValue.create_table(True)
|
||||
def _do_fill(self, method, *args, **kwargs):
|
||||
_class_method = getattr(super(AsyncManager, self), method)
|
||||
pf = partial(_class_method, self._model_class)
|
||||
return pf(*args, **kwargs)
|
||||
|
||||
# OPTIONAL: close synchronous connection
|
||||
database.close()
|
||||
def new(self, *args, **kwargs):
|
||||
return self._do_fill('create', *args, **kwargs)
|
||||
|
||||
# OPTIONAL: disable any future syncronous calls
|
||||
objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
|
||||
def get(self, *args, **kwargs):
|
||||
return self._do_fill('get', *args, **kwargs)
|
||||
|
||||
def execute(self, query):
|
||||
return execute(query)
|
||||
|
||||
|
||||
app = Sanic('peewee_example')
|
||||
def _get_meta_db_class(db):
|
||||
"""creating a declartive class model for db"""
|
||||
class _BlockedMeta(BaseModel):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
_instance = super(_BlockedMeta, cls).__new__(cls, name, bases, attrs)
|
||||
_instance.objects = AsyncManager(_instance, db)
|
||||
return _instance
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def setup(app, loop):
|
||||
database = PostgresqlDatabase(database='test',
|
||||
class _Base(Model, metaclass=_BlockedMeta):
|
||||
|
||||
def to_dict(self):
|
||||
return self._data
|
||||
|
||||
class Meta:
|
||||
database=db
|
||||
return _Base
|
||||
|
||||
|
||||
def declarative_base(*args, **kwargs):
|
||||
"""Returns a new Modeled Class after inheriting meta and Model classes"""
|
||||
db = PostgresqlDatabase(*args, **kwargs)
|
||||
return _get_meta_db_class(db)
|
||||
|
||||
|
||||
AsyncBaseModel = declarative_base(database='test',
|
||||
host='127.0.0.1',
|
||||
user='postgres',
|
||||
password='mysecretpassword')
|
||||
|
||||
objects = Manager(database, loop=loop)
|
||||
# let's create a simple key value store:
|
||||
class KeyValue(AsyncBaseModel):
|
||||
key = peewee.CharField(max_length=40, unique=True)
|
||||
text = peewee.TextField(default='')
|
||||
|
||||
|
||||
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)
|
||||
obj = await KeyValue.objects.new(key=key, text=value)
|
||||
return json({'object_id': obj.id})
|
||||
|
||||
|
||||
@@ -63,7 +100,7 @@ async def get(request):
|
||||
"""
|
||||
Load all objects from database
|
||||
"""
|
||||
all_objects = await objects.execute(KeyValue.select())
|
||||
all_objects = await KeyValue.objects.execute(KeyValue.select())
|
||||
serialized_obj = []
|
||||
for obj in all_objects:
|
||||
serialized_obj.append({
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"test": True})
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.log import log
|
||||
from sanic.response import json, text, file
|
||||
from sanic import response
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
app = Sanic(__name__)
|
||||
@@ -10,17 +10,17 @@ app = Sanic(__name__)
|
||||
|
||||
@app.route("/")
|
||||
async def test_async(request):
|
||||
return json({"test": True})
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
@app.route("/sync", methods=['GET', 'POST'])
|
||||
def test_sync(request):
|
||||
return json({"test": True})
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
@app.route("/dynamic/<name>/<id:int>")
|
||||
def test_params(request, name, id):
|
||||
return text("yeehaww {} {}".format(name, id))
|
||||
return response.text("yeehaww {} {}".format(name, id))
|
||||
|
||||
|
||||
@app.route("/exception")
|
||||
@@ -31,11 +31,11 @@ def exception(request):
|
||||
async def test_await(request):
|
||||
import asyncio
|
||||
await asyncio.sleep(5)
|
||||
return text("I'm feeling sleepy")
|
||||
return response.text("I'm feeling sleepy")
|
||||
|
||||
@app.route("/file")
|
||||
async def test_file(request):
|
||||
return await file(os.path.abspath("setup.py"))
|
||||
return await response.file(os.path.abspath("setup.py"))
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
@@ -44,7 +44,7 @@ async def test_file(request):
|
||||
|
||||
@app.exception(ServerError)
|
||||
async def test(request, exception):
|
||||
return json({"exception": "{}".format(exception), "status": exception.status_code}, status=exception.status_code)
|
||||
return response.json({"exception": "{}".format(exception), "status": exception.status_code}, status=exception.status_code)
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
@@ -53,23 +53,28 @@ async def test(request, exception):
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
return json({"received": True, "message": request.json})
|
||||
return response.json({"received": True, "message": request.json})
|
||||
|
||||
|
||||
@app.route("/form")
|
||||
def post_json(request):
|
||||
return json({"received": True, "form_data": request.form, "test": request.form.get('test')})
|
||||
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
|
||||
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
|
||||
return response.json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
# Run Server
|
||||
# ----------------------------------------------- #
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def before_start(app, loop):
|
||||
log.info("SERVER STARTING")
|
||||
|
||||
|
||||
@app.listener('after_server_start')
|
||||
def after_start(app, loop):
|
||||
log.info("OH OH OH OH OHHHHHHHH")
|
||||
@@ -77,7 +82,13 @@ def after_start(app, loop):
|
||||
|
||||
@app.listener('before_server_stop')
|
||||
def before_stop(app, loop):
|
||||
log.info("SERVER STOPPING")
|
||||
|
||||
|
||||
@app.listener('after_server_stop')
|
||||
def after_stop(app, loop):
|
||||
log.info("TRIED EVERYTHING")
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
18
examples/url_for_example.py
Normal file
18
examples/url_for_example.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
# generate a URL for the endpoint `post_handler`
|
||||
url = app.url_for('post_handler', post_id=5)
|
||||
# the URL is `/posts/5`, redirect to it
|
||||
return response.redirect(url)
|
||||
|
||||
@app.route('/posts/<post_id>')
|
||||
async def post_handler(request, post_id):
|
||||
return response.text('Post - {}'.format(post_id))
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
@@ -1,4 +1,4 @@
|
||||
from sanic.response import text
|
||||
from sanic import response
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
@@ -15,25 +15,25 @@ bp = Blueprint("bp", host="bp.example.com")
|
||||
"somethingelse.com",
|
||||
"therestofyourdomains.com"])
|
||||
async def hello(request):
|
||||
return text("Some defaults")
|
||||
return response.text("Some defaults")
|
||||
|
||||
@app.route('/', host="example.com")
|
||||
async def hello(request):
|
||||
return text("Answer")
|
||||
return response.text("Answer")
|
||||
|
||||
@app.route('/', host="sub.example.com")
|
||||
async def hello(request):
|
||||
return text("42")
|
||||
return response.text("42")
|
||||
|
||||
@bp.route("/question")
|
||||
async def hello(request):
|
||||
return text("What is the meaning of life?")
|
||||
return response.text("What is the meaning of life?")
|
||||
|
||||
@bp.route("/answer")
|
||||
async def hello(request):
|
||||
return text("42")
|
||||
return response.text("42")
|
||||
|
||||
app.register_blueprint(bp)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
29
examples/websocket.html
Normal file
29
examples/websocket.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
var ws = new WebSocket('ws://' + document.domain + ':' + location.port + '/feed'),
|
||||
messages = document.createElement('ul');
|
||||
ws.onmessage = function (event) {
|
||||
var messages = document.getElementsByTagName('ul')[0],
|
||||
message = document.createElement('li'),
|
||||
content = document.createTextNode('Received: ' + event.data);
|
||||
message.appendChild(content);
|
||||
messages.appendChild(message);
|
||||
};
|
||||
document.body.appendChild(messages);
|
||||
window.setInterval(function() {
|
||||
data = 'bye!'
|
||||
ws.send(data);
|
||||
var messages = document.getElementsByTagName('ul')[0],
|
||||
message = document.createElement('li'),
|
||||
content = document.createTextNode('Sent: ' + data);
|
||||
message.appendChild(content);
|
||||
messages.appendChild(message);
|
||||
}, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
23
examples/websocket.py
Normal file
23
examples/websocket.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import file
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
return await file('websocket.html')
|
||||
|
||||
|
||||
@app.websocket('/feed')
|
||||
async def feed(request, ws):
|
||||
while True:
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print('Received: ' + data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
@@ -1,18 +1,11 @@
|
||||
aiocache
|
||||
aiofiles
|
||||
aiohttp
|
||||
aiohttp==1.3.5
|
||||
chardet<=2.3.0
|
||||
beautifulsoup4
|
||||
bottle
|
||||
coverage
|
||||
falcon
|
||||
gunicorn
|
||||
httptools
|
||||
kyoukai
|
||||
flake8
|
||||
pytest
|
||||
recommonmark
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
tornado
|
||||
tox
|
||||
ujson
|
||||
uvloop
|
||||
|
||||
@@ -2,3 +2,4 @@ aiofiles
|
||||
httptools
|
||||
ujson
|
||||
uvloop
|
||||
websockets
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
__version__ = '0.4.0'
|
||||
__version__ = '0.5.4'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
||||
@@ -8,6 +8,10 @@ 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('--cert', dest='cert', type=str,
|
||||
help='location of certificate for SSL')
|
||||
parser.add_argument('--key', dest='key', type=str,
|
||||
help='location of keyfile for SSL.')
|
||||
parser.add_argument('--workers', dest='workers', type=int, default=1, )
|
||||
parser.add_argument('--debug', dest='debug', action="store_true")
|
||||
parser.add_argument('module')
|
||||
@@ -24,13 +28,17 @@ if __name__ == "__main__":
|
||||
raise ValueError("Module is not a Sanic app, it is a {}. "
|
||||
"Perhaps you meant {}.app?"
|
||||
.format(type(app).__name__, args.module))
|
||||
if args.cert is not None or args.key is not None:
|
||||
ssl = {'cert': args.cert, 'key': args.key}
|
||||
else:
|
||||
ssl = None
|
||||
|
||||
app.run(host=args.host, port=args.port,
|
||||
workers=args.workers, debug=args.debug)
|
||||
except ImportError:
|
||||
workers=args.workers, debug=args.debug, ssl=ssl)
|
||||
except ImportError as e:
|
||||
log.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(module_name))
|
||||
.format(e.name))
|
||||
except ValueError as e:
|
||||
log.error("{}".format(e))
|
||||
|
||||
356
sanic/app.py
356
sanic/app.py
@@ -1,29 +1,36 @@
|
||||
import logging
|
||||
import logging.config
|
||||
import re
|
||||
import warnings
|
||||
from asyncio import get_event_loop
|
||||
from asyncio import get_event_loop, ensure_future, CancelledError
|
||||
from collections import deque, defaultdict
|
||||
from functools import partial
|
||||
from inspect import isawaitable, stack, getmodulename
|
||||
from traceback import format_exc
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
from ssl import create_default_context, Purpose
|
||||
|
||||
from sanic.config import Config
|
||||
from sanic.config import Config, LOGGING
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import ServerError, URLBuildError, SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.log import log
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.response import HTTPResponse, StreamingHTTPResponse
|
||||
from sanic.router import Router
|
||||
from sanic.server import serve, serve_multiple, HttpProtocol
|
||||
from sanic.server import serve, serve_multiple, HttpProtocol, Signal
|
||||
from sanic.static import register as static_register
|
||||
from sanic.testing import TestClient
|
||||
from sanic.testing import SanicTestClient
|
||||
from sanic.views import CompositionView
|
||||
from sanic.websocket import WebSocketProtocol, ConnectionClosed
|
||||
|
||||
|
||||
class Sanic:
|
||||
|
||||
def __init__(self, name=None, router=None, error_handler=None):
|
||||
def __init__(self, name=None, router=None, error_handler=None,
|
||||
load_env=True, request_class=None,
|
||||
log_config=LOGGING):
|
||||
if log_config:
|
||||
logging.config.dictConfig(log_config)
|
||||
# Only set up a default log handler if the
|
||||
# end-user application didn't set anything up.
|
||||
if not logging.root.handlers and log.level == logging.NOTSET:
|
||||
@@ -41,8 +48,10 @@ class Sanic:
|
||||
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.request_class = request_class
|
||||
self.error_handler = error_handler or ErrorHandler()
|
||||
self.config = Config()
|
||||
self.config = Config(load_env=load_env)
|
||||
self.log_config = log_config
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
self.blueprints = {}
|
||||
@@ -51,6 +60,8 @@ class Sanic:
|
||||
self.sock = None
|
||||
self.listeners = defaultdict(list)
|
||||
self.is_running = False
|
||||
self.websocket_enabled = False
|
||||
self.websocket_tasks = []
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
@@ -98,7 +109,8 @@ class Sanic:
|
||||
return decorator
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False):
|
||||
"""Decorate a function to be registered as a route
|
||||
|
||||
:param uri: path of the URL
|
||||
@@ -114,34 +126,42 @@ class Sanic:
|
||||
|
||||
def response(handler):
|
||||
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||
host=host)
|
||||
host=host, strict_slashes=strict_slashes)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"GET"}), host=host)
|
||||
def get(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=frozenset({"GET"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def post(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"POST"}), host=host)
|
||||
def post(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=frozenset({"POST"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def put(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"PUT"}), host=host)
|
||||
def put(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=frozenset({"PUT"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def head(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"HEAD"}), host=host)
|
||||
def head(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def options(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
|
||||
def options(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def patch(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"PATCH"}), host=host)
|
||||
def patch(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def delete(self, uri, host=None):
|
||||
return self.route(uri, methods=frozenset({"DELETE"}), host=host)
|
||||
def delete(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=frozenset({"DELETE"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False):
|
||||
"""A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
@@ -165,9 +185,71 @@ class Sanic:
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes)(handler)
|
||||
return handler
|
||||
|
||||
# Decorator
|
||||
def websocket(self, uri, host=None, strict_slashes=False):
|
||||
"""Decorate a function to be registered as a websocket route
|
||||
:param uri: path of the URL
|
||||
:param host:
|
||||
:return: decorated function
|
||||
"""
|
||||
self.enable_websocket()
|
||||
|
||||
# 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):
|
||||
async def websocket_handler(request, *args, **kwargs):
|
||||
request.app = self
|
||||
protocol = request.transport.get_protocol()
|
||||
ws = await protocol.websocket_handshake(request)
|
||||
|
||||
# schedule the application handler
|
||||
# its future is kept in self.websocket_tasks in case it
|
||||
# needs to be cancelled due to the server being stopped
|
||||
fut = ensure_future(handler(request, ws, *args, **kwargs))
|
||||
self.websocket_tasks.append(fut)
|
||||
try:
|
||||
await fut
|
||||
except (CancelledError, ConnectionClosed):
|
||||
pass
|
||||
self.websocket_tasks.remove(fut)
|
||||
await ws.close()
|
||||
|
||||
self.router.add(uri=uri, handler=websocket_handler,
|
||||
methods=frozenset({'GET'}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None,
|
||||
strict_slashes=False):
|
||||
"""A helper method to register a function as a websocket route."""
|
||||
return self.websocket(uri, host=host,
|
||||
strict_slashes=strict_slashes)(handler)
|
||||
|
||||
def enable_websocket(self, enable=True):
|
||||
"""Enable or disable the support for websocket.
|
||||
|
||||
Websocket is enabled automatically if websocket routes are
|
||||
added to the application.
|
||||
"""
|
||||
if not self.websocket_enabled:
|
||||
# if the server is stopped, we want to cancel any ongoing
|
||||
# websocket tasks, to allow the server to exit promptly
|
||||
@self.listener('before_server_stop')
|
||||
def cancel_websocket_tasks(app, loop):
|
||||
for task in self.websocket_tasks:
|
||||
task.cancel()
|
||||
|
||||
self.websocket_enabled = enable
|
||||
|
||||
def remove_route(self, uri, clean_cache=True, host=None):
|
||||
self.router.remove(uri, clean_cache, host)
|
||||
|
||||
@@ -181,7 +263,11 @@ class Sanic:
|
||||
|
||||
def response(handler):
|
||||
for exception in exceptions:
|
||||
self.error_handler.add(exception, handler)
|
||||
if isinstance(exception, (tuple, list)):
|
||||
for e in exception:
|
||||
self.error_handler.add(e, handler)
|
||||
else:
|
||||
self.error_handler.add(exception, handler)
|
||||
return handler
|
||||
|
||||
return response
|
||||
@@ -207,7 +293,7 @@ class Sanic:
|
||||
attach_to=middleware_or_request)
|
||||
|
||||
# Static Files
|
||||
def static(self, uri, file_or_directory, pattern='.+',
|
||||
def static(self, uri, file_or_directory, pattern=r'/?.+',
|
||||
use_modified_since=True, use_content_range=False):
|
||||
"""Register a root to serve files from. The input can either be a
|
||||
file or a directory. See
|
||||
@@ -254,7 +340,7 @@ class Sanic:
|
||||
the output URL's query string.
|
||||
|
||||
:param view_name: string referencing the view name
|
||||
:param **kwargs: keys and values that are used to build request
|
||||
:param \*\*kwargs: keys and values that are used to build request
|
||||
parameters and query string arguments.
|
||||
|
||||
:return: the built URL
|
||||
@@ -345,14 +431,17 @@ class Sanic:
|
||||
def converted_response_type(self, response):
|
||||
pass
|
||||
|
||||
async def handle_request(self, request, response_callback):
|
||||
async def handle_request(self, request, write_callback, stream_callback):
|
||||
"""Take a request from the HTTP Server and return 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 write_callback: Synchronous response function to be
|
||||
called with the response as the only argument
|
||||
:param stream_callback: Coroutine that handles streaming a
|
||||
StreamingHTTPResponse if produced by the handler.
|
||||
|
||||
:return: Nothing
|
||||
"""
|
||||
try:
|
||||
@@ -361,17 +450,7 @@ class Sanic:
|
||||
# -------------------------------------------- #
|
||||
|
||||
request.app = self
|
||||
|
||||
response = False
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
for middleware in self.request_middleware:
|
||||
response = middleware(request)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
if response:
|
||||
break
|
||||
|
||||
response = await self._run_request_middleware(request)
|
||||
# No middleware results
|
||||
if not response:
|
||||
# -------------------------------------------- #
|
||||
@@ -379,7 +458,8 @@ class Sanic:
|
||||
# -------------------------------------------- #
|
||||
|
||||
# Fetch handler from router
|
||||
handler, args, kwargs = self.router.get(request)
|
||||
handler, args, kwargs, uri = self.router.get(request)
|
||||
request.uri_template = uri
|
||||
if handler is None:
|
||||
raise ServerError(
|
||||
("'None' was returned while requesting a "
|
||||
@@ -389,20 +469,6 @@ class Sanic:
|
||||
response = handler(request, *args, **kwargs)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
|
||||
# -------------------------------------------- #
|
||||
# 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
|
||||
@@ -420,8 +486,23 @@ class Sanic:
|
||||
else:
|
||||
response = HTTPResponse(
|
||||
"An error occurred while handling an error")
|
||||
finally:
|
||||
# -------------------------------------------- #
|
||||
# Response Middleware
|
||||
# -------------------------------------------- #
|
||||
try:
|
||||
response = await self._run_response_middleware(request,
|
||||
response)
|
||||
except:
|
||||
log.exception(
|
||||
'Exception occured in one of response middleware handlers'
|
||||
)
|
||||
|
||||
response_callback(response)
|
||||
# pass the response to the correct callback
|
||||
if isinstance(response, StreamingHTTPResponse):
|
||||
await stream_callback(response)
|
||||
else:
|
||||
write_callback(response)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Testing
|
||||
@@ -429,57 +510,59 @@ class Sanic:
|
||||
|
||||
@property
|
||||
def test_client(self):
|
||||
return TestClient(self)
|
||||
return SanicTestClient(self)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Execution
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, workers=1, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None, register_sys_signals=True):
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False, ssl=None,
|
||||
sock=None, workers=1, protocol=None,
|
||||
backlog=100, stop_event=None, register_sys_signals=True,
|
||||
log_config=LOGGING):
|
||||
"""Run the HTTP Server and listen until keyboard interrupt or term
|
||||
signal. On termination, drain connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param before_start: Functions to be executed before the server starts
|
||||
accepting connections
|
||||
:param after_start: Functions to be executed after the server starts
|
||||
accepting connections
|
||||
:param before_stop: Functions to be executed when a stop signal is
|
||||
received before it is respected
|
||||
:param after_stop: Functions to be executed when all requests are
|
||||
complete
|
||||
:param ssl: SSLContext for SSL encryption of worker(s)
|
||||
:param ssl: SSLContext, or location of certificate and key
|
||||
for SSL encryption of worker(s)
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param workers: Number of processes
|
||||
received before it is respected
|
||||
:param loop:
|
||||
received before it is respected
|
||||
:param backlog:
|
||||
:param stop_event:
|
||||
:param register_sys_signals:
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
if log_config:
|
||||
logging.config.dictConfig(log_config)
|
||||
if protocol is None:
|
||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||
else HttpProtocol)
|
||||
if stop_event is not None:
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("stop_event will be removed from future versions.",
|
||||
DeprecationWarning)
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, before_start=before_start,
|
||||
after_start=after_start, before_stop=before_stop,
|
||||
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
|
||||
loop=loop, protocol=protocol, backlog=backlog,
|
||||
stop_event=stop_event, register_sys_signals=register_sys_signals)
|
||||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||
workers=workers, protocol=protocol, backlog=backlog,
|
||||
register_sys_signals=register_sys_signals,
|
||||
has_log=log_config is not None)
|
||||
|
||||
try:
|
||||
self.is_running = True
|
||||
if workers == 1:
|
||||
serve(**server_settings)
|
||||
else:
|
||||
serve_multiple(server_settings, workers, stop_event)
|
||||
serve_multiple(server_settings, workers)
|
||||
except:
|
||||
log.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
raise
|
||||
finally:
|
||||
self.is_running = False
|
||||
log.info("Server Stopped")
|
||||
@@ -488,49 +571,78 @@ class Sanic:
|
||||
"""This kills the Sanic"""
|
||||
get_event_loop().stop()
|
||||
|
||||
def __call__(self):
|
||||
"""gunicorn compatibility"""
|
||||
return self
|
||||
|
||||
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
|
||||
before_start=None, after_start=None,
|
||||
before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None):
|
||||
ssl=None, sock=None, protocol=None,
|
||||
backlog=100, stop_event=None,
|
||||
log_config=LOGGING):
|
||||
"""Asynchronous version of `run`.
|
||||
|
||||
NOTE: This does not support multiprocessing and is not the preferred
|
||||
way to run a Sanic application.
|
||||
"""
|
||||
if log_config:
|
||||
logging.config.dictConfig(log_config)
|
||||
if protocol is None:
|
||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||
else HttpProtocol)
|
||||
if stop_event is not None:
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("stop_event will be removed from future versions.",
|
||||
DeprecationWarning)
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, before_start=before_start,
|
||||
after_start=after_start, before_stop=before_stop,
|
||||
after_stop=after_stop, ssl=ssl, sock=sock,
|
||||
loop=loop or get_event_loop(), protocol=protocol,
|
||||
backlog=backlog, stop_event=stop_event,
|
||||
run_async=True)
|
||||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||
loop=get_event_loop(), protocol=protocol,
|
||||
backlog=backlog, run_async=True,
|
||||
has_log=log_config is not None)
|
||||
|
||||
return await serve(**server_settings)
|
||||
|
||||
async def _run_request_middleware(self, request):
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
for middleware in self.request_middleware:
|
||||
response = middleware(request)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
if response:
|
||||
return response
|
||||
return None
|
||||
|
||||
async def _run_response_middleware(self, request, 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
|
||||
return response
|
||||
|
||||
def _helper(self, host="127.0.0.1", port=8000, debug=False,
|
||||
before_start=None, after_start=None, before_stop=None,
|
||||
after_stop=None, ssl=None, sock=None, workers=1, loop=None,
|
||||
ssl=None, sock=None, workers=1, loop=None,
|
||||
protocol=HttpProtocol, backlog=100, stop_event=None,
|
||||
register_sys_signals=True, run_async=False):
|
||||
register_sys_signals=True, run_async=False, has_log=True):
|
||||
"""Helper function used by `run` and `create_server`."""
|
||||
|
||||
if loop is not None:
|
||||
if isinstance(ssl, dict):
|
||||
# try common aliaseses
|
||||
cert = ssl.get('cert') or ssl.get('certificate')
|
||||
key = ssl.get('key') or ssl.get('keyfile')
|
||||
if cert is None or key is None:
|
||||
raise ValueError("SSLContext or certificate and key required.")
|
||||
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(cert, keyfile=key)
|
||||
ssl = context
|
||||
if stop_event is not None:
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Passing a loop will be deprecated in version"
|
||||
" 0.4.0 https://github.com/channelcat/sanic/"
|
||||
"pull/335 has more information.",
|
||||
DeprecationWarning)
|
||||
|
||||
# Deprecate this
|
||||
if any(arg is not None for arg in (after_stop, after_start,
|
||||
before_start, before_stop)):
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Passing a before_start, before_stop, after_start or"
|
||||
"after_stop callback will be deprecated in next "
|
||||
"major version after 0.4.0",
|
||||
warnings.warn("stop_event will be removed from future versions.",
|
||||
DeprecationWarning)
|
||||
|
||||
self.error_handler.debug = debug
|
||||
@@ -538,36 +650,35 @@ class Sanic:
|
||||
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
'request_class': self.request_class,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'sock': sock,
|
||||
'ssl': ssl,
|
||||
'signal': Signal(),
|
||||
'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,
|
||||
'keep_alive': self.config.KEEP_ALIVE,
|
||||
'loop': loop,
|
||||
'register_sys_signals': register_sys_signals,
|
||||
'backlog': backlog
|
||||
'backlog': backlog,
|
||||
'has_log': has_log
|
||||
}
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Register start/stop events
|
||||
# -------------------------------------------- #
|
||||
|
||||
for event_name, settings_name, reverse, args in (
|
||||
("before_server_start", "before_start", False, before_start),
|
||||
("after_server_start", "after_start", False, after_start),
|
||||
("before_server_stop", "before_stop", True, before_stop),
|
||||
("after_server_stop", "after_stop", True, after_stop),
|
||||
for event_name, settings_name, reverse in (
|
||||
("before_server_start", "before_start", False),
|
||||
("after_server_start", "after_start", False),
|
||||
("before_server_stop", "before_stop", True),
|
||||
("after_server_stop", "after_stop", True),
|
||||
):
|
||||
listeners = self.listeners[event_name].copy()
|
||||
if args:
|
||||
if callable(args):
|
||||
listeners.append(args)
|
||||
else:
|
||||
listeners.extend(args)
|
||||
if reverse:
|
||||
listeners.reverse()
|
||||
# Prepend sanic to the arguments when listeners are triggered
|
||||
@@ -583,9 +694,10 @@ class Sanic:
|
||||
server_settings['run_async'] = True
|
||||
|
||||
# Serve
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
if host and port:
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return server_settings
|
||||
|
||||
@@ -3,7 +3,9 @@ from collections import defaultdict, namedtuple
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureRoute = namedtuple('Route',
|
||||
['handler', 'uri', 'methods',
|
||||
'host', 'strict_slashes'])
|
||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
||||
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
||||
@@ -23,6 +25,7 @@ class Blueprint:
|
||||
self.host = host
|
||||
|
||||
self.routes = []
|
||||
self.websocket_routes = []
|
||||
self.exceptions = []
|
||||
self.listeners = defaultdict(list)
|
||||
self.middlewares = []
|
||||
@@ -43,7 +46,20 @@ class Blueprint:
|
||||
app.route(
|
||||
uri=uri[1:] if uri.startswith('//') else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes
|
||||
)(future.handler)
|
||||
|
||||
for future in self.websocket_routes:
|
||||
# attach the blueprint name to the handler so that it can be
|
||||
# prefixed properly in the router
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.websocket(
|
||||
uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes
|
||||
)(future.handler)
|
||||
|
||||
# Middleware
|
||||
@@ -70,19 +86,21 @@ class Blueprint:
|
||||
for listener in listeners:
|
||||
app.listener(event)(listener)
|
||||
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False):
|
||||
"""Create a blueprint route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
"""
|
||||
def decorator(handler):
|
||||
route = FutureRoute(handler, uri, methods, host)
|
||||
route = FutureRoute(handler, uri, methods, host, strict_slashes)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False):
|
||||
"""Create a blueprint route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
@@ -103,7 +121,30 @@ class Blueprint:
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes)(handler)
|
||||
return handler
|
||||
|
||||
def websocket(self, uri, host=None, strict_slashes=False):
|
||||
"""Create a blueprint websocket route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
"""
|
||||
def decorator(handler):
|
||||
route = FutureRoute(handler, uri, [], host, strict_slashes)
|
||||
self.websocket_routes.append(route)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None):
|
||||
"""Create a blueprint websocket route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:return: function or class instance
|
||||
"""
|
||||
self.websocket(uri=uri, host=host)(handler)
|
||||
return handler
|
||||
|
||||
def listener(self, event):
|
||||
@@ -149,23 +190,30 @@ class Blueprint:
|
||||
self.statics.append(static)
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None):
|
||||
return self.route(uri, methods=["GET"], host=host)
|
||||
def get(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=["GET"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def post(self, uri, host=None):
|
||||
return self.route(uri, methods=["POST"], host=host)
|
||||
def post(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=["POST"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def put(self, uri, host=None):
|
||||
return self.route(uri, methods=["PUT"], host=host)
|
||||
def put(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=["PUT"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def head(self, uri, host=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host)
|
||||
def head(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=["HEAD"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def options(self, uri, host=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host)
|
||||
def options(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def patch(self, uri, host=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host)
|
||||
def patch(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=["PATCH"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
def delete(self, uri, host=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host)
|
||||
def delete(self, uri, host=None, strict_slashes=False):
|
||||
return self.route(uri, methods=["DELETE"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
|
||||
129
sanic/config.py
129
sanic/config.py
@@ -1,9 +1,122 @@
|
||||
from sanic.defaultFilter import DefaultFilter
|
||||
import os
|
||||
import sys
|
||||
import syslog
|
||||
import platform
|
||||
import types
|
||||
|
||||
SANIC_PREFIX = 'SANIC_'
|
||||
|
||||
_address_dict = {
|
||||
'Windows': ('localhost', 514),
|
||||
'Darwin': '/var/run/syslog',
|
||||
'Linux': '/dev/log',
|
||||
'FreeBSD': '/dev/log'
|
||||
}
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'filters': {
|
||||
'accessFilter': {
|
||||
'()': DefaultFilter,
|
||||
'param': [0, 10, 20]
|
||||
},
|
||||
'errorFilter': {
|
||||
'()': DefaultFilter,
|
||||
'param': [30, 40, 50]
|
||||
}
|
||||
},
|
||||
'formatters': {
|
||||
'simple': {
|
||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
},
|
||||
'access': {
|
||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: ' +
|
||||
'%(request)s %(message)s %(status)d %(byte)d',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'internal': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'simple',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
'accessStream': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'access',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
'errorStream': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['errorFilter'],
|
||||
'formatter': 'simple',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
# before you use accessSysLog, be sure that log levels
|
||||
# 0, 10, 20 have been enabled in you syslog configuration
|
||||
# otherwise you won't be able to see the output in syslog
|
||||
# logging file.
|
||||
'accessSysLog': {
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': _address_dict.get(platform.system(),
|
||||
('localhost', 514)),
|
||||
'facility': syslog.LOG_DAEMON,
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'access'
|
||||
},
|
||||
'errorSysLog': {
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': _address_dict.get(platform.system(),
|
||||
('localhost', 514)),
|
||||
'facility': syslog.LOG_DAEMON,
|
||||
'filters': ['errorFilter'],
|
||||
'formatter': 'simple'
|
||||
},
|
||||
'accessTimedRotatingFile': {
|
||||
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'access',
|
||||
'when': 'D',
|
||||
'interval': 1,
|
||||
'backupCount': 7,
|
||||
'filename': 'access.log'
|
||||
},
|
||||
'errorTimedRotatingFile': {
|
||||
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||
'filters': ['errorFilter'],
|
||||
'when': 'D',
|
||||
'interval': 1,
|
||||
'backupCount': 7,
|
||||
'filename': 'error.log',
|
||||
'formatter': 'simple'
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'sanic': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['internal', 'errorStream']
|
||||
},
|
||||
'network': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['accessStream', 'errorStream']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# this happens when using container or systems without syslog
|
||||
# keep things in config would cause file not exists error
|
||||
_addr = LOGGING['handlers']['accessSysLog']['address']
|
||||
if type(_addr) is str and not os.path.exists(_addr):
|
||||
LOGGING['handlers'].pop('accessSysLog')
|
||||
LOGGING['handlers'].pop('errorSysLog')
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None):
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
||||
super().__init__(defaults or {})
|
||||
self.LOGO = """
|
||||
▄▄▄▄▄
|
||||
@@ -28,6 +141,10 @@ class Config(dict):
|
||||
"""
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
|
||||
if load_env:
|
||||
self.load_environment_vars()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
@@ -90,3 +207,13 @@ class Config(dict):
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
|
||||
def load_environment_vars(self):
|
||||
"""
|
||||
Looks for any SANIC_ prefixed environment variables and applies
|
||||
them to the configuration if present.
|
||||
"""
|
||||
for k, v in os.environ.items():
|
||||
if k.startswith(SANIC_PREFIX):
|
||||
_, config_key = k.split(SANIC_PREFIX, 1)
|
||||
self[config_key] = v
|
||||
|
||||
@@ -19,7 +19,7 @@ _Translator.update({
|
||||
|
||||
|
||||
def _quote(str):
|
||||
r"""Quote a string for use in a cookie header.
|
||||
"""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.
|
||||
|
||||
13
sanic/defaultFilter.py
Normal file
13
sanic/defaultFilter.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import logging
|
||||
|
||||
|
||||
class DefaultFilter(logging.Filter):
|
||||
def __init__(self, param=None):
|
||||
self.param = param
|
||||
|
||||
def filter(self, record):
|
||||
if self.param is None:
|
||||
return True
|
||||
if record.levelno in self.param:
|
||||
return True
|
||||
return False
|
||||
@@ -47,6 +47,10 @@ TRACEBACK_STYLE = '''
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.tb-border {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.frame-descriptor {
|
||||
background-color: #e2eafb;
|
||||
}
|
||||
@@ -63,20 +67,35 @@ TRACEBACK_WRAPPER_HTML = '''
|
||||
{style}
|
||||
</head>
|
||||
<body>
|
||||
<h1>{exc_name}</h1>
|
||||
<h3><code>{exc_value}</code></h3>
|
||||
<div class="tb-wrapper">
|
||||
<p class="tb-header">Traceback (most recent call last):</p>
|
||||
{frame_html}
|
||||
<p class="summary">
|
||||
{inner_html}
|
||||
<div class="summary">
|
||||
<p>
|
||||
<b>{exc_name}: {exc_value}</b>
|
||||
while handling uri <code>{uri}</code>
|
||||
while handling path <code>{path}</code>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
TRACEBACK_WRAPPER_INNER_HTML = '''
|
||||
<h1>{exc_name}</h1>
|
||||
<h3><code>{exc_value}</code></h3>
|
||||
<div class="tb-wrapper">
|
||||
<p class="tb-header">Traceback (most recent call last):</p>
|
||||
{frame_html}
|
||||
</div>
|
||||
'''
|
||||
|
||||
TRACEBACK_BORDER = '''
|
||||
<div class="tb-border">
|
||||
<b><i>
|
||||
The above exception was the direct cause of the
|
||||
following exception:
|
||||
</i></b>
|
||||
</div>
|
||||
'''
|
||||
|
||||
TRACEBACK_LINE_HTML = '''
|
||||
<div class="frame-line">
|
||||
<p class="frame-descriptor">
|
||||
|
||||
@@ -9,35 +9,63 @@ from sanic.exceptions import (
|
||||
SanicException,
|
||||
TRACEBACK_LINE_HTML,
|
||||
TRACEBACK_STYLE,
|
||||
TRACEBACK_WRAPPER_HTML)
|
||||
TRACEBACK_WRAPPER_HTML,
|
||||
TRACEBACK_WRAPPER_INNER_HTML,
|
||||
TRACEBACK_BORDER)
|
||||
from sanic.log import log
|
||||
from sanic.response import text, html
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
handlers = None
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
self.handlers = []
|
||||
self.cached_handlers = {}
|
||||
self.debug = False
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
frames = extract_tb(tb)
|
||||
def _render_exception(self, exception):
|
||||
frames = extract_tb(exception.__traceback__)
|
||||
|
||||
frame_html = []
|
||||
for frame in frames:
|
||||
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
|
||||
|
||||
return TRACEBACK_WRAPPER_INNER_HTML.format(
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
frame_html=''.join(frame_html))
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
exceptions = []
|
||||
|
||||
while exc_value:
|
||||
exceptions.append(self._render_exception(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
return TRACEBACK_WRAPPER_HTML.format(
|
||||
style=TRACEBACK_STYLE,
|
||||
exc_name=exc_type.__name__,
|
||||
exc_value=exc_value,
|
||||
frame_html=''.join(frame_html),
|
||||
uri=request.url)
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
|
||||
path=request.path)
|
||||
|
||||
def add(self, exception, handler):
|
||||
self.handlers[exception] = handler
|
||||
self.handlers.append((exception, handler))
|
||||
|
||||
def lookup(self, exception):
|
||||
handler = self.cached_handlers.get(exception, self._missing)
|
||||
if handler is self._missing:
|
||||
for exception_class, handler in self.handlers:
|
||||
if isinstance(exception, exception_class):
|
||||
self.cached_handlers[type(exception)] = handler
|
||||
return handler
|
||||
self.cached_handlers[type(exception)] = None
|
||||
handler = None
|
||||
return handler
|
||||
|
||||
def response(self, request, exception):
|
||||
"""Fetches and executes an exception handler and returns a response
|
||||
@@ -47,9 +75,13 @@ class ErrorHandler:
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.handlers.get(type(exception), self.default)
|
||||
handler = self.lookup(exception)
|
||||
response = None
|
||||
try:
|
||||
response = handler(request=request, exception=exception)
|
||||
if handler:
|
||||
response = handler(request=request, exception=exception)
|
||||
if response is None:
|
||||
response = self.default(request=request, exception=exception)
|
||||
except Exception:
|
||||
self.log(format_exc())
|
||||
if self.debug:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger('sanic')
|
||||
netlog = logging.getLogger('network')
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 urllib.parse import parse_qs, urlunparse
|
||||
|
||||
try:
|
||||
from ujson import loads as json_loads
|
||||
@@ -36,24 +36,20 @@ class RequestParameters(dict):
|
||||
class Request(dict):
|
||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||
__slots__ = (
|
||||
'app', 'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'query_string', 'body',
|
||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip',
|
||||
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip', '_parsed_url', 'uri_template'
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
# TODO: Content-Encoding detection
|
||||
url_parsed = parse_url(url_bytes)
|
||||
self._parsed_url = parse_url(url_bytes)
|
||||
self.app = None
|
||||
self.url = url_parsed.path.decode('utf-8')
|
||||
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
self.method = method
|
||||
self.transport = transport
|
||||
self.query_string = None
|
||||
if url_parsed.query:
|
||||
self.query_string = url_parsed.query.decode('utf-8')
|
||||
|
||||
# Init but do not inhale
|
||||
self.body = []
|
||||
@@ -61,6 +57,7 @@ class Request(dict):
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
self.parsed_args = None
|
||||
self.uri_template = None
|
||||
self._cookies = None
|
||||
|
||||
@property
|
||||
@@ -69,6 +66,8 @@ class Request(dict):
|
||||
try:
|
||||
self.parsed_json = json_loads(self.body)
|
||||
except Exception:
|
||||
if not self.body:
|
||||
return None
|
||||
raise InvalidUsage("Failed when parsing body as json")
|
||||
|
||||
return self.parsed_json
|
||||
@@ -80,9 +79,10 @@ class Request(dict):
|
||||
: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
|
||||
if 'Token ' in auth_header:
|
||||
return auth_header.partition('Token ')[-1]
|
||||
else:
|
||||
return auth_header
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
@@ -123,6 +123,10 @@ class Request(dict):
|
||||
self.parsed_args = RequestParameters()
|
||||
return self.parsed_args
|
||||
|
||||
@property
|
||||
def raw_args(self):
|
||||
return {k: v[0] for k, v in self.args.items()}
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
@@ -142,6 +146,46 @@ class Request(dict):
|
||||
self._ip = self.transport.get_extra_info('peername')
|
||||
return self._ip
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
if self.app.websocket_enabled \
|
||||
and self.headers.get('upgrade') == 'websocket':
|
||||
scheme = 'ws'
|
||||
else:
|
||||
scheme = 'http'
|
||||
|
||||
if self.transport.get_extra_info('sslcontext'):
|
||||
scheme += 's'
|
||||
|
||||
return scheme
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
# it appears that httptools doesn't return the host
|
||||
# so pull it from the headers
|
||||
return self.headers.get('Host', '')
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._parsed_url.path.decode('utf-8')
|
||||
|
||||
@property
|
||||
def query_string(self):
|
||||
if self._parsed_url.query:
|
||||
return self._parsed_url.query.decode('utf-8')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return urlunparse((
|
||||
self.scheme,
|
||||
self.host,
|
||||
self.path,
|
||||
None,
|
||||
self.query_string,
|
||||
None))
|
||||
|
||||
|
||||
File = namedtuple('File', ['type', 'body', 'name'])
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from ujson import dumps as json_dumps
|
||||
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except:
|
||||
from json import dumps as json_dumps
|
||||
|
||||
from aiofiles import open as open_async
|
||||
|
||||
@@ -73,37 +77,16 @@ ALL_STATUS_CODES = {
|
||||
}
|
||||
|
||||
|
||||
class HTTPResponse:
|
||||
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||
class BaseHTTPResponse:
|
||||
def _encode_body(self, data):
|
||||
try:
|
||||
# Try to encode it regularly
|
||||
return data.encode()
|
||||
except AttributeError:
|
||||
# Convert it to a str if you can't
|
||||
return str(data).encode()
|
||||
|
||||
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:
|
||||
try:
|
||||
# Try to encode it regularly
|
||||
self.body = body.encode()
|
||||
except AttributeError:
|
||||
# Convert it to a str if you can't
|
||||
self.body = str(body).encode()
|
||||
else:
|
||||
self.body = body_bytes
|
||||
|
||||
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
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
if keep_alive and keep_alive_timeout is not None:
|
||||
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
|
||||
self.headers['Content-Length'] = self.headers.get(
|
||||
'Content-Length', len(self.body))
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
def _parse_headers(self):
|
||||
headers = b''
|
||||
for name, value in self.headers.items():
|
||||
try:
|
||||
@@ -115,25 +98,133 @@ class HTTPResponse:
|
||||
b'%b: %b\r\n' % (
|
||||
str(name).encode(), str(value).encode('utf-8')))
|
||||
|
||||
return headers
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
self._cookies = CookieJar(self.headers)
|
||||
return self._cookies
|
||||
|
||||
|
||||
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = (
|
||||
'transport', 'streaming_fn',
|
||||
'status', 'content_type', 'headers', '_cookies')
|
||||
|
||||
def __init__(self, streaming_fn, status=200, headers=None,
|
||||
content_type='text/plain'):
|
||||
self.content_type = content_type
|
||||
self.streaming_fn = streaming_fn
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self._cookies = None
|
||||
|
||||
def write(self, data):
|
||||
"""Writes a chunk of data to the streaming response.
|
||||
|
||||
:param data: bytes-ish data to be written.
|
||||
"""
|
||||
if type(data) != bytes:
|
||||
data = self._encode_body(data)
|
||||
|
||||
self.transport.write(
|
||||
b"%x\r\n%b\r\n" % (len(data), data))
|
||||
|
||||
async def stream(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
"""Streams headers, runs the `streaming_fn` callback that writes
|
||||
content to the response body, then finalizes the response body.
|
||||
"""
|
||||
headers = self.get_headers(
|
||||
version, keep_alive=keep_alive,
|
||||
keep_alive_timeout=keep_alive_timeout)
|
||||
self.transport.write(headers)
|
||||
|
||||
await self.streaming_fn(self)
|
||||
self.transport.write(b'0\r\n\r\n')
|
||||
|
||||
def get_headers(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
# This is all returned in a kind-of funky way
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
if keep_alive and keep_alive_timeout is not None:
|
||||
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
|
||||
|
||||
self.headers['Transfer-Encoding'] = 'chunked'
|
||||
self.headers.pop('Content-Length', None)
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
|
||||
headers = self._parse_headers()
|
||||
|
||||
# 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'%b'
|
||||
b'%b\r\n') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
timeout_header,
|
||||
headers
|
||||
)
|
||||
|
||||
|
||||
class HTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||
|
||||
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 = self._encode_body(body)
|
||||
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
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
if keep_alive and keep_alive_timeout is not None:
|
||||
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
|
||||
self.headers['Content-Length'] = self.headers.get(
|
||||
'Content-Length', len(self.body))
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
|
||||
headers = self._parse_headers()
|
||||
|
||||
# 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, b'UNKNOWN RESPONSE')
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'Connection: %b\r\n'
|
||||
b'%b'
|
||||
b'%b\r\n'
|
||||
b'%b') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
b'keep-alive' if keep_alive else b'close',
|
||||
timeout_header,
|
||||
headers,
|
||||
self.body
|
||||
)
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
b'keep-alive' if keep_alive else b'close',
|
||||
timeout_header,
|
||||
headers,
|
||||
self.body
|
||||
)
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
@@ -161,11 +252,11 @@ def text(body, status=200, headers=None,
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param content_type:
|
||||
the content type (string) of the response
|
||||
:param content_type: the content type (string) of the response
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
return HTTPResponse(
|
||||
body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
|
||||
|
||||
def raw(body, status=200, headers=None,
|
||||
@@ -175,8 +266,7 @@ def raw(body, status=200, headers=None,
|
||||
:param body: Response data.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param content_type:
|
||||
the content type (string) of the response
|
||||
:param content_type: the content type (string) of the response.
|
||||
"""
|
||||
return HTTPResponse(body_bytes=body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
@@ -220,6 +310,35 @@ async def file(location, mime_type=None, headers=None, _range=None):
|
||||
body_bytes=out_stream)
|
||||
|
||||
|
||||
def stream(
|
||||
streaming_fn, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||
|
||||
Example usage::
|
||||
|
||||
@app.route("/")
|
||||
async def index(request):
|
||||
async def streaming_fn(response):
|
||||
await response.write('foo')
|
||||
await response.write('bar')
|
||||
|
||||
return stream(streaming_fn, content_type='text/plain')
|
||||
|
||||
:param streaming_fn: A coroutine accepts a response and
|
||||
writes content to that response.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return StreamingHTTPResponse(
|
||||
streaming_fn,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
status=status
|
||||
)
|
||||
|
||||
|
||||
def redirect(to, headers=None, status=302,
|
||||
content_type="text/html; charset=utf-8"):
|
||||
"""Abort execution and cause a 302 redirect (by default).
|
||||
|
||||
@@ -8,7 +8,7 @@ from sanic.views import CompositionView
|
||||
|
||||
Route = namedtuple(
|
||||
'Route',
|
||||
['handler', 'methods', 'pattern', 'parameters', 'name'])
|
||||
['handler', 'methods', 'pattern', 'parameters', 'name', 'uri'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
|
||||
REGEX_TYPES = {
|
||||
@@ -16,6 +16,7 @@ REGEX_TYPES = {
|
||||
'int': (int, r'\d+'),
|
||||
'number': (float, r'[0-9\\.]+'),
|
||||
'alpha': (str, r'[A-Za-z]+'),
|
||||
'path': (str, r'[^/].*?'),
|
||||
}
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
@@ -71,13 +72,15 @@ class Router:
|
||||
self.routes_always_check = []
|
||||
self.hosts = set()
|
||||
|
||||
def parse_parameter_string(self, parameter_string):
|
||||
@classmethod
|
||||
def parse_parameter_string(cls, parameter_string):
|
||||
"""Parse a parameter string into its constituent name, type, and
|
||||
pattern
|
||||
|
||||
For example:
|
||||
`parse_parameter_string('<param_one:[A-z]>')` ->
|
||||
('param_one', str, '[A-z]')
|
||||
For example::
|
||||
|
||||
parse_parameter_string('<param_one:[A-z]>')` ->
|
||||
('param_one', str, '[A-z]')
|
||||
|
||||
:param parameter_string: String to parse
|
||||
:return: tuple containing
|
||||
@@ -95,13 +98,24 @@ class Router:
|
||||
|
||||
return name, _type, pattern
|
||||
|
||||
def add(self, uri, methods, handler, host=None):
|
||||
def add(self, uri, methods, handler, host=None, strict_slashes=False):
|
||||
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host)
|
||||
slash_is_missing = (not uri[-1].endswith('/')
|
||||
and not self.routes_all.get(uri + '/', False))
|
||||
without_slash_is_missing = (not self.routes_all.get(uri[:-1], False)
|
||||
and uri is not '/')
|
||||
|
||||
if strict_slashes:
|
||||
return
|
||||
|
||||
# Add versions with and without trailing /
|
||||
slash_is_missing = (
|
||||
not uri[-1] == '/'
|
||||
and not self.routes_all.get(uri + '/', False)
|
||||
)
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == '/'
|
||||
and not self.routes_all.get(uri[:-1], False)
|
||||
and not uri == '/'
|
||||
)
|
||||
# add version with trailing slash
|
||||
if slash_is_missing:
|
||||
self._add(uri + '/', methods, handler, host)
|
||||
@@ -132,9 +146,6 @@ class Router:
|
||||
for host_ in host:
|
||||
self.add(uri, methods, handler, host_)
|
||||
return
|
||||
else:
|
||||
# default host
|
||||
self.hosts.add('*')
|
||||
|
||||
# Dict for faster lookups of if method allowed
|
||||
if methods:
|
||||
@@ -152,10 +163,10 @@ class Router:
|
||||
parameters.append(parameter)
|
||||
|
||||
# Mark the whole route as unhashable if it has the hash key in it
|
||||
if re.search('(^|[^^]){1}/', pattern):
|
||||
if re.search(r'(^|[^^]){1}/', pattern):
|
||||
properties['unhashable'] = True
|
||||
# Mark the route as unhashable if it matches the hash key
|
||||
elif re.search(pattern, '/'):
|
||||
elif re.search(r'/', pattern):
|
||||
properties['unhashable'] = True
|
||||
|
||||
return '({})'.format(pattern)
|
||||
@@ -214,7 +225,7 @@ class Router:
|
||||
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters, name=handler_name)
|
||||
parameters=parameters, name=handler_name, uri=uri)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if properties['unhashable']:
|
||||
@@ -276,14 +287,14 @@ class Router:
|
||||
"""
|
||||
# No virtual hosts specified; default behavior
|
||||
if not self.hosts:
|
||||
return self._get(request.url, request.method, '')
|
||||
return self._get(request.path, request.method, '')
|
||||
# virtual hosts specified; try to match route to the host header
|
||||
try:
|
||||
return self._get(request.url, request.method,
|
||||
return self._get(request.path, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
# try default hosts
|
||||
except NotFound:
|
||||
return self._get(request.url, request.method, '')
|
||||
return self._get(request.path, request.method, '')
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(self, url, method, host):
|
||||
@@ -333,4 +344,4 @@ class Router:
|
||||
route_handler = route.handler
|
||||
if hasattr(route_handler, 'handlers'):
|
||||
route_handler = route_handler.handlers[method]
|
||||
return route_handler, [], kwargs
|
||||
return route_handler, [], kwargs, route.uri
|
||||
|
||||
175
sanic/server.py
175
sanic/server.py
@@ -1,14 +1,19 @@
|
||||
import asyncio
|
||||
import os
|
||||
import traceback
|
||||
import warnings
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from multiprocessing import Process, Event
|
||||
from os import set_inheritable
|
||||
from signal import SIGTERM, SIGINT
|
||||
from signal import signal as signal_func
|
||||
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||
from multiprocessing import Process
|
||||
from signal import (
|
||||
SIGTERM, SIGINT,
|
||||
signal as signal_func,
|
||||
Signals
|
||||
)
|
||||
from socket import (
|
||||
socket,
|
||||
SOL_SOCKET,
|
||||
SO_REUSEADDR,
|
||||
)
|
||||
from time import time
|
||||
|
||||
from httptools import HttpRequestParser
|
||||
@@ -19,7 +24,8 @@ try:
|
||||
except ImportError:
|
||||
async_loop = asyncio
|
||||
|
||||
from sanic.log import log
|
||||
from sanic.log import log, netlog
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.request import Request
|
||||
from sanic.exceptions import (
|
||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
|
||||
@@ -58,12 +64,16 @@ class HttpProtocol(asyncio.Protocol):
|
||||
'parser', 'request', 'url', 'headers',
|
||||
# request config
|
||||
'request_handler', 'request_timeout', 'request_max_size',
|
||||
'request_class',
|
||||
# enable or disable access log / error log purpose
|
||||
'has_log',
|
||||
# connection management
|
||||
'_total_request_size', '_timeout_handler', '_last_communication_time')
|
||||
|
||||
def __init__(self, *, loop, request_handler, error_handler,
|
||||
signal=Signal(), connections=set(), request_timeout=60,
|
||||
request_max_size=None):
|
||||
request_max_size=None, request_class=None, has_log=True,
|
||||
keep_alive=True):
|
||||
self.loop = loop
|
||||
self.transport = None
|
||||
self.request = None
|
||||
@@ -71,15 +81,24 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.url = None
|
||||
self.headers = None
|
||||
self.signal = signal
|
||||
self.has_log = has_log
|
||||
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.request_class = request_class or Request
|
||||
self._total_request_size = 0
|
||||
self._timeout_handler = None
|
||||
self._last_request_time = None
|
||||
self._request_handler_task = None
|
||||
self._keep_alive = keep_alive
|
||||
|
||||
@property
|
||||
def keep_alive(self):
|
||||
return (self._keep_alive
|
||||
and not self.signal.stopped
|
||||
and self.parser.should_keep_alive())
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Connection
|
||||
@@ -145,7 +164,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.headers.append((name.decode().casefold(), value.decode()))
|
||||
|
||||
def on_headers_complete(self):
|
||||
self.request = Request(
|
||||
self.request = self.request_class(
|
||||
url_bytes=self.url,
|
||||
headers=CIDict(self.headers),
|
||||
version=self.parser.get_http_version(),
|
||||
@@ -157,22 +176,76 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.request.body.append(body)
|
||||
|
||||
def on_message_complete(self):
|
||||
if self.request.body:
|
||||
self.request.body = b''.join(self.request.body)
|
||||
self.request.body = b''.join(self.request.body)
|
||||
|
||||
self._request_handler_task = self.loop.create_task(
|
||||
self.request_handler(self.request, self.write_response))
|
||||
self.request_handler(
|
||||
self.request,
|
||||
self.write_response,
|
||||
self.stream_response))
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Responding
|
||||
# -------------------------------------------- #
|
||||
|
||||
def write_response(self, response):
|
||||
keep_alive = (
|
||||
self.parser.should_keep_alive() and not self.signal.stopped)
|
||||
"""
|
||||
Writes response content synchronously to the transport.
|
||||
"""
|
||||
try:
|
||||
keep_alive = self.keep_alive
|
||||
self.transport.write(
|
||||
response.output(
|
||||
self.request.version, keep_alive, self.request_timeout))
|
||||
self.request.version, keep_alive,
|
||||
self.request_timeout))
|
||||
if self.has_log:
|
||||
netlog.info('', extra={
|
||||
'status': response.status,
|
||||
'byte': len(response.body),
|
||||
'host': '%s:%d' % self.request.ip,
|
||||
'request': '%s %s' % (self.request.method,
|
||||
self.request.url)
|
||||
})
|
||||
except AttributeError:
|
||||
log.error(
|
||||
('Invalid response object for url {}, '
|
||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
||||
self.url, type(response)))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before response written @ {}'.format(
|
||||
self.request.ip))
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
repr(e)))
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
else:
|
||||
self._last_request_time = current_time
|
||||
self.cleanup()
|
||||
|
||||
async def stream_response(self, response):
|
||||
"""
|
||||
Streams a response to the client asynchronously. Attaches
|
||||
the transport to the response so the response consumer can
|
||||
write to the response as needed.
|
||||
"""
|
||||
|
||||
try:
|
||||
keep_alive = self.keep_alive
|
||||
response.transport = self.transport
|
||||
await response.stream(
|
||||
self.request.version, keep_alive, self.request_timeout)
|
||||
if self.has_log:
|
||||
netlog.info('', extra={
|
||||
'status': response.status,
|
||||
'byte': -1,
|
||||
'host': '%s:%d' % self.request.ip,
|
||||
'request': '%s %s' % (self.request.method,
|
||||
self.request.url)
|
||||
})
|
||||
except AttributeError:
|
||||
log.error(
|
||||
('Invalid response object for url {}, '
|
||||
@@ -191,7 +264,6 @@ class HttpProtocol(asyncio.Protocol):
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
else:
|
||||
# Record that we received data
|
||||
self._last_request_time = current_time
|
||||
self.cleanup()
|
||||
|
||||
@@ -209,10 +281,25 @@ class HttpProtocol(asyncio.Protocol):
|
||||
"Writing error failed, connection closed {}".format(repr(e)),
|
||||
from_error=True)
|
||||
finally:
|
||||
if self.has_log:
|
||||
extra = {
|
||||
'status': response.status,
|
||||
'host': '',
|
||||
'request': str(self.request) + str(self.url)
|
||||
}
|
||||
if response and isinstance(response, HTTPResponse):
|
||||
extra['byte'] = len(response.body)
|
||||
else:
|
||||
extra['byte'] = -1
|
||||
if self.request:
|
||||
extra['host'] = '%s:%d' % self.request.ip,
|
||||
extra['request'] = '%s %s' % (self.request.method,
|
||||
self.url)
|
||||
netlog.info('', extra=extra)
|
||||
self.transport.close()
|
||||
|
||||
def bail_out(self, message, from_error=False):
|
||||
if from_error and self.transport.is_closing():
|
||||
if from_error or self.transport.is_closing():
|
||||
log.error(
|
||||
("Transport closed @ {} and exception "
|
||||
"experienced during error handling").format(
|
||||
@@ -271,7 +358,8 @@ 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, ssl=None, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
register_sys_signals=True, run_async=False):
|
||||
register_sys_signals=True, run_async=False, connections=None,
|
||||
signal=Signal(), request_class=None, has_log=True, keep_alive=True):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
@@ -287,7 +375,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
`app` instance and `loop`
|
||||
:param after_stop: function to be executed when a stop signal is
|
||||
received after it is respected. Takes arguments
|
||||
`app` instance and `loop`
|
||||
`app` instance and `loop`
|
||||
:param debug: enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
:param ssl: SSLContext
|
||||
@@ -296,6 +384,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
:param reuse_port: `True` for multiple workers
|
||||
:param loop: asyncio compatible event loop
|
||||
:param protocol: subclass of asyncio protocol class
|
||||
:param request_class: Request class to use
|
||||
:param has_log: disable/enable access log and error log
|
||||
:return: Nothing
|
||||
"""
|
||||
if not run_async:
|
||||
@@ -307,8 +397,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
|
||||
trigger_events(before_start, loop)
|
||||
|
||||
connections = set()
|
||||
signal = Signal()
|
||||
connections = connections if connections is not None else set()
|
||||
server = partial(
|
||||
protocol,
|
||||
loop=loop,
|
||||
@@ -318,6 +407,9 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
error_handler=error_handler,
|
||||
request_timeout=request_timeout,
|
||||
request_max_size=request_max_size,
|
||||
request_class=request_class,
|
||||
has_log=has_log,
|
||||
keep_alive=keep_alive,
|
||||
)
|
||||
|
||||
server_coroutine = loop.create_server(
|
||||
@@ -379,7 +471,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
loop.close()
|
||||
|
||||
|
||||
def serve_multiple(server_settings, workers, stop_event=None):
|
||||
def serve_multiple(server_settings, workers):
|
||||
"""Start multiple server processes simultaneously. Stop on interrupt
|
||||
and terminate signals, and drain connections when complete.
|
||||
|
||||
@@ -388,27 +480,26 @@ def serve_multiple(server_settings, workers, stop_event=None):
|
||||
:param stop_event: if provided, is used as a stop signal
|
||||
:return:
|
||||
"""
|
||||
if server_settings.get('loop', None) is not None:
|
||||
if server_settings.get('debug', False):
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Passing a loop will be deprecated in version 0.4.0"
|
||||
" https://github.com/channelcat/sanic/pull/335"
|
||||
" has more information.", DeprecationWarning)
|
||||
server_settings['reuse_port'] = True
|
||||
|
||||
sock = socket()
|
||||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
sock.bind((server_settings['host'], server_settings['port']))
|
||||
set_inheritable(sock.fileno(), True)
|
||||
server_settings['sock'] = sock
|
||||
server_settings['host'] = None
|
||||
server_settings['port'] = None
|
||||
# Handling when custom socket is not provided.
|
||||
if server_settings.get('sock') is None:
|
||||
sock = socket()
|
||||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
sock.bind((server_settings['host'], server_settings['port']))
|
||||
sock.set_inheritable(True)
|
||||
server_settings['sock'] = sock
|
||||
server_settings['host'] = None
|
||||
server_settings['port'] = None
|
||||
|
||||
if stop_event is None:
|
||||
stop_event = Event()
|
||||
def sig_handler(signal, frame):
|
||||
log.info("Received signal {}. Shutting down.".format(
|
||||
Signals(signal).name))
|
||||
for process in processes:
|
||||
os.kill(process.pid, SIGINT)
|
||||
|
||||
signal_func(SIGINT, lambda s, f: stop_event.set())
|
||||
signal_func(SIGTERM, lambda s, f: stop_event.set())
|
||||
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
|
||||
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
|
||||
|
||||
processes = []
|
||||
for _ in range(workers):
|
||||
@@ -423,6 +514,4 @@ def serve_multiple(server_settings, workers, stop_event=None):
|
||||
# the above processes will block this until they're stopped
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
sock.close()
|
||||
|
||||
asyncio.get_event_loop().stop()
|
||||
server_settings.get('sock').close()
|
||||
|
||||
@@ -48,14 +48,18 @@ def register(app, uri, file_or_directory, pattern,
|
||||
# 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
|
||||
root_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)
|
||||
file_path = path.abspath(unquote(file_path))
|
||||
if not file_path.startswith(path.abspath(unquote(root_path))):
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
try:
|
||||
headers = {}
|
||||
# Check if the client has been sent this file before
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import traceback
|
||||
|
||||
from sanic.log import log
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 42101
|
||||
|
||||
|
||||
class TestClient:
|
||||
class SanicTestClient:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
@@ -17,10 +19,15 @@ class TestClient:
|
||||
host=HOST, port=PORT, uri=uri)
|
||||
|
||||
log.info(url)
|
||||
async with aiohttp.ClientSession(cookies=cookies) as session:
|
||||
conn = aiohttp.TCPConnector(verify_ssl=False)
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, connector=conn) as session:
|
||||
async with getattr(
|
||||
session, method.lower())(url, *args, **kwargs) as response:
|
||||
response.text = await response.text()
|
||||
try:
|
||||
response.text = await response.text()
|
||||
except UnicodeDecodeError as e:
|
||||
response.text = None
|
||||
response.body = await response.read()
|
||||
return response
|
||||
|
||||
@@ -45,6 +52,8 @@ class TestClient:
|
||||
**request_kwargs)
|
||||
results[-1] = response
|
||||
except Exception as e:
|
||||
log.error(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
exceptions.append(e)
|
||||
self.app.stop()
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from sanic.testing import TestClient
|
||||
|
||||
|
||||
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
warnings.warn(
|
||||
"Use of sanic_endpoint_test will be deprecated in"
|
||||
"the next major version after 0.4.0. Please use the `test_client` "
|
||||
"available on the app object.", DeprecationWarning)
|
||||
|
||||
test_client = TestClient(app)
|
||||
return test_client._sanic_endpoint_test(
|
||||
method, uri, gather_request, debug, server_kwargs,
|
||||
*request_args, **request_kwargs)
|
||||
67
sanic/websocket.py
Normal file
67
sanic/websocket.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.server import HttpProtocol
|
||||
from httptools import HttpParserUpgrade
|
||||
from websockets import handshake, WebSocketCommonProtocol, InvalidHandshake
|
||||
from websockets import ConnectionClosed # noqa
|
||||
|
||||
|
||||
class WebSocketProtocol(HttpProtocol):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.websocket = None
|
||||
|
||||
def connection_timeout(self):
|
||||
# timeouts make no sense for websocket routes
|
||||
if self.websocket is None:
|
||||
super().connection_timeout()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if self.websocket is not None:
|
||||
self.websocket.connection_lost(exc)
|
||||
super().connection_lost(exc)
|
||||
|
||||
def data_received(self, data):
|
||||
if self.websocket is not None:
|
||||
# pass the data to the websocket protocol
|
||||
self.websocket.data_received(data)
|
||||
else:
|
||||
try:
|
||||
super().data_received(data)
|
||||
except HttpParserUpgrade:
|
||||
# this is okay, it just indicates we've got an upgrade request
|
||||
pass
|
||||
|
||||
def write_response(self, response):
|
||||
if self.websocket is not None:
|
||||
# websocket requests do not write a response
|
||||
self.transport.close()
|
||||
else:
|
||||
super().write_response(response)
|
||||
|
||||
async def websocket_handshake(self, request):
|
||||
# let the websockets package do the handshake with the client
|
||||
headers = []
|
||||
|
||||
def get_header(k):
|
||||
return request.headers.get(k, '')
|
||||
|
||||
def set_header(k, v):
|
||||
headers.append((k, v))
|
||||
|
||||
try:
|
||||
key = handshake.check_request(get_header)
|
||||
handshake.build_response(set_header, key)
|
||||
except InvalidHandshake:
|
||||
raise InvalidUsage('Invalid websocket request')
|
||||
|
||||
# write the 101 response back to the client
|
||||
rv = b'HTTP/1.1 101 Switching Protocols\r\n'
|
||||
for k, v in headers:
|
||||
rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n'
|
||||
rv += b'\r\n'
|
||||
request.transport.write(rv)
|
||||
|
||||
# hook up the websocket protocol
|
||||
self.websocket = WebSocketCommonProtocol()
|
||||
self.websocket.connection_made(request.transport)
|
||||
return self.websocket
|
||||
166
sanic/worker.py
Normal file
166
sanic/worker.py
Normal file
@@ -0,0 +1,166 @@
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
import uvloop
|
||||
import gunicorn.workers.base as base
|
||||
|
||||
from sanic.server import trigger_events, serve, HttpProtocol, Signal
|
||||
from sanic.websocket import WebSocketProtocol
|
||||
|
||||
|
||||
class GunicornWorker(base.Worker):
|
||||
|
||||
def __init__(self, *args, **kw): # pragma: no cover
|
||||
super().__init__(*args, **kw)
|
||||
cfg = self.cfg
|
||||
if cfg.is_ssl:
|
||||
self.ssl_context = self._create_ssl_context(cfg)
|
||||
else:
|
||||
self.ssl_context = None
|
||||
self.servers = []
|
||||
self.connections = set()
|
||||
self.exit_code = 0
|
||||
self.signal = Signal()
|
||||
|
||||
def init_process(self):
|
||||
# create new event_loop after fork
|
||||
asyncio.get_event_loop().close()
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
super().init_process()
|
||||
|
||||
def run(self):
|
||||
is_debug = self.log.loglevel == logging.DEBUG
|
||||
protocol = (WebSocketProtocol if self.app.callable.websocket_enabled
|
||||
else HttpProtocol)
|
||||
self._server_settings = self.app.callable._helper(
|
||||
host=None,
|
||||
port=None,
|
||||
loop=self.loop,
|
||||
debug=is_debug,
|
||||
protocol=protocol,
|
||||
ssl=self.ssl_context,
|
||||
run_async=True)
|
||||
self._server_settings['signal'] = self.signal
|
||||
self._server_settings.pop('sock')
|
||||
trigger_events(self._server_settings.get('before_start', []),
|
||||
self.loop)
|
||||
self._server_settings['before_start'] = ()
|
||||
|
||||
self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
|
||||
try:
|
||||
self.loop.run_until_complete(self._runner)
|
||||
self.app.callable.is_running = True
|
||||
trigger_events(self._server_settings.get('after_start', []),
|
||||
self.loop)
|
||||
self.loop.run_until_complete(self._check_alive())
|
||||
trigger_events(self._server_settings.get('before_stop', []),
|
||||
self.loop)
|
||||
self.loop.run_until_complete(self.close())
|
||||
finally:
|
||||
trigger_events(self._server_settings.get('after_stop', []),
|
||||
self.loop)
|
||||
self.loop.close()
|
||||
|
||||
sys.exit(self.exit_code)
|
||||
|
||||
async def close(self):
|
||||
if self.servers:
|
||||
# stop accepting connections
|
||||
self.log.info("Stopping server: %s, connections: %s",
|
||||
self.pid, len(self.connections))
|
||||
for server in self.servers:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
self.servers.clear()
|
||||
|
||||
# prepare connections for closing
|
||||
self.signal.stopped = True
|
||||
for conn in self.connections:
|
||||
conn.close_if_idle()
|
||||
|
||||
while self.connections:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _run(self):
|
||||
for sock in self.sockets:
|
||||
self.servers.append(await serve(
|
||||
sock=sock,
|
||||
connections=self.connections,
|
||||
**self._server_settings
|
||||
))
|
||||
|
||||
async def _check_alive(self):
|
||||
# If our parent changed then we shut down.
|
||||
pid = os.getpid()
|
||||
try:
|
||||
while self.alive:
|
||||
self.notify()
|
||||
|
||||
if pid == os.getpid() and self.ppid != os.getppid():
|
||||
self.alive = False
|
||||
self.log.info("Parent changed, shutting down: %s", self)
|
||||
else:
|
||||
await asyncio.sleep(1.0, loop=self.loop)
|
||||
except (Exception, BaseException, GeneratorExit, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _create_ssl_context(cfg):
|
||||
""" Creates SSLContext instance for usage in asyncio.create_server.
|
||||
See ssl.SSLSocket.__init__ for more details.
|
||||
"""
|
||||
ctx = ssl.SSLContext(cfg.ssl_version)
|
||||
ctx.load_cert_chain(cfg.certfile, cfg.keyfile)
|
||||
ctx.verify_mode = cfg.cert_reqs
|
||||
if cfg.ca_certs:
|
||||
ctx.load_verify_locations(cfg.ca_certs)
|
||||
if cfg.ciphers:
|
||||
ctx.set_ciphers(cfg.ciphers)
|
||||
return ctx
|
||||
|
||||
def init_signals(self):
|
||||
# Set up signals through the event loop API.
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit,
|
||||
signal.SIGQUIT, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit,
|
||||
signal.SIGTERM, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGINT, self.handle_quit,
|
||||
signal.SIGINT, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch,
|
||||
signal.SIGWINCH, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1,
|
||||
signal.SIGUSR1, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort,
|
||||
signal.SIGABRT, None)
|
||||
|
||||
# Don't let SIGTERM and SIGUSR1 disturb active requests
|
||||
# by interrupting system calls
|
||||
signal.siginterrupt(signal.SIGTERM, False)
|
||||
signal.siginterrupt(signal.SIGUSR1, False)
|
||||
|
||||
def handle_quit(self, sig, frame):
|
||||
self.alive = False
|
||||
self.cfg.worker_int(self)
|
||||
|
||||
def handle_abort(self, sig, frame):
|
||||
self.alive = False
|
||||
self.exit_code = 1
|
||||
self.cfg.worker_abort(self)
|
||||
79
setup.py
79
setup.py
@@ -4,38 +4,77 @@ Sanic
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
from distutils.errors import DistutilsPlatformError
|
||||
from distutils.util import strtobool
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
|
||||
__file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp:
|
||||
def open_local(paths, mode='r', encoding='utf8'):
|
||||
path = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
*paths
|
||||
)
|
||||
|
||||
return codecs.open(path, mode, encoding)
|
||||
|
||||
|
||||
with open_local(['sanic', '__init__.py'], encoding='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=version,
|
||||
url='http://github.com/channelcat/sanic/',
|
||||
license='MIT',
|
||||
author='Channel Cat',
|
||||
author_email='channelcat@gmail.com',
|
||||
description='A microframework based on uvloop, httptools, and learnings of flask',
|
||||
packages=['sanic'],
|
||||
platforms='any',
|
||||
install_requires=[
|
||||
'uvloop>=0.5.3;platform_system!="Windows"',
|
||||
'httptools>=0.0.9',
|
||||
'ujson>=1.35',
|
||||
'aiofiles>=0.3.0',
|
||||
],
|
||||
classifiers=[
|
||||
|
||||
with open_local(['README.rst']) as rm:
|
||||
long_description = rm.read()
|
||||
|
||||
setup_kwargs = {
|
||||
'name': 'sanic',
|
||||
'version': version,
|
||||
'url': 'http://github.com/channelcat/sanic/',
|
||||
'license': 'MIT',
|
||||
'author': 'Channel Cat',
|
||||
'author_email': 'channelcat@gmail.com',
|
||||
'description': (
|
||||
'A microframework based on uvloop, httptools, and learnings of flask'),
|
||||
'long_description': long_description,
|
||||
'packages': ['sanic'],
|
||||
'platforms': 'any',
|
||||
'classifiers': [
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Environment :: Web Environment',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
ujson = 'ujson>=1.35'
|
||||
uvloop = 'uvloop>=0.5.3'
|
||||
|
||||
requirements = [
|
||||
'httptools>=0.0.9',
|
||||
uvloop,
|
||||
ujson,
|
||||
'aiofiles>=0.3.0',
|
||||
'websockets>=3.2',
|
||||
]
|
||||
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
||||
print("Installing without uJSON")
|
||||
requirements.remove(ujson)
|
||||
|
||||
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
|
||||
print("Installing without uvLoop")
|
||||
requirements.remove(uvloop)
|
||||
|
||||
try:
|
||||
setup_kwargs['install_requires'] = requirements
|
||||
setup(**setup_kwargs)
|
||||
except DistutilsPlatformError as exception:
|
||||
requirements.remove(ujson)
|
||||
requirements.remove(uvloop)
|
||||
print("Installing without uJSON or uvLoop")
|
||||
setup_kwargs['install_requires'] = requirements
|
||||
setup(**setup_kwargs)
|
||||
|
||||
22
tests/certs/selfsigned.cert
Normal file
22
tests/certs/selfsigned.cert
Normal file
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDtTCCAp2gAwIBAgIJAO6wb0FSc/rNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTcwMzAzMTUyODAzWhcNMTkxMTI4MTUyODAzWjBF
|
||||
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7mdB6Bflh
|
||||
V5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLwdpaoTGqE
|
||||
vYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49IAouBHq3
|
||||
h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ8gjaC8/1
|
||||
w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJPXtNrTwf
|
||||
qEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABo4GnMIGkMB0GA1UdDgQWBBRa46Ix
|
||||
9s9tmMqu+Zz1mocHghm4NTB1BgNVHSMEbjBsgBRa46Ix9s9tmMqu+Zz1mocHghm4
|
||||
NaFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
|
||||
BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAO6wb0FSc/rNMAwGA1UdEwQF
|
||||
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBACdrnM8zb7abxAJsU5WLn1IR0f2+EFA7
|
||||
ezBEJBM4bn0IZrXuP5ThZ2wieJlshG0C16XN9+zifavHci+AtQwWsB0f/ppHdvWQ
|
||||
7wt7JN88w+j0DNIYEadRCjWxR3gRAXPgKu3sdyScKFq8MvB49A2EdXRmQSTIM6Fj
|
||||
teRbE+poxewFT0mhurf3xrtGiSALmv7uAzhRDqpYUzcUlbOGgkyFLYAOOdvZvei+
|
||||
mfXDi4HKYxgyv53JxBARMdajnCHXM7zQ6Tjc8j1HRtmDQ3XapUB559KfxfODGQq5
|
||||
zmeoZWU4duxcNXJM0Eiz1CJ39JoWwi8sqaGi/oskuyAh7YKyVTn8xa8=
|
||||
-----END CERTIFICATE-----
|
||||
27
tests/certs/selfsigned.key
Normal file
27
tests/certs/selfsigned.key
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7
|
||||
mdB6BflhV5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLw
|
||||
dpaoTGqEvYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49
|
||||
IAouBHq3h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ
|
||||
8gjaC8/1w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJ
|
||||
PXtNrTwfqEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABAoIBAFgVasxTf3aaXbNo
|
||||
7JzXMWb7W4iAG2GRNmZZzHA7hTSKFvS7jc3SX3n6WvDtEvlOi8ay2RyRNgEjBDP6
|
||||
VZ/w2jUJjS5k7dN0Qb9nhPr5B9fS/0CAppcVfsx5/KEVFzniWOPyzQYyW7FJKu8h
|
||||
4G5hrp/Ie4UH5tKtB6YUZB/wliyyQUkAZdBcoy1hfkOZLAXb1oofArKsiQUHIRA5
|
||||
th1yyS4cZP8Upngd1EE+d95dFHM2F6iI2lj6DHuu+JxUZ+wKXoNimdG7JniRtIf4
|
||||
56GoDov83Ey+XbIS6FSQc9nY0ijBDcubl/yP3roCQpE+MZ9BNEo5uj7YmCtAMYLW
|
||||
TXTNBGUCgYEA4wdkH1NLdub2NcpqwmSA0AtbRvDkt0XTDWWwmuMr/+xPVa4sUKHs
|
||||
80THQEX/WAZroP6IPbMP6BJhzb53vECukgC65qPxu6M9D1lBGtglxgen4AMu1bKK
|
||||
gnM8onwARGIo/2ay6qRRZZCxg0TvBky3hbTcIM2zVrnKU6VVyGKHSV8CgYEAygxs
|
||||
WQYrACv3XN6ZEzyxy08JgjbcnkPWK/m3VPcyHgdEkDu8+nDdUVdbF/js2JWMMx5g
|
||||
vrPhZ7jVLOXGcLr5mVU4dG5tW5lU0bMy+YYxpEQDiBKlpXgfOsQnakHj7cCZ6bay
|
||||
mKjJck2oEAQS9bqOJN/Ts5vhOmc8rmhkO7hnAh8CgYEArhVDy9Vl/1WYo6SD+m1w
|
||||
bJbYtewPpQzwicxZAFuDqKk+KDf3GRkhBWTO2FUUOB4sN3YVaCI+5zf5MPeE/qAm
|
||||
fCP9LM+3k6bXMkbBamEljdTfACHQruJJ3T+Z1gn5dnZCc5z/QncfRx8NTtfz5MO8
|
||||
0dTeGnVAuBacs0kLHy2WCUcCgYALNBkl7pOf1NBIlAdE686oCV/rmoMtO3G6yoQB
|
||||
8BsVUy3YGZfnAy8ifYeNkr3/XHuDsiGHMY5EJBmd/be9NID2oaUZv63MsHnljtw6
|
||||
vdgu1Z6kgvQwcrK4nXvaBoFPA6kFLp5EnMde0TOKf89VVNzg6pBgmzon9OWGfj9g
|
||||
mF8N3QKBgQCeoLwxUxpzEA0CPHm7DWF0LefVGllgZ23Eqncdy0QRku5zwwibszbL
|
||||
sWaR3uDCc3oYcbSGCDVx3cSkvMAJNalc5ZHPfoV9W0+v392/rrExo5iwD8CSoCb2
|
||||
gFWkeR7PBrD3NzFzFAWyiudzhBKHfRsB0MpCXbJV/WLqTlGIbEypjg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
BIN
tests/static/python.png
Normal file
BIN
tests/static/python.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
|
||||
from sanic import Sanic
|
||||
@@ -23,6 +24,33 @@ def test_bp():
|
||||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
def test_bp_strict_slash():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
@bp.get('/get', strict_slashes=True)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.post('/post/', strict_slashes=True)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = app.test_client.post('/post/')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_bp_with_url_prefix():
|
||||
app = Sanic('test_text')
|
||||
bp = Blueprint('test_text', url_prefix='/test1')
|
||||
@@ -236,6 +264,7 @@ def test_bp_static():
|
||||
def test_bp_shorthand():
|
||||
app = Sanic('test_shorhand_routes')
|
||||
blueprint = Blueprint('test_shorhand_routes')
|
||||
ev = asyncio.Event()
|
||||
|
||||
@blueprint.get('/get')
|
||||
def handler(request):
|
||||
@@ -265,6 +294,10 @@ def test_bp_shorthand():
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@blueprint.websocket('/ws')
|
||||
async def handler(request, ws):
|
||||
ev.set()
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
@@ -308,3 +341,11 @@ def test_bp_shorthand():
|
||||
|
||||
request, response = app.test_client.get('/delete')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13'})
|
||||
assert response.status == 101
|
||||
assert ev.is_set()
|
||||
|
||||
@@ -16,6 +16,17 @@ def test_load_from_object():
|
||||
assert app.config.CONFIG_VALUE == 'should be used'
|
||||
assert 'not_for_config' not in app.config
|
||||
|
||||
def test_auto_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic()
|
||||
assert app.config.TEST_ANSWER == "42"
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
def test_auto_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic(load_env=False)
|
||||
assert getattr(app.config, 'TEST_ANSWER', None) == None
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
def test_load_from_file():
|
||||
app = Sanic('test_load_from_file')
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import sanic
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from threading import Event
|
||||
import asyncio
|
||||
|
||||
|
||||
def test_create_task():
|
||||
e = Event()
|
||||
|
||||
async def coro():
|
||||
await asyncio.sleep(0.05)
|
||||
e.set()
|
||||
|
||||
app = sanic.Sanic()
|
||||
app = Sanic('test_create_task')
|
||||
app.add_task(coro)
|
||||
|
||||
@app.route('/early')
|
||||
@@ -22,9 +23,8 @@ def test_create_task():
|
||||
await asyncio.sleep(0.1)
|
||||
return text(e.is_set())
|
||||
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/early')
|
||||
request, response = app.test_client.get('/early')
|
||||
assert response.body == b'False'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/late')
|
||||
request, response = app.test_client.get('/late')
|
||||
assert response.body == b'True'
|
||||
|
||||
@@ -44,6 +44,21 @@ def exception_app():
|
||||
|
||||
return app
|
||||
|
||||
def test_catch_exception_list():
|
||||
app = Sanic('exception_list')
|
||||
@app.exception([SanicExceptionTestException, NotFound])
|
||||
def exception_list(request, exception):
|
||||
return text("ok")
|
||||
|
||||
@app.route('/')
|
||||
def exception(request):
|
||||
raise SanicExceptionTestException("You won't see me")
|
||||
|
||||
request, response = app.test_client.get('/random')
|
||||
assert response.text == 'ok'
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'ok'
|
||||
|
||||
def test_no_exception(exception_app):
|
||||
"""Test that a route works without an exception"""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.handlers import ErrorHandler
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
exception_handler_app = Sanic('test_exception_handler')
|
||||
@@ -30,10 +31,19 @@ def handler_4(request):
|
||||
@exception_handler_app.route('/5')
|
||||
def handler_5(request):
|
||||
class CustomServerError(ServerError):
|
||||
status_code=200
|
||||
pass
|
||||
raise CustomServerError('Custom server error')
|
||||
|
||||
|
||||
@exception_handler_app.route('/6/<arg:int>')
|
||||
def handler_6(request, arg):
|
||||
try:
|
||||
foo = 1 / arg
|
||||
except Exception as e:
|
||||
raise e from ValueError("{}".format(arg))
|
||||
return text(foo)
|
||||
|
||||
|
||||
@exception_handler_app.exception(NotFound, ServerError)
|
||||
def handler_exception(request, exception):
|
||||
return text("OK")
|
||||
@@ -75,9 +85,70 @@ def test_html_traceback_output_in_debug_mode():
|
||||
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||
assert (
|
||||
"NameError: name 'bar' "
|
||||
"is not defined while handling uri /4") == summary_text
|
||||
"is not defined while handling path /4") == summary_text
|
||||
|
||||
|
||||
def test_inherited_exception_handler():
|
||||
request, response = exception_handler_app.test_client.get('/5')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_chained_exception_handler():
|
||||
request, response = exception_handler_app.test_client.get(
|
||||
'/6/0', debug=True)
|
||||
assert response.status == 500
|
||||
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
html = str(soup)
|
||||
|
||||
assert 'response = handler(request, *args, **kwargs)' in html
|
||||
assert 'handler_6' in html
|
||||
assert 'foo = 1 / arg' in html
|
||||
assert 'ValueError' in html
|
||||
assert 'The above exception was the direct cause' in html
|
||||
|
||||
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||
assert (
|
||||
"ZeroDivisionError: division by zero "
|
||||
"while handling path /6/0") == summary_text
|
||||
|
||||
|
||||
def test_exception_handler_lookup():
|
||||
class CustomError(Exception):
|
||||
pass
|
||||
|
||||
class CustomServerError(ServerError):
|
||||
pass
|
||||
|
||||
def custom_error_handler():
|
||||
pass
|
||||
|
||||
def server_error_handler():
|
||||
pass
|
||||
|
||||
def import_error_handler():
|
||||
pass
|
||||
|
||||
try:
|
||||
ModuleNotFoundError
|
||||
except:
|
||||
class ModuleNotFoundError(ImportError):
|
||||
pass
|
||||
|
||||
handler = ErrorHandler()
|
||||
handler.add(ImportError, import_error_handler)
|
||||
handler.add(CustomError, custom_error_handler)
|
||||
handler.add(ServerError, server_error_handler)
|
||||
|
||||
assert handler.lookup(ImportError()) == import_error_handler
|
||||
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
|
||||
assert handler.lookup(CustomError()) == custom_error_handler
|
||||
assert handler.lookup(ServerError('Error')) == server_error_handler
|
||||
assert handler.lookup(CustomServerError('Error')) == server_error_handler
|
||||
|
||||
# once again to ensure there is no caching bug
|
||||
assert handler.lookup(ImportError()) == import_error_handler
|
||||
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
|
||||
assert handler.lookup(CustomError()) == custom_error_handler
|
||||
assert handler.lookup(ServerError('Error')) == server_error_handler
|
||||
assert handler.lookup(CustomServerError('Error')) == server_error_handler
|
||||
|
||||
@@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.request import Request
|
||||
from sanic.response import json, text, HTTPResponse
|
||||
from sanic.exceptions import NotFound
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -53,6 +54,27 @@ def test_middleware_response():
|
||||
assert isinstance(results[2], HTTPResponse)
|
||||
|
||||
|
||||
def test_middleware_response_exception():
|
||||
app = Sanic('test_middleware_response_exception')
|
||||
result = {'status_code': None}
|
||||
|
||||
@app.middleware('response')
|
||||
async def process_response(reqest, response):
|
||||
result['status_code'] = response.status
|
||||
return response
|
||||
|
||||
@app.exception(NotFound)
|
||||
async def error_handler(request, exception):
|
||||
return text('OK', exception.status_code)
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return text('FAIL')
|
||||
|
||||
request, response = app.test_client.get('/page_not_found')
|
||||
assert response.text == 'OK'
|
||||
assert result['status_code'] == 404
|
||||
|
||||
def test_middleware_override_request():
|
||||
app = Sanic('test_middleware_override_request')
|
||||
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import PayloadTooLarge
|
||||
|
||||
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)
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
def test_payload_too_large_from_error_handler():
|
||||
data_received_app = Sanic('data_received')
|
||||
data_received_app.config.REQUEST_MAX_SIZE = 1
|
||||
|
||||
@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)
|
||||
|
||||
response = data_received_app.test_client.get('/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():
|
||||
data_received_default_app = Sanic('data_received_default')
|
||||
data_received_default_app.config.REQUEST_MAX_SIZE = 1
|
||||
|
||||
@data_received_default_app.route('/1')
|
||||
async def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
response = data_received_default_app.test_client.get(
|
||||
'/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():
|
||||
on_header_default_app = Sanic('on_header')
|
||||
on_header_default_app.config.REQUEST_MAX_SIZE = 500
|
||||
|
||||
@on_header_default_app.post('/1')
|
||||
async def handler3(request):
|
||||
return text('OK')
|
||||
|
||||
data = 'a' * 1000
|
||||
response = on_header_default_app.test_client.post(
|
||||
'/1', gather_request=False, data=data)
|
||||
|
||||
@@ -88,4 +88,7 @@ def test_chained_redirect(redirect_app):
|
||||
assert request.url.endswith('/1')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.url.endswith('/3')
|
||||
try:
|
||||
assert response.url.endswith('/3')
|
||||
except AttributeError:
|
||||
assert response.url.path.endswith('/3')
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
from json import loads as json_loads, dumps as json_dumps
|
||||
from urllib.parse import urlparse
|
||||
import os
|
||||
import ssl
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.response import json, text, redirect
|
||||
from sanic.response import json, text
|
||||
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -85,20 +90,28 @@ def test_json():
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
try:
|
||||
results = json_loads(response.text)
|
||||
except:
|
||||
raise ValueError("Expected JSON response but got '{}'".format(response))
|
||||
results = json_loads(response.text)
|
||||
|
||||
assert results.get('test') == True
|
||||
|
||||
def test_empty_json():
|
||||
app = Sanic('test_json')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
assert request.json == None
|
||||
return json(request.json)
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.status == 200
|
||||
assert response.text == 'null'
|
||||
|
||||
def test_invalid_json():
|
||||
app = Sanic('test_json')
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return json(request.json())
|
||||
return json(request.json)
|
||||
|
||||
data = "I am not json"
|
||||
request, response = app.test_client.get('/', data=data)
|
||||
@@ -120,6 +133,17 @@ def test_query_string():
|
||||
assert request.args.get('test2') == 'false'
|
||||
|
||||
|
||||
def test_uri_template():
|
||||
app = Sanic('test_uri_template')
|
||||
|
||||
@app.route('/foo/<id:int>/bar/<name:[A-z]+>')
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/foo/123/bar/baz')
|
||||
assert request.uri_template == '/foo/<id:int>/bar/<name:[A-z]+>'
|
||||
|
||||
|
||||
def test_token():
|
||||
app = Sanic('test_post_token')
|
||||
|
||||
@@ -128,6 +152,16 @@ def test_token():
|
||||
return text('OK')
|
||||
|
||||
# uuid4 generated token.
|
||||
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
||||
headers = {
|
||||
'content-type': 'application/json',
|
||||
'Authorization': '{}'.format(token)
|
||||
}
|
||||
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
|
||||
assert request.token == token
|
||||
|
||||
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
||||
headers = {
|
||||
'content-type': 'application/json',
|
||||
@@ -138,6 +172,18 @@ def test_token():
|
||||
|
||||
assert request.token == token
|
||||
|
||||
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
||||
headers = {
|
||||
'content-type': 'application/json',
|
||||
'Authorization': 'Bearer Token {}'.format(token)
|
||||
}
|
||||
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
|
||||
assert request.token == token
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# POST
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -192,3 +238,61 @@ def test_post_form_multipart_form_data():
|
||||
request, response = app.test_client.post(data=payload, headers=headers)
|
||||
|
||||
assert request.form.get('test') == 'OK'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path,query,expected_url', [
|
||||
('/foo', '', 'http://{}:{}/foo'),
|
||||
('/bar/baz', '', 'http://{}:{}/bar/baz'),
|
||||
('/moo/boo', 'arg1=val1', 'http://{}:{}/moo/boo?arg1=val1')
|
||||
])
|
||||
def test_url_attributes_no_ssl(path, query, expected_url):
|
||||
app = Sanic('test_url_attrs_no_ssl')
|
||||
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, path)
|
||||
|
||||
request, response = app.test_client.get(path + '?{}'.format(query))
|
||||
assert request.url == expected_url.format(HOST, PORT)
|
||||
|
||||
parsed = urlparse(request.url)
|
||||
|
||||
assert parsed.scheme == request.scheme
|
||||
assert parsed.path == request.path
|
||||
assert parsed.query == request.query_string
|
||||
assert parsed.netloc == request.host
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path,query,expected_url', [
|
||||
('/foo', '', 'https://{}:{}/foo'),
|
||||
('/bar/baz', '', 'https://{}:{}/bar/baz'),
|
||||
('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1')
|
||||
])
|
||||
def test_url_attributes_with_ssl(path, query, expected_url):
|
||||
app = Sanic('test_url_attrs_with_ssl')
|
||||
|
||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(
|
||||
os.path.join(current_dir, 'certs/selfsigned.cert'),
|
||||
keyfile=os.path.join(current_dir, 'certs/selfsigned.key'))
|
||||
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.add_route(handler, path)
|
||||
|
||||
request, response = app.test_client.get(
|
||||
'https://{}:{}'.format(HOST, PORT) + path + '?{}'.format(query),
|
||||
server_kwargs={'ssl': context})
|
||||
assert request.url == expected_url.format(HOST, PORT)
|
||||
|
||||
parsed = urlparse(request.url)
|
||||
|
||||
assert parsed.scheme == request.scheme
|
||||
assert parsed.path == request.path
|
||||
assert parsed.query == request.query_string
|
||||
assert parsed.netloc == request.host
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import asyncio
|
||||
import pytest
|
||||
from random import choice
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.response import HTTPResponse, stream, StreamingHTTPResponse
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
def test_response_body_not_a_string():
|
||||
"""Test when a response body sent from the application is not a string"""
|
||||
@@ -15,3 +19,78 @@ def test_response_body_not_a_string():
|
||||
|
||||
request, response = app.test_client.get('/hello')
|
||||
assert response.text == str(random_num)
|
||||
|
||||
|
||||
async def sample_streaming_fn(response):
|
||||
response.write('foo,')
|
||||
await asyncio.sleep(.001)
|
||||
response.write('bar')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def streaming_app():
|
||||
app = Sanic('streaming')
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return stream(sample_streaming_fn, content_type='text/csv')
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_streaming_adds_correct_headers(streaming_app):
|
||||
request, response = streaming_app.test_client.get('/')
|
||||
assert response.headers['Transfer-Encoding'] == 'chunked'
|
||||
assert response.headers['Content-Type'] == 'text/csv'
|
||||
|
||||
|
||||
def test_streaming_returns_correct_content(streaming_app):
|
||||
request, response = streaming_app.test_client.get('/')
|
||||
assert response.text == 'foo,bar'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('status', [200, 201, 400, 401])
|
||||
def test_stream_response_status_returns_correct_headers(status):
|
||||
response = StreamingHTTPResponse(sample_streaming_fn, status=status)
|
||||
headers = response.get_headers()
|
||||
assert b"HTTP/1.1 %s" % str(status).encode() in headers
|
||||
|
||||
|
||||
@pytest.mark.parametrize('keep_alive_timeout', [10, 20, 30])
|
||||
def test_stream_response_keep_alive_returns_correct_headers(
|
||||
keep_alive_timeout):
|
||||
response = StreamingHTTPResponse(sample_streaming_fn)
|
||||
headers = response.get_headers(
|
||||
keep_alive=True, keep_alive_timeout=keep_alive_timeout)
|
||||
|
||||
assert b"Keep-Alive: %s\r\n" % str(keep_alive_timeout).encode() in headers
|
||||
|
||||
|
||||
def test_stream_response_includes_chunked_header():
|
||||
response = StreamingHTTPResponse(sample_streaming_fn)
|
||||
headers = response.get_headers()
|
||||
assert b"Transfer-Encoding: chunked\r\n" in headers
|
||||
|
||||
|
||||
def test_stream_response_writes_correct_content_to_transport(streaming_app):
|
||||
response = StreamingHTTPResponse(sample_streaming_fn)
|
||||
response.transport = MagicMock(asyncio.Transport)
|
||||
|
||||
@streaming_app.listener('after_server_start')
|
||||
async def run_stream(app, loop):
|
||||
await response.stream()
|
||||
assert response.transport.write.call_args_list[1][0][0] == (
|
||||
b'4\r\nfoo,\r\n'
|
||||
)
|
||||
|
||||
assert response.transport.write.call_args_list[2][0][0] == (
|
||||
b'3\r\nbar\r\n'
|
||||
)
|
||||
|
||||
assert response.transport.write.call_args_list[3][0][0] == (
|
||||
b'0\r\n\r\n'
|
||||
)
|
||||
|
||||
app.stop()
|
||||
|
||||
streaming_app.run(host=HOST, port=PORT)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
@@ -22,6 +23,29 @@ def test_shorthand_routes_get():
|
||||
request, response = app.test_client.post('/get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_route_strict_slash():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
|
||||
@app.get('/get', strict_slashes=True)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@app.post('/post/', strict_slashes=True)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = app.test_client.post('/post/')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.status == 404
|
||||
|
||||
def test_route_optional_slash():
|
||||
app = Sanic('test_route_optional_slash')
|
||||
|
||||
@@ -214,6 +238,30 @@ def test_dynamic_route_regex():
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_dynamic_route_path():
|
||||
app = Sanic('test_dynamic_route_path')
|
||||
|
||||
@app.route('/<path:path>/info')
|
||||
async def handler(request, path):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/path/1/info')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = app.test_client.get('/info')
|
||||
assert response.status == 404
|
||||
|
||||
@app.route('/<path:path>')
|
||||
async def handler1(request, path):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/info')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = app.test_client.get('/whatever/you/set')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_dynamic_route_unhashable():
|
||||
app = Sanic('test_dynamic_route_unhashable')
|
||||
|
||||
@@ -234,6 +282,23 @@ def test_dynamic_route_unhashable():
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_websocket_route():
|
||||
app = Sanic('test_websocket_route')
|
||||
ev = asyncio.Event()
|
||||
|
||||
@app.websocket('/ws')
|
||||
async def handler(request, ws):
|
||||
ev.set()
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13'})
|
||||
assert response.status == 101
|
||||
assert ev.is_set()
|
||||
|
||||
|
||||
def test_route_duplicate():
|
||||
app = Sanic('test_route_duplicate')
|
||||
|
||||
@@ -498,6 +563,19 @@ def test_remove_inexistent_route():
|
||||
with pytest.raises(RouteDoesNotExist):
|
||||
app.remove_route('/test')
|
||||
|
||||
def test_removing_slash():
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.get('/rest/<resource>')
|
||||
def get(_):
|
||||
pass
|
||||
|
||||
@app.post('/rest/<resource>')
|
||||
def post(_):
|
||||
pass
|
||||
|
||||
assert len(app.router.routes_all.keys()) == 2
|
||||
|
||||
|
||||
def test_remove_unhashable_route():
|
||||
app = Sanic('test_remove_unhashable_route')
|
||||
|
||||
@@ -25,7 +25,7 @@ def get_file_content(static_file_directory, file_name):
|
||||
return file.read()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_static_file(static_file_directory, file_name):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
|
||||
@@ -15,13 +15,13 @@ def test_methods(method):
|
||||
|
||||
class DummyView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
return text('', headers={'method': 'GET'})
|
||||
|
||||
def post(self, request):
|
||||
return text('', headers={'method': 'POST'})
|
||||
|
||||
def put(self, request):
|
||||
async def put(self, request):
|
||||
return text('', headers={'method': 'PUT'})
|
||||
|
||||
def head(self, request):
|
||||
@@ -30,7 +30,7 @@ def test_methods(method):
|
||||
def options(self, request):
|
||||
return text('', headers={'method': 'OPTIONS'})
|
||||
|
||||
def patch(self, request):
|
||||
async def patch(self, request):
|
||||
return text('', headers={'method': 'PATCH'})
|
||||
|
||||
def delete(self, request):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user