Compare commits
326 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5930bb67a6 | ||
|
|
4c360f43fd | ||
|
|
6387f5ddc9 | ||
|
|
23a0308d40 | ||
|
|
a7d563d566 | ||
|
|
f2d91bd4d2 | ||
|
|
8c628c69fb | ||
|
|
9b24fbb2f3 | ||
|
|
468f4ac7f1 | ||
|
|
be1ca93a23 | ||
|
|
662c7c7f62 | ||
|
|
3e4bec7f2c | ||
|
|
df4970a73d | ||
|
|
c5070bd449 | ||
|
|
eb3d0a3f87 | ||
|
|
c09129ec63 | ||
|
|
2a44a27236 | ||
|
|
bb9ff7cec1 | ||
|
|
850b63f642 | ||
|
|
a9c669f17b | ||
|
|
075affec23 | ||
|
|
2b5f8d20de | ||
|
|
243f240e5f | ||
|
|
0a25868a86 | ||
|
|
fccbc1adc4 | ||
|
|
3f6a978328 | ||
|
|
a6077a1790 | ||
|
|
179a07942e | ||
|
|
c3aed01096 | ||
|
|
028778ed56 | ||
|
|
2d72874b0b | ||
|
|
4c45d30400 | ||
|
|
ecbe5c839f | ||
|
|
ed1f367a8a | ||
|
|
a4185a0ba7 | ||
|
|
e81a8ce073 | ||
|
|
e506c89304 | ||
|
|
fcdc9c83c5 | ||
|
|
be0d539746 | ||
|
|
4f9739ed2c | ||
|
|
0df37fa653 | ||
|
|
3e932505b0 | ||
|
|
01be691936 | ||
|
|
134c414fe5 | ||
|
|
c54a8b10bb | ||
|
|
6fc3381229 | ||
|
|
927c0e082e | ||
|
|
7674e917e4 | ||
|
|
e13f42c17b | ||
|
|
b7d4121586 | ||
|
|
fbcd4b9767 | ||
|
|
17c5e28727 | ||
|
|
e62b29ca44 | ||
|
|
1e4b1c4d1a | ||
|
|
ae91852cd5 | ||
|
|
2011f3a0b2 | ||
|
|
228a31ee0a | ||
|
|
8bf2bdff74 | ||
|
|
41862eca61 | ||
|
|
21307b397b | ||
|
|
3f9c94ba4a | ||
|
|
aa270d3ac2 | ||
|
|
a15d9552c4 | ||
|
|
2363c0653e | ||
|
|
651c98d19a | ||
|
|
c1a7e0e3cd | ||
|
|
80b32d0c71 | ||
|
|
3842eb36fd | ||
|
|
7c7bedfa5d | ||
|
|
5dafa9a170 | ||
|
|
b397637bb9 | ||
|
|
95a0b2db2c | ||
|
|
83864f890a | ||
|
|
a019ff61e3 | ||
|
|
b3ada6308b | ||
|
|
4e50295bf0 | ||
|
|
32eb8abb63 | ||
|
|
84b41123f2 | ||
|
|
23f2d33394 | ||
|
|
97f288a534 | ||
|
|
68d5039c5f | ||
|
|
9d07988d75 | ||
|
|
1eaa2e3a5f | ||
|
|
c7f2399ded | ||
|
|
650ab61c2e | ||
|
|
b7df86e7dd | ||
|
|
72b445621b | ||
|
|
ba0e9baffa | ||
|
|
503622438a | ||
|
|
d5e9aae425 | ||
|
|
a2666a2b8a | ||
|
|
966b05b47e | ||
|
|
78fe97b9cb | ||
|
|
d2094fed38 | ||
|
|
e2d65ba57c | ||
|
|
c9d8ab4b27 | ||
|
|
891f99d71d | ||
|
|
3f47fa9f99 | ||
|
|
b1c23fdbaa | ||
|
|
62e0e5b9ec | ||
|
|
fb61834a2e | ||
|
|
8fbbe94fe1 | ||
|
|
ab706dda7d | ||
|
|
b2d4132a14 | ||
|
|
322cf89c92 | ||
|
|
09acd64ba1 | ||
|
|
072fcfe03e | ||
|
|
13079c6e30 | ||
|
|
1b1a51c1bb | ||
|
|
39d134994d | ||
|
|
5f9e98554f | ||
|
|
0d9a21718f | ||
|
|
daf42c5f43 | ||
|
|
3685b4de85 | ||
|
|
2631f10c5e | ||
|
|
f21db60859 | ||
|
|
c15158224b | ||
|
|
a57c14c70b | ||
|
|
bb2bd2fe53 | ||
|
|
aebe2b5809 | ||
|
|
9172399b8c | ||
|
|
22c0d97783 | ||
|
|
3ead529693 | ||
|
|
e36f398aa6 | ||
|
|
18cd4caf70 | ||
|
|
80df45ba6d | ||
|
|
16d262e3e5 | ||
|
|
83e3d4ca1f | ||
|
|
1c9141bd5d | ||
|
|
1b984422db | ||
|
|
b6453e9fac | ||
|
|
7b8e3624b8 | ||
|
|
8a56da84e6 | ||
|
|
14a00490e2 | ||
|
|
29bf967a7e | ||
|
|
eeb79f2587 | ||
|
|
6d1741694d | ||
|
|
746dccf8f9 | ||
|
|
28a897e599 | ||
|
|
21ebf6d777 | ||
|
|
a2dbbb25a1 | ||
|
|
2a64dabe82 | ||
|
|
046ca6eaf1 | ||
|
|
3661afa461 | ||
|
|
12f1985375 | ||
|
|
bb800c9db8 | ||
|
|
262048df95 | ||
|
|
9255eb6902 | ||
|
|
56d386f152 | ||
|
|
193dbe89cd | ||
|
|
601e158ffe | ||
|
|
42b9fa3779 | ||
|
|
4767a67acd | ||
|
|
4c8cc84b64 | ||
|
|
c5efc4b64b | ||
|
|
25e2151fdf | ||
|
|
cb10e261a2 | ||
|
|
bd89c7f269 | ||
|
|
d4ef151cc4 | ||
|
|
669cfa33df | ||
|
|
f70ab2f68a | ||
|
|
900020ddc9 | ||
|
|
ec428135ce | ||
|
|
8e2a1a61a5 | ||
|
|
5fb8f5d3e7 | ||
|
|
c68523150f | ||
|
|
ae2b8f0056 | ||
|
|
ef6d78c580 | ||
|
|
ccd4c9615c | ||
|
|
7d6e60ab7d | ||
|
|
9615e37ef9 | ||
|
|
6be12ba773 | ||
|
|
03855d316b | ||
|
|
9f07109616 | ||
|
|
6a4a3f617f | ||
|
|
f32c9be41f | ||
|
|
d83d829e0a | ||
|
|
99e56ef74b | ||
|
|
df23692802 | ||
|
|
b68a7fe7ae | ||
|
|
5c9ba189bc | ||
|
|
5631a31099 | ||
|
|
f4bc0efc31 | ||
|
|
53f45810ff | ||
|
|
d58151a0eb | ||
|
|
de582d2fc7 | ||
|
|
653ac7ee14 | ||
|
|
0b4769289a | ||
|
|
3bedb223fc | ||
|
|
94a1720e04 | ||
|
|
d0c8808340 | ||
|
|
dd32d81726 | ||
|
|
378a732968 | ||
|
|
b2e82685b4 | ||
|
|
566940e052 | ||
|
|
dab802fbf4 | ||
|
|
7bca95205d | ||
|
|
669e2ed5b0 | ||
|
|
783eb1a6e8 | ||
|
|
9b9599b12f | ||
|
|
6ed0d3def7 | ||
|
|
c42731a55c | ||
|
|
abf8534ea9 | ||
|
|
773a66bc5b | ||
|
|
269100eac1 | ||
|
|
2a15583b87 | ||
|
|
b534df242b | ||
|
|
734730640a | ||
|
|
bee7cfa6aa | ||
|
|
d5813152ab | ||
|
|
eacf78b83c | ||
|
|
348964fe12 | ||
|
|
e5c7589fc0 | ||
|
|
4260528645 | ||
|
|
34fe26e51b | ||
|
|
8a59907319 | ||
|
|
52deebaf65 | ||
|
|
1e05b22fbc | ||
|
|
ab56af5d15 | ||
|
|
123f00eee6 | ||
|
|
42bf103269 | ||
|
|
c8d2a462e3 | ||
|
|
08794ae1cf | ||
|
|
4f70dba935 | ||
|
|
b926a2c9b0 | ||
|
|
52bdd1d5a2 | ||
|
|
bc7d0f0da5 | ||
|
|
6a8e9c9e95 | ||
|
|
211a922f3c | ||
|
|
2758a3ade6 | ||
|
|
ef3c9eae73 | ||
|
|
9cf2e1b519 | ||
|
|
51c2f7a599 | ||
|
|
5bdd046b11 | ||
|
|
95526a82de | ||
|
|
af7ad0a621 | ||
|
|
1473753d43 | ||
|
|
b36bd21813 | ||
|
|
f8f0241c27 | ||
|
|
1af16836d4 | ||
|
|
757974714e | ||
|
|
eed22a7a24 | ||
|
|
0242bc999f | ||
|
|
b89c533818 | ||
|
|
2cb05ab865 | ||
|
|
391639210e | ||
|
|
99f34c9f50 | ||
|
|
d418cc9950 | ||
|
|
6dfafb0787 | ||
|
|
7067295e67 | ||
|
|
2af229eb1a | ||
|
|
8dd8e9916e | ||
|
|
96af1fe7cf | ||
|
|
cb3a03356b | ||
|
|
68aa2ae3ce | ||
|
|
52de354e24 | ||
|
|
f4f90cada4 | ||
|
|
62420e0f40 | ||
|
|
102e651741 | ||
|
|
65daaaf64b | ||
|
|
b7a6f36e95 | ||
|
|
a86a10b128 | ||
|
|
0b728ade3a | ||
|
|
74f05108d7 | ||
|
|
9d4d15ddc7 | ||
|
|
0c5c6dff8f | ||
|
|
391fcdc83d | ||
|
|
d76d5e2c5f | ||
|
|
f0ada573bb | ||
|
|
ec5b790b51 | ||
|
|
613b23748d | ||
|
|
cea1547e08 | ||
|
|
fd5ae01e1d | ||
|
|
9b6b93d467 | ||
|
|
ca179c12a1 | ||
|
|
4d527035ae | ||
|
|
19b42830ea | ||
|
|
f5162f8ab1 | ||
|
|
ff38a3c6b6 | ||
|
|
94e85686b5 | ||
|
|
aea4a8ed33 | ||
|
|
05dd3b2e9d | ||
|
|
040468755c | ||
|
|
50b359fdb2 | ||
|
|
72f2e18a84 | ||
|
|
b36dc22b45 | ||
|
|
15b1c875f5 | ||
|
|
13804dc380 | ||
|
|
9bea23da29 | ||
|
|
7005fabd4d | ||
|
|
de8c37ad00 | ||
|
|
a80499c4b7 | ||
|
|
82f7f847ba | ||
|
|
4880761fe0 | ||
|
|
87ab0b386d | ||
|
|
c42c274002 | ||
|
|
2d82b8951f | ||
|
|
b7702bc3e8 | ||
|
|
9c6b83501f | ||
|
|
5189d8b14c | ||
|
|
e13053ed89 | ||
|
|
efa77cf5ec | ||
|
|
f6355bd075 | ||
|
|
e3dfce88ff | ||
|
|
939b5ea095 | ||
|
|
e6fba01682 | ||
|
|
1623d397be | ||
|
|
09678d601d | ||
|
|
67d51f7e1b | ||
|
|
f2a55d01ea | ||
|
|
bf029c1b9d | ||
|
|
375ebd39f0 | ||
|
|
a33ebbaf11 | ||
|
|
19b304b0fc | ||
|
|
0b64fe6746 | ||
|
|
e978121d58 | ||
|
|
956793e066 | ||
|
|
1bfbc67c62 | ||
|
|
b5287184e9 | ||
|
|
7c9957e058 | ||
|
|
fca7cb9fb0 | ||
|
|
268d254d85 | ||
|
|
181adebf82 | ||
|
|
63bbcb5152 | ||
|
|
9150767574 | ||
|
|
75f2180cb1 |
@@ -2,11 +2,6 @@ version: "{branch}.{build}"
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
- TOXENV: py35-no-ext
|
|
||||||
PYTHON: "C:\\Python35-x64"
|
|
||||||
PYTHON_VERSION: "3.5.x"
|
|
||||||
PYTHON_ARCH: "64"
|
|
||||||
|
|
||||||
- TOXENV: py36-no-ext
|
- TOXENV: py36-no-ext
|
||||||
PYTHON: "C:\\Python36-x64"
|
PYTHON: "C:\\Python36-x64"
|
||||||
PYTHON_VERSION: "3.6.x"
|
PYTHON_VERSION: "3.6.x"
|
||||||
|
|||||||
@@ -5,3 +5,11 @@ omit = site-packages, sanic/utils.py, sanic/__main__.py
|
|||||||
|
|
||||||
[html]
|
[html]
|
||||||
directory = coverage
|
directory = coverage
|
||||||
|
|
||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
no cov
|
||||||
|
no qa
|
||||||
|
noqa
|
||||||
|
NOQA
|
||||||
|
pragma: no cover
|
||||||
|
|||||||
20
.github/stale.yml
vendored
Normal file
20
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 90
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 30
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- bug
|
||||||
|
- urgent
|
||||||
|
- necessary
|
||||||
|
- help wanted
|
||||||
|
- RFC
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: stale
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs. If this
|
||||||
|
is incorrect, please respond with an update. Thank you for your contributions.
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: false
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,8 +10,11 @@ coverage
|
|||||||
settings.py
|
settings.py
|
||||||
.idea/*
|
.idea/*
|
||||||
.cache/*
|
.cache/*
|
||||||
|
.mypy_cache/
|
||||||
.python-version
|
.python-version
|
||||||
docs/_build/
|
docs/_build/
|
||||||
docs/_api/
|
docs/_api/
|
||||||
build/*
|
build/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
dist/*
|
||||||
|
pip-wheel-metadata/
|
||||||
|
|||||||
43
.travis.yml
43
.travis.yml
@@ -5,26 +5,57 @@ cache:
|
|||||||
- $HOME/.cache/pip
|
- $HOME/.cache/pip
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- env: TOX_ENV=py35
|
|
||||||
python: 3.5
|
|
||||||
- env: TOX_ENV=py35-no-ext
|
|
||||||
python: 3.5
|
|
||||||
- env: TOX_ENV=py36
|
- env: TOX_ENV=py36
|
||||||
python: 3.6
|
python: 3.6
|
||||||
|
name: "Python 3.6 with Extensions"
|
||||||
- env: TOX_ENV=py36-no-ext
|
- env: TOX_ENV=py36-no-ext
|
||||||
python: 3.6
|
python: 3.6
|
||||||
|
name: "Python 3.6 without Extensions"
|
||||||
- env: TOX_ENV=py37
|
- env: TOX_ENV=py37
|
||||||
python: 3.7
|
python: 3.7
|
||||||
dist: xenial
|
dist: xenial
|
||||||
sudo: true
|
sudo: true
|
||||||
|
name: "Python 3.7 with Extensions"
|
||||||
- env: TOX_ENV=py37-no-ext
|
- env: TOX_ENV=py37-no-ext
|
||||||
python: 3.7
|
python: 3.7
|
||||||
dist: xenial
|
dist: xenial
|
||||||
sudo: true
|
sudo: true
|
||||||
|
name: "Python 3.7 without Extensions"
|
||||||
|
- env: TOX_ENV=type-checking
|
||||||
|
python: 3.6
|
||||||
|
name: "Python 3.6 Type checks"
|
||||||
|
- env: TOX_ENV=type-checking
|
||||||
|
python: 3.7
|
||||||
|
name: "Python 3.7 Type checks"
|
||||||
- env: TOX_ENV=lint
|
- env: TOX_ENV=lint
|
||||||
python: 3.6
|
python: 3.6
|
||||||
|
name: "Python 3.6 Linter checks"
|
||||||
- env: TOX_ENV=check
|
- env: TOX_ENV=check
|
||||||
python: 3.6
|
python: 3.6
|
||||||
|
name: "Python 3.6 Package checks"
|
||||||
|
- env: TOX_ENV=security
|
||||||
|
python: 3.7
|
||||||
|
dist: xenial
|
||||||
|
sudo: true
|
||||||
|
name: "Python 3.7 Bandit security scan"
|
||||||
|
- env: TOX_ENV=docs
|
||||||
|
python: 3.7
|
||||||
|
dist: xenial
|
||||||
|
sudo: true
|
||||||
|
name: "Python 3.7 Documentation tests"
|
||||||
|
- env: TOX_ENV=pyNightly
|
||||||
|
python: 'nightly'
|
||||||
|
name: "Python nightly with Extensions"
|
||||||
|
- env: TOX_ENV=pyNightly-no-ext
|
||||||
|
python: 'nightly'
|
||||||
|
name: "Python nightly Extensions"
|
||||||
|
allow_failures:
|
||||||
|
- env: TOX_ENV=pyNightly
|
||||||
|
python: 'nightly'
|
||||||
|
name: "Python nightly with Extensions"
|
||||||
|
- env: TOX_ENV=pyNightly-no-ext
|
||||||
|
python: 'nightly'
|
||||||
|
name: "Python nightly Extensions"
|
||||||
install:
|
install:
|
||||||
- pip install -U tox
|
- pip install -U tox
|
||||||
- pip install codecov
|
- pip install codecov
|
||||||
@@ -33,9 +64,9 @@ after_success:
|
|||||||
- codecov
|
- codecov
|
||||||
deploy:
|
deploy:
|
||||||
provider: pypi
|
provider: pypi
|
||||||
user: channelcat
|
user: brewmaster
|
||||||
password:
|
password:
|
||||||
secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
|
secure: "GoawLwmbtJOgKB6AJ0ZSYUUnNwIoonseHBxaAUH3zu79TS/Afrq+yB3lsVaMSG0CbyDgN4FrfD1phT1NzbvZ1VcLIOTDtCrmpQ1kLDw+zwgF40ab8sp8fPkKVHHHfCCs1mjltHIpxQa5lZTJcAs6Bpi/lbUWWwYxFzSV8pHw4W4hY09EHUd2o+evLTSVxaploetSt725DJUYKICUr2eAtCC11IDnIW4CzBJEx6krVV3uhzfTJW0Ls17x0c6sdZ9icMnV/G9xO/eQH6RIHe4xcrWJ6cmLDNKoGAkJp+BKr1CeVVg7Jw/MzPjvZKL2/ki6Beue1y6GUIy7lOS7jPVaOEhJ23b0zQwFcLMZw+Tt+E3v6QfHk+B/WBBBnM3zUZed9UI+QyW8+lqLLt39sQX0FO0P3eaDh8qTXtUuon2jTyFMMAMTFRTNpJmpAzuBH9yeMmDeALPTh0HphI+BkoUl5q1QbWFYjjnZMH2CatApxpLybt9A7rwm//PbOG0TSI93GEKNQ4w5DYryKTfwHzRBptNSephJSuxZYEfJsmUtas5es1D7Fe0PkyjxNNSU+eO+8wsTlitLUsJO4k0jAgy+cEKdU7YJ3J0GZVXocSkrNnUfd2hQPcJ3UtEJx3hLqqr8EM7EZBAasc1yGHh36NFetclzFY24YPih0G1+XurhTys="
|
||||||
on:
|
on:
|
||||||
tags: true
|
tags: true
|
||||||
distributions: "sdist bdist_wheel"
|
distributions: "sdist bdist_wheel"
|
||||||
|
|||||||
99
CHANGELOG.md
99
CHANGELOG.md
@@ -1,99 +0,0 @@
|
|||||||
Version 0.8
|
|
||||||
-----------
|
|
||||||
0.8.3
|
|
||||||
- Changes:
|
|
||||||
- Ownership changed to org 'huge-success'
|
|
||||||
|
|
||||||
0.8.0
|
|
||||||
- Changes:
|
|
||||||
- Add Server-Sent Events extension (Innokenty Lebedev)
|
|
||||||
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
|
|
||||||
- Sanitize URL before redirection (aveao)
|
|
||||||
- Add url_bytes to request (johndoe46)
|
|
||||||
- py37 support for travisci (yunstanford)
|
|
||||||
- Auto reloader support for OSX (garyo)
|
|
||||||
- Add UUID route support (Volodymyr Maksymiv)
|
|
||||||
- Add pausable response streams (Ashley Sommer)
|
|
||||||
- Add weakref to request slots (vopankov)
|
|
||||||
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
|
|
||||||
- Allow streaming handlers in add_route (kinware)
|
|
||||||
- use travis_retry for tox (Raphael Deem)
|
|
||||||
- update aiohttp version for test client (yunstanford)
|
|
||||||
- add redirect import for clarity (yingshaoxo)
|
|
||||||
- Update HTTP Entity headers (Arnulfo Solís)
|
|
||||||
- Add register_listener method (Stephan Fitzpatrick)
|
|
||||||
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
|
|
||||||
- Content-length header on 204/304 responses (Arnulfo Solís)
|
|
||||||
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
|
|
||||||
- Update development status from pre-alpha to beta (Maksim Anisenkov)
|
|
||||||
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
|
|
||||||
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
|
||||||
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
|
|
||||||
- Add support for blueprint groups and nesting (Elias Tarhini)
|
|
||||||
- Remove uvloop for windows setup (Aleksandr Kurlov)
|
|
||||||
- Auto Reload (Yaser Amari)
|
|
||||||
- Documentation updates/fixups (multiple contributors)
|
|
||||||
|
|
||||||
- Fixes:
|
|
||||||
- Fix: auto_reload in Linux (Ashley Sommer)
|
|
||||||
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
|
|
||||||
- Fix: disable auto_reload by default on windows (abuckenheimer)
|
|
||||||
- Fix (1143): Turn off access log with gunicorn (hqy)
|
|
||||||
- Fix (1268): Support status code for file response (Cosmo Borsky)
|
|
||||||
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
|
|
||||||
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
|
|
||||||
- Fix (1242): Responses for CI header (yunstanford)
|
|
||||||
- Fix (1237): add version constraint for websockets (yunstanford)
|
|
||||||
- Fix (1231): memory leak - always release resource (Phillip Xu)
|
|
||||||
- Fix (1221): make request truthy if transport exists (Raphael Deem)
|
|
||||||
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
|
|
||||||
- Fix try_everything examples (PyManiacGR, kot83)
|
|
||||||
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
|
|
||||||
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
|
|
||||||
- Fix: raw requires bytes-like object (cloudship)
|
|
||||||
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
|
|
||||||
- Fix: Bug in multipart/form-data parser (DirkGuijt)
|
|
||||||
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
|
|
||||||
- Fix: Parameter check (Howie Hu)
|
|
||||||
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
|
|
||||||
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
|
|
||||||
- Fix: single quote in readme.rst (Cosven)
|
|
||||||
- Fix: method typos (Dmitry Dygalo)
|
|
||||||
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
|
|
||||||
- Fix (1042): Exception Handling (Raphael Deem)
|
|
||||||
- Fix: Chinese URIs (Howie Hu)
|
|
||||||
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
|
|
||||||
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
|
|
||||||
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
|
|
||||||
- Fix (1065): allow add_task after server starts (Raphael Deem)
|
|
||||||
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
|
|
||||||
- Fix (1062): inject the app in add_task method (Raphael Deem)
|
|
||||||
- Fix: update environment.yml for readthedocs (Eli Uriegas)
|
|
||||||
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
|
|
||||||
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
|
|
||||||
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
|
|
||||||
|
|
||||||
Note: Changelog was unmaintained between 0.1 and 0.7
|
|
||||||
|
|
||||||
Version 0.1
|
|
||||||
-----------
|
|
||||||
- 0.1.7
|
|
||||||
- Reversed static url and directory arguments to meet spec
|
|
||||||
- 0.1.6
|
|
||||||
- Static files
|
|
||||||
- Lazy Cookie Loading
|
|
||||||
- 0.1.5
|
|
||||||
- Cookies
|
|
||||||
- Blueprint listeners and ordering
|
|
||||||
- Faster Router
|
|
||||||
- Fix: Incomplete file reads on medium+ sized post requests
|
|
||||||
- Breaking: after_start and before_stop now pass sanic as their first argument
|
|
||||||
- 0.1.4
|
|
||||||
- Multiprocessing
|
|
||||||
- 0.1.3
|
|
||||||
- Blueprint support
|
|
||||||
- Faster Response processing
|
|
||||||
- 0.1.1 - 0.1.2
|
|
||||||
- Struggling to update pypi via CI
|
|
||||||
- 0.1.0
|
|
||||||
- Released to public
|
|
||||||
433
CHANGELOG.rst
Normal file
433
CHANGELOG.rst
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
Version 19.12.0
|
||||||
|
===============
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
- Fix blueprint middleware application
|
||||||
|
|
||||||
|
Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was
|
||||||
|
being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike.
|
||||||
|
|
||||||
|
As part of this change, the blueprint based middleware application is enforced based on where they are
|
||||||
|
registered.
|
||||||
|
|
||||||
|
- If you register a middleware via :code:`@blueprint.middleware` then it will apply only to the routes defined by the blueprint.
|
||||||
|
- If you register a middleware via :code:`@blueprint_group.middleware` then it will apply to all blueprint based routes that are part of the group.
|
||||||
|
- If you define a middleware via :code:`@app.middleware` then it will be applied on all available routes (`#37 <https://github.com/huge-success/sanic/issues/37>`__)
|
||||||
|
- Fix `url_for` behavior with missing SERVER_NAME
|
||||||
|
|
||||||
|
If the `SERVER_NAME` was missing in the `app.config` entity, the `url_for` on the `request` and `app` were failing
|
||||||
|
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/huge-success/sanic/issues/1707>`__)
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
|
- Move docs from RST to MD
|
||||||
|
|
||||||
|
Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in
|
||||||
|
the future to update documentation. (`#1691 <https://github.com/huge-success/sanic/issues/1691>`__)
|
||||||
|
- Fix documentation for `get` and `getlist` of the `request.args`
|
||||||
|
|
||||||
|
Add additional example for showing the usage of `getlist` and fix the documentation string for `request.args` behavior (`#1704 <https://github.com/huge-success/sanic/issues/1704>`__)
|
||||||
|
|
||||||
|
|
||||||
|
Version 19.6.3
|
||||||
|
==============
|
||||||
|
|
||||||
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
|
- Enable Towncrier Support
|
||||||
|
|
||||||
|
As part of this feature, `towncrier` is being introduced as a mechanism to partially automate the process
|
||||||
|
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/huge-success/sanic/issues/1631>`__)
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
|
- Documentation infrastructure changes
|
||||||
|
|
||||||
|
- Enable having a single common `CHANGELOG` file for both GitHub page and documentation
|
||||||
|
- Fix Sphinix deprecation warnings
|
||||||
|
- Fix documentation warnings due to invalid `rst` indentation
|
||||||
|
- Enable common contribution guidelines file across GitHub and documentation via `CONTRIBUTING.rst` (`#1631 <https://github.com/huge-success/sanic/issues/1631>`__)
|
||||||
|
|
||||||
|
|
||||||
|
Version 19.6.2
|
||||||
|
==============
|
||||||
|
|
||||||
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_
|
||||||
|
Remove ``aiohttp`` dependencey and create new ``SanicTestClient`` based upon
|
||||||
|
`requests-async <https://github.com/encode/requests-async>`_
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1475 <https://github.com/huge-success/sanic/pull/1475>`_
|
||||||
|
Added ASGI support (Beta)
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1436 <https://github.com/huge-success/sanic/pull/1436>`_
|
||||||
|
Add Configure support from object string
|
||||||
|
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1587 <https://github.com/huge-success/sanic/pull/1587>`_
|
||||||
|
Add missing handle for Expect header.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1560 <https://github.com/huge-success/sanic/pull/1560>`_
|
||||||
|
Allow to disable Transfer-Encoding: chunked.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1558 <https://github.com/huge-success/sanic/pull/1558>`_
|
||||||
|
Fix graceful shutdown.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1594 <https://github.com/huge-success/sanic/pull/1594>`_
|
||||||
|
Strict Slashes behavior fix
|
||||||
|
|
||||||
|
Deprecations and Removals
|
||||||
|
*************************
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1544 <https://github.com/huge-success/sanic/pull/1544>`_
|
||||||
|
Drop dependency on distutil
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_
|
||||||
|
Drop support for Python 3.5
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1568 <https://github.com/huge-success/sanic/pull/1568>`_
|
||||||
|
Deprecate route removal.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
Sanic will not support Python 3.5 from version 19.6 and forward. However,
|
||||||
|
version 18.12LTS will have its support period extended thru December 2020, and
|
||||||
|
therefore passing Python's official support version 3.5, which is set to expire
|
||||||
|
in September 2020.
|
||||||
|
|
||||||
|
|
||||||
|
Version 19.3
|
||||||
|
============
|
||||||
|
|
||||||
|
Features
|
||||||
|
********
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1497 <https://github.com/huge-success/sanic/pull/1497>`_
|
||||||
|
Add support for zero-length and RFC 5987 encoded filename for
|
||||||
|
multipart/form-data requests.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1484 <https://github.com/huge-success/sanic/pull/1484>`_
|
||||||
|
The type of ``expires`` attribute of ``sanic.cookies.Cookie`` is now
|
||||||
|
enforced to be of type ``datetime``.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1482 <https://github.com/huge-success/sanic/pull/1482>`_
|
||||||
|
Add support for the ``stream`` parameter of ``sanic.Sanic.add_route()``
|
||||||
|
available to ``sanic.Blueprint.add_route()``.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1481 <https://github.com/huge-success/sanic/pull/1481>`_
|
||||||
|
Accept negative values for route parameters with type ``int`` or ``number``.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1476 <https://github.com/huge-success/sanic/pull/1476>`_
|
||||||
|
Deprecated the use of ``sanic.request.Request.raw_args`` - it has a
|
||||||
|
fundamental flaw in which is drops repeated query string parameters.
|
||||||
|
Added ``sanic.request.Request.query_args`` as a replacement for the
|
||||||
|
original use-case.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1472 <https://github.com/huge-success/sanic/pull/1472>`_
|
||||||
|
Remove an unwanted ``None`` check in Request class ``repr`` implementation.
|
||||||
|
This changes the default ``repr`` of a Request from ``<Request>`` to
|
||||||
|
``<Request: None />``
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1470 <https://github.com/huge-success/sanic/pull/1470>`_
|
||||||
|
Added 2 new parameters to ``sanic.app.Sanic.create_server``\ :
|
||||||
|
|
||||||
|
|
||||||
|
* ``return_asyncio_server`` - whether to return an asyncio.Server.
|
||||||
|
* ``asyncio_server_kwargs`` - kwargs to pass to ``loop.create_server`` for
|
||||||
|
the event loop that sanic is using.
|
||||||
|
|
||||||
|
This is a breaking change.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1499 <https://github.com/huge-success/sanic/pull/1499>`_
|
||||||
|
Added a set of test cases that test and benchmark route resolution.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1457 <https://github.com/huge-success/sanic/pull/1457>`_
|
||||||
|
The type of the ``"max-age"`` value in a ``sanic.cookies.Cookie`` is now
|
||||||
|
enforced to be an integer. Non-integer values are replaced with ``0``.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1445 <https://github.com/huge-success/sanic/pull/1445>`_
|
||||||
|
Added the ``endpoint`` attribute to an incoming ``request``\ , containing the
|
||||||
|
name of the handler function.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1423 <https://github.com/huge-success/sanic/pull/1423>`_
|
||||||
|
Improved request streaming. ``request.stream`` is now a bounded-size buffer
|
||||||
|
instead of an unbounded queue. Callers must now call
|
||||||
|
``await request.stream.read()`` instead of ``await request.stream.get()``
|
||||||
|
to read each portion of the body.
|
||||||
|
|
||||||
|
This is a breaking change.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
********
|
||||||
|
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1502 <https://github.com/huge-success/sanic/pull/1502>`_
|
||||||
|
Sanic was prefetching ``time.time()`` and updating it once per second to
|
||||||
|
avoid excessive ``time.time()`` calls. The implementation was observed to
|
||||||
|
cause memory leaks in some cases. The benefit of the prefetch appeared
|
||||||
|
to negligible, so this has been removed. Fixes
|
||||||
|
`#1500 <https://github.com/huge-success/sanic/pull/1500>`_
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1501 <https://github.com/huge-success/sanic/pull/1501>`_
|
||||||
|
Fix a bug in the auto-reloader when the process was launched as a module
|
||||||
|
i.e. ``python -m init0.mod1`` where the sanic server is started
|
||||||
|
in ``init0/mod1.py`` with ``debug`` enabled and imports another module in
|
||||||
|
``init0``.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1376 <https://github.com/huge-success/sanic/pull/1376>`_
|
||||||
|
Allow sanic test client to bind to a random port by specifying
|
||||||
|
``port=None`` when constructing a ``SanicTestClient``
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1399 <https://github.com/huge-success/sanic/pull/1399>`_
|
||||||
|
Added the ability to specify middleware on a blueprint group, so that all
|
||||||
|
routes produced from the blueprints in the group have the middleware
|
||||||
|
applied.
|
||||||
|
|
||||||
|
*
|
||||||
|
`#1442 <https://github.com/huge-success/sanic/pull/1442>`_
|
||||||
|
Allow the the use the ``SANIC_ACCESS_LOG`` environment variable to
|
||||||
|
enable/disable the access log when not explicitly passed to ``app.run()``.
|
||||||
|
This allows the access log to be disabled for example when running via
|
||||||
|
gunicorn.
|
||||||
|
|
||||||
|
Developer infrastructure
|
||||||
|
************************
|
||||||
|
|
||||||
|
* `#1529 <https://github.com/huge-success/sanic/pull/1529>`_ Update project PyPI credentials
|
||||||
|
* `#1515 <https://github.com/huge-success/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
|
||||||
|
* `#1490 <https://github.com/huge-success/sanic/pull/1490>`_ Fix python version in doc build
|
||||||
|
* `#1478 <https://github.com/huge-success/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
|
||||||
|
* `#1464 <https://github.com/huge-success/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
**********************
|
||||||
|
|
||||||
|
* `#1516 <https://github.com/huge-success/sanic/pull/1516>`_ Fix typo at the exception documentation
|
||||||
|
* `#1510 <https://github.com/huge-success/sanic/pull/1510>`_ fix typo in Asyncio example
|
||||||
|
* `#1486 <https://github.com/huge-success/sanic/pull/1486>`_ Documentation typo
|
||||||
|
* `#1477 <https://github.com/huge-success/sanic/pull/1477>`_ Fix grammar in README.md
|
||||||
|
* `#1489 <https://github.com/huge-success/sanic/pull/1489>`_ Added "databases" to the extensions list
|
||||||
|
* `#1483 <https://github.com/huge-success/sanic/pull/1483>`_ Add sanic-zipkin to extensions list
|
||||||
|
* `#1487 <https://github.com/huge-success/sanic/pull/1487>`_ Removed link to deleted repo, Sanic-OAuth, from the extensions list
|
||||||
|
* `#1460 <https://github.com/huge-success/sanic/pull/1460>`_ 18.12 changelog
|
||||||
|
* `#1449 <https://github.com/huge-success/sanic/pull/1449>`_ Add example of amending request object
|
||||||
|
* `#1446 <https://github.com/huge-success/sanic/pull/1446>`_ Update README
|
||||||
|
* `#1444 <https://github.com/huge-success/sanic/pull/1444>`_ Update README
|
||||||
|
* `#1443 <https://github.com/huge-success/sanic/pull/1443>`_ Update README, including new logo
|
||||||
|
* `#1440 <https://github.com/huge-success/sanic/pull/1440>`_ fix minor type and pip install instruction mismatch
|
||||||
|
* `#1424 <https://github.com/huge-success/sanic/pull/1424>`_ Documentation Enhancements
|
||||||
|
|
||||||
|
Note: 19.3.0 was skipped for packagement purposes and not released on PyPI
|
||||||
|
|
||||||
|
Version 18.12
|
||||||
|
=============
|
||||||
|
|
||||||
|
18.12.0
|
||||||
|
*******
|
||||||
|
|
||||||
|
*
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
|
||||||
|
* Improved codebase test coverage from 81% to 91%.
|
||||||
|
* Added stream_large_files and host examples in static_file document
|
||||||
|
* Added methods to append and finish body content on Request (#1379)
|
||||||
|
* Integrated with .appveyor.yml for windows ci support
|
||||||
|
* Added documentation for AF_INET6 and AF_UNIX socket usage
|
||||||
|
* Adopt black/isort for codestyle
|
||||||
|
* Cancel task when connection_lost
|
||||||
|
* Simplify request ip and port retrieval logic
|
||||||
|
* Handle config error in load config file.
|
||||||
|
* Integrate with codecov for CI
|
||||||
|
* Add missed documentation for config section.
|
||||||
|
* Deprecate Handler.log
|
||||||
|
* Pinned httptools requirement to version 0.0.10+
|
||||||
|
|
||||||
|
*
|
||||||
|
Fixes:
|
||||||
|
|
||||||
|
|
||||||
|
* Fix ``remove_entity_headers`` helper function (#1415)
|
||||||
|
* Fix TypeError when use Blueprint.group() to group blueprint with default url_prefix, Use os.path.normpath to avoid invalid url_prefix like api//v1
|
||||||
|
f8a6af1 Rename the ``http`` module to ``helpers`` to prevent conflicts with the built-in Python http library (fixes #1323)
|
||||||
|
* Fix unittests on windows
|
||||||
|
* Fix Namespacing of sanic logger
|
||||||
|
* Fix missing quotes in decorator example
|
||||||
|
* Fix redirect with quoted param
|
||||||
|
* Fix doc for latest blueprint code
|
||||||
|
* Fix build of latex documentation relating to markdown lists
|
||||||
|
* Fix loop exception handling in app.py
|
||||||
|
* Fix content length mismatch in windows and other platform
|
||||||
|
* Fix Range header handling for static files (#1402)
|
||||||
|
* Fix the logger and make it work (#1397)
|
||||||
|
* Fix type pikcle->pickle in multiprocessing test
|
||||||
|
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
||||||
|
* Fix document for logging
|
||||||
|
|
||||||
|
Version 0.8
|
||||||
|
===========
|
||||||
|
|
||||||
|
0.8.3
|
||||||
|
*****
|
||||||
|
|
||||||
|
* Changes:
|
||||||
|
|
||||||
|
* Ownership changed to org 'huge-success'
|
||||||
|
|
||||||
|
0.8.0
|
||||||
|
*****
|
||||||
|
|
||||||
|
* Changes:
|
||||||
|
|
||||||
|
|
||||||
|
* Add Server-Sent Events extension (Innokenty Lebedev)
|
||||||
|
* Graceful handling of request_handler_task cancellation (Ashley Sommer)
|
||||||
|
* Sanitize URL before redirection (aveao)
|
||||||
|
* Add url_bytes to request (johndoe46)
|
||||||
|
* py37 support for travisci (yunstanford)
|
||||||
|
* Auto reloader support for OSX (garyo)
|
||||||
|
* Add UUID route support (Volodymyr Maksymiv)
|
||||||
|
* Add pausable response streams (Ashley Sommer)
|
||||||
|
* Add weakref to request slots (vopankov)
|
||||||
|
* remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
|
||||||
|
* Allow streaming handlers in add_route (kinware)
|
||||||
|
* use travis_retry for tox (Raphael Deem)
|
||||||
|
* update aiohttp version for test client (yunstanford)
|
||||||
|
* add redirect import for clarity (yingshaoxo)
|
||||||
|
* Update HTTP Entity headers (Arnulfo Solís)
|
||||||
|
* Add register_listener method (Stephan Fitzpatrick)
|
||||||
|
* Remove uvloop/ujson dependencies for Windows (abuckenheimer)
|
||||||
|
* Content-length header on 204/304 responses (Arnulfo Solís)
|
||||||
|
* Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
|
||||||
|
* Update development status from pre-alpha to beta (Maksim Anisenkov)
|
||||||
|
* KeepAlive Timout log level changed to debug (Arnulfo Solís)
|
||||||
|
* Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
||||||
|
* Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
|
||||||
|
* Add support for blueprint groups and nesting (Elias Tarhini)
|
||||||
|
* Remove uvloop for windows setup (Aleksandr Kurlov)
|
||||||
|
* Auto Reload (Yaser Amari)
|
||||||
|
* Documentation updates/fixups (multiple contributors)
|
||||||
|
|
||||||
|
* Fixes:
|
||||||
|
|
||||||
|
|
||||||
|
* Fix: auto_reload in Linux (Ashley Sommer)
|
||||||
|
* Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
|
||||||
|
* Fix: disable auto_reload by default on windows (abuckenheimer)
|
||||||
|
* Fix (1143): Turn off access log with gunicorn (hqy)
|
||||||
|
* Fix (1268): Support status code for file response (Cosmo Borsky)
|
||||||
|
* Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
|
||||||
|
* Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
|
||||||
|
* Fix (1242): Responses for CI header (yunstanford)
|
||||||
|
* Fix (1237): add version constraint for websockets (yunstanford)
|
||||||
|
* Fix (1231): memory leak - always release resource (Phillip Xu)
|
||||||
|
* Fix (1221): make request truthy if transport exists (Raphael Deem)
|
||||||
|
* Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
|
||||||
|
* Fix try_everything examples (PyManiacGR, kot83)
|
||||||
|
* Fix (1158): default to auto_reload in debug mode (Raphael Deem)
|
||||||
|
* Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
|
||||||
|
* Fix: raw requires bytes-like object (cloudship)
|
||||||
|
* Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
|
||||||
|
* Fix: Bug in multipart/form-data parser (DirkGuijt)
|
||||||
|
* Fix: Exception for missing parameter when value is null (NyanKiyoshi)
|
||||||
|
* Fix: Parameter check (Howie Hu)
|
||||||
|
* Fix (1089): Routing issue with named parameters and different methods (yunstanford)
|
||||||
|
* Fix (1085): Signal handling in multi-worker mode (yunstanford)
|
||||||
|
* Fix: single quote in readme.rst (Cosven)
|
||||||
|
* Fix: method typos (Dmitry Dygalo)
|
||||||
|
* Fix: log_response correct output for ip and port (Wibowo Arindrarto)
|
||||||
|
* Fix (1042): Exception Handling (Raphael Deem)
|
||||||
|
* Fix: Chinese URIs (Howie Hu)
|
||||||
|
* Fix (1079): timeout bug when self.transport is None (Raphael Deem)
|
||||||
|
* Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
|
||||||
|
* Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
|
||||||
|
* Fix (1065): allow add_task after server starts (Raphael Deem)
|
||||||
|
* Fix (1061): double quotes in unauthorized exception (Raphael Deem)
|
||||||
|
* Fix (1062): inject the app in add_task method (Raphael Deem)
|
||||||
|
* Fix: update environment.yml for readthedocs (Eli Uriegas)
|
||||||
|
* Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
|
||||||
|
* Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
|
||||||
|
* Fix: IPv6 Address and Socket Data Format (Dan Palmer)
|
||||||
|
|
||||||
|
Note: Changelog was unmaintained between 0.1 and 0.7
|
||||||
|
|
||||||
|
Version 0.1
|
||||||
|
===========
|
||||||
|
|
||||||
|
|
||||||
|
0.1.7
|
||||||
|
*****
|
||||||
|
|
||||||
|
* Reversed static url and directory arguments to meet spec
|
||||||
|
|
||||||
|
0.1.6
|
||||||
|
*****
|
||||||
|
|
||||||
|
* Static files
|
||||||
|
* Lazy Cookie Loading
|
||||||
|
|
||||||
|
0.1.5
|
||||||
|
*****
|
||||||
|
|
||||||
|
* Cookies
|
||||||
|
* Blueprint listeners and ordering
|
||||||
|
* Faster Router
|
||||||
|
* Fix: Incomplete file reads on medium+ sized post requests
|
||||||
|
* Breaking: after_start and before_stop now pass sanic as their first argument
|
||||||
|
|
||||||
|
0.1.4
|
||||||
|
*****
|
||||||
|
|
||||||
|
* Multiprocessing
|
||||||
|
|
||||||
|
0.1.3
|
||||||
|
*****
|
||||||
|
|
||||||
|
* Blueprint support
|
||||||
|
* Faster Response processing
|
||||||
|
|
||||||
|
0.1.1 - 0.1.2
|
||||||
|
*************
|
||||||
|
|
||||||
|
* Struggling to update pypi via CI
|
||||||
|
|
||||||
|
0.1.0
|
||||||
|
*****
|
||||||
|
|
||||||
|
* Released to public
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
We are committed to providing a friendly, safe and welcoming environment for all,
|
|
||||||
regardless of gender, sexual orientation, disability, ethnicity, religion,
|
|
||||||
or similar personal characteristic.
|
|
||||||
Our [code of conduct](./CONDUCT.md) sets the standards for behavior.
|
|
||||||
|
|
||||||
## 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. All pull requests must be consistent with the existing code.
|
|
||||||
5. If you decide to remove/change anything from any common interface
|
|
||||||
a deprecation message should accompany it.
|
|
||||||
6. If you implement a new feature you should have at least one unit
|
|
||||||
test to accompany it.
|
|
||||||
7. An example must be one of the following:
|
|
||||||
* Example of how to use Sanic
|
|
||||||
* Example of how to use Sanic extensions
|
|
||||||
* Example of how to use Sanic and asynchronous library
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
252
CONTRIBUTING.rst
Normal file
252
CONTRIBUTING.rst
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
We are committed to providing a friendly, safe and welcoming environment for all,
|
||||||
|
regardless of gender, sexual orientation, disability, ethnicity, religion,
|
||||||
|
or similar personal characteristic.
|
||||||
|
Our `code of conduct <./CONDUCT.md>`_ sets the standards for behavior.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip3 install -e . ".[dev]"
|
||||||
|
|
||||||
|
Dependency Changes
|
||||||
|
------------------
|
||||||
|
|
||||||
|
``Sanic`` doesn't use ``requirements*.txt`` files to manage any kind of dependencies related to it in order to simplify the
|
||||||
|
effort required in managing the dependencies. Please make sure you have read and understood the following section of
|
||||||
|
the document that explains the way ``sanic`` manages dependencies inside the ``setup.py`` file.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Dependency Type
|
||||||
|
- Usage
|
||||||
|
- Installation
|
||||||
|
* - requirements
|
||||||
|
- Bare minimum dependencies required for sanic to function
|
||||||
|
- ``pip3 install -e .``
|
||||||
|
* - tests_require / extras_require['test']
|
||||||
|
- Dependencies required to run the Unit Tests for ``sanic``
|
||||||
|
- ``pip3 install -e '.[test]'``
|
||||||
|
* - extras_require['dev']
|
||||||
|
- Additional Development requirements to add contributing
|
||||||
|
- ``pip3 install -e '.[dev]'``
|
||||||
|
* - extras_require['docs']
|
||||||
|
- Dependencies required to enable building and enhancing sanic documentation
|
||||||
|
- ``pip3 install -e '.[docs]'``
|
||||||
|
|
||||||
|
|
||||||
|
Running all tests
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
To run the tests for Sanic it is recommended to use tox like so:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox
|
||||||
|
|
||||||
|
See it's that simple!
|
||||||
|
|
||||||
|
``tox.ini`` contains different environments. Running ``tox`` without any arguments will
|
||||||
|
run all unittests, perform lint and other checks.
|
||||||
|
|
||||||
|
Run unittests
|
||||||
|
-------------
|
||||||
|
|
||||||
|
``tox`` environment -> ``[testenv]``
|
||||||
|
|
||||||
|
To execute only unittests, run ``tox`` with environment like so:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -e py36 -v -- tests/test_config.py
|
||||||
|
# or
|
||||||
|
tox -e py37 -v -- tests/test_config.py
|
||||||
|
|
||||||
|
Run lint checks
|
||||||
|
---------------
|
||||||
|
|
||||||
|
``tox`` environment -> ``[testenv:lint]``
|
||||||
|
|
||||||
|
Permform ``flake8``\ , ``black`` and ``isort`` checks.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -e lint
|
||||||
|
|
||||||
|
Run other checks
|
||||||
|
----------------
|
||||||
|
|
||||||
|
``tox`` environment -> ``[testenv:check]``
|
||||||
|
|
||||||
|
Perform other checks.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -e check
|
||||||
|
|
||||||
|
Run Static Analysis
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
``tox`` environment -> ``[testenv:security]``
|
||||||
|
|
||||||
|
Perform static analysis security scan
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -e security
|
||||||
|
|
||||||
|
Run Documentation sanity check
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
``tox`` environment -> ``[testenv:docs]``
|
||||||
|
|
||||||
|
Perform sanity check on documentation
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -e docs
|
||||||
|
|
||||||
|
|
||||||
|
Code Style
|
||||||
|
----------
|
||||||
|
|
||||||
|
To maintain the code consistency, Sanic uses following tools.
|
||||||
|
|
||||||
|
|
||||||
|
#. `isort <https://github.com/timothycrosley/isort>`_
|
||||||
|
#. `black <https://github.com/python/black>`_
|
||||||
|
#. `flake8 <https://github.com/PyCQA/flake8>`_
|
||||||
|
|
||||||
|
isort
|
||||||
|
*****
|
||||||
|
|
||||||
|
``isort`` sorts Python imports. It divides imports into three
|
||||||
|
categories sorted each in alphabetical order.
|
||||||
|
|
||||||
|
|
||||||
|
#. built-in
|
||||||
|
#. third-party
|
||||||
|
#. project-specific
|
||||||
|
|
||||||
|
black
|
||||||
|
*****
|
||||||
|
|
||||||
|
``black`` is a Python code formatter.
|
||||||
|
|
||||||
|
flake8
|
||||||
|
******
|
||||||
|
|
||||||
|
``flake8`` is a Python style guide that wraps following tools into one.
|
||||||
|
|
||||||
|
|
||||||
|
#. PyFlakes
|
||||||
|
#. pycodestyle
|
||||||
|
#. Ned Batchelder's McCabe script
|
||||||
|
|
||||||
|
``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks.
|
||||||
|
|
||||||
|
Refer `tox <https://tox.readthedocs.io/en/latest/index.html>`_ documentation for more details.
|
||||||
|
|
||||||
|
Pull requests
|
||||||
|
-------------
|
||||||
|
|
||||||
|
So the pull request approval rules are pretty simple:
|
||||||
|
|
||||||
|
#. All pull requests must have a changelog details associated with it.
|
||||||
|
#. 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.
|
||||||
|
#. All pull requests must be consistent with the existing code.
|
||||||
|
#. 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.
|
||||||
|
#. An example must be one of the following:
|
||||||
|
|
||||||
|
* Example of how to use Sanic
|
||||||
|
* Example of how to use Sanic extensions
|
||||||
|
* Example of how to use Sanic and asynchronous library
|
||||||
|
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
---------
|
||||||
|
|
||||||
|
It is mandatory to add documentation for Change log as part of your Pull request when you fix/contribute something
|
||||||
|
to the ``sanic`` community. This will enable us in generating better and well defined change logs during the
|
||||||
|
release which can aid community users in a great way.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Single line explaining the details of the PR in brief
|
||||||
|
|
||||||
|
Detailed description of what the PR is about and what changes or enhancements are being done.
|
||||||
|
No need to include examples or any other details here. But it is important that you provide
|
||||||
|
enough context here to let user understand what this change is all about and why it is being
|
||||||
|
introduced into the ``sanic`` codebase.
|
||||||
|
|
||||||
|
Make sure you leave an line space after the first line to make sure the document rendering is clean
|
||||||
|
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Contribution Type
|
||||||
|
- Changelog file name format
|
||||||
|
- Changelog file location
|
||||||
|
* - Features
|
||||||
|
- <git_issue>.feature.rst
|
||||||
|
- ``changelogs``
|
||||||
|
* - Bugfixes
|
||||||
|
- <git_issue>.bugfix.rst
|
||||||
|
- ``changelogs``
|
||||||
|
* - Improved Documentation
|
||||||
|
- <git_issue>.doc.rst
|
||||||
|
- ``changelogs``
|
||||||
|
* - Deprecations and Removals
|
||||||
|
- <git_issue>.removal.rst
|
||||||
|
- ``changelogs``
|
||||||
|
* - Miscellaneous internal changes
|
||||||
|
- <git_issue>.misc.rst
|
||||||
|
- ``changelogs``
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sphinx-apidoc -fo docs/_api/ sanic
|
||||||
|
sphinx-build -b html docs docs/_build
|
||||||
|
|
||||||
|
# There is a simple make command provided to ease the work required in generating
|
||||||
|
# the documentation
|
||||||
|
make docs
|
||||||
|
|
||||||
|
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.
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2016-present Channel Cat
|
Copyright (c) 2016-present Sanic Community
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
18
MANIFEST.in
18
MANIFEST.in
@@ -1,7 +1,15 @@
|
|||||||
include README.rst
|
# Non Code related contents
|
||||||
include MANIFEST.in
|
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include setup.py
|
include README.rst
|
||||||
|
include pyproject.toml
|
||||||
|
|
||||||
recursive-exclude * __pycache__
|
# Setup
|
||||||
recursive-exclude * *.py[co]
|
include setup.py
|
||||||
|
include Makefile
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
include .coveragerc
|
||||||
|
graft tests
|
||||||
|
|
||||||
|
global-exclude __pycache__
|
||||||
|
global-exclude *.py[co]
|
||||||
95
Makefile
95
Makefile
@@ -1,4 +1,95 @@
|
|||||||
test:
|
.PHONY: help test test-coverage install docker-test black fix-import beautify
|
||||||
find . -name "*.pyc" -delete
|
|
||||||
|
.DEFAULT: help
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo "test"
|
||||||
|
@echo " Run Sanic Unit Tests"
|
||||||
|
@echo "test-coverage"
|
||||||
|
@echo " Run Sanic Unit Tests with Coverage"
|
||||||
|
@echo "install"
|
||||||
|
@echo " Install Sanic"
|
||||||
|
@echo "docker-test"
|
||||||
|
@echo " Run Sanic Unit Tests using Docker"
|
||||||
|
@echo "black"
|
||||||
|
@echo " Analyze and fix linting issues using Black"
|
||||||
|
@echo "fix-import"
|
||||||
|
@echo " Analyze and fix import order using isort"
|
||||||
|
@echo "beautify [sort_imports=1] [include_tests=1]"
|
||||||
|
@echo " Analyze and fix linting issue using black and optionally fix import sort using isort"
|
||||||
|
@echo ""
|
||||||
|
@echo "docs"
|
||||||
|
@echo " Generate Sanic documentation"
|
||||||
|
@echo ""
|
||||||
|
@echo "clean-docs"
|
||||||
|
@echo " Clean Sanic documentation"
|
||||||
|
@echo ""
|
||||||
|
@echo "docs-test"
|
||||||
|
@echo " Test Sanic Documentation for errors"
|
||||||
|
@echo ""
|
||||||
|
@echo "changelog"
|
||||||
|
@echo " Generate changelog for Sanic to prepare for new release"
|
||||||
|
@echo ""
|
||||||
|
@echo "release"
|
||||||
|
@echo " Prepare Sanic for a new changes by version bump and changelog"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
|
||||||
|
clean:
|
||||||
|
find . ! -path "./.eggs/*" -name "*.pyc" -exec rm {} \;
|
||||||
|
find . ! -path "./.eggs/*" -name "*.pyo" -exec rm {} \;
|
||||||
|
find . ! -path "./.eggs/*" -name ".coverage" -exec rm {} \;
|
||||||
|
rm -rf build/* > /dev/null 2>&1
|
||||||
|
rm -rf dist/* > /dev/null 2>&1
|
||||||
|
|
||||||
|
test: clean
|
||||||
|
python setup.py test
|
||||||
|
|
||||||
|
test-coverage: clean
|
||||||
|
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
|
||||||
|
|
||||||
|
install:
|
||||||
|
python setup.py install
|
||||||
|
|
||||||
|
docker-test: clean
|
||||||
docker build -t sanic/test-image -f docker/Dockerfile .
|
docker build -t sanic/test-image -f docker/Dockerfile .
|
||||||
docker run -t sanic/test-image tox
|
docker run -t sanic/test-image tox
|
||||||
|
|
||||||
|
beautify: black
|
||||||
|
ifdef sort_imports
|
||||||
|
ifdef include_tests
|
||||||
|
$(warning It is suggested that you do not run sort import on tests)
|
||||||
|
isort -rc sanic tests
|
||||||
|
else
|
||||||
|
$(info Sorting Imports)
|
||||||
|
isort -rc sanic tests
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
black:
|
||||||
|
black --config ./.black.toml sanic tests
|
||||||
|
|
||||||
|
fix-import: black
|
||||||
|
isort sanic tests
|
||||||
|
|
||||||
|
|
||||||
|
docs-clean:
|
||||||
|
cd docs && make clean
|
||||||
|
|
||||||
|
docs: docs-clean
|
||||||
|
cd docs && make html
|
||||||
|
|
||||||
|
docs-test: docs-clean
|
||||||
|
cd docs && make dummy
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
python scripts/changelog.py
|
||||||
|
|
||||||
|
release:
|
||||||
|
ifdef version
|
||||||
|
python scripts/release.py --release-version ${version} --generate-changelog
|
||||||
|
else
|
||||||
|
python scripts/release.py --generate-changelog
|
||||||
|
endif
|
||||||
|
|
||||||
|
|||||||
168
README.rst
168
README.rst
@@ -1,15 +1,88 @@
|
|||||||
Sanic
|
.. image:: https://raw.githubusercontent.com/huge-success/sanic-assets/master/png/sanic-framework-logo-400x97.png
|
||||||
=====
|
:alt: Sanic | Build fast. Run fast.
|
||||||
|
|
||||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |AppVeyor Build Status| |Documentation| |Codecov| |PyPI| |PyPI version| |Code style black|
|
Sanic | Build fast. Run fast.
|
||||||
|
=============================
|
||||||
|
|
||||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
|
.. start-badges
|
||||||
|
|
||||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
.. list-table::
|
||||||
|
:stub-columns: 1
|
||||||
|
|
||||||
Sanic is developed `on GitHub <https://github.com/huge-success/sanic/>`_. We also have `a community discussion board <https://community.sanicframework.org/>`_. Contributions are welcome!
|
* - Build
|
||||||
|
- | |Build Status| |AppVeyor Build Status| |Codecov|
|
||||||
|
* - Docs
|
||||||
|
- |Documentation|
|
||||||
|
* - Package
|
||||||
|
- | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black|
|
||||||
|
* - Support
|
||||||
|
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby| |Awesome|
|
||||||
|
* - Stats
|
||||||
|
- | |Downloads| |Conda downloads|
|
||||||
|
|
||||||
|
.. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg
|
||||||
|
:target: https://community.sanicframework.org/
|
||||||
|
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
|
||||||
|
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||||
|
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
|
||||||
|
:target: https://codecov.io/gh/huge-success/sanic
|
||||||
|
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/huge-success/sanic
|
||||||
|
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
||||||
|
:target: https://ci.appveyor.com/project/huge-success/sanic
|
||||||
|
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||||
|
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
||||||
|
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
||||||
|
:target: https://pypi.python.org/pypi/sanic/
|
||||||
|
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
||||||
|
:target: https://pypi.python.org/pypi/sanic/
|
||||||
|
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||||
|
:target: https://github.com/ambv/black
|
||||||
|
.. |Wheel| image:: https://img.shields.io/pypi/wheel/sanic.svg
|
||||||
|
:alt: PyPI Wheel
|
||||||
|
:target: https://pypi.python.org/pypi/sanic
|
||||||
|
.. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg
|
||||||
|
:alt: Supported implementations
|
||||||
|
:target: https://pypi.python.org/pypi/sanic
|
||||||
|
.. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg
|
||||||
|
:alt: Awesome Sanic List
|
||||||
|
:target: https://github.com/mekicha/awesome-sanic
|
||||||
|
.. |Downloads| image:: https://pepy.tech/badge/sanic/month
|
||||||
|
:alt: Downloads
|
||||||
|
:target: https://pepy.tech/project/sanic
|
||||||
|
.. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg
|
||||||
|
:alt: Downloads
|
||||||
|
:target: https://anaconda.org/conda-forge/sanic
|
||||||
|
|
||||||
|
.. end-badges
|
||||||
|
|
||||||
|
Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
||||||
|
|
||||||
|
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
|
||||||
|
|
||||||
|
The project is maintained by the community, for the community. **Contributions are welcome!**
|
||||||
|
|
||||||
|
The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
``pip3 install sanic``
|
||||||
|
|
||||||
|
Sanic makes use of ``uvloop`` and ``ujson`` to help with performance. If you do not want to use those packages, simply add an environmental variable ``SANIC_NO_UVLOOP=true`` or ``SANIC_NO_UJSON=true`` at install time.
|
||||||
|
|
||||||
|
.. code:: shell
|
||||||
|
|
||||||
|
$ export SANIC_NO_UVLOOP=true
|
||||||
|
$ export SANIC_NO_UJSON=true
|
||||||
|
$ pip3 install --no-binary :all: sanic
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
|
||||||
|
use ``sanic`` with ``ujson`` dependency.
|
||||||
|
|
||||||
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/huge-success/sanic/issues/396>`_ that we use to track those projects!
|
|
||||||
|
|
||||||
Hello World Example
|
Hello World Example
|
||||||
-------------------
|
-------------------
|
||||||
@@ -27,17 +100,27 @@ Hello World Example
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=8000)
|
app.run(host='0.0.0.0', port=8000)
|
||||||
|
|
||||||
|
Sanic can now be easily run using ``python3 hello.py``.
|
||||||
|
|
||||||
Installation
|
.. code::
|
||||||
------------
|
|
||||||
|
|
||||||
- ``pip install sanic``
|
[2018-12-30 11:37:41 +0200] [13564] [INFO] Goin' Fast @ http://0.0.0.0:8000
|
||||||
|
[2018-12-30 11:37:41 +0200] [13564] [INFO] Starting worker [13564]
|
||||||
|
|
||||||
To install sanic without uvloop or ujson using bash, you can provide either or both of these environmental variables
|
And, we can verify it is working: ``curl localhost:8000 -i``
|
||||||
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 pip install sanic``
|
.. code::
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Connection: keep-alive
|
||||||
|
Keep-Alive: 5
|
||||||
|
Content-Length: 17
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"hello":"world"}
|
||||||
|
|
||||||
|
**Now, let's go build something fast!**
|
||||||
|
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
@@ -45,59 +128,18 @@ Documentation
|
|||||||
|
|
||||||
`Documentation on Readthedocs <http://sanic.readthedocs.io/>`_.
|
`Documentation on Readthedocs <http://sanic.readthedocs.io/>`_.
|
||||||
|
|
||||||
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
|
Changelog
|
||||||
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
---------
|
||||||
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
|
|
||||||
:target: https://codecov.io/gh/huge-success/sanic
|
`Release Changelogs <https://github.com/huge-success/sanic/blob/master/CHANGELOG.rst>`_.
|
||||||
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
|
|
||||||
:target: https://travis-ci.org/huge-success/sanic
|
|
||||||
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
|
||||||
:target: https://ci.appveyor.com/project/huge-success/sanic
|
|
||||||
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
|
||||||
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
|
||||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
|
||||||
:target: https://pypi.python.org/pypi/sanic/
|
|
||||||
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
|
||||||
:target: https://pypi.python.org/pypi/sanic/
|
|
||||||
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
||||||
:target: https://github.com/ambv/black
|
|
||||||
|
|
||||||
Questions and Discussion
|
Questions and Discussion
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
`Ask a question or join the conversation <https://community.sanicframework.org/>`_.
|
`Ask a question or join the conversation <https://community.sanicframework.org/>`_.
|
||||||
|
|
||||||
|
Contribution
|
||||||
|
------------
|
||||||
|
|
||||||
Examples
|
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/huge-success/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://sanic.readthedocs.io/en/latest/sanic/contributing.html>`_.
|
||||||
--------
|
|
||||||
`Non-Core examples <https://github.com/huge-success/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
|
|
||||||
|
|
||||||
`Extensions <https://github.com/huge-success/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
|
|
||||||
|
|
||||||
`Projects <https://github.com/huge-success/sanic/wiki/Projects/>`_. Sanic in production use.
|
|
||||||
|
|
||||||
|
|
||||||
Final Thoughts
|
|
||||||
--------------
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
▄▄▄▄▄
|
|
||||||
▀▀▀██████▄▄▄ _______________
|
|
||||||
▄▄▄▄▄ █████████▄ / \
|
|
||||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
|
||||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
|
||||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
|
||||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
|
||||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
|
||||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
|
||||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
|
||||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
|
||||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
|
||||||
▀▀█████████▀
|
|
||||||
▄▄██▀██████▀█
|
|
||||||
▄██▀ ▀▀▀ █
|
|
||||||
▄█ ▐▌
|
|
||||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
|
||||||
▌ ▐ ▀▀▄▄▄▀
|
|
||||||
▀▀▄▄▀
|
|
||||||
|
|||||||
25
SECURITY.md
Normal file
25
SECURITY.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
|
||||||
|
|
||||||
|
| Version | LTS | Supported |
|
||||||
|
| ------- | ------------------ | ------------------ |
|
||||||
|
| 19.6.0 | | :white_check_mark: |
|
||||||
|
| 19.3.1 | | :heavy_check_mark: |
|
||||||
|
| 18.12.0 | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|
| 0.8.3 | | :x: |
|
||||||
|
| 0.7.0 | | :x: |
|
||||||
|
| 0.6.0 | | :x: |
|
||||||
|
| 0.5.4 | | :x: |
|
||||||
|
| 0.4.1 | | :x: |
|
||||||
|
| 0.3.1 | | :x: |
|
||||||
|
| 0.2.0 | | :x: |
|
||||||
|
| 0.1.9 | | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button.
|
||||||
|
|
||||||
|
This will help to not publicize the issue until the team can address it and resolve it.
|
||||||
2
changelogs/.gitignore
vendored
Normal file
2
changelogs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Except this file
|
||||||
|
!.gitignore
|
||||||
11
docs/conf.py
11
docs/conf.py
@@ -10,10 +10,8 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Add support for Markdown documentation using Recommonmark
|
|
||||||
from recommonmark.parser import CommonMarkParser
|
|
||||||
|
|
||||||
# Add support for auto-doc
|
# Add support for auto-doc
|
||||||
|
import recommonmark
|
||||||
from recommonmark.transform import AutoStructify
|
from recommonmark.transform import AutoStructify
|
||||||
|
|
||||||
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
|
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
|
||||||
@@ -25,12 +23,11 @@ import sanic
|
|||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio']
|
extensions = ['sphinx.ext.autodoc', "recommonmark"]
|
||||||
|
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
|
||||||
# Enable support for both Restructured Text and Markdown
|
# Enable support for both Restructured Text and Markdown
|
||||||
source_parsers = {'.md': CommonMarkParser}
|
|
||||||
source_suffix = ['.rst', '.md']
|
source_suffix = ['.rst', '.md']
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
@@ -38,7 +35,7 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'Sanic'
|
project = 'Sanic'
|
||||||
copyright = '2016, Sanic contributors'
|
copyright = '2018, Sanic contributors'
|
||||||
author = 'Sanic contributors'
|
author = 'Sanic contributors'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
@@ -149,6 +146,6 @@ suppress_warnings = ['image.nonlocal_uri']
|
|||||||
def setup(app):
|
def setup(app):
|
||||||
app.add_config_value('recommonmark_config', {
|
app.add_config_value('recommonmark_config', {
|
||||||
'enable_eval_rst': True,
|
'enable_eval_rst': True,
|
||||||
'enable_auto_doc_ref': True,
|
'enable_auto_doc_ref': False,
|
||||||
}, True)
|
}, True)
|
||||||
app.add_transform(AutoStructify)
|
app.add_transform(AutoStructify)
|
||||||
|
|||||||
468
docs/index.html
Normal file
468
docs/index.html
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="generator" content="Docutils 0.15.2: http://docutils.sourceforge.net/" />
|
||||||
|
<title>index.rst</title>
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
/*
|
||||||
|
:Author: David Goodger (goodger@python.org)
|
||||||
|
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||||
|
:Copyright: This stylesheet has been placed in the public domain.
|
||||||
|
|
||||||
|
Default cascading style sheet for the HTML output of Docutils.
|
||||||
|
|
||||||
|
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||||
|
customize this style sheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* used to remove borders from tables and images */
|
||||||
|
.borderless, table.borderless td, table.borderless th {
|
||||||
|
border: 0 }
|
||||||
|
|
||||||
|
table.borderless td, table.borderless th {
|
||||||
|
/* Override padding for "table.docutils td" with "! important".
|
||||||
|
The right padding separates the table cells. */
|
||||||
|
padding: 0 0.5em 0 0 ! important }
|
||||||
|
|
||||||
|
.first {
|
||||||
|
/* Override more specific margin styles with "! important". */
|
||||||
|
margin-top: 0 ! important }
|
||||||
|
|
||||||
|
.last, .with-subtitle {
|
||||||
|
margin-bottom: 0 ! important }
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none }
|
||||||
|
|
||||||
|
.subscript {
|
||||||
|
vertical-align: sub;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
.superscript {
|
||||||
|
vertical-align: super;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
a.toc-backref {
|
||||||
|
text-decoration: none ;
|
||||||
|
color: black }
|
||||||
|
|
||||||
|
blockquote.epigraph {
|
||||||
|
margin: 2em 5em ; }
|
||||||
|
|
||||||
|
dl.docutils dd {
|
||||||
|
margin-bottom: 0.5em }
|
||||||
|
|
||||||
|
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||||
|
dl.docutils dt {
|
||||||
|
font-weight: bold }
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.abstract {
|
||||||
|
margin: 2em 5em }
|
||||||
|
|
||||||
|
div.abstract p.topic-title {
|
||||||
|
font-weight: bold ;
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||||
|
div.hint, div.important, div.note, div.tip, div.warning {
|
||||||
|
margin: 2em ;
|
||||||
|
border: medium outset ;
|
||||||
|
padding: 1em }
|
||||||
|
|
||||||
|
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||||
|
div.important p.admonition-title, div.note p.admonition-title,
|
||||||
|
div.tip p.admonition-title {
|
||||||
|
font-weight: bold ;
|
||||||
|
font-family: sans-serif }
|
||||||
|
|
||||||
|
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||||
|
div.danger p.admonition-title, div.error p.admonition-title,
|
||||||
|
div.warning p.admonition-title, .code .error {
|
||||||
|
color: red ;
|
||||||
|
font-weight: bold ;
|
||||||
|
font-family: sans-serif }
|
||||||
|
|
||||||
|
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||||
|
compound paragraphs.
|
||||||
|
div.compound .compound-first, div.compound .compound-middle {
|
||||||
|
margin-bottom: 0.5em }
|
||||||
|
|
||||||
|
div.compound .compound-last, div.compound .compound-middle {
|
||||||
|
margin-top: 0.5em }
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.dedication {
|
||||||
|
margin: 2em 5em ;
|
||||||
|
text-align: center ;
|
||||||
|
font-style: italic }
|
||||||
|
|
||||||
|
div.dedication p.topic-title {
|
||||||
|
font-weight: bold ;
|
||||||
|
font-style: normal }
|
||||||
|
|
||||||
|
div.figure {
|
||||||
|
margin-left: 2em ;
|
||||||
|
margin-right: 2em }
|
||||||
|
|
||||||
|
div.footer, div.header {
|
||||||
|
clear: both;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
div.line-block {
|
||||||
|
display: block ;
|
||||||
|
margin-top: 1em ;
|
||||||
|
margin-bottom: 1em }
|
||||||
|
|
||||||
|
div.line-block div.line-block {
|
||||||
|
margin-top: 0 ;
|
||||||
|
margin-bottom: 0 ;
|
||||||
|
margin-left: 1.5em }
|
||||||
|
|
||||||
|
div.sidebar {
|
||||||
|
margin: 0 0 0.5em 1em ;
|
||||||
|
border: medium outset ;
|
||||||
|
padding: 1em ;
|
||||||
|
background-color: #ffffee ;
|
||||||
|
width: 40% ;
|
||||||
|
float: right ;
|
||||||
|
clear: right }
|
||||||
|
|
||||||
|
div.sidebar p.rubric {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-size: medium }
|
||||||
|
|
||||||
|
div.system-messages {
|
||||||
|
margin: 5em }
|
||||||
|
|
||||||
|
div.system-messages h1 {
|
||||||
|
color: red }
|
||||||
|
|
||||||
|
div.system-message {
|
||||||
|
border: medium outset ;
|
||||||
|
padding: 1em }
|
||||||
|
|
||||||
|
div.system-message p.system-message-title {
|
||||||
|
color: red ;
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
div.topic {
|
||||||
|
margin: 2em }
|
||||||
|
|
||||||
|
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||||
|
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||||
|
margin-top: 0.4em }
|
||||||
|
|
||||||
|
h1.title {
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
h2.subtitle {
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
hr.docutils {
|
||||||
|
width: 75% }
|
||||||
|
|
||||||
|
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||||
|
clear: left ;
|
||||||
|
float: left ;
|
||||||
|
margin-right: 1em }
|
||||||
|
|
||||||
|
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||||
|
clear: right ;
|
||||||
|
float: right ;
|
||||||
|
margin-left: 1em }
|
||||||
|
|
||||||
|
img.align-center, .figure.align-center, object.align-center {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.align-center {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left }
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
clear: both ;
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right }
|
||||||
|
|
||||||
|
/* reset inner alignment in figures */
|
||||||
|
div.align-right {
|
||||||
|
text-align: inherit }
|
||||||
|
|
||||||
|
/* div.align-center * { */
|
||||||
|
/* text-align: left } */
|
||||||
|
|
||||||
|
.align-top {
|
||||||
|
vertical-align: top }
|
||||||
|
|
||||||
|
.align-middle {
|
||||||
|
vertical-align: middle }
|
||||||
|
|
||||||
|
.align-bottom {
|
||||||
|
vertical-align: bottom }
|
||||||
|
|
||||||
|
ol.simple, ul.simple {
|
||||||
|
margin-bottom: 1em }
|
||||||
|
|
||||||
|
ol.arabic {
|
||||||
|
list-style: decimal }
|
||||||
|
|
||||||
|
ol.loweralpha {
|
||||||
|
list-style: lower-alpha }
|
||||||
|
|
||||||
|
ol.upperalpha {
|
||||||
|
list-style: upper-alpha }
|
||||||
|
|
||||||
|
ol.lowerroman {
|
||||||
|
list-style: lower-roman }
|
||||||
|
|
||||||
|
ol.upperroman {
|
||||||
|
list-style: upper-roman }
|
||||||
|
|
||||||
|
p.attribution {
|
||||||
|
text-align: right ;
|
||||||
|
margin-left: 50% }
|
||||||
|
|
||||||
|
p.caption {
|
||||||
|
font-style: italic }
|
||||||
|
|
||||||
|
p.credits {
|
||||||
|
font-style: italic ;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
p.label {
|
||||||
|
white-space: nowrap }
|
||||||
|
|
||||||
|
p.rubric {
|
||||||
|
font-weight: bold ;
|
||||||
|
font-size: larger ;
|
||||||
|
color: maroon ;
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
p.sidebar-title {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-weight: bold ;
|
||||||
|
font-size: larger }
|
||||||
|
|
||||||
|
p.sidebar-subtitle {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
p.topic-title {
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
pre.address {
|
||||||
|
margin-bottom: 0 ;
|
||||||
|
margin-top: 0 ;
|
||||||
|
font: inherit }
|
||||||
|
|
||||||
|
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||||
|
margin-left: 2em ;
|
||||||
|
margin-right: 2em }
|
||||||
|
|
||||||
|
pre.code .ln { color: grey; } /* line numbers */
|
||||||
|
pre.code, code { background-color: #eeeeee }
|
||||||
|
pre.code .comment, code .comment { color: #5C6576 }
|
||||||
|
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||||
|
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||||
|
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||||
|
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||||
|
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||||
|
|
||||||
|
span.classifier {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-style: oblique }
|
||||||
|
|
||||||
|
span.classifier-delimiter {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
span.interpreted {
|
||||||
|
font-family: sans-serif }
|
||||||
|
|
||||||
|
span.option {
|
||||||
|
white-space: nowrap }
|
||||||
|
|
||||||
|
span.pre {
|
||||||
|
white-space: pre }
|
||||||
|
|
||||||
|
span.problematic {
|
||||||
|
color: red }
|
||||||
|
|
||||||
|
span.section-subtitle {
|
||||||
|
/* font-size relative to parent (h1..h6 element) */
|
||||||
|
font-size: 80% }
|
||||||
|
|
||||||
|
table.citation {
|
||||||
|
border-left: solid 1px gray;
|
||||||
|
margin-left: 1px }
|
||||||
|
|
||||||
|
table.docinfo {
|
||||||
|
margin: 2em 4em }
|
||||||
|
|
||||||
|
table.docutils {
|
||||||
|
margin-top: 0.5em ;
|
||||||
|
margin-bottom: 0.5em }
|
||||||
|
|
||||||
|
table.footnote {
|
||||||
|
border-left: solid 1px black;
|
||||||
|
margin-left: 1px }
|
||||||
|
|
||||||
|
table.docutils td, table.docutils th,
|
||||||
|
table.docinfo td, table.docinfo th {
|
||||||
|
padding-left: 0.5em ;
|
||||||
|
padding-right: 0.5em ;
|
||||||
|
vertical-align: top }
|
||||||
|
|
||||||
|
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||||
|
font-weight: bold ;
|
||||||
|
text-align: left ;
|
||||||
|
white-space: nowrap ;
|
||||||
|
padding-left: 0 }
|
||||||
|
|
||||||
|
/* "booktabs" style (no vertical lines) */
|
||||||
|
table.docutils.booktabs {
|
||||||
|
border: 0px;
|
||||||
|
border-top: 2px solid;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
table.docutils.booktabs * {
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
table.docutils.booktabs th {
|
||||||
|
border-bottom: thin solid;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||||
|
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||||
|
font-size: 100% }
|
||||||
|
|
||||||
|
ul.auto-toc {
|
||||||
|
list-style-type: none }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="document">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="section" id="sanic">
|
||||||
|
<h1>Sanic</h1>
|
||||||
|
<p>Sanic is a Python 3.6+ web server and web framework that's written to go fast. It allows the usage of the async/await syntax added in Python 3.5, which makes your code non-blocking and speedy.</p>
|
||||||
|
<p>The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.</p>
|
||||||
|
<p>Sanic is developed <a class="reference external" href="https://github.com/channelcat/sanic/">on GitHub</a>. Contributions are welcome!</p>
|
||||||
|
<div class="section" id="sanic-aspires-to-be-simple">
|
||||||
|
<h2>Sanic aspires to be simple</h2>
|
||||||
|
<pre class="code python literal-block">
|
||||||
|
<span class="keyword namespace">from</span> <span class="name namespace">sanic</span> <span class="keyword namespace">import</span> <span class="name">Sanic</span>
|
||||||
|
<span class="keyword namespace">from</span> <span class="name namespace">sanic.response</span> <span class="keyword namespace">import</span> <span class="name">json</span>
|
||||||
|
|
||||||
|
<span class="name">app</span> <span class="operator">=</span> <span class="name">Sanic</span><span class="punctuation">()</span>
|
||||||
|
|
||||||
|
<span class="name decorator">@app.route</span><span class="punctuation">(</span><span class="literal string double">"/"</span><span class="punctuation">)</span>
|
||||||
|
<span class="name">async</span> <span class="keyword">def</span> <span class="name function">test</span><span class="punctuation">(</span><span class="name">request</span><span class="punctuation">):</span>
|
||||||
|
<span class="keyword">return</span> <span class="name">json</span><span class="punctuation">({</span><span class="literal string double">"hello"</span><span class="punctuation">:</span> <span class="literal string double">"world"</span><span class="punctuation">})</span>
|
||||||
|
|
||||||
|
<span class="keyword">if</span> <span class="name variable magic">__name__</span> <span class="operator">==</span> <span class="literal string double">"__main__"</span><span class="punctuation">:</span>
|
||||||
|
<span class="name">app</span><span class="operator">.</span><span class="name">run</span><span class="punctuation">(</span><span class="name">host</span><span class="operator">=</span><span class="literal string double">"0.0.0.0"</span><span class="punctuation">,</span> <span class="name">port</span><span class="operator">=</span><span class="literal number integer">8000</span><span class="punctuation">)</span>
|
||||||
|
</pre>
|
||||||
|
<div class="admonition note">
|
||||||
|
<p class="first admonition-title">Note</p>
|
||||||
|
<p class="last">Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru
|
||||||
|
December 2020. Official Python support for version 3.5 is set to expire in September 2020.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="guides">
|
||||||
|
<h1>Guides</h1>
|
||||||
|
<div class="system-message">
|
||||||
|
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 6)</p>
|
||||||
|
<p>Unknown directive type "toctree".</p>
|
||||||
|
<pre class="literal-block">
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
sanic/getting_started
|
||||||
|
sanic/config
|
||||||
|
sanic/logging
|
||||||
|
sanic/request_data
|
||||||
|
sanic/response
|
||||||
|
sanic/cookies
|
||||||
|
sanic/routing
|
||||||
|
sanic/blueprints
|
||||||
|
sanic/static_files
|
||||||
|
sanic/versioning
|
||||||
|
sanic/exceptions
|
||||||
|
sanic/middleware
|
||||||
|
sanic/websocket
|
||||||
|
sanic/decorators
|
||||||
|
sanic/streaming
|
||||||
|
sanic/class_based_views
|
||||||
|
sanic/custom_protocol
|
||||||
|
sanic/sockets
|
||||||
|
sanic/ssl
|
||||||
|
sanic/debug_mode
|
||||||
|
sanic/testing
|
||||||
|
sanic/deploying
|
||||||
|
sanic/extensions
|
||||||
|
sanic/examples
|
||||||
|
sanic/changelog
|
||||||
|
sanic/contributing
|
||||||
|
sanic/api_reference
|
||||||
|
sanic/asyncio_python37
|
||||||
|
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="module-documentation">
|
||||||
|
<h1>Module Documentation</h1>
|
||||||
|
<div class="system-message">
|
||||||
|
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 42)</p>
|
||||||
|
<p>Unknown directive type "toctree".</p>
|
||||||
|
<pre class="literal-block">
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li><p class="first"><a href="#id1"><span class="problematic" id="id2">:ref:`genindex`</span></a></p>
|
||||||
|
<div class="system-message" id="id1">
|
||||||
|
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 44); <em><a href="#id2">backlink</a></em></p>
|
||||||
|
<p>Unknown interpreted text role "ref".</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li><p class="first"><a href="#id3"><span class="problematic" id="id4">:ref:`modindex`</span></a></p>
|
||||||
|
<div class="system-message" id="id3">
|
||||||
|
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 45); <em><a href="#id4">backlink</a></em></p>
|
||||||
|
<p>Unknown interpreted text role "ref".</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li><p class="first"><a href="#id5"><span class="problematic" id="id6">:ref:`search`</span></a></p>
|
||||||
|
<div class="system-message" id="id5">
|
||||||
|
<p class="system-message-title">System Message: ERROR/3 (<tt class="docutils">E:/OneDrive/GitHub/sanic/docs/index.rst</tt>, line 46); <em><a href="#id6">backlink</a></em></p>
|
||||||
|
<p>Unknown interpreted text role "ref".</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -7,30 +7,33 @@ Guides
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
sanic/getting_started
|
sanic/getting_started
|
||||||
sanic/routing
|
sanic/config
|
||||||
|
sanic/logging
|
||||||
sanic/request_data
|
sanic/request_data
|
||||||
sanic/response
|
sanic/response
|
||||||
|
sanic/cookies
|
||||||
|
sanic/routing
|
||||||
|
sanic/blueprints
|
||||||
sanic/static_files
|
sanic/static_files
|
||||||
|
sanic/versioning
|
||||||
sanic/exceptions
|
sanic/exceptions
|
||||||
sanic/middleware
|
sanic/middleware
|
||||||
sanic/blueprints
|
|
||||||
sanic/websocket
|
sanic/websocket
|
||||||
sanic/config
|
|
||||||
sanic/cookies
|
|
||||||
sanic/decorators
|
sanic/decorators
|
||||||
sanic/streaming
|
sanic/streaming
|
||||||
sanic/class_based_views
|
sanic/class_based_views
|
||||||
sanic/custom_protocol
|
sanic/custom_protocol
|
||||||
sanic/sockets
|
sanic/sockets
|
||||||
sanic/ssl
|
sanic/ssl
|
||||||
sanic/logging
|
|
||||||
sanic/versioning
|
|
||||||
sanic/debug_mode
|
sanic/debug_mode
|
||||||
sanic/testing
|
sanic/testing
|
||||||
sanic/deploying
|
sanic/deploying
|
||||||
sanic/extensions
|
sanic/extensions
|
||||||
|
sanic/examples
|
||||||
|
sanic/changelog
|
||||||
sanic/contributing
|
sanic/contributing
|
||||||
sanic/api_reference
|
sanic/api_reference
|
||||||
|
sanic/asyncio_python37
|
||||||
|
|
||||||
|
|
||||||
Module Documentation
|
Module Documentation
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ sanic.blueprints module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.blueprint_group module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.blueprint_group
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
sanic.config module
|
sanic.config module
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|||||||
58
docs/sanic/asyncio_python37.rst
Normal file
58
docs/sanic/asyncio_python37.rst
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
Python 3.7 AsyncIO examples
|
||||||
|
###########################
|
||||||
|
|
||||||
|
With Python 3.7 AsyncIO got major update for the following types:
|
||||||
|
|
||||||
|
- asyncio.AbstractEventLoop
|
||||||
|
- asyncio.AbstractServer
|
||||||
|
|
||||||
|
|
||||||
|
This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import socket
|
||||||
|
import os
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def test(request):
|
||||||
|
return json({"hello": "world"})
|
||||||
|
|
||||||
|
|
||||||
|
server_socket = '/tmp/sanic.sock'
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(server_socket)
|
||||||
|
finally:
|
||||||
|
sock.bind(server_socket)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
srv_coro = app.create_server(
|
||||||
|
sock=sock,
|
||||||
|
return_asyncio_server=True,
|
||||||
|
asyncio_server_kwargs=dict(
|
||||||
|
start_serving=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
srv = loop.run_until_complete(srv_coro)
|
||||||
|
try:
|
||||||
|
assert srv.is_serving() is False
|
||||||
|
loop.run_until_complete(srv.start_serving())
|
||||||
|
assert srv.is_serving() is True
|
||||||
|
loop.run_until_complete(srv.serve_forever())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
srv.close()
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
Please note that uvloop does not support these features yet.
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
# Blueprints
|
|
||||||
|
|
||||||
Blueprints are objects that can be used for sub-routing within an application.
|
|
||||||
Instead of adding routes to the application instance, blueprints define similar
|
|
||||||
methods for adding routes, which are then registered with the application in a
|
|
||||||
flexible and pluggable manner.
|
|
||||||
|
|
||||||
Blueprints are especially useful for larger applications, where your
|
|
||||||
application logic can be broken down into several groups or areas of
|
|
||||||
responsibility.
|
|
||||||
|
|
||||||
## My First Blueprint
|
|
||||||
|
|
||||||
The following shows a very simple blueprint that registers a handler-function at
|
|
||||||
the root `/` of your application.
|
|
||||||
|
|
||||||
Suppose you save this file as `my_blueprint.py`, which can be imported into your
|
|
||||||
main application later.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import json
|
|
||||||
from sanic import Blueprint
|
|
||||||
|
|
||||||
bp = Blueprint('my_blueprint')
|
|
||||||
|
|
||||||
@bp.route('/')
|
|
||||||
async def bp_root(request):
|
|
||||||
return json({'my': 'blueprint'})
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Registering blueprints
|
|
||||||
|
|
||||||
Blueprints must be registered with the application.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
from my_blueprint import bp
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
app.blueprint(bp)
|
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
This will add the blueprint to the application and register any routes defined
|
|
||||||
by that blueprint. In this example, the registered routes in the `app.router`
|
|
||||||
will look like:
|
|
||||||
|
|
||||||
```python
|
|
||||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Blueprint groups and nesting
|
|
||||||
|
|
||||||
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
|
|
||||||
|
|
||||||
```
|
|
||||||
api/
|
|
||||||
├──content/
|
|
||||||
│ ├──authors.py
|
|
||||||
│ ├──static.py
|
|
||||||
│ └──__init__.py
|
|
||||||
├──info.py
|
|
||||||
└──__init__.py
|
|
||||||
app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Initialization of this app's blueprint hierarchy could go as follows:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# api/content/authors.py
|
|
||||||
from sanic import Blueprint
|
|
||||||
|
|
||||||
authors = Blueprint('content_authors', url_prefix='/authors')
|
|
||||||
```
|
|
||||||
```python
|
|
||||||
# api/content/static.py
|
|
||||||
from sanic import Blueprint
|
|
||||||
|
|
||||||
static = Blueprint('content_static', url_prefix='/static')
|
|
||||||
```
|
|
||||||
```python
|
|
||||||
# api/content/__init__.py
|
|
||||||
from sanic import Blueprint
|
|
||||||
|
|
||||||
from .static import static
|
|
||||||
from .authors import authors
|
|
||||||
|
|
||||||
content = Blueprint.group(static, authors, url_prefix='/content')
|
|
||||||
```
|
|
||||||
```python
|
|
||||||
# api/info.py
|
|
||||||
from sanic import Blueprint
|
|
||||||
|
|
||||||
info = Blueprint('info', url_prefix='/info')
|
|
||||||
```
|
|
||||||
```python
|
|
||||||
# api/__init__.py
|
|
||||||
from sanic import Blueprint
|
|
||||||
|
|
||||||
from .content import content
|
|
||||||
from .info import info
|
|
||||||
|
|
||||||
api = Blueprint.group(content, info, url_prefix='/api')
|
|
||||||
```
|
|
||||||
|
|
||||||
And registering these blueprints in `app.py` can now be done like so:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# app.py
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
from .api import api
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
app.blueprint(api)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using blueprints
|
|
||||||
|
|
||||||
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 print_on_request(request):
|
|
||||||
print("I am a spy")
|
|
||||||
|
|
||||||
@bp.middleware('request')
|
|
||||||
async def halt_request(request):
|
|
||||||
return text('I halted the request')
|
|
||||||
|
|
||||||
@bp.middleware('response')
|
|
||||||
async def halt_response(request, response):
|
|
||||||
return text('I halted the response')
|
|
||||||
```
|
|
||||||
|
|
||||||
### Exceptions
|
|
||||||
|
|
||||||
Exceptions can be applied exclusively to blueprints globally.
|
|
||||||
|
|
||||||
```python
|
|
||||||
@bp.exception(NotFound)
|
|
||||||
def ignore_404s(request, exception):
|
|
||||||
return text("Yep, I totally found the page: {}".format(request.url))
|
|
||||||
```
|
|
||||||
|
|
||||||
### Static files
|
|
||||||
|
|
||||||
Static files can be served globally, under the blueprint prefix.
|
|
||||||
|
|
||||||
```python
|
|
||||||
|
|
||||||
# suppose bp.name == 'bp'
|
|
||||||
|
|
||||||
bp.static('/web/path', '/folder/to/serve')
|
|
||||||
# also you can pass name parameter to it for url_for
|
|
||||||
bp.static('/web/path', '/folder/to/server', name='uploads')
|
|
||||||
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Start and stop
|
|
||||||
|
|
||||||
Blueprints can run functions during the start and stop process of the server.
|
|
||||||
If running in multiprocessor mode (more than 1 worker), these are triggered
|
|
||||||
after the workers fork.
|
|
||||||
|
|
||||||
Available events are:
|
|
||||||
|
|
||||||
- `before_server_start`: Executed before the server begins to accept connections
|
|
||||||
- `after_server_start`: Executed after the server begins to accept connections
|
|
||||||
- `before_server_stop`: Executed before the server stops accepting connections
|
|
||||||
- `after_server_stop`: Executed after the server is stopped and all requests are complete
|
|
||||||
|
|
||||||
```python
|
|
||||||
bp = Blueprint('my_blueprint')
|
|
||||||
|
|
||||||
@bp.listener('before_server_start')
|
|
||||||
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()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Use-case: API versioning
|
|
||||||
|
|
||||||
Blueprints can be very useful for API versioning, where one blueprint may point
|
|
||||||
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
|
|
||||||
|
|
||||||
When a blueprint is initialised, it can take an optional `url_prefix` argument,
|
|
||||||
which will be prepended to all routes defined on the blueprint. This feature
|
|
||||||
can be used to implement our API versioning scheme.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# blueprints.py
|
|
||||||
from sanic.response import text
|
|
||||||
from sanic import Blueprint
|
|
||||||
|
|
||||||
blueprint_v1 = Blueprint('v1', url_prefix='/v1')
|
|
||||||
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')
|
|
||||||
```
|
|
||||||
|
|
||||||
When we register our blueprints on the app, the routes `/v1` and `/v2` will now
|
|
||||||
point to the individual blueprints, which allows the creation of *sub-sites*
|
|
||||||
for each API version.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# main.py
|
|
||||||
from sanic import Sanic
|
|
||||||
from blueprints import blueprint_v1, blueprint_v2
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
app.blueprint(blueprint_v1, url_prefix='/v1')
|
|
||||||
app.blueprint(blueprint_v2, url_prefix='/v2')
|
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
## URL Building with `url_for`
|
|
||||||
|
|
||||||
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
|
|
||||||
takes the format `<blueprint_name>.<handler_name>`. For example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@blueprint_v1.route('/')
|
|
||||||
async def root(request):
|
|
||||||
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
|
|
||||||
return redirect(url)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint_v1.route('/post/<post_id>')
|
|
||||||
async def post_handler(request, post_id):
|
|
||||||
return text('Post {} in Blueprint V1'.format(post_id))
|
|
||||||
```
|
|
||||||
301
docs/sanic/blueprints.rst
Normal file
301
docs/sanic/blueprints.rst
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
Blueprints
|
||||||
|
==========
|
||||||
|
|
||||||
|
Blueprints are objects that can be used for sub-routing within an application.
|
||||||
|
Instead of adding routes to the application instance, blueprints define similar
|
||||||
|
methods for adding routes, which are then registered with the application in a
|
||||||
|
flexible and pluggable manner.
|
||||||
|
|
||||||
|
Blueprints are especially useful for larger applications, where your
|
||||||
|
application logic can be broken down into several groups or areas of
|
||||||
|
responsibility.
|
||||||
|
|
||||||
|
My First Blueprint
|
||||||
|
------------------
|
||||||
|
|
||||||
|
The following shows a very simple blueprint that registers a handler-function at
|
||||||
|
the root `/` of your application.
|
||||||
|
|
||||||
|
Suppose you save this file as `my_blueprint.py`, which can be imported into your
|
||||||
|
main application later.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import json
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('my_blueprint')
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
async def bp_root(request):
|
||||||
|
return json({'my': 'blueprint'})
|
||||||
|
|
||||||
|
Registering blueprints
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Blueprints must be registered with the application.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from my_blueprint import bp
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
app.blueprint(bp)
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||||
|
|
||||||
|
This will add the blueprint to the application and register any routes defined
|
||||||
|
by that blueprint. In this example, the registered routes in the `app.router`
|
||||||
|
will look like:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
|
||||||
|
|
||||||
|
Blueprint groups and nesting
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
|
||||||
|
|
||||||
|
| api/
|
||||||
|
| ├──content/
|
||||||
|
| │ ├──authors.py
|
||||||
|
| │ ├──static.py
|
||||||
|
| │ └──__init__.py
|
||||||
|
| ├──info.py
|
||||||
|
| └──__init__.py
|
||||||
|
| app.py
|
||||||
|
|
||||||
|
Initialization of this app's blueprint hierarchy could go as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# api/content/authors.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
authors = Blueprint('content_authors', url_prefix='/authors')
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# api/content/static.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
static = Blueprint('content_static', url_prefix='/static')
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# api/content/__init__.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
from .static import static
|
||||||
|
from .authors import authors
|
||||||
|
|
||||||
|
content = Blueprint.group(static, authors, url_prefix='/content')
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# api/info.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
info = Blueprint('info', url_prefix='/info')
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# api/__init__.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
from .content import content
|
||||||
|
from .info import info
|
||||||
|
|
||||||
|
api = Blueprint.group(content, info, url_prefix='/api')
|
||||||
|
|
||||||
|
And registering these blueprints in `app.py` can now be done like so:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# app.py
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
from .api import api
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
app.blueprint(api)
|
||||||
|
|
||||||
|
Using Blueprints
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Blueprints have almost 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.
|
||||||
|
|
||||||
|
Blueprint Middleware
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Using blueprints allows you to also register middleware globally.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@bp.middleware
|
||||||
|
async def print_on_request(request):
|
||||||
|
print("I am a spy")
|
||||||
|
|
||||||
|
@bp.middleware('request')
|
||||||
|
async def halt_request(request):
|
||||||
|
return text('I halted the request')
|
||||||
|
|
||||||
|
@bp.middleware('response')
|
||||||
|
async def halt_response(request, response):
|
||||||
|
return text('I halted the response')
|
||||||
|
|
||||||
|
|
||||||
|
Blueprint Group Middleware
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the
|
||||||
|
current blueprint group under consideration.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
bp1 = Blueprint('bp1', url_prefix='/bp1')
|
||||||
|
bp2 = Blueprint('bp2', url_prefix='/bp2')
|
||||||
|
|
||||||
|
@bp1.middleware('request')
|
||||||
|
async def bp1_only_middleware(request):
|
||||||
|
print('applied on Blueprint : bp1 Only')
|
||||||
|
|
||||||
|
@bp1.route('/')
|
||||||
|
async def bp1_route(request):
|
||||||
|
return text('bp1')
|
||||||
|
|
||||||
|
@bp2.route('/<param>')
|
||||||
|
async def bp2_route(request, param):
|
||||||
|
return text(param)
|
||||||
|
|
||||||
|
group = Blueprint.group(bp1, bp2)
|
||||||
|
|
||||||
|
@group.middleware('request')
|
||||||
|
async def group_middleware(request):
|
||||||
|
print('common middleware applied for both bp1 and bp2')
|
||||||
|
|
||||||
|
# Register Blueprint group under the app
|
||||||
|
app.blueprint(group)
|
||||||
|
|
||||||
|
Exceptions
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Exceptions can be applied exclusively to blueprints globally.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@bp.exception(NotFound)
|
||||||
|
def ignore_404s(request, exception):
|
||||||
|
return text("Yep, I totally found the page: {}".format(request.url))
|
||||||
|
|
||||||
|
Static files
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Static files can be served globally, under the blueprint prefix.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# suppose bp.name == 'bp'
|
||||||
|
|
||||||
|
bp.static('/web/path', '/folder/to/serve')
|
||||||
|
# also you can pass name parameter to it for url_for
|
||||||
|
bp.static('/web/path', '/folder/to/server', name='uploads')
|
||||||
|
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
|
||||||
|
|
||||||
|
Start and stop
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Blueprints can run functions during the start and stop process of the server.
|
||||||
|
If running in multiprocessor mode (more than 1 worker), these are triggered
|
||||||
|
after the workers fork.
|
||||||
|
|
||||||
|
Available events are:
|
||||||
|
|
||||||
|
- `before_server_start`: Executed before the server begins to accept connections
|
||||||
|
- `after_server_start`: Executed after the server begins to accept connections
|
||||||
|
- `before_server_stop`: Executed before the server stops accepting connections
|
||||||
|
- `after_server_stop`: Executed after the server is stopped and all requests are complete
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
bp = Blueprint('my_blueprint')
|
||||||
|
|
||||||
|
@bp.listener('before_server_start')
|
||||||
|
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()
|
||||||
|
|
||||||
|
Use-case: API versioning
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Blueprints can be very useful for API versioning, where one blueprint may point
|
||||||
|
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
|
||||||
|
|
||||||
|
When a blueprint is initialised, it can take an optional `version` argument,
|
||||||
|
which will be prepended to all routes defined on the blueprint. This feature
|
||||||
|
can be used to implement our API versioning scheme.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# blueprints.py
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
blueprint_v1 = Blueprint('v1', url_prefix='/api', version="v1")
|
||||||
|
blueprint_v2 = Blueprint('v2', url_prefix='/api', version="v2")
|
||||||
|
|
||||||
|
@blueprint_v1.route('/')
|
||||||
|
async def api_v1_root(request):
|
||||||
|
return text('Welcome to version 1 of our documentation')
|
||||||
|
|
||||||
|
@blueprint_v2.route('/')
|
||||||
|
async def api_v2_root(request):
|
||||||
|
return text('Welcome to version 2 of our documentation')
|
||||||
|
|
||||||
|
When we register our blueprints on the app, the routes `/v1/api` and `/v2/api` will now
|
||||||
|
point to the individual blueprints, which allows the creation of *sub-sites*
|
||||||
|
for each API version.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# main.py
|
||||||
|
from sanic import Sanic
|
||||||
|
from blueprints import blueprint_v1, blueprint_v2
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
app.blueprint(blueprint_v1)
|
||||||
|
app.blueprint(blueprint_v2)
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||||
|
|
||||||
|
URL Building with `url_for`
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
|
||||||
|
takes the format `<blueprint_name>.<handler_name>`. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@blueprint_v1.route('/')
|
||||||
|
async def root(request):
|
||||||
|
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/api/post/5'
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint_v1.route('/post/<post_id>')
|
||||||
|
async def post_handler(request, post_id):
|
||||||
|
return text('Post {} in Blueprint V1'.format(post_id))
|
||||||
4
docs/sanic/changelog.rst
Normal file
4
docs/sanic/changelog.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Changelog
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. include:: ../../CHANGELOG.rst
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
# Class-Based Views
|
|
||||||
|
|
||||||
Class-based views are simply classes which implement response behaviour to
|
|
||||||
requests. They provide a way to compartmentalise handling of different HTTP
|
|
||||||
request types at the same endpoint. Rather than defining and decorating three
|
|
||||||
different handler functions, one for each of an endpoint's supported request
|
|
||||||
type, the endpoint can be assigned a class-based view.
|
|
||||||
|
|
||||||
## Defining views
|
|
||||||
|
|
||||||
A class-based view should subclass `HTTPMethodView`. You can then implement
|
|
||||||
class methods for every HTTP request type you want to support. If a request is
|
|
||||||
received that has no defined method, a `405: Method not allowed` response will
|
|
||||||
be generated.
|
|
||||||
|
|
||||||
To register a class-based view on an endpoint, the `app.add_route` method is
|
|
||||||
used. The first argument should be the defined class with the method `as_view`
|
|
||||||
invoked, and the second should be the URL endpoint.
|
|
||||||
|
|
||||||
The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class
|
|
||||||
using all these methods would look like the following.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.views import HTTPMethodView
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
app = Sanic('some_name')
|
|
||||||
|
|
||||||
class SimpleView(HTTPMethodView):
|
|
||||||
|
|
||||||
def get(self, request):
|
|
||||||
return text('I am get method')
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
return text('I am post method')
|
|
||||||
|
|
||||||
def put(self, request):
|
|
||||||
return text('I am put method')
|
|
||||||
|
|
||||||
def patch(self, request):
|
|
||||||
return text('I am patch method')
|
|
||||||
|
|
||||||
def delete(self, request):
|
|
||||||
return text('I am delete method')
|
|
||||||
|
|
||||||
app.add_route(SimpleView.as_view(), '/')
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
in the method definition.
|
|
||||||
|
|
||||||
```python
|
|
||||||
class NameView(HTTPMethodView):
|
|
||||||
|
|
||||||
def get(self, request, name):
|
|
||||||
return text('Hello {}'.format(name))
|
|
||||||
|
|
||||||
app.add_route(NameView.as_view(), '/<name>')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Decorators
|
|
||||||
|
|
||||||
If you want to add any decorators to the class, you can set the `decorators`
|
|
||||||
class variable. These will be applied to the class when `as_view` is called.
|
|
||||||
|
|
||||||
```python
|
|
||||||
class ViewWithDecorator(HTTPMethodView):
|
|
||||||
decorators = [some_decorator_here]
|
|
||||||
|
|
||||||
def get(self, request, name):
|
|
||||||
return text('Hello I have a decorator')
|
|
||||||
|
|
||||||
def post(self, request, name):
|
|
||||||
return text("Hello I also have a decorator")
|
|
||||||
|
|
||||||
app.add_route(ViewWithDecorator.as_view(), '/url')
|
|
||||||
```
|
|
||||||
|
|
||||||
But if you just want to decorate some functions and not all functions, you can do as follows:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class ViewWithSomeDecorator(HTTPMethodView):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@some_decorator_here
|
|
||||||
def get(request, name):
|
|
||||||
return text("Hello I have a decorator")
|
|
||||||
|
|
||||||
def post(self, request, name):
|
|
||||||
return text("Hello I don't have any decorators")
|
|
||||||
```
|
|
||||||
|
|
||||||
## URL Building
|
|
||||||
|
|
||||||
If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint
|
|
||||||
that you will pass into `url_for`. For example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@app.route('/')
|
|
||||||
def index(request):
|
|
||||||
url = app.url_for('SpecialClassView')
|
|
||||||
return redirect(url)
|
|
||||||
|
|
||||||
|
|
||||||
class SpecialClassView(HTTPMethodView):
|
|
||||||
def get(self, request):
|
|
||||||
return text('Hello from the Special Class View!')
|
|
||||||
|
|
||||||
|
|
||||||
app.add_route(SpecialClassView.as_view(), '/special_class_view')
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Using CompositionView
|
|
||||||
|
|
||||||
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
|
|
||||||
move handler functions outside of the view class.
|
|
||||||
|
|
||||||
Handler functions for each supported HTTP method are defined elsewhere in the
|
|
||||||
source, and then added to the view using the `CompositionView.add` method. The
|
|
||||||
first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`),
|
|
||||||
and the second is the handler function. The following example shows
|
|
||||||
`CompositionView` usage with both an external handler function and an inline
|
|
||||||
lambda:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.views import CompositionView
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
def get_handler(request):
|
|
||||||
return text('I am a get method')
|
|
||||||
|
|
||||||
view = CompositionView()
|
|
||||||
view.add(['GET'], get_handler)
|
|
||||||
view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
|
|
||||||
|
|
||||||
# Use the new view to handle requests to the base URL
|
|
||||||
app.add_route(view, '/')
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: currently you cannot build a URL for a CompositionView using `url_for`.
|
|
||||||
169
docs/sanic/class_based_views.rst
Normal file
169
docs/sanic/class_based_views.rst
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
Class-Based Views
|
||||||
|
=================
|
||||||
|
|
||||||
|
Class-based views are simply classes which implement response behaviour to
|
||||||
|
requests. They provide a way to compartmentalise handling of different HTTP
|
||||||
|
request types at the same endpoint. Rather than defining and decorating three
|
||||||
|
different handler functions, one for each of an endpoint's supported request
|
||||||
|
type, the endpoint can be assigned a class-based view.
|
||||||
|
|
||||||
|
Defining views
|
||||||
|
--------------
|
||||||
|
|
||||||
|
A class-based view should subclass `HTTPMethodView`. You can then implement
|
||||||
|
class methods for every HTTP request type you want to support. If a request is
|
||||||
|
received that has no defined method, a `405: Method not allowed` response will
|
||||||
|
be generated.
|
||||||
|
|
||||||
|
To register a class-based view on an endpoint, the `app.add_route` method is
|
||||||
|
used. The first argument should be the defined class with the method `as_view`
|
||||||
|
invoked, and the second should be the URL endpoint.
|
||||||
|
|
||||||
|
The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class
|
||||||
|
using all these methods would look like the following.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.views import HTTPMethodView
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic('some_name')
|
||||||
|
|
||||||
|
class SimpleView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
return text('I am post method')
|
||||||
|
|
||||||
|
def put(self, request):
|
||||||
|
return text('I am put method')
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
return text('I am patch method')
|
||||||
|
|
||||||
|
def delete(self, request):
|
||||||
|
return text('I am delete method')
|
||||||
|
|
||||||
|
app.add_route(SimpleView.as_view(), '/')
|
||||||
|
|
||||||
|
You can also use `async` syntax.
|
||||||
|
|
||||||
|
.. code-block:: 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
|
||||||
|
in the method definition.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class NameView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request, name):
|
||||||
|
return text('Hello {}'.format(name))
|
||||||
|
|
||||||
|
app.add_route(NameView.as_view(), '/<name>')
|
||||||
|
|
||||||
|
Decorators
|
||||||
|
----------
|
||||||
|
|
||||||
|
If you want to add any decorators to the class, you can set the `decorators`
|
||||||
|
class variable. These will be applied to the class when `as_view` is called.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ViewWithDecorator(HTTPMethodView):
|
||||||
|
decorators = [some_decorator_here]
|
||||||
|
|
||||||
|
def get(self, request, name):
|
||||||
|
return text('Hello I have a decorator')
|
||||||
|
|
||||||
|
def post(self, request, name):
|
||||||
|
return text("Hello I also have a decorator")
|
||||||
|
|
||||||
|
app.add_route(ViewWithDecorator.as_view(), '/url')
|
||||||
|
|
||||||
|
But if you just want to decorate some functions and not all functions, you can do as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ViewWithSomeDecorator(HTTPMethodView):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@some_decorator_here
|
||||||
|
def get(request, name):
|
||||||
|
return text("Hello I have a decorator")
|
||||||
|
|
||||||
|
def post(self, request, name):
|
||||||
|
return text("Hello I don't have any decorators")
|
||||||
|
|
||||||
|
URL Building
|
||||||
|
------------
|
||||||
|
|
||||||
|
If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint
|
||||||
|
that you will pass into `url_for`. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index(request):
|
||||||
|
url = app.url_for('SpecialClassView')
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialClassView(HTTPMethodView):
|
||||||
|
def get(self, request):
|
||||||
|
return text('Hello from the Special Class View!')
|
||||||
|
|
||||||
|
|
||||||
|
app.add_route(SpecialClassView.as_view(), '/special_class_view')
|
||||||
|
|
||||||
|
Using CompositionView
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
|
||||||
|
move handler functions outside of the view class.
|
||||||
|
|
||||||
|
Handler functions for each supported HTTP method are defined elsewhere in the
|
||||||
|
source, and then added to the view using the `CompositionView.add` method. The
|
||||||
|
first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`),
|
||||||
|
and the second is the handler function. The following example shows
|
||||||
|
`CompositionView` usage with both an external handler function and an inline
|
||||||
|
lambda:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.views import CompositionView
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
def get_handler(request):
|
||||||
|
return text('I am a get method')
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['GET'], get_handler)
|
||||||
|
view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
|
||||||
|
|
||||||
|
# Use the new view to handle requests to the base URL
|
||||||
|
app.add_route(view, '/')
|
||||||
|
|
||||||
|
Note: currently you cannot build a URL for a CompositionView using `url_for`.
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
# Configuration
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
|
|
||||||
|
|
||||||
```
|
|
||||||
app = Sanic('myapp')
|
|
||||||
app.config.DB_NAME = 'appdb'
|
|
||||||
app.config.DB_USER = 'appuser'
|
|
||||||
```
|
|
||||||
|
|
||||||
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
|
|
||||||
|
|
||||||
```
|
|
||||||
db_settings = {
|
|
||||||
'DB_HOST': 'localhost',
|
|
||||||
'DB_NAME': 'appdb',
|
|
||||||
'DB_USER': 'appuser'
|
|
||||||
}
|
|
||||||
app.config.update(db_settings)
|
|
||||||
```
|
|
||||||
|
|
||||||
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
|
|
||||||
|
|
||||||
## Loading Configuration
|
|
||||||
|
|
||||||
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 and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic:
|
|
||||||
|
|
||||||
```python
|
|
||||||
app = Sanic(load_env='MYAPP_')
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
|
|
||||||
|
|
||||||
```python
|
|
||||||
app = Sanic(load_env=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:
|
|
||||||
|
|
||||||
```
|
|
||||||
import myapp.default_settings
|
|
||||||
|
|
||||||
app = Sanic('myapp')
|
|
||||||
app.config.from_object(myapp.default_settings)
|
|
||||||
```
|
|
||||||
|
|
||||||
You could use a class or any other object as well.
|
|
||||||
|
|
||||||
### From a File
|
|
||||||
|
|
||||||
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
|
|
||||||
|
|
||||||
```
|
|
||||||
app = Sanic('myapp')
|
|
||||||
app.config.from_envvar('MYAPP_SETTINGS')
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py
|
|
||||||
INFO: Goin' Fast @ http://0.0.0.0:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase variables are added to the configuration. Most commonly the configuration consists of simple key value pairs:
|
|
||||||
|
|
||||||
```
|
|
||||||
# config_file
|
|
||||||
DB_HOST = 'localhost'
|
|
||||||
DB_NAME = 'appdb'
|
|
||||||
DB_USER = 'appuser'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Builtin Configuration Values
|
|
||||||
|
|
||||||
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 to arrive (sec) |
|
|
||||||
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
|
|
||||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
|
||||||
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
|
|
||||||
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long take to force close non-idle connection (sec) |
|
|
||||||
| ACCESS_LOG | True | Disable or enable access log |
|
|
||||||
|
|
||||||
### The different Timeout variables:
|
|
||||||
|
|
||||||
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly.
|
|
||||||
|
|
||||||
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response.
|
|
||||||
|
|
||||||
### What is Keep Alive? And what does the Keep Alive Timeout value do?
|
|
||||||
|
|
||||||
Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server.
|
|
||||||
|
|
||||||
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request.
|
|
||||||
|
|
||||||
The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long.
|
|
||||||
|
|
||||||
For reference:
|
|
||||||
```
|
|
||||||
Apache httpd server default keepalive timeout = 5 seconds
|
|
||||||
Nginx server default keepalive timeout = 75 seconds
|
|
||||||
Nginx performance tuning guidelines uses keepalive = 15 seconds
|
|
||||||
IE (5-9) client hard keepalive limit = 60 seconds
|
|
||||||
Firefox client hard keepalive limit = 115 seconds
|
|
||||||
Opera 11 client hard keepalive limit = 120 seconds
|
|
||||||
Chrome 13+ client keepalive limit > 300+ seconds
|
|
||||||
```
|
|
||||||
242
docs/sanic/config.rst
Normal file
242
docs/sanic/config.rst
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
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
|
||||||
|
------
|
||||||
|
|
||||||
|
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic('myapp')
|
||||||
|
app.config.DB_NAME = 'appdb'
|
||||||
|
app.config.DB_USER = 'appuser'
|
||||||
|
|
||||||
|
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
db_settings = {
|
||||||
|
'DB_HOST': 'localhost',
|
||||||
|
'DB_NAME': 'appdb',
|
||||||
|
'DB_USER': 'appuser'
|
||||||
|
}
|
||||||
|
app.config.update(db_settings)
|
||||||
|
|
||||||
|
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
|
||||||
|
|
||||||
|
Loading Configuration
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
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 and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic(load_env='MYAPP_')
|
||||||
|
|
||||||
|
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic(load_env=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:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import myapp.default_settings
|
||||||
|
|
||||||
|
app = Sanic('myapp')
|
||||||
|
app.config.from_object(myapp.default_settings)
|
||||||
|
|
||||||
|
or also by path to config:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic('myapp')
|
||||||
|
app.config.from_object('config.path.config.Class')
|
||||||
|
|
||||||
|
You could use a class or any other object as well.
|
||||||
|
|
||||||
|
From a File
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic('myapp')
|
||||||
|
app.config.from_envvar('MYAPP_SETTINGS')
|
||||||
|
|
||||||
|
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
#$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py
|
||||||
|
#INFO: Goin' Fast @ http://0.0.0.0:8000
|
||||||
|
|
||||||
|
|
||||||
|
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase variables are added to the configuration. Most commonly the configuration consists of simple key value pairs:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# config_file
|
||||||
|
DB_HOST = 'localhost'
|
||||||
|
DB_NAME = 'appdb'
|
||||||
|
DB_USER = 'appuser'
|
||||||
|
|
||||||
|
Builtin Configuration Values
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
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_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| ACCESS_LOG | True | Disable or enable access log |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| PROXIES_COUNT | -1 | The number of proxy servers in front of the app (e.g. nginx; see below) |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| FORWARDED_FOR_HEADER | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| REAL_IP_HEADER | "X-Real-IP" | The name of "X-Real-IP" HTTP header that contains real client ip |
|
||||||
|
+---------------------------+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
The different Timeout variables:
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
`REQUEST_TIMEOUT`
|
||||||
|
#################
|
||||||
|
|
||||||
|
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the
|
||||||
|
Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the
|
||||||
|
`REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates an `HTTP 408` response
|
||||||
|
and sends that to the client. Set this parameter's value higher if your clients routinely pass very large request payloads
|
||||||
|
or upload requests very slowly.
|
||||||
|
|
||||||
|
`RESPONSE_TIMEOUT`
|
||||||
|
##################
|
||||||
|
|
||||||
|
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the
|
||||||
|
Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT`
|
||||||
|
value (in seconds), this is considered a Server Error so Sanic generates an `HTTP 503` response and sends that to the
|
||||||
|
client. Set this parameter's value higher if your application is likely to have long-running process that delay the
|
||||||
|
generation of a response.
|
||||||
|
|
||||||
|
`KEEP_ALIVE_TIMEOUT`
|
||||||
|
####################
|
||||||
|
|
||||||
|
What is Keep Alive? And what does the Keep Alive Timeout value do?
|
||||||
|
******************************************************************
|
||||||
|
|
||||||
|
`Keep-Alive` is a HTTP feature introduced in `HTTP 1.1`. When sending a HTTP request, the client (usually a web browser application)
|
||||||
|
can set a `Keep-Alive` header to indicate the http server (Sanic) to not close the TCP connection after it has send the response.
|
||||||
|
This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient
|
||||||
|
network traffic for both the client and the server.
|
||||||
|
|
||||||
|
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application,
|
||||||
|
set it to `False` to cause all client connections to close immediately after a response is sent, regardless of
|
||||||
|
the `Keep-Alive` header on the request.
|
||||||
|
|
||||||
|
The amount of time the server holds the TCP connection open is decided by the server itself.
|
||||||
|
In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds.
|
||||||
|
This is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for
|
||||||
|
the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless
|
||||||
|
you know your clients are using a browser which supports TCP connections held open for that long.
|
||||||
|
|
||||||
|
For reference:
|
||||||
|
|
||||||
|
* Apache httpd server default keepalive timeout = 5 seconds
|
||||||
|
* Nginx server default keepalive timeout = 75 seconds
|
||||||
|
* Nginx performance tuning guidelines uses keepalive = 15 seconds
|
||||||
|
* IE (5-9) client hard keepalive limit = 60 seconds
|
||||||
|
* Firefox client hard keepalive limit = 115 seconds
|
||||||
|
* Opera 11 client hard keepalive limit = 120 seconds
|
||||||
|
* Chrome 13+ client keepalive limit > 300+ seconds
|
||||||
|
|
||||||
|
|
||||||
|
Proxy configuration
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain ip of a proxy,
|
||||||
|
typically `127.0.0.1`. Sanic may be configured to use proxy headers for determining the true client IP,
|
||||||
|
available as `request.remote_addr`. The full external URL is also constructed from header fields if available.
|
||||||
|
|
||||||
|
Without proper precautions, a malicious client may use proxy headers to spoof its own IP. To avoid such issues, Sanic does not use any proxy headers unless explicitly enabled.
|
||||||
|
|
||||||
|
Services behind reverse proxies must configure `FORWARDED_SECRET`, `REAL_IP_HEADER` and/or `PROXIES_COUNT`.
|
||||||
|
|
||||||
|
Forwarded header
|
||||||
|
################
|
||||||
|
|
||||||
|
.. Forwarded: for="1.2.3.4"; proto="https"; host="yoursite.com"; secret="Pr0xy", for="10.0.0.1"; proto="http"; host="proxy.internal"; by="_1234proxy"
|
||||||
|
|
||||||
|
* Set `FORWARDED_SECRET` to an identifier used by the proxy of interest.
|
||||||
|
|
||||||
|
The secret is used to securely identify a specific proxy server. Given the above header, secret `Pr0xy` would use the
|
||||||
|
information on the first line and secret `_1234proxy` would use the second line. The secret must exactly match the value
|
||||||
|
of `secret` or `by`. A secret in `by` must begin with an underscore and use only characters specified in
|
||||||
|
`RFC 7239 section 6.3 <https://tools.ietf.org/html/rfc7239#section-6.3>`_, while `secret` has no such restrictions.
|
||||||
|
|
||||||
|
Sanic ignores any elements without the secret key, and will not even parse the header if no secret is set.
|
||||||
|
|
||||||
|
All other proxy headers are ignored once a trusted forwarded element is found, as it already carries complete information about the client.
|
||||||
|
|
||||||
|
Traditional proxy headers
|
||||||
|
#########################
|
||||||
|
|
||||||
|
.. X-Real-IP: 1.2.3.4
|
||||||
|
X-Forwarded-For: 1.2.3.4, 10.0.0.1
|
||||||
|
X-Forwarded-Proto: https
|
||||||
|
X-Forwarded-Host: yoursite.com
|
||||||
|
|
||||||
|
|
||||||
|
* Set `REAL_IP_HEADER` to `x-real-ip`, `true-client-ip`, `cf-connecting-ip` or other name of such header.
|
||||||
|
* Set `PROXIES_COUNT` to the number of entries expected in `x-forwarded-for` (name configurable via `FORWARDED_FOR_HEADER`).
|
||||||
|
|
||||||
|
If client IP is found by one of these methods, Sanic uses the following headers for URL parts:
|
||||||
|
|
||||||
|
* `x-forwarded-proto`, `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-path` and if necessary, `x-scheme`.
|
||||||
|
|
||||||
|
Proxy config if using ...
|
||||||
|
#########################
|
||||||
|
|
||||||
|
* a proxy that supports `forwarded`: set `FORWARDED_SECRET` to the value that the proxy inserts in the header
|
||||||
|
* Apache Traffic Server: `CONFIG proxy.config.http.insert_forwarded STRING for|proto|host|by=_secret`
|
||||||
|
* NGHTTPX: `nghttpx --add-forwarded=for,proto,host,by --forwarded-for=ip --forwarded-by=_secret`
|
||||||
|
* NGINX: after `the official instructions <https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/>`_, add anywhere in your config:
|
||||||
|
|
||||||
|
.. proxy_set_header Forwarded "$proxy_add_forwarded;by=\"_$server_name\";proto=$scheme;host=\"$http_host\";path=\"$request_uri\";secret=_secret";
|
||||||
|
|
||||||
|
* a custom header with client IP: set `REAL_IP_HEADER` to the name of that header
|
||||||
|
* `x-forwarded-for`: set `PROXIES_COUNT` to `1` for a single proxy, or a greater number to allow Sanic to select the correct IP
|
||||||
|
* no proxies: no configuration required!
|
||||||
|
|
||||||
|
Changes in Sanic 19.9
|
||||||
|
#####################
|
||||||
|
|
||||||
|
Earlier Sanic versions had unsafe default settings. From 19.9 onwards proxy settings must be set manually, and support for negative PROXIES_COUNT has been removed.
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# 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:
|
|
||||||
* 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
|
|
||||||
|
|
||||||
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.
|
|
||||||
1
docs/sanic/contributing.rst
Normal file
1
docs/sanic/contributing.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.. include:: ../../CONTRIBUTING.rst
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# Custom Protocols
|
|
||||||
|
|
||||||
*Note: this is advanced usage, and most readers will not need such functionality.*
|
|
||||||
|
|
||||||
You can change the behavior of Sanic's protocol by specifying a custom
|
|
||||||
protocol, which should be a subclass
|
|
||||||
of
|
|
||||||
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
|
||||||
This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method.
|
|
||||||
|
|
||||||
The constructor of the custom protocol class receives the following keyword
|
|
||||||
arguments from Sanic.
|
|
||||||
|
|
||||||
- `loop`: an `asyncio`-compatible event loop.
|
|
||||||
- `connections`: a `set` to store protocol objects. When Sanic receives
|
|
||||||
`SIGINT` or `SIGTERM`, it executes `protocol.close_if_idle` for all protocol
|
|
||||||
objects stored in this set.
|
|
||||||
- `signal`: a `sanic.server.Signal` object with the `stopped` attribute. When
|
|
||||||
Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` is assigned `True`.
|
|
||||||
- `request_handler`: a coroutine that takes a `sanic.request.Request` object
|
|
||||||
and a `response` callback as arguments.
|
|
||||||
- `error_handler`: a `sanic.exceptions.Handler` which is called when exceptions
|
|
||||||
are raised.
|
|
||||||
- `request_timeout`: the number of seconds before a request times out.
|
|
||||||
- `request_max_size`: an integer specifying the maximum size of a request, in bytes.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
An error occurs in the default protocol if a handler function does not return
|
|
||||||
an `HTTPResponse` object.
|
|
||||||
|
|
||||||
By overriding the `write_response` protocol method, if a handler returns a
|
|
||||||
string it will be converted to an `HTTPResponse object`.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.server import HttpProtocol
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomHttpProtocol(HttpProtocol):
|
|
||||||
|
|
||||||
def __init__(self, *, loop, request_handler, error_handler,
|
|
||||||
signal, connections, request_timeout, request_max_size):
|
|
||||||
super().__init__(
|
|
||||||
loop=loop, request_handler=request_handler,
|
|
||||||
error_handler=error_handler, signal=signal,
|
|
||||||
connections=connections, request_timeout=request_timeout,
|
|
||||||
request_max_size=request_max_size)
|
|
||||||
|
|
||||||
def write_response(self, response):
|
|
||||||
if isinstance(response, str):
|
|
||||||
response = text(response)
|
|
||||||
self.transport.write(
|
|
||||||
response.output(self.request.version)
|
|
||||||
)
|
|
||||||
self.transport.close()
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
async def string(request):
|
|
||||||
return 'string'
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/1')
|
|
||||||
async def response(request):
|
|
||||||
return text('response')
|
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
|
||||||
```
|
|
||||||
76
docs/sanic/custom_protocol.rst
Normal file
76
docs/sanic/custom_protocol.rst
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
Custom Protocols
|
||||||
|
================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This is advanced usage, and most readers will not need such functionality.
|
||||||
|
|
||||||
|
You can change the behavior of Sanic's protocol by specifying a custom
|
||||||
|
protocol, which should be a subclass
|
||||||
|
of `asyncio.protocol <https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes>`_.
|
||||||
|
This protocol can then be passed as the keyword argument ``protocol`` to the ``sanic.run`` method.
|
||||||
|
|
||||||
|
The constructor of the custom protocol class receives the following keyword
|
||||||
|
arguments from Sanic.
|
||||||
|
|
||||||
|
- ``loop``: an ``asyncio``-compatible event loop.
|
||||||
|
- ``connections``: a ``set`` to store protocol objects. When Sanic receives
|
||||||
|
``SIGINT`` or ``SIGTERM``, it executes ``protocol.close_if_idle`` for all protocol
|
||||||
|
objects stored in this set.
|
||||||
|
- ``signal``: a ``sanic.server.Signal`` object with the ``stopped`` attribute. When
|
||||||
|
Sanic receives ``SIGINT`` or ``SIGTERM``, ``signal.stopped`` is assigned ``True``.
|
||||||
|
- ``request_handler``: a coroutine that takes a ``sanic.request.Request`` object
|
||||||
|
and a ``response`` callback as arguments.
|
||||||
|
- ``error_handler``: a ``sanic.exceptions.Handler`` which is called when exceptions
|
||||||
|
are raised.
|
||||||
|
- ``request_timeout``: the number of seconds before a request times out.
|
||||||
|
- ``request_max_size``: an integer specifying the maximum size of a request, in bytes.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
An error occurs in the default protocol if a handler function does not return
|
||||||
|
an ``HTTPResponse`` object.
|
||||||
|
|
||||||
|
By overriding the ``write_response`` protocol method, if a handler returns a
|
||||||
|
string it will be converted to an ``HTTPResponse object``.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.server import HttpProtocol
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomHttpProtocol(HttpProtocol):
|
||||||
|
|
||||||
|
def __init__(self, *, loop, request_handler, error_handler,
|
||||||
|
signal, connections, request_timeout, request_max_size):
|
||||||
|
super().__init__(
|
||||||
|
loop=loop, request_handler=request_handler,
|
||||||
|
error_handler=error_handler, signal=signal,
|
||||||
|
connections=connections, request_timeout=request_timeout,
|
||||||
|
request_max_size=request_max_size)
|
||||||
|
|
||||||
|
def write_response(self, response):
|
||||||
|
if isinstance(response, str):
|
||||||
|
response = text(response)
|
||||||
|
self.transport.write(
|
||||||
|
response.output(self.request.version)
|
||||||
|
)
|
||||||
|
self.transport.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def string(request):
|
||||||
|
return 'string'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/1')
|
||||||
|
async def response(request):
|
||||||
|
return text('response')
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ and by default will enable the Auto Reload feature.
|
|||||||
Setting the debug mode
|
Setting the debug mode
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
By setting the ``debug`` mode a more verbose output from Sanic will be outputed
|
By setting the ``debug`` mode a more verbose output from Sanic will be output
|
||||||
and the Automatic Reloader will be activated.
|
and the Automatic Reloader will be activated.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@@ -50,4 +50,4 @@ the ``auto_reload`` argument will activate or deactivate the Automatic Reloader.
|
|||||||
return json({"hello": "world"})
|
return json({"hello": "world"})
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000, auto_reload=True)
|
app.run(host="0.0.0.0", port=8000, auto_reload=True)
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
# 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'})
|
|
||||||
```
|
|
||||||
|
|
||||||
40
docs/sanic/decorators.rst
Normal file
40
docs/sanic/decorators.rst
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: 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'})
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# Deploying
|
|
||||||
|
|
||||||
Deploying Sanic is made simple by the inbuilt webserver. After defining an
|
|
||||||
instance of `sanic.Sanic`, we can call the `run` method with the following
|
|
||||||
keyword arguments:
|
|
||||||
|
|
||||||
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
|
||||||
- `port` *(default `8000`)*: Port to host the server on.
|
|
||||||
- `debug` *(default `False`)*: Enables debug output (slows server).
|
|
||||||
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
|
|
||||||
- `sock` *(default `None`)*: Socket for the server to accept connections from.
|
|
||||||
- `workers` *(default `1`)*: Number of worker processes to spawn.
|
|
||||||
- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is
|
|
||||||
specified, Sanic creates its own event loop.
|
|
||||||
- `protocol` *(default `HttpProtocol`)*: Subclass
|
|
||||||
of
|
|
||||||
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
|
||||||
|
|
||||||
## Workers
|
|
||||||
|
|
||||||
By default, Sanic listens in the main process using only one CPU core. To crank
|
|
||||||
up the juice, just specify the number of workers in the `run` arguments.
|
|
||||||
|
|
||||||
```python
|
|
||||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
|
||||||
```
|
|
||||||
|
|
||||||
Sanic will automatically spin up multiple processes and route traffic between
|
|
||||||
them. We recommend as many workers as you have available cores.
|
|
||||||
|
|
||||||
## Running via command
|
|
||||||
|
|
||||||
If you like using command line arguments, you can launch a Sanic server by
|
|
||||||
executing the module. For example, if you initialized Sanic as `app` in a file
|
|
||||||
named `server.py`, you could run the server like so:
|
|
||||||
|
|
||||||
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
|
|
||||||
|
|
||||||
With this way of running sanic, it is not necessary to invoke `app.run` in your
|
|
||||||
Python file. If you do, make sure you wrap it so that it only executes when
|
|
||||||
directly run by the interpreter.
|
|
||||||
|
|
||||||
```python
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
|
|
||||||
```
|
|
||||||
|
|
||||||
If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker
|
|
||||||
after it has processed a given number of requests. This can be a convenient way to help limit the effects
|
|
||||||
of the memory leak.
|
|
||||||
|
|
||||||
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
|
|
||||||
|
|
||||||
## 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()
|
|
||||||
```
|
|
||||||
201
docs/sanic/deploying.rst
Normal file
201
docs/sanic/deploying.rst
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Deploying
|
||||||
|
=========
|
||||||
|
|
||||||
|
Deploying Sanic is very simple using one of three options: the inbuilt webserver,
|
||||||
|
an `ASGI webserver <https://asgi.readthedocs.io/en/latest/implementations.html>`_, or `gunicorn`.
|
||||||
|
It is also very common to place Sanic behind a reverse proxy, like `nginx`.
|
||||||
|
|
||||||
|
Running via Sanic webserver
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
After defining an instance of `sanic.Sanic`, we can call the `run` method with the following
|
||||||
|
keyword arguments:
|
||||||
|
|
||||||
|
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
||||||
|
- `port` *(default `8000`)*: Port to host the server on.
|
||||||
|
- `debug` *(default `False`)*: Enables debug output (slows server).
|
||||||
|
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
|
||||||
|
- `sock` *(default `None`)*: Socket for the server to accept connections from.
|
||||||
|
- `workers` *(default `1`)*: Number of worker processes to spawn.
|
||||||
|
- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is specified, Sanic creates its own event loop.
|
||||||
|
- `protocol` *(default `HttpProtocol`)*: Subclass of `asyncio.protocol <https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes>`_.
|
||||||
|
- `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server).
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=1337, access_log=False)
|
||||||
|
|
||||||
|
In the above example, we decided to turn off the access log in order to increase performance.
|
||||||
|
|
||||||
|
Workers
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
By default, Sanic listens in the main process using only one CPU core. To crank
|
||||||
|
up the juice, just specify the number of workers in the `run` arguments.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||||
|
|
||||||
|
Sanic will automatically spin up multiple processes and route traffic between
|
||||||
|
them. We recommend as many workers as you have available cores.
|
||||||
|
|
||||||
|
Running via command
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If you like using command line arguments, you can launch a Sanic webserver by
|
||||||
|
executing the module. For example, if you initialized Sanic as `app` in a file
|
||||||
|
named `server.py`, you could run the server like so:
|
||||||
|
|
||||||
|
.. python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4
|
||||||
|
|
||||||
|
With this way of running sanic, it is not necessary to invoke `app.run` in your
|
||||||
|
Python file. If you do, make sure you wrap it so that it only executes when
|
||||||
|
directly run by the interpreter.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||||
|
|
||||||
|
Running via ASGI
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Sanic is also ASGI-compliant. This means you can use your preferred ASGI webserver
|
||||||
|
to run Sanic. The three main implementations of ASGI are
|
||||||
|
`Daphne <http://github.com/django/daphne>`_, `Uvicorn <https://www.uvicorn.org/>`_,
|
||||||
|
and `Hypercorn <https://pgjones.gitlab.io/hypercorn/index.html>`_.
|
||||||
|
|
||||||
|
Follow their documentation for the proper way to run them, but it should look
|
||||||
|
something like:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
daphne myapp:app
|
||||||
|
uvicorn myapp:app
|
||||||
|
hypercorn myapp:app
|
||||||
|
|
||||||
|
A couple things to note when using ASGI:
|
||||||
|
|
||||||
|
1. When using the Sanic webserver, websockets will run using the `websockets <https://websockets.readthedocs.io/>`_ package.
|
||||||
|
In ASGI mode, there is no need for this package since websockets are managed in the ASGI server.
|
||||||
|
2. The ASGI `lifespan protocol <https://asgi.readthedocs.io/en/latest/specs/lifespan.html>`, supports
|
||||||
|
only two server events: startup and shutdown. Sanic has four: before startup, after startup,
|
||||||
|
before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and shutdown events will
|
||||||
|
run consecutively and not actually around the server process beginning and ending (since that
|
||||||
|
is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and
|
||||||
|
`before_server_stop`.
|
||||||
|
3. ASGI mode is still in "beta" as of Sanic v19.6.
|
||||||
|
|
||||||
|
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 myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
|
||||||
|
|
||||||
|
|
||||||
|
If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker
|
||||||
|
after it has processed a given number of requests. This can be a convenient way to help limit the effects
|
||||||
|
of the memory leak.
|
||||||
|
|
||||||
|
See the `Gunicorn Docs <http://docs.gunicorn.org/en/latest/settings.html#max-requests>`_ for more information.
|
||||||
|
|
||||||
|
Other deployment considerations
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Running behind a reverse proxy
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Sanic can be used with a reverse proxy (e.g. nginx). There's a simple example of nginx configuration:
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name example.org;
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
If you want to get real client ip, you should configure `X-Real-IP` and `X-Forwarded-For` HTTP headers and set `app.config.PROXIES_COUNT` to `1`; see the configuration page for more information.
|
||||||
|
|
||||||
|
Disable debug logging for performance
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False)
|
||||||
|
|
||||||
|
Running via Gunicorn you can set Environment variable `SANIC_ACCESS_LOG="False"`
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning
|
||||||
|
|
||||||
|
Or you can rewrite app config directly
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app.config.ACCESS_LOG = False
|
||||||
|
|
||||||
|
Asynchronous support and sharing the loop
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
task = asyncio.ensure_future(server)
|
||||||
|
loop.run_forever()
|
||||||
|
|
||||||
|
Caveat: using this method, calling `app.create_server()` will trigger "before_server_start" server events, but not
|
||||||
|
"after_server_start", "before_server_stop", or "after_server_stop" server events.
|
||||||
|
|
||||||
|
For more advanced use-cases, you can trigger these events using the AsyncioServer object, returned by awaiting
|
||||||
|
the server task.
|
||||||
|
|
||||||
|
Here is an incomplete example (please see `run_async_advanced.py` in examples for something more complete):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
|
||||||
|
server = loop.run_until_complete(serv_task)
|
||||||
|
server.after_start()
|
||||||
|
try:
|
||||||
|
loop.run_forever()
|
||||||
|
except KeyboardInterrupt as e:
|
||||||
|
loop.stop()
|
||||||
|
finally:
|
||||||
|
server.before_stop()
|
||||||
|
|
||||||
|
# Wait for server to close
|
||||||
|
close_task = server.close()
|
||||||
|
loop.run_until_complete(close_task)
|
||||||
|
|
||||||
|
# Complete all tasks on the loop
|
||||||
|
for connection in server.connections:
|
||||||
|
connection.close_if_idle()
|
||||||
|
server.after_stop()
|
||||||
167
docs/sanic/examples.rst
Normal file
167
docs/sanic/examples.rst
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
Examples
|
||||||
|
========
|
||||||
|
|
||||||
|
This section of the documentation is a simple collection of example code that can help you get a quick start
|
||||||
|
on your application development. Most of these examples are categorized and provide you with a link to the
|
||||||
|
working code example in the `Sanic Repository <https://github.com/huge-success/sanic/tree/master/examples>`_
|
||||||
|
|
||||||
|
|
||||||
|
Basic Examples
|
||||||
|
--------------
|
||||||
|
|
||||||
|
This section of the examples are a collection of code that provide a simple use case example of the sanic application.
|
||||||
|
|
||||||
|
Simple Apps
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A simple sanic application with a single ``async`` method with ``text`` and ``json`` type response.
|
||||||
|
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/teapot.py
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/simple_server.py
|
||||||
|
|
||||||
|
|
||||||
|
Simple App with ``Sanic Views``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Showcasing the simple mechanism of using :class:`sanic.viewes.HTTPMethodView` as well as a way to extend the same
|
||||||
|
into providing a custom ``async`` behavior for ``view``.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/simple_async_view.py
|
||||||
|
|
||||||
|
|
||||||
|
URL Redirect
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/redirect_example.py
|
||||||
|
|
||||||
|
|
||||||
|
Named URL redirection
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
``Sanic`` provides an easy to use way of redirecting the requests via a helper method called ``url_for`` that takes a
|
||||||
|
unique url name as argument and returns you the actual route assigned for it. This will help in simplifying the
|
||||||
|
efforts required in redirecting the user between different section of the application.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/url_for_example.py
|
||||||
|
|
||||||
|
Blueprints
|
||||||
|
~~~~~~~~~~
|
||||||
|
``Sanic`` provides an amazing feature to group your APIs and routes under a logical collection that can easily be
|
||||||
|
imported and plugged into any of your sanic application and it's called ``blueprints``
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/blueprints.py
|
||||||
|
|
||||||
|
Logging Enhancements
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Even though ``Sanic`` comes with a battery of Logging support it allows the end users to customize the way logging
|
||||||
|
is handled in the application runtime.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/override_logging.py
|
||||||
|
|
||||||
|
The following sample provides an example code that demonstrates the usage of :func:`sanic.app.Sanic.middleware` in order
|
||||||
|
to provide a mechanism to assign a unique request ID for each of the incoming requests and log them via
|
||||||
|
`aiotask-context <https://github.com/Skyscanner/aiotask-context>`_.
|
||||||
|
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/log_request_id.py
|
||||||
|
|
||||||
|
Sanic Streaming Support
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
``Sanic`` framework comes with in-built support for streaming large files and the following code explains the process
|
||||||
|
to setup a ``Sanic`` application with streaming support.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/request_stream/server.py
|
||||||
|
|
||||||
|
Sample Client app to show the usage of streaming application by a client code.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/request_stream/client.py
|
||||||
|
|
||||||
|
Sanic Concurrency Support
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
``Sanic`` supports the ability to start an app with multiple worker support. However, it's important to be able to limit
|
||||||
|
the concurrency per process/loop in order to ensure an efficient execution. The following section of the code provides a
|
||||||
|
brief example of how to limit the concurrency with the help of :class:`asyncio.Semaphore`
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/limit_concurrency.py
|
||||||
|
|
||||||
|
|
||||||
|
Sanic Deployment via Docker
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Deploying a ``sanic`` app via ``docker`` and ``docker-compose`` is an easy task to achieve and the following example
|
||||||
|
provides a deployment of the sample ``simple_server.py``
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/Dockerfile
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/docker-compose.yml
|
||||||
|
|
||||||
|
|
||||||
|
Monitoring and Error Handling
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
``Sanic`` provides an extendable bare minimum implementation of a global exception handler via
|
||||||
|
:class:`sanic.handlers.ErrorHandler`. This example shows how to extend it to enable some custom behaviors.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/exception_monitoring.py
|
||||||
|
|
||||||
|
Monitoring using external Service Providers
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* `LogDNA <https://logdna.com/>`_
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/logdna_example.py
|
||||||
|
|
||||||
|
* `RayGun <https://raygun.com/>`_
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/raygun_example.py
|
||||||
|
|
||||||
|
* `Rollbar <https://rollbar.com>`_
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/rollbar_example.py
|
||||||
|
|
||||||
|
* `Sentry <http://sentry.io>`_
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/sentry_example.py
|
||||||
|
|
||||||
|
|
||||||
|
Security
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
The following sample code shows a simple decorator based authentication and authorization mechanism that can be setup
|
||||||
|
to secure your ``sanic`` api endpoints.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/authorized_sanic.py
|
||||||
|
|
||||||
|
Sanic Websocket
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
``Sanic`` provides an ability to easily add a route and map it to a ``websocket`` handlers.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/websocket.html
|
||||||
|
.. literalinclude:: ../../examples/websocket.py
|
||||||
|
|
||||||
|
vhost Suppport
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/vhosts.py
|
||||||
|
|
||||||
|
Unit Testing With Parallel Test Run Support
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The following example shows you how to get up and running with unit testing ``sanic`` application with parallel test
|
||||||
|
execution support provided by the ``pytest-xdist`` plugin.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/pytest_xdist.py
|
||||||
|
|
||||||
|
|
||||||
|
Amending Request Object
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The ``request`` object in ``Sanic`` is a kind of ``dict`` object, this means that ``request`` object can be manipulated as a regular ``dict`` object.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/amending_request_object.py
|
||||||
|
|
||||||
|
For more examples and useful samples please visit the `Huge-Sanic's GitHub Page <https://github.com/huge-success/sanic/tree/master/examples>`_
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# Exceptions
|
|
||||||
|
|
||||||
Exceptions can be thrown from within request handlers and will automatically be
|
|
||||||
handled by Sanic. Exceptions take a message as their first argument, and can
|
|
||||||
also take a status code to be passed back in the HTTP response.
|
|
||||||
|
|
||||||
## Throwing an exception
|
|
||||||
|
|
||||||
To throw an exception, simply `raise` the relevant exception from the
|
|
||||||
`sanic.exceptions` module.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.exceptions import ServerError
|
|
||||||
|
|
||||||
@app.route('/killme')
|
|
||||||
async def i_am_ready_to_die(request):
|
|
||||||
raise ServerError("Something bad happened", status_code=500)
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also use the `abort` function with the appropriate status code:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.exceptions import abort
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
@app.route('/youshallnotpass')
|
|
||||||
async def no_no(request):
|
|
||||||
abort(401)
|
|
||||||
# this won't happen
|
|
||||||
text("OK")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Handling exceptions
|
|
||||||
|
|
||||||
To override Sanic's default handling of an exception, the `@app.exception`
|
|
||||||
decorator is used. The decorator expects a list of exceptions to handle as
|
|
||||||
arguments. You can pass `SanicException` to catch them all! The decorated
|
|
||||||
exception handler function must take a `Request` and `Exception` object as
|
|
||||||
arguments.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
from sanic.exceptions import NotFound
|
|
||||||
|
|
||||||
@app.exception(NotFound)
|
|
||||||
async def ignore_404s(request, exception):
|
|
||||||
return text("Yep, I totally found the page: {}".format(request.url))
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also add an exception handler as such:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
async def server_error_handler(request, exception):
|
|
||||||
return text("Oops, server error", status=500)
|
|
||||||
|
|
||||||
app = Sanic()
|
|
||||||
app.error_handler.add(Exception, server_error_handler)
|
|
||||||
```
|
|
||||||
|
|
||||||
In some cases, you might want want to add some more error handling
|
|
||||||
functionality to what is provided by default. In that case, you
|
|
||||||
can subclass Sanic's default error handler as such:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.handlers import ErrorHandler
|
|
||||||
|
|
||||||
class CustomErrorHandler(ErrorHandler):
|
|
||||||
def default(self, request, exception):
|
|
||||||
''' handles errors that have no error handlers assigned '''
|
|
||||||
# You custom error handling logic...
|
|
||||||
return super().default(request, exception)
|
|
||||||
|
|
||||||
app = Sanic()
|
|
||||||
app.error_handler = CustomErrorHandler()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Useful exceptions
|
|
||||||
|
|
||||||
Some of the most useful exceptions are presented below:
|
|
||||||
|
|
||||||
- `NotFound`: called when a suitable route for the request isn't found.
|
|
||||||
- `ServerError`: called when something goes wrong inside the server. This
|
|
||||||
usually occurs if there is an exception raised in user code.
|
|
||||||
|
|
||||||
See the `sanic.exceptions` module for the full list of exceptions to throw.
|
|
||||||
92
docs/sanic/exceptions.rst
Normal file
92
docs/sanic/exceptions.rst
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
Exceptions
|
||||||
|
==========
|
||||||
|
|
||||||
|
Exceptions can be thrown from within request handlers and will automatically be
|
||||||
|
handled by Sanic. Exceptions take a message as their first argument, and can
|
||||||
|
also take a status code to be passed back in the HTTP response.
|
||||||
|
|
||||||
|
Throwing an exception
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
To throw an exception, simply `raise` the relevant exception from the
|
||||||
|
`sanic.exceptions` module.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.exceptions import ServerError
|
||||||
|
|
||||||
|
@app.route('/killme')
|
||||||
|
async def i_am_ready_to_die(request):
|
||||||
|
raise ServerError("Something bad happened", status_code=500)
|
||||||
|
|
||||||
|
You can also use the `abort` function with the appropriate status code:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.exceptions import abort
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
@app.route('/youshallnotpass')
|
||||||
|
async def no_no(request):
|
||||||
|
abort(401)
|
||||||
|
# this won't happen
|
||||||
|
text("OK")
|
||||||
|
|
||||||
|
Handling exceptions
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
To override Sanic's default handling of an exception, the `@app.exception`
|
||||||
|
decorator is used. The decorator expects a list of exceptions to handle as
|
||||||
|
arguments. You can pass `SanicException` to catch them all! The decorated
|
||||||
|
exception handler function must take a `Request` and `Exception` object as
|
||||||
|
arguments.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic.exceptions import NotFound
|
||||||
|
|
||||||
|
@app.exception(NotFound)
|
||||||
|
async def ignore_404s(request, exception):
|
||||||
|
return text("Yep, I totally found the page: {}".format(request.url))
|
||||||
|
|
||||||
|
You can also add an exception handler as such:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
async def server_error_handler(request, exception):
|
||||||
|
return text("Oops, server error", status=500)
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
|
app.error_handler.add(Exception, server_error_handler)
|
||||||
|
|
||||||
|
In some cases, you might want to add some more error handling
|
||||||
|
functionality to what is provided by default. In that case, you
|
||||||
|
can subclass Sanic's default error handler as such:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.handlers import ErrorHandler
|
||||||
|
|
||||||
|
class CustomErrorHandler(ErrorHandler):
|
||||||
|
def default(self, request, exception):
|
||||||
|
''' handles errors that have no error handlers assigned '''
|
||||||
|
# You custom error handling logic...
|
||||||
|
return super().default(request, exception)
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
|
app.error_handler = CustomErrorHandler()
|
||||||
|
|
||||||
|
Useful exceptions
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Some of the most useful exceptions are presented below:
|
||||||
|
|
||||||
|
- `NotFound`: called when a suitable route for the request isn't found.
|
||||||
|
- `ServerError`: called when something goes wrong inside the server. This
|
||||||
|
usually occurs if there is an exception raised in user code.
|
||||||
|
|
||||||
|
See the `sanic.exceptions` module for the full list of exceptions to throw.
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Extensions
|
|
||||||
|
|
||||||
A list of Sanic extensions created by the community.
|
|
||||||
- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins.
|
|
||||||
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
|
|
||||||
Allows using redis, memcache or an in memory store.
|
|
||||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
|
||||||
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
|
|
||||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
|
||||||
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic.
|
|
||||||
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for Sanic
|
|
||||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
|
||||||
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
|
||||||
- [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-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
|
|
||||||
- [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.
|
|
||||||
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
|
|
||||||
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
|
|
||||||
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
|
|
||||||
- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/))
|
|
||||||
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
|
|
||||||
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
|
|
||||||
- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.
|
|
||||||
- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
|
|
||||||
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.
|
|
||||||
4
docs/sanic/extensions.rst
Normal file
4
docs/sanic/extensions.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Extensions
|
||||||
|
==========
|
||||||
|
|
||||||
|
Moved to the `awesome-sanic <https://github.com/mekicha/awesome-sanic>`_ list.
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# Getting Started
|
|
||||||
|
|
||||||
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
|
|
||||||
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
|
|
||||||
syntax, so earlier versions of python won't work.
|
|
||||||
|
|
||||||
## 1. Install Sanic
|
|
||||||
|
|
||||||
```
|
|
||||||
python3 -m pip install sanic
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Create a file called `main.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import json
|
|
||||||
|
|
||||||
app = Sanic()
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
async def test(request):
|
|
||||||
return json({"hello": "world"})
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(host="0.0.0.0", port=8000)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Run the server
|
|
||||||
|
|
||||||
```
|
|
||||||
python3 main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Check your browser
|
|
||||||
|
|
||||||
Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
|
||||||
the message *Hello world!*.
|
|
||||||
|
|
||||||
You now have a working Sanic server!
|
|
||||||
62
docs/sanic/getting_started.rst
Normal file
62
docs/sanic/getting_started.rst
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
Getting Started
|
||||||
|
===============
|
||||||
|
|
||||||
|
Make sure you have both `pip <https://pip.pypa.io/en/stable/installing/>`_ and at
|
||||||
|
least version 3.6 of Python before starting. Sanic uses the new `async`/`await`
|
||||||
|
syntax, so earlier versions of python won't work.
|
||||||
|
|
||||||
|
1. Install Sanic
|
||||||
|
----------------
|
||||||
|
|
||||||
|
> If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip3 install sanic
|
||||||
|
|
||||||
|
To install sanic without `uvloop` or `ujson` 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 `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`)
|
||||||
|
to true will stop that features installation.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic
|
||||||
|
|
||||||
|
You can also install Sanic from `conda-forge <https://anaconda.org/conda-forge/sanic>`_
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
conda config --add channels conda-forge
|
||||||
|
conda install sanic
|
||||||
|
|
||||||
|
2. Create a file called `main.py`
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def test(request):
|
||||||
|
return json({"hello": "world"})
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
|
3. Run the server
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python3 main.py
|
||||||
|
|
||||||
|
4. Check your browser
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Open the address `http://0.0.0.0:8000 <http://0.0.0.0:8000>`_ in your web browser. You should see
|
||||||
|
the message *Hello world!*.
|
||||||
|
|
||||||
|
You now have a working Sanic server!
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
Sanic
|
Sanic
|
||||||
=================================
|
=================================
|
||||||
|
|
||||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
|
Sanic is a Python 3.6+ web server and web framework that's written to go fast. It allows the usage of the async/await syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
||||||
|
|
||||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.
|
||||||
|
|
||||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||||
|
|
||||||
@@ -22,4 +22,9 @@ Sanic aspires to be simple
|
|||||||
return json({"hello": "world"})
|
return json({"hello": "world"})
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru
|
||||||
|
December 2020. Official Python support for version 3.5 is set to expire in September 2020.
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
# 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 to create a new configuration.
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
A simple example using default settings would be like this:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.log import logger
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
app = Sanic('test')
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
async def test(request):
|
|
||||||
logger.info('Here is your log')
|
|
||||||
return text('Hello World!')
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(debug=True, access_log=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
After the server is running, you can see some messages looks like:
|
|
||||||
```
|
|
||||||
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
|
|
||||||
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
|
|
||||||
```
|
|
||||||
|
|
||||||
You can send a request to server and it will print the log messages:
|
|
||||||
```
|
|
||||||
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
|
|
||||||
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
|
|
||||||
```
|
|
||||||
|
|
||||||
To use your own logging config, simply use `logging.config.dictConfig`, or
|
|
||||||
pass `log_config` when you initialize `Sanic` app:
|
|
||||||
|
|
||||||
```python
|
|
||||||
app = Sanic('test', log_config=LOGGING_CONFIG)
|
|
||||||
```
|
|
||||||
|
|
||||||
And to close logging, simply assign access_log=False:
|
|
||||||
|
|
||||||
```python
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(access_log=False)
|
|
||||||
```
|
|
||||||
|
|
||||||
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 debug messages
|
|
||||||
app.run(debug=False, access_log=False)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration.
|
|
||||||
|
|
||||||
There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
|
|
||||||
|
|
||||||
- sanic.root:<br>
|
|
||||||
Used to log internal messages.
|
|
||||||
|
|
||||||
- sanic.error:<br>
|
|
||||||
Used to log error logs.
|
|
||||||
|
|
||||||
- sanic.access:<br>
|
|
||||||
Used to log access logs.
|
|
||||||
|
|
||||||
#### Log format:
|
|
||||||
|
|
||||||
In addition to default parameters provided by python (asctime, levelname, message),
|
|
||||||
Sanic provides additional parameters for access logger with:
|
|
||||||
|
|
||||||
- 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
|
|
||||||
```
|
|
||||||
103
docs/sanic/logging.rst
Normal file
103
docs/sanic/logging.rst
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
Logging
|
||||||
|
=======
|
||||||
|
|
||||||
|
Sanic allows you to do different types of logging (access log, error
|
||||||
|
log) on the requests based on the `python3 logging API`_. You should
|
||||||
|
have some basic knowledge on python3 logging if you want to create a new
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
A simple example using default settings would be like this:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.log import logger
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic('test')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def test(request):
|
||||||
|
logger.info('Here is your log')
|
||||||
|
return text('Hello World!')
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True, access_log=True)
|
||||||
|
|
||||||
|
After the server is running, you can see some messages looks like:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
|
||||||
|
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
|
||||||
|
|
||||||
|
You can send a request to server and it will print the log messages:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
|
||||||
|
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
|
||||||
|
|
||||||
|
To use your own logging config, simply use
|
||||||
|
``logging.config.dictConfig``, or pass ``log_config`` when you
|
||||||
|
initialize ``Sanic`` app:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
app = Sanic('test', log_config=LOGGING_CONFIG)
|
||||||
|
|
||||||
|
And to close logging, simply assign access_log=False:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(access_log=False)
|
||||||
|
|
||||||
|
This would skip calling logging functions when handling requests. And
|
||||||
|
you could even do further in production to gain extra speed:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# disable debug messages
|
||||||
|
app.run(debug=False, access_log=False)
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
By default, ``log_config`` parameter is set to use
|
||||||
|
``sanic.log.LOGGING_CONFIG_DEFAULTS`` dictionary for configuration.
|
||||||
|
|
||||||
|
There are three ``loggers`` used in sanic, and **must be defined if you
|
||||||
|
want to create your own logging configuration**:
|
||||||
|
|
||||||
|
================ ==============================
|
||||||
|
Logger Name Usecase
|
||||||
|
================ ==============================
|
||||||
|
``sanic.root`` Used to log internal messages.
|
||||||
|
``sanic.error`` Used to log error logs.
|
||||||
|
``sanic.access`` Used to log access logs.
|
||||||
|
================ ==============================
|
||||||
|
|
||||||
|
Log format:
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
In addition to default parameters provided by python (``asctime``,
|
||||||
|
``levelname``, ``message``), Sanic provides additional parameters for
|
||||||
|
access logger with:
|
||||||
|
|
||||||
|
===================== ========================================== ========
|
||||||
|
Log Context Parameter Parameter Value Datatype
|
||||||
|
===================== ========================================== ========
|
||||||
|
``host`` ``request.ip`` str
|
||||||
|
``request`` ``request.method`` + " " + ``request.url`` str
|
||||||
|
``status`` ``response.status`` int
|
||||||
|
``byte`` ``len(response.body)`` int
|
||||||
|
===================== ========================================== ========
|
||||||
|
|
||||||
|
The default access log format is ``%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d``
|
||||||
|
|
||||||
|
.. _python3 logging API: https://docs.python.org/3/howto/logging.html
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
# 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 provides 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'`.
|
|
||||||
|
|
||||||
* Request middleware receives only the `request` as argument.
|
|
||||||
* Response middleware receives both the `request` and `response`.
|
|
||||||
|
|
||||||
The simplest middleware doesn't modify the request or response at all:
|
|
||||||
|
|
||||||
```
|
|
||||||
@app.middleware('request')
|
|
||||||
async def print_on_request(request):
|
|
||||||
print("I print when a request is received by the server")
|
|
||||||
|
|
||||||
@app.middleware('response')
|
|
||||||
async def print_on_response(request, response):
|
|
||||||
print("I print when a response is returned by the server")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Modifying the request or response
|
|
||||||
|
|
||||||
Middleware can modify the request or response parameter it is given, *as long
|
|
||||||
as it does not return it*. The following example shows a practical use-case for
|
|
||||||
this.
|
|
||||||
|
|
||||||
```
|
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
@app.middleware('response')
|
|
||||||
async def custom_banner(request, response):
|
|
||||||
response.headers["Server"] = "Fake-Server"
|
|
||||||
|
|
||||||
@app.middleware('response')
|
|
||||||
async def prevent_xss(request, response):
|
|
||||||
response.headers["x-xss-protection"] = "1; mode=block"
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000)
|
|
||||||
```
|
|
||||||
|
|
||||||
The above code will apply the two middleware in order. First, the middleware
|
|
||||||
**custom_banner** will change the HTTP response header *Server* to
|
|
||||||
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
|
|
||||||
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
|
|
||||||
are invoked *after* a user function returns a response.
|
|
||||||
|
|
||||||
## Responding early
|
|
||||||
|
|
||||||
If middleware returns a `HTTPResponse` object, the request will stop processing
|
|
||||||
and the response will be returned. If this occurs to a request before the
|
|
||||||
relevant user route handler is reached, the handler will never be called.
|
|
||||||
Returning a response will also prevent any further middleware from running.
|
|
||||||
|
|
||||||
```
|
|
||||||
@app.middleware('request')
|
|
||||||
async def halt_request(request):
|
|
||||||
return text('I halted the request')
|
|
||||||
|
|
||||||
@app.middleware('response')
|
|
||||||
async def halt_response(request, response):
|
|
||||||
return text('I halted the response')
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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:
|
|
||||||
|
|
||||||
```
|
|
||||||
@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()
|
|
||||||
```
|
|
||||||
|
|
||||||
It's also possible to register a listener using the `register_listener` method.
|
|
||||||
This may be useful if you define your listeners in another module besides
|
|
||||||
the one you instantiate your app in.
|
|
||||||
|
|
||||||
```
|
|
||||||
app = Sanic()
|
|
||||||
|
|
||||||
async def setup_db(app, loop):
|
|
||||||
app.db = await db_setup()
|
|
||||||
|
|
||||||
app.register_listener(setup_db, 'before_server_start')
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```
|
|
||||||
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())
|
|
||||||
```
|
|
||||||
|
|
||||||
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
|
|
||||||
|
|
||||||
```
|
|
||||||
async def notify_server_started_after_five_seconds(app):
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
print(app.name)
|
|
||||||
|
|
||||||
app.add_task(notify_server_started_after_five_seconds)
|
|
||||||
```
|
|
||||||
|
|
||||||
Or you can pass the app explicitly for the same effect:
|
|
||||||
|
|
||||||
```
|
|
||||||
async def notify_server_started_after_five_seconds(app):
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
print(app.name)
|
|
||||||
|
|
||||||
app.add_task(notify_server_started_after_five_seconds(app))
|
|
||||||
`
|
|
||||||
177
docs/sanic/middleware.rst
Normal file
177
docs/sanic/middleware.rst
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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 provides 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'`.
|
||||||
|
|
||||||
|
* Request middleware receives only the `request` as argument.
|
||||||
|
* Response middleware receives both the `request` and `response`.
|
||||||
|
|
||||||
|
The simplest middleware doesn't modify the request or response at all:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.middleware('request')
|
||||||
|
async def print_on_request(request):
|
||||||
|
print("I print when a request is received by the server")
|
||||||
|
|
||||||
|
@app.middleware('response')
|
||||||
|
async def print_on_response(request, response):
|
||||||
|
print("I print when a response is returned by the server")
|
||||||
|
|
||||||
|
Modifying the request or response
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Middleware can modify the request or response parameter it is given, *as long
|
||||||
|
as it does not return it*. The following example shows a practical use-case for
|
||||||
|
this.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware('request')
|
||||||
|
async def add_key(request):
|
||||||
|
# Arbitrary data may be stored in request context:
|
||||||
|
request.ctx.foo = 'bar'
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware('response')
|
||||||
|
async def custom_banner(request, response):
|
||||||
|
response.headers["Server"] = "Fake-Server"
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware('response')
|
||||||
|
async def prevent_xss(request, response):
|
||||||
|
response.headers["x-xss-protection"] = "1; mode=block"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def index(request):
|
||||||
|
return sanic.response.text(request.ctx.foo)
|
||||||
|
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
|
The three middlewares are executed in order:
|
||||||
|
|
||||||
|
1. The first request middleware **add_key** adds a new key `foo` into request context.
|
||||||
|
2. Request is routed to handler **index**, which gets the key from context and returns a text response.
|
||||||
|
3. The first response middleware **custom_banner** changes the HTTP response header *Server* to say *Fake-Server*
|
||||||
|
4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks.
|
||||||
|
|
||||||
|
Responding early
|
||||||
|
----------------
|
||||||
|
|
||||||
|
If middleware returns a `HTTPResponse` object, the request will stop processing
|
||||||
|
and the response will be returned. If this occurs to a request before the
|
||||||
|
relevant user route handler is reached, the handler will never be called.
|
||||||
|
Returning a response will also prevent any further middleware from running.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.middleware('request')
|
||||||
|
async def halt_request(request):
|
||||||
|
return text('I halted the request')
|
||||||
|
|
||||||
|
@app.middleware('response')
|
||||||
|
async def halt_response(request, response):
|
||||||
|
return text('I halted the response')
|
||||||
|
|
||||||
|
Custom context
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Arbitrary data may be stored in `request.ctx`. A typical use case
|
||||||
|
would be to store the user object acquired from database in an authentication
|
||||||
|
middleware. Keys added are accessible to all later middleware as well as
|
||||||
|
the handler over the duration of the request.
|
||||||
|
|
||||||
|
Custom context is reserved for applications and extensions. Sanic itself makes
|
||||||
|
no use of it.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. code-block:: 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()
|
||||||
|
|
||||||
|
It's also possible to register a listener using the `register_listener` method.
|
||||||
|
This may be useful if you define your listeners in another module besides
|
||||||
|
the one you instantiate your app in.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
|
|
||||||
|
async def setup_db(app, loop):
|
||||||
|
app.db = await db_setup()
|
||||||
|
|
||||||
|
app.register_listener(setup_db, 'before_server_start')
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. code-block:: 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())
|
||||||
|
|
||||||
|
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def notify_server_started_after_five_seconds(app):
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
print(app.name)
|
||||||
|
|
||||||
|
app.add_task(notify_server_started_after_five_seconds)
|
||||||
|
|
||||||
|
Or you can pass the app explicitly for the same effect:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def notify_server_started_after_five_seconds(app):
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
print(app.name)
|
||||||
|
|
||||||
|
app.add_task(notify_server_started_after_five_seconds(app))
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# Request Data
|
|
||||||
|
|
||||||
When an endpoint receives a HTTP request, the route function is passed a
|
|
||||||
`Request` object.
|
|
||||||
|
|
||||||
The following variables are accessible as properties on `Request` objects:
|
|
||||||
|
|
||||||
- `json` (any) - JSON body
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import json
|
|
||||||
|
|
||||||
@app.route("/json")
|
|
||||||
def post_json(request):
|
|
||||||
return json({ "received": True, "message": request.json })
|
|
||||||
```
|
|
||||||
|
|
||||||
- `args` (dict) - Query string variables. A query string is the section of a
|
|
||||||
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
|
||||||
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
|
|
||||||
The request's `query_string` variable holds the unparsed string value.
|
|
||||||
|
|
||||||
```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')
|
|
||||||
|
|
||||||
file_parameters = {
|
|
||||||
'body': test_file.body,
|
|
||||||
'name': test_file.name,
|
|
||||||
'type': test_file.type,
|
|
||||||
}
|
|
||||||
|
|
||||||
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
|
||||||
```
|
|
||||||
|
|
||||||
- `form` (dict) - Posted form variables.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import json
|
|
||||||
|
|
||||||
@app.route("/form")
|
|
||||||
def post_json(request):
|
|
||||||
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
|
||||||
```
|
|
||||||
|
|
||||||
- `body` (bytes) - Posted raw body. This property allows retrieval of the
|
|
||||||
request's raw data, regardless of content type.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
@app.route("/users", methods=["POST",])
|
|
||||||
def create_user(request):
|
|
||||||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
|
||||||
```
|
|
||||||
|
|
||||||
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
|
|
||||||
|
|
||||||
- `method` (str) - HTTP method of the request (ie `GET`, `POST`).
|
|
||||||
|
|
||||||
- `ip` (str) - IP address of the requester.
|
|
||||||
|
|
||||||
- `port` (str) - Port address of the requester.
|
|
||||||
|
|
||||||
- `socket` (tuple) - (IP, port) 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.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import json
|
|
||||||
from sanic import Blueprint
|
|
||||||
|
|
||||||
bp = Blueprint('my_blueprint')
|
|
||||||
|
|
||||||
@bp.route('/')
|
|
||||||
async def bp_root(request):
|
|
||||||
if request.app.config['DEBUG']:
|
|
||||||
return json({'status': 'debug'})
|
|
||||||
else:
|
|
||||||
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>/`
|
|
||||||
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
|
||||||
|
|
||||||
|
|
||||||
## Accessing values using `get` and `getlist`
|
|
||||||
|
|
||||||
The request properties which return a dictionary actually return a subclass of
|
|
||||||
`dict` called `RequestParameters`. The key difference when using this object is
|
|
||||||
the distinction between the `get` and `getlist` methods.
|
|
||||||
|
|
||||||
- `get(key, default=None)` operates as normal, except that when the value of
|
|
||||||
the given key is a list, *only the first item is returned*.
|
|
||||||
- `getlist(key, default=None)` operates as normal, *returning the entire list*.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.request import RequestParameters
|
|
||||||
|
|
||||||
args = RequestParameters()
|
|
||||||
args['titles'] = ['Post 1', 'Post 2']
|
|
||||||
|
|
||||||
args.get('titles') # => 'Post 1'
|
|
||||||
|
|
||||||
args.getlist('titles') # => ['Post 1', 'Post 2']
|
|
||||||
```
|
|
||||||
274
docs/sanic/request_data.rst
Normal file
274
docs/sanic/request_data.rst
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
Request Data
|
||||||
|
============
|
||||||
|
|
||||||
|
When an endpoint receives a HTTP request, the route function is passed a
|
||||||
|
`Request` object.
|
||||||
|
|
||||||
|
The following variables are accessible as properties on `Request` objects:
|
||||||
|
|
||||||
|
- `json` (any) - JSON body
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/json")
|
||||||
|
def post_json(request):
|
||||||
|
return json({ "received": True, "message": request.json })
|
||||||
|
|
||||||
|
|
||||||
|
- `args` (dict) - Query string variables. A query string is the section of a
|
||||||
|
URL that resembles ``?key1=value1&key2=value2``.
|
||||||
|
If that URL were to be parsed, the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
|
||||||
|
The request's `query_string` variable holds the unparsed string value. Property is providing the default parsing
|
||||||
|
strategy. If you would like to change it look to the section below (`Changing the default parsing rules of the queryset`).
|
||||||
|
|
||||||
|
.. code-block:: 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 })
|
||||||
|
|
||||||
|
- `query_args` (list) - On many cases you would need to access the url arguments in
|
||||||
|
a less packed form. `query_args` is the list of `(key, value)` tuples.
|
||||||
|
Property is providing the default parsing strategy. If you would like to change it look to the section below
|
||||||
|
(`Changing the default parsing rules of the queryset`). For the same previous URL queryset `?key1=value1&key2=value2`,
|
||||||
|
the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`. And in case of the multiple params
|
||||||
|
with the same key like `?key1=value1&key2=value2&key1=value3` the `query_args` list would look like
|
||||||
|
`[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`.
|
||||||
|
|
||||||
|
The difference between Request.args and Request.query_args for the queryset `?key1=value1&key2=value2&key1=value3`
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/test_request_args")
|
||||||
|
async def test_request_args(request):
|
||||||
|
return json({
|
||||||
|
"parsed": True,
|
||||||
|
"url": request.url,
|
||||||
|
"query_string": request.query_string,
|
||||||
|
"args": request.args,
|
||||||
|
"raw_args": request.raw_args,
|
||||||
|
"query_args": request.query_args,
|
||||||
|
})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
|
Output
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"parsed":true,
|
||||||
|
"url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3",
|
||||||
|
"query_string":"key1=value1&key2=value2&key1=value3",
|
||||||
|
"args":{"key1":["value1","value3"],"key2":["value2"]},
|
||||||
|
"raw_args":{"key1":"value1","key2":"value2"},
|
||||||
|
"query_args":[["key1","value1"],["key2","value2"],["key1","value3"]]
|
||||||
|
}
|
||||||
|
|
||||||
|
- `raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions.
|
||||||
|
|
||||||
|
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/files")
|
||||||
|
def post_json(request):
|
||||||
|
test_file = request.files.get('test')
|
||||||
|
|
||||||
|
file_parameters = {
|
||||||
|
'body': test_file.body,
|
||||||
|
'name': test_file.name,
|
||||||
|
'type': test_file.type,
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
||||||
|
|
||||||
|
- `form` (dict) - Posted form variables.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/form")
|
||||||
|
def post_json(request):
|
||||||
|
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
||||||
|
|
||||||
|
- `body` (bytes) - Posted raw body. This property allows retrieval of the
|
||||||
|
request's raw data, regardless of content type.
|
||||||
|
|
||||||
|
.. code-block:: 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)
|
||||||
|
|
||||||
|
|
||||||
|
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
|
||||||
|
|
||||||
|
- `method` (str) - HTTP method of the request (ie `GET`, `POST`).
|
||||||
|
|
||||||
|
- `ip` (str) - IP address of the requester.
|
||||||
|
|
||||||
|
- `port` (str) - Port address of the requester.
|
||||||
|
|
||||||
|
- `socket` (tuple) - (IP, port) 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.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import json
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('my_blueprint')
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
async def bp_root(request):
|
||||||
|
if request.app.config['DEBUG']:
|
||||||
|
return json({'status': 'debug'})
|
||||||
|
else:
|
||||||
|
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|https|ws|wss' or arbitrary value given by the headers.
|
||||||
|
- `host`: The host associated with the request(which in the `Host` header): `localhost:8080`
|
||||||
|
- `server_name`: The hostname of the server, without port number. the value is seeked in this order: `config.SERVER_NAME`, `x-forwarded-host` header, :func:`Request.host`
|
||||||
|
- `server_port`: Like `server_name`. Seeked in this order: `x-forwarded-port` header, :func:`Request.host`, actual port used by the transport layer socket.
|
||||||
|
- `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>/`
|
||||||
|
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
||||||
|
|
||||||
|
- `url_for`: Just like `sanic.Sanic.url_for`, but automatically determine `scheme` and `netloc` base on the request. Since this method is aiming to generate correct schema & netloc, `_external` is implied.
|
||||||
|
|
||||||
|
|
||||||
|
Changing the default parsing rules of the queryset
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
The default parameters that are using internally in `args` and `query_args` properties to parse queryset:
|
||||||
|
|
||||||
|
- `keep_blank_values` (bool): `False` - flag indicating whether blank values in
|
||||||
|
percent-encoded queries should be treated as blank strings.
|
||||||
|
A true value indicates that blanks should be retained as blank
|
||||||
|
strings. The default false value indicates that blank values
|
||||||
|
are to be ignored and treated as if they were not included.
|
||||||
|
- `strict_parsing` (bool): `False` - flag indicating what to do with parsing errors. If
|
||||||
|
false (the default), errors are silently ignored. If true,
|
||||||
|
errors raise a ValueError exception.
|
||||||
|
- `encoding` and `errors` (str): 'utf-8' and 'replace' - specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
|
||||||
|
If you would like to change that default parameters you could call `get_args` and `get_query_args` methods
|
||||||
|
with the new values.
|
||||||
|
|
||||||
|
For the queryset `/?test1=value1&test2=&test3=value3`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/query_string")
|
||||||
|
def query_string(request):
|
||||||
|
args_with_blank_values = request.get_args(keep_blank_values=True)
|
||||||
|
return json({
|
||||||
|
"parsed": True,
|
||||||
|
"url": request.url,
|
||||||
|
"args_with_blank_values": args_with_blank_values,
|
||||||
|
"query_string": request.query_string
|
||||||
|
})
|
||||||
|
|
||||||
|
The output will be:
|
||||||
|
|
||||||
|
.. code-block:: JSON
|
||||||
|
|
||||||
|
{
|
||||||
|
"parsed": true,
|
||||||
|
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3",
|
||||||
|
"args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]},
|
||||||
|
"query_string": "test1=value1&test2=&test3=value3"
|
||||||
|
}
|
||||||
|
|
||||||
|
Accessing values using `get` and `getlist`
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
The `request.args` returns a subclass of `dict` called `RequestParameters`.
|
||||||
|
The key difference when using this object is the distinction between the `get` and `getlist` methods.
|
||||||
|
|
||||||
|
- `get(key, default=None)` operates as normal, except that when the value of
|
||||||
|
the given key is a list, *only the first item is returned*.
|
||||||
|
- `getlist(key, default=None)` operates as normal, *returning the entire list*.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.request import RequestParameters
|
||||||
|
|
||||||
|
args = RequestParameters()
|
||||||
|
args['titles'] = ['Post 1', 'Post 2']
|
||||||
|
|
||||||
|
args.get('titles') # => 'Post 1'
|
||||||
|
|
||||||
|
args.getlist('titles') # => ['Post 1', 'Post 2']
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
app = Sanic(name="example")
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def get_handler(request):
|
||||||
|
return json({
|
||||||
|
"p1": request.args.getlist("p1")
|
||||||
|
})
|
||||||
|
|
||||||
|
Accessing the handler name with the request.endpoint attribute
|
||||||
|
--------------------------------------------------------------
|
||||||
|
|
||||||
|
The `request.endpoint` attribute holds the handler's name. For instance, the below
|
||||||
|
route will return "hello".
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def hello(request):
|
||||||
|
return text(request.endpoint)
|
||||||
|
|
||||||
|
Or, with a blueprint it will be include both, separated by a period. For example, the below route would return foo.bar:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic import Blueprint
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
blueprint = Blueprint('foo')
|
||||||
|
|
||||||
|
@blueprint.get('/')
|
||||||
|
async def bar(request):
|
||||||
|
return text(request.endpoint)
|
||||||
|
|
||||||
|
app.blueprint(blueprint)
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
# 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')
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Streaming
|
|
||||||
For large files, a combination of File and Streaming above
|
|
||||||
```python
|
|
||||||
from sanic import response
|
|
||||||
|
|
||||||
@app.route('/big_file.png')
|
|
||||||
async def handle_request(request):
|
|
||||||
return await response.file_stream('/srv/www/whatever.png')
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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(b'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
|
|
||||||
)
|
|
||||||
```
|
|
||||||
126
docs/sanic/response.rst
Normal file
126
docs/sanic/response.rst
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
Response
|
||||||
|
========
|
||||||
|
|
||||||
|
Use functions in `sanic.response` module to create responses.
|
||||||
|
|
||||||
|
Plain Text
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/text')
|
||||||
|
def handle_request(request):
|
||||||
|
return response.text('Hello world!')
|
||||||
|
|
||||||
|
HTML
|
||||||
|
----
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/html')
|
||||||
|
def handle_request(request):
|
||||||
|
return response.html('<p>Hello world!</p>')
|
||||||
|
|
||||||
|
JSON
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/json')
|
||||||
|
def handle_request(request):
|
||||||
|
return response.json({'message': 'Hello world!'})
|
||||||
|
|
||||||
|
File
|
||||||
|
----
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/file')
|
||||||
|
async def handle_request(request):
|
||||||
|
return await response.file('/srv/www/whatever.png')
|
||||||
|
|
||||||
|
Streaming
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
@app.route("/streaming")
|
||||||
|
async def index(request):
|
||||||
|
async def streaming_fn(response):
|
||||||
|
await response.write('foo')
|
||||||
|
await response.write('bar')
|
||||||
|
return response.stream(streaming_fn, content_type='text/plain')
|
||||||
|
|
||||||
|
See `Streaming <streaming.html>`_ for more information.
|
||||||
|
|
||||||
|
File Streaming
|
||||||
|
--------------
|
||||||
|
|
||||||
|
For large files, a combination of File and Streaming above
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
@app.route('/big_file.png')
|
||||||
|
async def handle_request(request):
|
||||||
|
return await response.file_stream('/srv/www/whatever.png')
|
||||||
|
|
||||||
|
Redirect
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/redirect')
|
||||||
|
def handle_request(request):
|
||||||
|
return response.redirect('/json')
|
||||||
|
|
||||||
|
Raw
|
||||||
|
---
|
||||||
|
|
||||||
|
Response without encoding the body
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/raw')
|
||||||
|
def handle_request(request):
|
||||||
|
return response.raw(b'raw data')
|
||||||
|
|
||||||
|
Modify headers or status
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
To modify headers or status code, pass the `headers` or `status` argument to those functions:
|
||||||
|
|
||||||
|
.. code-block:: 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
|
||||||
|
)
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
# Routing
|
|
||||||
|
|
||||||
Routing allows the user to specify handler functions for different URL endpoints.
|
|
||||||
|
|
||||||
A basic route looks like the following, where `app` is an instance of the
|
|
||||||
`Sanic` class:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import json
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
async def test(request):
|
|
||||||
return json({ "hello": "world" })
|
|
||||||
```
|
|
||||||
|
|
||||||
When the url `http://server.url/` is accessed (the base url of the server), the
|
|
||||||
final `/` is matched by the router to the handler function, `test`, which then
|
|
||||||
returns a JSON object.
|
|
||||||
|
|
||||||
Sanic handler functions must be defined using the `async def` syntax, as they
|
|
||||||
are asynchronous functions.
|
|
||||||
|
|
||||||
## Request parameters
|
|
||||||
|
|
||||||
Sanic comes with a basic router that supports request parameters.
|
|
||||||
|
|
||||||
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
|
|
||||||
Request parameters will be passed to the route handler functions as keyword
|
|
||||||
arguments.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
@app.route('/tag/<tag>')
|
|
||||||
async def tag_handler(request, tag):
|
|
||||||
return text('Tag - {}'.format(tag))
|
|
||||||
```
|
|
||||||
|
|
||||||
To specify a type for the parameter, add a `:type` after the parameter name,
|
|
||||||
inside the quotes. If the parameter does not match the specified type, Sanic
|
|
||||||
will throw a `NotFound` exception, resulting in a `404: Page not found` error
|
|
||||||
on the URL.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
@app.route('/number/<integer_arg:int>')
|
|
||||||
async def integer_handler(request, integer_arg):
|
|
||||||
return text('Integer - {}'.format(integer_arg))
|
|
||||||
|
|
||||||
@app.route('/number/<number_arg:number>')
|
|
||||||
async def number_handler(request, number_arg):
|
|
||||||
return text('Number - {}'.format(number_arg))
|
|
||||||
|
|
||||||
@app.route('/person/<name:[A-z]+>')
|
|
||||||
async def person_handler(request, name):
|
|
||||||
return text('Person - {}'.format(name))
|
|
||||||
|
|
||||||
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
|
|
||||||
async def folder_handler(request, folder_id):
|
|
||||||
return text('Folder - {}'.format(folder_id))
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## HTTP request types
|
|
||||||
|
|
||||||
By default, a route defined on a URL will be available for only GET requests to that URL.
|
|
||||||
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
|
||||||
which allows the handler function to work with any of the HTTP methods in the list.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
@app.route('/post', methods=['POST'])
|
|
||||||
async def post_handler(request):
|
|
||||||
return text('POST request - {}'.format(request.json))
|
|
||||||
|
|
||||||
@app.route('/get', methods=['GET'])
|
|
||||||
async def get_handler(request):
|
|
||||||
return text('GET request - {}'.format(request.args))
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
|
|
||||||
|
|
||||||
```python
|
|
||||||
@app.route('/get', methods=['GET'], host='example.com')
|
|
||||||
async def get_handler(request):
|
|
||||||
return text('GET request - {}'.format(request.args))
|
|
||||||
|
|
||||||
# if the host header doesn't match example.com, this route will be used
|
|
||||||
@app.route('/get', methods=['GET'])
|
|
||||||
async def get_handler(request):
|
|
||||||
return text('GET request in default - {}'.format(request.args))
|
|
||||||
```
|
|
||||||
|
|
||||||
There are also shorthand method decorators:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
@app.post('/post')
|
|
||||||
async def post_handler(request):
|
|
||||||
return text('POST request - {}'.format(request.json))
|
|
||||||
|
|
||||||
@app.get('/get')
|
|
||||||
async def get_handler(request):
|
|
||||||
return text('GET request - {}'.format(request.args))
|
|
||||||
|
|
||||||
```
|
|
||||||
## The `add_route` method
|
|
||||||
|
|
||||||
As we have seen, routes are often specified using the `@app.route` decorator.
|
|
||||||
However, this decorator is really just a wrapper for the `app.add_route`
|
|
||||||
method, which is used as follows:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
# Define the handler functions
|
|
||||||
async def handler1(request):
|
|
||||||
return text('OK')
|
|
||||||
|
|
||||||
async def handler2(request, name):
|
|
||||||
return text('Folder - {}'.format(name))
|
|
||||||
|
|
||||||
async def person_handler2(request, name):
|
|
||||||
return text('Person - {}'.format(name))
|
|
||||||
|
|
||||||
# Add each handler function as a route
|
|
||||||
app.add_route(handler1, '/test')
|
|
||||||
app.add_route(handler2, '/folder/<name>')
|
|
||||||
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
|
|
||||||
```
|
|
||||||
|
|
||||||
## URL building with `url_for`
|
|
||||||
|
|
||||||
Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import redirect
|
|
||||||
|
|
||||||
@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 redirect(url)
|
|
||||||
|
|
||||||
@app.route('/posts/<post_id>')
|
|
||||||
async def post_handler(request, post_id):
|
|
||||||
return text('Post - {}'.format(post_id))
|
|
||||||
```
|
|
||||||
|
|
||||||
Other things to keep in mind when using `url_for`:
|
|
||||||
|
|
||||||
- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example:
|
|
||||||
```python
|
|
||||||
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
|
|
||||||
# /posts/5?arg_one=one&arg_two=two
|
|
||||||
```
|
|
||||||
- Multivalue argument can be passed to `url_for`. For example:
|
|
||||||
```python
|
|
||||||
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
|
|
||||||
# /posts/5?arg_one=one&arg_one=two
|
|
||||||
```
|
|
||||||
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not support now and will be ignored). For example:
|
|
||||||
```python
|
|
||||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
|
|
||||||
# /posts/5?arg_one=one#anchor
|
|
||||||
|
|
||||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
|
|
||||||
# //server/posts/5?arg_one=one
|
|
||||||
# _external requires passed argument _server or SERVER_NAME in app.config or url will be same as no _external
|
|
||||||
|
|
||||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
|
|
||||||
# http://server/posts/5?arg_one=one
|
|
||||||
# when specifying _scheme, _external must be True
|
|
||||||
|
|
||||||
# you can pass all special arguments one time
|
|
||||||
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
|
|
||||||
## About `strict_slashes`
|
|
||||||
|
|
||||||
You can make `routes` strict to trailing slash or not, it's configurable.
|
|
||||||
|
|
||||||
```python
|
|
||||||
|
|
||||||
# provide default strict_slashes value for all routes
|
|
||||||
app = Sanic('test_route_strict_slash', strict_slashes=True)
|
|
||||||
|
|
||||||
# you can also overwrite strict_slashes value for specific route
|
|
||||||
@app.get('/get', strict_slashes=False)
|
|
||||||
def handler(request):
|
|
||||||
return text('OK')
|
|
||||||
|
|
||||||
# It also works for blueprints
|
|
||||||
bp = Blueprint('test_bp_strict_slash', strict_slashes=True)
|
|
||||||
|
|
||||||
@bp.get('/bp/get', strict_slashes=False)
|
|
||||||
def handler(request):
|
|
||||||
return text('OK')
|
|
||||||
|
|
||||||
app.blueprint(bp)
|
|
||||||
```
|
|
||||||
|
|
||||||
## User defined route name
|
|
||||||
|
|
||||||
You can pass `name` to change the route name to avoid using the default name (`handler.__name__`).
|
|
||||||
|
|
||||||
```python
|
|
||||||
|
|
||||||
app = Sanic('test_named_route')
|
|
||||||
|
|
||||||
@app.get('/get', name='get_handler')
|
|
||||||
def handler(request):
|
|
||||||
return text('OK')
|
|
||||||
|
|
||||||
# then you need use `app.url_for('get_handler')`
|
|
||||||
# instead of # `app.url_for('handler')`
|
|
||||||
|
|
||||||
# It also works for blueprints
|
|
||||||
bp = Blueprint('test_named_bp')
|
|
||||||
|
|
||||||
@bp.get('/bp/get', name='get_handler')
|
|
||||||
def handler(request):
|
|
||||||
return text('OK')
|
|
||||||
|
|
||||||
app.blueprint(bp)
|
|
||||||
|
|
||||||
# then you need use `app.url_for('test_named_bp.get_handler')`
|
|
||||||
# instead of `app.url_for('test_named_bp.handler')`
|
|
||||||
|
|
||||||
# different names can be used for same url with different methods
|
|
||||||
|
|
||||||
@app.get('/test', name='route_test')
|
|
||||||
def handler(request):
|
|
||||||
return text('OK')
|
|
||||||
|
|
||||||
@app.post('/test', name='route_post')
|
|
||||||
def handler2(request):
|
|
||||||
return text('OK POST')
|
|
||||||
|
|
||||||
@app.put('/test', name='route_put')
|
|
||||||
def handler3(request):
|
|
||||||
return text('OK PUT')
|
|
||||||
|
|
||||||
# below url are the same, you can use any of them
|
|
||||||
# '/test'
|
|
||||||
app.url_for('route_test')
|
|
||||||
# app.url_for('route_post')
|
|
||||||
# app.url_for('route_put')
|
|
||||||
|
|
||||||
# for same handler name with different methods
|
|
||||||
# you need specify the name (it's url_for issue)
|
|
||||||
@app.get('/get')
|
|
||||||
def handler(request):
|
|
||||||
return text('OK')
|
|
||||||
|
|
||||||
@app.post('/post', name='post_handler')
|
|
||||||
def handler(request):
|
|
||||||
return text('OK')
|
|
||||||
|
|
||||||
# then
|
|
||||||
# app.url_for('handler') == '/get'
|
|
||||||
# app.url_for('post_handler') == '/post'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build URL for static files
|
|
||||||
|
|
||||||
You can use `url_for` for static file url building now.
|
|
||||||
If it's for file directly, `filename` can be ignored.
|
|
||||||
|
|
||||||
```python
|
|
||||||
|
|
||||||
app = Sanic('test_static')
|
|
||||||
app.static('/static', './static')
|
|
||||||
app.static('/uploads', './uploads', name='uploads')
|
|
||||||
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
|
||||||
|
|
||||||
bp = Blueprint('bp', url_prefix='bp')
|
|
||||||
bp.static('/static', './static')
|
|
||||||
bp.static('/uploads', './uploads', name='uploads')
|
|
||||||
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
|
||||||
app.blueprint(bp)
|
|
||||||
|
|
||||||
# then build the url
|
|
||||||
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
|
||||||
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
|
||||||
app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
|
|
||||||
app.url_for('static', name='best_png') == '/the_best.png'
|
|
||||||
|
|
||||||
# blueprint url building
|
|
||||||
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
|
||||||
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
|
|
||||||
app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'
|
|
||||||
|
|
||||||
```
|
|
||||||
433
docs/sanic/routing.rst
Normal file
433
docs/sanic/routing.rst
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
Routing
|
||||||
|
-------
|
||||||
|
|
||||||
|
Routing allows the user to specify handler functions for different URL endpoints.
|
||||||
|
|
||||||
|
A basic route looks like the following, where `app` is an instance of the
|
||||||
|
`Sanic` class:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def test(request):
|
||||||
|
return json({ "hello": "world" })
|
||||||
|
|
||||||
|
When the url `http://server.url/` is accessed (the base url of the server), the
|
||||||
|
final `/` is matched by the router to the handler function, `test`, which then
|
||||||
|
returns a JSON object.
|
||||||
|
|
||||||
|
Sanic handler functions must be defined using the `async def` syntax, as they
|
||||||
|
are asynchronous functions.
|
||||||
|
|
||||||
|
Request parameters
|
||||||
|
==================
|
||||||
|
|
||||||
|
Sanic comes with a basic router that supports request parameters.
|
||||||
|
|
||||||
|
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
|
||||||
|
Request parameters will be passed to the route handler functions as keyword
|
||||||
|
arguments.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
@app.route('/tag/<tag>')
|
||||||
|
async def tag_handler(request, tag):
|
||||||
|
return text('Tag - {}'.format(tag))
|
||||||
|
|
||||||
|
To specify a type for the parameter, add a `:type` after the parameter name,
|
||||||
|
inside the quotes. If the parameter does not match the specified type, Sanic
|
||||||
|
will throw a `NotFound` exception, resulting in a `404: Page not found` error
|
||||||
|
on the URL.
|
||||||
|
|
||||||
|
Supported types
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* `string`
|
||||||
|
* "Bob"
|
||||||
|
* "Python 3"
|
||||||
|
* `int`
|
||||||
|
* 10
|
||||||
|
* 20
|
||||||
|
* 30
|
||||||
|
* -10
|
||||||
|
* (No floats work here)
|
||||||
|
* `number`
|
||||||
|
* 1
|
||||||
|
* 1.5
|
||||||
|
* 10
|
||||||
|
* -10
|
||||||
|
* `alpha`
|
||||||
|
* "Bob"
|
||||||
|
* "Python"
|
||||||
|
* (If it contains a symbol or a non alphanumeric character it will fail)
|
||||||
|
* `path`
|
||||||
|
* "hello"
|
||||||
|
* "hello.text"
|
||||||
|
* "hello world"
|
||||||
|
* `uuid`
|
||||||
|
* 123a123a-a12a-1a1a-a1a1-1a12a1a12345 (UUIDv4 Support)
|
||||||
|
* `regex expression`
|
||||||
|
|
||||||
|
If no type is set then a string is expected. The argument given to the function will always be a string, independent of the type.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
@app.route('/string/<string_arg:string>')
|
||||||
|
async def string_handler(request, string_arg):
|
||||||
|
return text('String - {}'.format(string_arg))
|
||||||
|
|
||||||
|
@app.route('/int/<integer_arg:int>')
|
||||||
|
async def integer_handler(request, integer_arg):
|
||||||
|
return text('Integer - {}'.format(integer_arg))
|
||||||
|
|
||||||
|
@app.route('/number/<number_arg:number>')
|
||||||
|
async def number_handler(request, number_arg):
|
||||||
|
return text('Number - {}'.format(number_arg))
|
||||||
|
|
||||||
|
@app.route('/alpha/<alpha_arg:alpha>')
|
||||||
|
async def number_handler(request, alpha_arg):
|
||||||
|
return text('Alpha - {}'.format(alpha_arg))
|
||||||
|
|
||||||
|
@app.route('/path/<path_arg:path>')
|
||||||
|
async def number_handler(request, path_arg):
|
||||||
|
return text('Path - {}'.format(path_arg))
|
||||||
|
|
||||||
|
@app.route('/uuid/<uuid_arg:uuid>')
|
||||||
|
async def number_handler(request, uuid_arg):
|
||||||
|
return text('Uuid - {}'.format(uuid_arg))
|
||||||
|
|
||||||
|
@app.route('/person/<name:[A-z]+>')
|
||||||
|
async def person_handler(request, name):
|
||||||
|
return text('Person - {}'.format(name))
|
||||||
|
|
||||||
|
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
|
||||||
|
async def folder_handler(request, folder_id):
|
||||||
|
return text('Folder - {}'.format(folder_id))
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
`str` is not a valid type tag. If you want `str` recognition then you must use `string`
|
||||||
|
|
||||||
|
HTTP request types
|
||||||
|
==================
|
||||||
|
|
||||||
|
By default, a route defined on a URL will be available for only GET requests to that URL.
|
||||||
|
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
||||||
|
which allows the handler function to work with any of the HTTP methods in the list.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
@app.route('/post', methods=['POST'])
|
||||||
|
async def post_handler(request):
|
||||||
|
return text('POST request - {}'.format(request.json))
|
||||||
|
|
||||||
|
@app.route('/get', methods=['GET'])
|
||||||
|
async def get_handler(request):
|
||||||
|
return text('GET request - {}'.format(request.args))
|
||||||
|
|
||||||
|
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route('/get', methods=['GET'], host='example.com')
|
||||||
|
async def get_handler(request):
|
||||||
|
return text('GET request - {}'.format(request.args))
|
||||||
|
|
||||||
|
# if the host header doesn't match example.com, this route will be used
|
||||||
|
@app.route('/get', methods=['GET'])
|
||||||
|
async def get_handler(request):
|
||||||
|
return text('GET request in default - {}'.format(request.args))
|
||||||
|
|
||||||
|
There are also shorthand method decorators:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
@app.post('/post')
|
||||||
|
async def post_handler(request):
|
||||||
|
return text('POST request - {}'.format(request.json))
|
||||||
|
|
||||||
|
@app.get('/get')
|
||||||
|
async def get_handler(request):
|
||||||
|
return text('GET request - {}'.format(request.args))
|
||||||
|
|
||||||
|
The `add_route` method
|
||||||
|
======================
|
||||||
|
|
||||||
|
As we have seen, routes are often specified using the `@app.route` decorator.
|
||||||
|
However, this decorator is really just a wrapper for the `app.add_route`
|
||||||
|
method, which is used as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
# Define the handler functions
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
async def handler2(request, name):
|
||||||
|
return text('Folder - {}'.format(name))
|
||||||
|
|
||||||
|
async def person_handler2(request, name):
|
||||||
|
return text('Person - {}'.format(name))
|
||||||
|
|
||||||
|
# Add each handler function as a route
|
||||||
|
app.add_route(handler1, '/test')
|
||||||
|
app.add_route(handler2, '/folder/<name>')
|
||||||
|
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
|
||||||
|
|
||||||
|
URL building with `url_for`
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import redirect
|
||||||
|
|
||||||
|
@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 redirect(url)
|
||||||
|
|
||||||
|
@app.route('/posts/<post_id>')
|
||||||
|
async def post_handler(request, post_id):
|
||||||
|
return text('Post - {}'.format(post_id))
|
||||||
|
|
||||||
|
Other things to keep in mind when using `url_for`:
|
||||||
|
|
||||||
|
- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
|
||||||
|
# /posts/5?arg_one=one&arg_two=two
|
||||||
|
|
||||||
|
- Multivalue argument can be passed to `url_for`. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
|
||||||
|
# /posts/5?arg_one=one&arg_one=two
|
||||||
|
|
||||||
|
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not supported now and will be ignored). For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
|
||||||
|
# /posts/5?arg_one=one#anchor
|
||||||
|
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
|
||||||
|
# //server/posts/5?arg_one=one
|
||||||
|
# _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external
|
||||||
|
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
|
||||||
|
# http://server/posts/5?arg_one=one
|
||||||
|
# when specifying _scheme, _external must be True
|
||||||
|
|
||||||
|
# you can pass all special arguments at once
|
||||||
|
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
|
||||||
|
# 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 raised.
|
||||||
|
|
||||||
|
WebSocket routes
|
||||||
|
================
|
||||||
|
|
||||||
|
Routes for the WebSocket protocol can be defined with the `@app.websocket`
|
||||||
|
decorator:
|
||||||
|
|
||||||
|
.. code-block:: 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:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def feed(request, ws):
|
||||||
|
pass
|
||||||
|
|
||||||
|
app.add_websocket_route(my_websocket_handler, '/feed')
|
||||||
|
|
||||||
|
Handlers to a WebSocket route are invoked with 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.
|
||||||
|
|
||||||
|
|
||||||
|
About `strict_slashes`
|
||||||
|
======================
|
||||||
|
|
||||||
|
You can make `routes` strict to trailing slash or not, it's configurable.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# provide default strict_slashes value for all routes
|
||||||
|
app = Sanic('test_route_strict_slash', strict_slashes=True)
|
||||||
|
|
||||||
|
# you can also overwrite strict_slashes value for specific route
|
||||||
|
@app.get('/get', strict_slashes=False)
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
# It also works for blueprints
|
||||||
|
bp = Blueprint('test_bp_strict_slash', strict_slashes=True)
|
||||||
|
|
||||||
|
@bp.get('/bp/get', strict_slashes=False)
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
|
||||||
|
The behavior of how the `strict_slashes` flag follows a defined hierarchy which decides if a specific route
|
||||||
|
falls under the `strict_slashes` behavior.
|
||||||
|
|
||||||
|
| Route/
|
||||||
|
| ├──Blueprint/
|
||||||
|
| ├──Application/
|
||||||
|
|
||||||
|
Above hierarchy defines how the `strict_slashes` flag will behave. The first non `None` value of the `strict_slashes`
|
||||||
|
found in the above order will be applied to the route in question.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic, Blueprint
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic("sample_strict_slashes", strict_slashes=True)
|
||||||
|
|
||||||
|
@app.get("/r1")
|
||||||
|
def r1(request):
|
||||||
|
return text("strict_slashes is applicable from App level")
|
||||||
|
|
||||||
|
@app.get("/r2", strict_slashes=False)
|
||||||
|
def r2(request):
|
||||||
|
return text("strict_slashes is not applicable due to False value set in route level")
|
||||||
|
|
||||||
|
bp = Blueprint("bp", strict_slashes=False)
|
||||||
|
|
||||||
|
@bp.get("/r3", strict_slashes=True)
|
||||||
|
def r3(request):
|
||||||
|
return text("strict_slashes applicable from blueprint route level")
|
||||||
|
|
||||||
|
bp1 = Blueprint("bp1", strict_slashes=True)
|
||||||
|
|
||||||
|
@bp.get("/r4")
|
||||||
|
def r3(request):
|
||||||
|
return text("strict_slashes applicable from blueprint level")
|
||||||
|
|
||||||
|
User defined route name
|
||||||
|
=======================
|
||||||
|
|
||||||
|
A custom route name can be used by passing a `name` argument while registering the route which will
|
||||||
|
override the default route name generated using the `handler.__name__` attribute.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic('test_named_route')
|
||||||
|
|
||||||
|
@app.get('/get', name='get_handler')
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
# then you need use `app.url_for('get_handler')`
|
||||||
|
# instead of # `app.url_for('handler')`
|
||||||
|
|
||||||
|
# It also works for blueprints
|
||||||
|
bp = Blueprint('test_named_bp')
|
||||||
|
|
||||||
|
@bp.get('/bp/get', name='get_handler')
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
|
||||||
|
# then you need use `app.url_for('test_named_bp.get_handler')`
|
||||||
|
# instead of `app.url_for('test_named_bp.handler')`
|
||||||
|
|
||||||
|
# different names can be used for same url with different methods
|
||||||
|
|
||||||
|
@app.get('/test', name='route_test')
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
@app.post('/test', name='route_post')
|
||||||
|
def handler2(request):
|
||||||
|
return text('OK POST')
|
||||||
|
|
||||||
|
@app.put('/test', name='route_put')
|
||||||
|
def handler3(request):
|
||||||
|
return text('OK PUT')
|
||||||
|
|
||||||
|
# below url are the same, you can use any of them
|
||||||
|
# '/test'
|
||||||
|
app.url_for('route_test')
|
||||||
|
# app.url_for('route_post')
|
||||||
|
# app.url_for('route_put')
|
||||||
|
|
||||||
|
# for same handler name with different methods
|
||||||
|
# you need specify the name (it's url_for issue)
|
||||||
|
@app.get('/get')
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
@app.post('/post', name='post_handler')
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
# then
|
||||||
|
# app.url_for('handler') == '/get'
|
||||||
|
# app.url_for('post_handler') == '/post'
|
||||||
|
|
||||||
|
Build URL for static files
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Sanic supports using `url_for` method to build static file urls. In case if the static url
|
||||||
|
is pointing to a directory, `filename` parameter to the `url_for` can be ignored. q
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
app = Sanic('test_static')
|
||||||
|
app.static('/static', './static')
|
||||||
|
app.static('/uploads', './uploads', name='uploads')
|
||||||
|
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||||
|
|
||||||
|
bp = Blueprint('bp', url_prefix='bp')
|
||||||
|
bp.static('/static', './static')
|
||||||
|
bp.static('/uploads', './uploads', name='uploads')
|
||||||
|
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||||
|
app.blueprint(bp)
|
||||||
|
|
||||||
|
# then build the url
|
||||||
|
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
||||||
|
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
||||||
|
app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
|
||||||
|
app.url_for('static', name='best_png') == '/the_best.png'
|
||||||
|
|
||||||
|
# blueprint url building
|
||||||
|
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
||||||
|
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
|
||||||
|
app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# Static Files
|
|
||||||
|
|
||||||
Static files and directories, such as an image file, are served by Sanic when
|
|
||||||
registered with the `app.static()` method. The method takes an endpoint URL and a
|
|
||||||
filename. The file specified will then be accessible via the given endpoint.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.blueprints import Blueprint
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
# Serves files from the static folder to the URL /static
|
|
||||||
app.static('/static', './static')
|
|
||||||
# use url_for to build the url, name defaults to 'static' and can be ignored
|
|
||||||
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
|
||||||
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
|
||||||
|
|
||||||
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
|
|
||||||
# is requested
|
|
||||||
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
|
||||||
|
|
||||||
# you can use url_for to build the static file url
|
|
||||||
# you can ignore name and filename parameters if you don't define it
|
|
||||||
app.url_for('static', name='best_png') == '/the_best.png'
|
|
||||||
app.url_for('static', name='best_png', filename='any') == '/the_best.png'
|
|
||||||
|
|
||||||
# you need define the name for other static files
|
|
||||||
app.static('/another.png', '/home/ubuntu/another.png', name='another')
|
|
||||||
app.url_for('static', name='another') == '/another.png'
|
|
||||||
app.url_for('static', name='another', filename='any') == '/another.png'
|
|
||||||
|
|
||||||
# also, you can use static for blueprint
|
|
||||||
bp = Blueprint('bp', url_prefix='/bp')
|
|
||||||
bp.static('/static', './static')
|
|
||||||
|
|
||||||
# servers the file directly
|
|
||||||
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
|
||||||
app.blueprint(bp)
|
|
||||||
|
|
||||||
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
|
||||||
app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000)
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note:** Sanic does not provide directory index when you serve a static directory.
|
|
||||||
|
|
||||||
## Virtual Host
|
|
||||||
|
|
||||||
The `app.static()` method also support **virtual host**. You can serve your static files with spefic **virtual host** with `host` argument. For example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
app.static('/static', './static')
|
|
||||||
app.static('/example_static', './example_static', host='www.example.com')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Streaming Large File
|
|
||||||
|
|
||||||
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
|
|
||||||
|
|
||||||
Here is an example:
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
|
|
||||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
|
|
||||||
```
|
|
||||||
92
docs/sanic/static_files.rst
Normal file
92
docs/sanic/static_files.rst
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
Static Files
|
||||||
|
============
|
||||||
|
|
||||||
|
Static files and directories, such as an image file, are served by Sanic when
|
||||||
|
registered with the `app.static()` method. The method takes an endpoint URL and a
|
||||||
|
filename. The file specified will then be accessible via the given endpoint.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
# Serves files from the static folder to the URL /static
|
||||||
|
app.static('/static', './static')
|
||||||
|
# use url_for to build the url, name defaults to 'static' and can be ignored
|
||||||
|
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
||||||
|
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
||||||
|
|
||||||
|
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
|
||||||
|
# is requested
|
||||||
|
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||||
|
|
||||||
|
# you can use url_for to build the static file url
|
||||||
|
# you can ignore name and filename parameters if you don't define it
|
||||||
|
app.url_for('static', name='best_png') == '/the_best.png'
|
||||||
|
app.url_for('static', name='best_png', filename='any') == '/the_best.png'
|
||||||
|
|
||||||
|
# you need define the name for other static files
|
||||||
|
app.static('/another.png', '/home/ubuntu/another.png', name='another')
|
||||||
|
app.url_for('static', name='another') == '/another.png'
|
||||||
|
app.url_for('static', name='another', filename='any') == '/another.png'
|
||||||
|
|
||||||
|
# also, you can use static for blueprint
|
||||||
|
bp = Blueprint('bp', url_prefix='/bp')
|
||||||
|
bp.static('/static', './static')
|
||||||
|
|
||||||
|
# specify a different content_type for your files
|
||||||
|
# such as adding 'charset'
|
||||||
|
app.static('/', '/public/index.html', content_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
# servers the file directly
|
||||||
|
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||||
|
app.blueprint(bp)
|
||||||
|
|
||||||
|
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
||||||
|
app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
|
> **Note:** Sanic does not provide directory index when you serve a static directory.
|
||||||
|
|
||||||
|
Virtual Host
|
||||||
|
------------
|
||||||
|
|
||||||
|
The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
app.static('/static', './static')
|
||||||
|
app.static('/example_static', './example_static', host='www.example.com')
|
||||||
|
|
||||||
|
Streaming Large File
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
|
||||||
|
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
|
||||||
|
|
||||||
|
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
|
||||||
|
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# Streaming
|
|
||||||
|
|
||||||
## Request Streaming
|
|
||||||
|
|
||||||
Sanic allows you to get request data by stream, as below. When the request ends, `request.stream.get()` returns `None`. Only post, put and patch decorator have stream argument.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.views import CompositionView
|
|
||||||
from sanic.views import HTTPMethodView
|
|
||||||
from sanic.views import stream as stream_decorator
|
|
||||||
from sanic.blueprints import Blueprint
|
|
||||||
from sanic.response import stream, text
|
|
||||||
|
|
||||||
bp = Blueprint('blueprint_request_stream')
|
|
||||||
app = Sanic('request_stream')
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleView(HTTPMethodView):
|
|
||||||
|
|
||||||
@stream_decorator
|
|
||||||
async def post(self, request):
|
|
||||||
result = ''
|
|
||||||
while True:
|
|
||||||
body = await request.stream.get()
|
|
||||||
if body is None:
|
|
||||||
break
|
|
||||||
result += body.decode('utf-8')
|
|
||||||
return text(result)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post('/stream', stream=True)
|
|
||||||
async def handler(request):
|
|
||||||
async def streaming(response):
|
|
||||||
while True:
|
|
||||||
body = await request.stream.get()
|
|
||||||
if body is None:
|
|
||||||
break
|
|
||||||
body = body.decode('utf-8').replace('1', 'A')
|
|
||||||
await response.write(body)
|
|
||||||
return stream(streaming)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.put('/bp_stream', stream=True)
|
|
||||||
async def bp_handler(request):
|
|
||||||
result = ''
|
|
||||||
while True:
|
|
||||||
body = await request.stream.get()
|
|
||||||
if body is None:
|
|
||||||
break
|
|
||||||
result += body.decode('utf-8').replace('1', 'A')
|
|
||||||
return text(result)
|
|
||||||
|
|
||||||
|
|
||||||
async def post_handler(request):
|
|
||||||
result = ''
|
|
||||||
while True:
|
|
||||||
body = await request.stream.get()
|
|
||||||
if body is None:
|
|
||||||
break
|
|
||||||
result += body.decode('utf-8')
|
|
||||||
return text(result)
|
|
||||||
|
|
||||||
app.blueprint(bp)
|
|
||||||
app.add_route(SimpleView.as_view(), '/method_view')
|
|
||||||
view = CompositionView()
|
|
||||||
view.add(['POST'], post_handler, stream=True)
|
|
||||||
app.add_route(view, '/composition_view')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='127.0.0.1', port=8000)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Response 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):
|
|
||||||
await response.write('foo,')
|
|
||||||
await 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)'):
|
|
||||||
await response.write(record[0])
|
|
||||||
|
|
||||||
return stream(stream_from_db)
|
|
||||||
```
|
|
||||||
147
docs/sanic/streaming.rst
Normal file
147
docs/sanic/streaming.rst
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
Streaming
|
||||||
|
=========
|
||||||
|
|
||||||
|
Request Streaming
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Sanic allows you to get request data by stream, as below. When the request ends, `await request.stream.read()` returns `None`. Only post, put and patch decorator have stream argument.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.views import CompositionView
|
||||||
|
from sanic.views import HTTPMethodView
|
||||||
|
from sanic.views import stream as stream_decorator
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
from sanic.response import stream, text
|
||||||
|
|
||||||
|
bp = Blueprint('blueprint_request_stream')
|
||||||
|
app = Sanic('request_stream')
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleView(HTTPMethodView):
|
||||||
|
|
||||||
|
@stream_decorator
|
||||||
|
async def post(self, request):
|
||||||
|
result = ''
|
||||||
|
while True:
|
||||||
|
body = await request.stream.read()
|
||||||
|
if body is None:
|
||||||
|
break
|
||||||
|
result += body.decode('utf-8')
|
||||||
|
return text(result)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post('/stream', stream=True)
|
||||||
|
async def handler(request):
|
||||||
|
async def streaming(response):
|
||||||
|
while True:
|
||||||
|
body = await request.stream.read()
|
||||||
|
if body is None:
|
||||||
|
break
|
||||||
|
body = body.decode('utf-8').replace('1', 'A')
|
||||||
|
await response.write(body)
|
||||||
|
return stream(streaming)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.put('/bp_stream', stream=True)
|
||||||
|
async def bp_put_handler(request):
|
||||||
|
result = ''
|
||||||
|
while True:
|
||||||
|
body = await request.stream.read()
|
||||||
|
if body is None:
|
||||||
|
break
|
||||||
|
result += body.decode('utf-8').replace('1', 'A')
|
||||||
|
return text(result)
|
||||||
|
|
||||||
|
|
||||||
|
# You can also use `bp.add_route()` with stream argument
|
||||||
|
async def bp_post_handler(request):
|
||||||
|
result = ''
|
||||||
|
while True:
|
||||||
|
body = await request.stream.read()
|
||||||
|
if body is None:
|
||||||
|
break
|
||||||
|
result += body.decode('utf-8').replace('1', 'A')
|
||||||
|
return text(result)
|
||||||
|
|
||||||
|
bp.add_route(bp_post_handler, '/bp_stream', methods=['POST'], stream=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def post_handler(request):
|
||||||
|
result = ''
|
||||||
|
while True:
|
||||||
|
body = await request.stream.read()
|
||||||
|
if body is None:
|
||||||
|
break
|
||||||
|
result += body.decode('utf-8')
|
||||||
|
return text(result)
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
app.add_route(SimpleView.as_view(), '/method_view')
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['POST'], post_handler, stream=True)
|
||||||
|
app.add_route(view, '/composition_view')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='127.0.0.1', port=8000)
|
||||||
|
|
||||||
|
Response 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:
|
||||||
|
|
||||||
|
.. code-block:: 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):
|
||||||
|
await response.write('foo,')
|
||||||
|
await 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:
|
||||||
|
|
||||||
|
.. code-block:: 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)'):
|
||||||
|
await response.write(record[0])
|
||||||
|
|
||||||
|
return stream(stream_from_db)
|
||||||
|
|
||||||
|
If a client supports HTTP/1.1, Sanic will use `chunked transfer encoding <https://en.wikipedia.org/wiki/Chunked_transfer_encoding>`_; you can explicitly enable or disable it using `chunked` option of the `stream` function.
|
||||||
|
|
||||||
|
File Streaming
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Sanic provides `sanic.response.file_stream` function that is useful when you want to send a large file. It returns a `StreamingHTTPResponse` object and will use chunked transfer encoding by default; for this reason Sanic doesn't add `Content-Length` HTTP header in the response. If you want to use this header, you can disable chunked transfer encoding and add it manually:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from aiofiles import os as async_os
|
||||||
|
from sanic.response import file_stream
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def index(request):
|
||||||
|
file_path = "/srv/www/whatever.png"
|
||||||
|
|
||||||
|
file_stat = await async_os.stat(file_path)
|
||||||
|
headers = {"Content-Length": str(file_stat.st_size)}
|
||||||
|
|
||||||
|
return await file_stream(
|
||||||
|
file_path,
|
||||||
|
headers=headers,
|
||||||
|
chunked=False,
|
||||||
|
)
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
# Testing
|
|
||||||
|
|
||||||
Sanic endpoints can be tested locally using the `test_client` object, which
|
|
||||||
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
|
||||||
library.
|
|
||||||
|
|
||||||
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
|
|
||||||
for you to run against your application. A simple example (using pytest) is like follows:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Import the Sanic app, usually created with Sanic(__name__)
|
|
||||||
from external_server import app
|
|
||||||
|
|
||||||
def test_index_returns_200():
|
|
||||||
request, response = app.test_client.get('/')
|
|
||||||
assert response.status == 200
|
|
||||||
|
|
||||||
def test_index_put_not_allowed():
|
|
||||||
request, response = app.test_client.put('/')
|
|
||||||
assert response.status == 405
|
|
||||||
```
|
|
||||||
|
|
||||||
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
|
|
||||||
your test request is executed against your application, using `aiohttp`.
|
|
||||||
|
|
||||||
The `test_client` methods accept the following arguments and keyword arguments:
|
|
||||||
|
|
||||||
- `uri` *(default `'/'`)* A string representing the URI to test.
|
|
||||||
- `gather_request` *(default `True`)* A boolean which determines whether the
|
|
||||||
original request will be returned by the function. If set to `True`, the
|
|
||||||
return value is a tuple of `(request, response)`, if `False` only the
|
|
||||||
response is returned.
|
|
||||||
- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
|
|
||||||
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
|
|
||||||
|
|
||||||
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the aiohttp ClientSession request.
|
|
||||||
|
|
||||||
For example, to supply data to a GET request, you would do the following:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def test_get_request_includes_data():
|
|
||||||
params = {'key1': 'value1', 'key2': 'value2'}
|
|
||||||
request, response = app.test_client.get('/', params=params)
|
|
||||||
assert request.args.get('key1') == 'value1'
|
|
||||||
```
|
|
||||||
|
|
||||||
And to supply data to a JSON POST request:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def test_post_json_request_includes_data():
|
|
||||||
data = {'key1': 'value1', 'key2': 'value2'}
|
|
||||||
request, response = app.test_client.post('/', data=json.dumps(data))
|
|
||||||
assert request.json.get('key1') == 'value1'
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
|
|
||||||
## pytest-sanic
|
|
||||||
|
|
||||||
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously.
|
|
||||||
Just write tests like,
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def test_sanic_db_find_by_id(app):
|
|
||||||
"""
|
|
||||||
Let's assume that, in db we have,
|
|
||||||
{
|
|
||||||
"id": "123",
|
|
||||||
"name": "Kobe Bryant",
|
|
||||||
"team": "Lakers",
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
doc = await app.db["players"].find_by_id("123")
|
|
||||||
assert doc.name == "Kobe Bryant"
|
|
||||||
assert doc.team == "Lakers"
|
|
||||||
```
|
|
||||||
|
|
||||||
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) also provides some useful fixtures, like loop, unused_port,
|
|
||||||
test_server, test_client.
|
|
||||||
|
|
||||||
```python
|
|
||||||
@pytest.yield_fixture
|
|
||||||
def app():
|
|
||||||
app = Sanic("test_sanic_app")
|
|
||||||
|
|
||||||
@app.route("/test_get", methods=['GET'])
|
|
||||||
async def test_get(request):
|
|
||||||
return response.json({"GET": True})
|
|
||||||
|
|
||||||
@app.route("/test_post", methods=['POST'])
|
|
||||||
async def test_post(request):
|
|
||||||
return response.json({"POST": True})
|
|
||||||
|
|
||||||
yield app
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def test_cli(loop, app, test_client):
|
|
||||||
return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol))
|
|
||||||
|
|
||||||
|
|
||||||
#########
|
|
||||||
# Tests #
|
|
||||||
#########
|
|
||||||
|
|
||||||
async def test_fixture_test_client_get(test_cli):
|
|
||||||
"""
|
|
||||||
GET request
|
|
||||||
"""
|
|
||||||
resp = await test_cli.get('/test_get')
|
|
||||||
assert resp.status == 200
|
|
||||||
resp_json = await resp.json()
|
|
||||||
assert resp_json == {"GET": True}
|
|
||||||
|
|
||||||
async def test_fixture_test_client_post(test_cli):
|
|
||||||
"""
|
|
||||||
POST request
|
|
||||||
"""
|
|
||||||
resp = await test_cli.post('/test_post')
|
|
||||||
assert resp.status == 200
|
|
||||||
resp_json = await resp.json()
|
|
||||||
assert resp_json == {"POST": True}
|
|
||||||
```
|
|
||||||
145
docs/sanic/testing.rst
Normal file
145
docs/sanic/testing.rst
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
Testing
|
||||||
|
=======
|
||||||
|
|
||||||
|
Sanic endpoints can be tested locally using the `test_client` object, which
|
||||||
|
depends on an additional package: `httpx <https://www.encode.io/httpx/>`_
|
||||||
|
library, which implements an API that mirrors the `requests` library.
|
||||||
|
|
||||||
|
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
|
||||||
|
for you to run against your application. A simple example (using pytest) is like follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Import the Sanic app, usually created with Sanic(__name__)
|
||||||
|
from external_server import app
|
||||||
|
|
||||||
|
def test_index_returns_200():
|
||||||
|
request, response = app.test_client.get('/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
def test_index_put_not_allowed():
|
||||||
|
request, response = app.test_client.put('/')
|
||||||
|
assert response.status == 405
|
||||||
|
|
||||||
|
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
|
||||||
|
your test request is executed against your application, using `httpx`.
|
||||||
|
|
||||||
|
The `test_client` methods accept the following arguments and keyword arguments:
|
||||||
|
|
||||||
|
- `uri` *(default `'/'`)* A string representing the URI to test.
|
||||||
|
- `gather_request` *(default `True`)* A boolean which determines whether the
|
||||||
|
original request will be returned by the function. If set to `True`, the
|
||||||
|
return value is a tuple of `(request, response)`, if `False` only the
|
||||||
|
response is returned.
|
||||||
|
- `server_kwargs` *(default `{}`)* a dict of additional arguments to pass into `app.run` before the test request is run.
|
||||||
|
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
|
||||||
|
|
||||||
|
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the request.
|
||||||
|
|
||||||
|
For example, to supply data to a GET request, you would do the following:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def test_get_request_includes_data():
|
||||||
|
params = {'key1': 'value1', 'key2': 'value2'}
|
||||||
|
request, response = app.test_client.get('/', params=params)
|
||||||
|
assert request.args.get('key1') == 'value1'
|
||||||
|
|
||||||
|
And to supply data to a JSON POST request:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def test_post_json_request_includes_data():
|
||||||
|
data = {'key1': 'value1', 'key2': 'value2'}
|
||||||
|
request, response = app.test_client.post('/', data=json.dumps(data))
|
||||||
|
assert request.json.get('key1') == 'value1'
|
||||||
|
|
||||||
|
More information about
|
||||||
|
the available arguments to `httpx` can be found
|
||||||
|
[in the documentation for `httpx <https://www.encode.io/httpx/>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Using a random port
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
If you need to test using a free unpriveleged port chosen by the kernel
|
||||||
|
instead of the default with `SanicTestClient`, you can do so by specifying
|
||||||
|
`port=None`. On most systems the port will be in the range 1024 to 65535.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Import the Sanic app, usually created with Sanic(__name__)
|
||||||
|
from external_server import app
|
||||||
|
from sanic.testing import SanicTestClient
|
||||||
|
|
||||||
|
def test_index_returns_200():
|
||||||
|
request, response = SanicTestClient(app, port=None).get('/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
pytest-sanic
|
||||||
|
------------
|
||||||
|
|
||||||
|
`pytest-sanic <https://github.com/yunstanford/pytest-sanic>`_ is a pytest plugin, it helps you to test your code asynchronously.
|
||||||
|
Just write tests like,
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def test_sanic_db_find_by_id(app):
|
||||||
|
"""
|
||||||
|
Let's assume that, in db we have,
|
||||||
|
{
|
||||||
|
"id": "123",
|
||||||
|
"name": "Kobe Bryant",
|
||||||
|
"team": "Lakers",
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
doc = await app.db["players"].find_by_id("123")
|
||||||
|
assert doc.name == "Kobe Bryant"
|
||||||
|
assert doc.team == "Lakers"
|
||||||
|
|
||||||
|
`pytest-sanic <https://github.com/yunstanford/pytest-sanic>`_ also provides some useful fixtures, like loop, unused_port,
|
||||||
|
test_server, test_client.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@pytest.yield_fixture
|
||||||
|
def app():
|
||||||
|
app = Sanic("test_sanic_app")
|
||||||
|
|
||||||
|
@app.route("/test_get", methods=['GET'])
|
||||||
|
async def test_get(request):
|
||||||
|
return response.json({"GET": True})
|
||||||
|
|
||||||
|
@app.route("/test_post", methods=['POST'])
|
||||||
|
async def test_post(request):
|
||||||
|
return response.json({"POST": True})
|
||||||
|
|
||||||
|
yield app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_cli(loop, app, test_client):
|
||||||
|
return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol))
|
||||||
|
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Tests #
|
||||||
|
#########
|
||||||
|
|
||||||
|
async def test_fixture_test_client_get(test_cli):
|
||||||
|
"""
|
||||||
|
GET request
|
||||||
|
"""
|
||||||
|
resp = await test_cli.get('/test_get')
|
||||||
|
assert resp.status == 200
|
||||||
|
resp_json = await resp.json()
|
||||||
|
assert resp_json == {"GET": True}
|
||||||
|
|
||||||
|
async def test_fixture_test_client_post(test_cli):
|
||||||
|
"""
|
||||||
|
POST request
|
||||||
|
"""
|
||||||
|
resp = await test_cli.post('/test_post')
|
||||||
|
assert resp.status == 200
|
||||||
|
resp_json = await resp.json()
|
||||||
|
assert resp_json == {"POST": True}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# Versioning
|
|
||||||
|
|
||||||
You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number.
|
|
||||||
|
|
||||||
## Per route
|
|
||||||
|
|
||||||
You can pass a version number to the routes directly.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import response
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/text', version=1)
|
|
||||||
def handle_request(request):
|
|
||||||
return response.text('Hello world! Version 1')
|
|
||||||
|
|
||||||
@app.route('/text', version=2)
|
|
||||||
def handle_request(request):
|
|
||||||
return response.text('Hello world! Version 2')
|
|
||||||
|
|
||||||
app.run(port=80)
|
|
||||||
```
|
|
||||||
|
|
||||||
Then with curl:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl localhost/v1/text
|
|
||||||
curl localhost/v2/text
|
|
||||||
```
|
|
||||||
|
|
||||||
## Global blueprint version
|
|
||||||
|
|
||||||
You can also pass a version number to the blueprint, which will apply to all routes.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic import response
|
|
||||||
from sanic.blueprints import Blueprint
|
|
||||||
|
|
||||||
bp = Blueprint('test', version=1)
|
|
||||||
|
|
||||||
@bp.route('/html')
|
|
||||||
def handle_request(request):
|
|
||||||
return response.html('<p>Hello world!</p>')
|
|
||||||
```
|
|
||||||
|
|
||||||
Then with curl:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl localhost/v1/html
|
|
||||||
```
|
|
||||||
54
docs/sanic/versioning.rst
Normal file
54
docs/sanic/versioning.rst
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
Versioning
|
||||||
|
==========
|
||||||
|
|
||||||
|
You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number.
|
||||||
|
|
||||||
|
Per route
|
||||||
|
---------
|
||||||
|
|
||||||
|
You can pass a version number to the routes directly.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/text', version=1)
|
||||||
|
def handle_request(request):
|
||||||
|
return response.text('Hello world! Version 1')
|
||||||
|
|
||||||
|
@app.route('/text', version=2)
|
||||||
|
def handle_request(request):
|
||||||
|
return response.text('Hello world! Version 2')
|
||||||
|
|
||||||
|
app.run(port=80)
|
||||||
|
|
||||||
|
Then with curl:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl localhost/v1/text
|
||||||
|
curl localhost/v2/text
|
||||||
|
|
||||||
|
|
||||||
|
Global blueprint version
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
You can also pass a version number to the blueprint, which will apply to all routes.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('test', version=1)
|
||||||
|
|
||||||
|
@bp.route('/html')
|
||||||
|
def handle_request(request):
|
||||||
|
return response.html('<p>Hello world!</p>')
|
||||||
|
|
||||||
|
Then with curl:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl localhost/v1/html
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
WebSocket
|
WebSocket
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Sanic supports websockets, to setup a WebSocket:
|
Sanic provides an easy to use abstraction on top of `websockets`.
|
||||||
|
Sanic Supports websocket versions 7 and 8.
|
||||||
|
|
||||||
|
To setup a WebSocket:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ decorator:
|
|||||||
app.add_websocket_route(feed, '/feed')
|
app.add_websocket_route(feed, '/feed')
|
||||||
|
|
||||||
|
|
||||||
Handlers for a WebSocket route are passed the request as first argument, and a
|
Handlers for a WebSocket route is invoked with the request as first argument, and a
|
||||||
WebSocket protocol object as second argument. The protocol object has ``send``
|
WebSocket protocol object as second argument. The protocol object has ``send``
|
||||||
and ``recv`` methods to send and receive data respectively.
|
and ``recv`` methods to send and receive data respectively.
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
name: py35
|
name: py36
|
||||||
dependencies:
|
dependencies:
|
||||||
- openssl=1.0.2g=0
|
- pip=18.1=py36_0
|
||||||
- pip=8.1.1=py35_0
|
- python=3.6=0
|
||||||
- python=3.5.1=0
|
- setuptools=40.4.3=py36_0
|
||||||
- readline=6.2=2
|
|
||||||
- setuptools=20.3=py35_0
|
|
||||||
- sqlite=3.9.2=0
|
|
||||||
- tk=8.5.18=0
|
|
||||||
- wheel=0.29.0=py35_0
|
|
||||||
- xz=5.0.5=1
|
|
||||||
- zlib=1.2.8=0
|
|
||||||
- pip:
|
- pip:
|
||||||
- uvloop>=0.5.3
|
|
||||||
- httptools>=0.0.10
|
- httptools>=0.0.10
|
||||||
|
- uvloop>=0.5.3
|
||||||
- ujson>=1.35
|
- ujson>=1.35
|
||||||
- aiofiles>=0.3.0
|
- aiofiles>=0.3.0
|
||||||
- websockets>=6.0
|
- websockets>=6.0,<7.0
|
||||||
- sphinxcontrib-asyncio>=0.2.0
|
|
||||||
- multidict>=4.0,<5.0
|
- multidict>=4.0,<5.0
|
||||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
- sphinx==1.8.3
|
||||||
|
- sphinx_rtd_theme==0.4.2
|
||||||
|
- recommonmark==0.5.0
|
||||||
|
- httpx==0.9.3
|
||||||
|
- sphinxcontrib-asyncio>=0.2.0
|
||||||
|
- docutils==0.14
|
||||||
|
- pygments==2.3.1
|
||||||
|
|||||||
30
examples/amending_request_object.py
Normal file
30
examples/amending_request_object.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import text
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
app = Sanic()
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware('request')
|
||||||
|
def append_request(request):
|
||||||
|
# Add new key with random value
|
||||||
|
request['num'] = randint(0, 100)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/pop')
|
||||||
|
def pop_handler(request):
|
||||||
|
# Pop key from request object
|
||||||
|
num = request.pop('num')
|
||||||
|
return text(num)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/key_exist')
|
||||||
|
def key_exist_handler(request):
|
||||||
|
# Check the key is exist or not
|
||||||
|
if 'num' in request:
|
||||||
|
return text('num exist in request')
|
||||||
|
|
||||||
|
return text('num does not exist in reqeust')
|
||||||
|
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
@@ -13,28 +13,26 @@ def check_request_for_authorization_status(request):
|
|||||||
return flag
|
return flag
|
||||||
|
|
||||||
|
|
||||||
def authorized():
|
def authorized(f):
|
||||||
def decorator(f):
|
@wraps(f)
|
||||||
@wraps(f)
|
async def decorated_function(request, *args, **kwargs):
|
||||||
async def decorated_function(request, *args, **kwargs):
|
# run some method that checks the request
|
||||||
# run some method that checks the request
|
# for the client's authorization status
|
||||||
# for the client's authorization status
|
is_authorized = check_request_for_authorization_status(request)
|
||||||
is_authorized = check_request_for_authorization_status(request)
|
|
||||||
|
|
||||||
if is_authorized:
|
if is_authorized:
|
||||||
# the user is authorized.
|
# the user is authorized.
|
||||||
# run the handler method and return the response
|
# run the handler method and return the response
|
||||||
response = await f(request, *args, **kwargs)
|
response = await f(request, *args, **kwargs)
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
# the user is not authorized.
|
# the user is not authorized.
|
||||||
return json({'status': 'not_authorized'}, 403)
|
return json({'status': 'not_authorized'}, 403)
|
||||||
return decorated_function
|
return decorated_function
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@authorized()
|
@authorized
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return json({'status': 'authorized'})
|
return json({'status': 'authorized'})
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ async def test(request):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
server = app.create_server(host="0.0.0.0", port=8000)
|
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.set_task_factory(context.task_factory)
|
loop.set_task_factory(context.task_factory)
|
||||||
task = asyncio.ensure_future(server)
|
task = asyncio.ensure_future(server)
|
||||||
|
|||||||
61
examples/logdna_example.py
Normal file
61
examples/logdna_example.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
from os import getenv
|
||||||
|
from platform import node
|
||||||
|
from uuid import getnode as get_mac
|
||||||
|
|
||||||
|
from logdna import LogDNAHandler
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
from sanic.request import Request
|
||||||
|
|
||||||
|
log = logging.getLogger('logdna')
|
||||||
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
def get_my_ip_address(remote_server="google.com"):
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
s.connect((remote_server, 80))
|
||||||
|
return s.getsockname()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_mac_address():
|
||||||
|
h = iter(hex(get_mac())[2:].zfill(12))
|
||||||
|
return ":".join(i + next(h) for i in h)
|
||||||
|
|
||||||
|
|
||||||
|
logdna_options = {
|
||||||
|
"app": __name__,
|
||||||
|
"index_meta": True,
|
||||||
|
"hostname": node(),
|
||||||
|
"ip": get_my_ip_address(),
|
||||||
|
"mac": get_mac_address()
|
||||||
|
}
|
||||||
|
|
||||||
|
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
|
||||||
|
|
||||||
|
logdna = logging.getLogger(__name__)
|
||||||
|
logdna.setLevel(logging.INFO)
|
||||||
|
logdna.addHandler(logdna_handler)
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware
|
||||||
|
def log_request(request: Request):
|
||||||
|
logdna.info("I was Here with a new Request to URL: {}".format(request.url))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def default(request):
|
||||||
|
return json({
|
||||||
|
"response": "I was here"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=getenv("PORT", 8080)
|
||||||
|
)
|
||||||
37
examples/raygun_example.py
Normal file
37
examples/raygun_example.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from os import getenv
|
||||||
|
|
||||||
|
from raygun4py.raygunprovider import RaygunSender
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.exceptions import SanicException
|
||||||
|
from sanic.handlers import ErrorHandler
|
||||||
|
|
||||||
|
|
||||||
|
class RaygunExceptionReporter(ErrorHandler):
|
||||||
|
|
||||||
|
def __init__(self, raygun_api_key=None):
|
||||||
|
super().__init__()
|
||||||
|
if raygun_api_key is None:
|
||||||
|
raygun_api_key = getenv("RAYGUN_API_KEY")
|
||||||
|
|
||||||
|
self.sender = RaygunSender(raygun_api_key)
|
||||||
|
|
||||||
|
def default(self, request, exception):
|
||||||
|
self.sender.send_exception(exception=exception)
|
||||||
|
return super().default(request, exception)
|
||||||
|
|
||||||
|
|
||||||
|
raygun_error_reporter = RaygunExceptionReporter()
|
||||||
|
app = Sanic(__name__, error_handler=raygun_error_reporter)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/raise")
|
||||||
|
async def test(request):
|
||||||
|
raise SanicException('You Broke It!')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=getenv("PORT", 8080)
|
||||||
|
)
|
||||||
30
examples/rollbar_example.py
Normal file
30
examples/rollbar_example.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import rollbar
|
||||||
|
|
||||||
|
from sanic.handlers import ErrorHandler
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.exceptions import SanicException
|
||||||
|
from os import getenv
|
||||||
|
|
||||||
|
rollbar.init(getenv("ROLLBAR_API_KEY"))
|
||||||
|
|
||||||
|
|
||||||
|
class RollbarExceptionHandler(ErrorHandler):
|
||||||
|
|
||||||
|
def default(self, request, exception):
|
||||||
|
rollbar.report_message(str(exception))
|
||||||
|
return super().default(request, exception)
|
||||||
|
|
||||||
|
|
||||||
|
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/raise")
|
||||||
|
def create_error(request):
|
||||||
|
raise SanicException("I was here and I don't like where I am")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=getenv("PORT", 8080)
|
||||||
|
)
|
||||||
88
examples/run_asgi.py
Normal file
88
examples/run_asgi.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
1. Create a simple Sanic app
|
||||||
|
0. Run with an ASGI server:
|
||||||
|
$ uvicorn run_asgi:app
|
||||||
|
or
|
||||||
|
$ hypercorn run_asgi:app
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from sanic import Sanic, response
|
||||||
|
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/text")
|
||||||
|
def handler_text(request):
|
||||||
|
return response.text("Hello")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/json")
|
||||||
|
def handler_json(request):
|
||||||
|
return response.json({"foo": "bar"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def handler_ws(request, ws):
|
||||||
|
name = "<someone>"
|
||||||
|
while True:
|
||||||
|
data = f"Hello {name}"
|
||||||
|
await ws.send(data)
|
||||||
|
name = await ws.recv()
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/file")
|
||||||
|
async def handler_file(request):
|
||||||
|
return await response.file(Path("../") / "setup.py")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/file_stream")
|
||||||
|
async def handler_file_stream(request):
|
||||||
|
return await response.file_stream(
|
||||||
|
Path("../") / "setup.py", chunk_size=1024
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/stream", stream=True)
|
||||||
|
async def handler_stream(request):
|
||||||
|
while True:
|
||||||
|
body = await request.stream.read()
|
||||||
|
if body is None:
|
||||||
|
break
|
||||||
|
body = body.decode("utf-8").replace("1", "A")
|
||||||
|
# await response.write(body)
|
||||||
|
return response.stream(body)
|
||||||
|
|
||||||
|
|
||||||
|
@app.listener("before_server_start")
|
||||||
|
async def listener_before_server_start(*args, **kwargs):
|
||||||
|
print("before_server_start")
|
||||||
|
|
||||||
|
|
||||||
|
@app.listener("after_server_start")
|
||||||
|
async def listener_after_server_start(*args, **kwargs):
|
||||||
|
print("after_server_start")
|
||||||
|
|
||||||
|
|
||||||
|
@app.listener("before_server_stop")
|
||||||
|
async def listener_before_server_stop(*args, **kwargs):
|
||||||
|
print("before_server_stop")
|
||||||
|
|
||||||
|
|
||||||
|
@app.listener("after_server_stop")
|
||||||
|
async def listener_after_server_stop(*args, **kwargs):
|
||||||
|
print("after_server_stop")
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware("request")
|
||||||
|
async def print_on_request(request):
|
||||||
|
print("print_on_request")
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware("response")
|
||||||
|
async def print_on_response(request, response):
|
||||||
|
print("print_on_response")
|
||||||
@@ -12,7 +12,7 @@ async def test(request):
|
|||||||
return response.json({"answer": "42"})
|
return response.json({"answer": "42"})
|
||||||
|
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
server = app.create_server(host="0.0.0.0", port=8000)
|
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
task = asyncio.ensure_future(server)
|
task = asyncio.ensure_future(server)
|
||||||
signal(SIGINT, lambda s, f: loop.stop())
|
signal(SIGINT, lambda s, f: loop.stop())
|
||||||
|
|||||||
38
examples/run_async_advanced.py
Normal file
38
examples/run_async_advanced.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from sanic import Sanic
|
||||||
|
from sanic import response
|
||||||
|
from signal import signal, SIGINT
|
||||||
|
import asyncio
|
||||||
|
import uvloop
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
@app.listener('after_server_start')
|
||||||
|
async def after_start_test(app, loop):
|
||||||
|
print("Async Server Started!")
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def test(request):
|
||||||
|
return response.json({"answer": "42"})
|
||||||
|
|
||||||
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
|
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
|
||||||
|
signal(SIGINT, lambda s, f: loop.stop())
|
||||||
|
server = loop.run_until_complete(serv_task)
|
||||||
|
server.after_start()
|
||||||
|
try:
|
||||||
|
loop.run_forever()
|
||||||
|
except KeyboardInterrupt as e:
|
||||||
|
loop.stop()
|
||||||
|
finally:
|
||||||
|
server.before_stop()
|
||||||
|
|
||||||
|
# Wait for server to close
|
||||||
|
close_task = server.close()
|
||||||
|
loop.run_until_complete(close_task)
|
||||||
|
|
||||||
|
# Complete all tasks on the loop
|
||||||
|
for connection in server.connections:
|
||||||
|
connection.close_if_idle()
|
||||||
|
server.after_stop()
|
||||||
35
examples/sentry_example.py
Normal file
35
examples/sentry_example.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from os import getenv
|
||||||
|
|
||||||
|
from sentry_sdk import init as sentry_init
|
||||||
|
from sentry_sdk.integrations.sanic import SanicIntegration
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
sentry_init(
|
||||||
|
dsn=getenv("SENTRY_DSN"),
|
||||||
|
integrations=[SanicIntegration()],
|
||||||
|
)
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
@app.route("/working")
|
||||||
|
async def working_path(request):
|
||||||
|
return json({
|
||||||
|
"response": "Working API Response"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
@app.route("/raise-error")
|
||||||
|
async def raise_error(request):
|
||||||
|
raise Exception("Testing Sentry Integration")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=getenv("PORT", 8080)
|
||||||
|
)
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
aiofiles
|
|
||||||
aiohttp>=2.3.0,<=3.2.1
|
|
||||||
chardet<=2.3.0
|
|
||||||
beautifulsoup4
|
|
||||||
coverage
|
|
||||||
httptools>=0.0.10
|
|
||||||
flake8
|
|
||||||
pytest==3.3.2
|
|
||||||
tox
|
|
||||||
ujson; sys_platform != "win32" and implementation_name == "cpython"
|
|
||||||
uvloop; sys_platform != "win32" and implementation_name == "cpython"
|
|
||||||
gunicorn
|
|
||||||
multidict>=4.0,<5.0
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
sphinx
|
|
||||||
sphinx_rtd_theme
|
|
||||||
recommonmark
|
|
||||||
sphinxcontrib-asyncio
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
aiofiles
|
|
||||||
httptools>=0.0.10
|
|
||||||
ujson; sys_platform != "win32" and implementation_name == "cpython"
|
|
||||||
uvloop; sys_platform != "win32" and implementation_name == "cpython"
|
|
||||||
websockets>=6.0,<7.0
|
|
||||||
multidict>=4.0,<5.0
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
|
from sanic.__version__ import __version__
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
|
||||||
__version__ = "18.12.0"
|
__all__ = ["Sanic", "Blueprint", "__version__"]
|
||||||
|
|
||||||
__all__ = ["Sanic", "Blueprint"]
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.log import logger
|
from sanic.log import logger
|
||||||
@@ -35,7 +36,10 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if args.cert is not None or args.key is not None:
|
if args.cert is not None or args.key is not None:
|
||||||
ssl = {"cert": args.cert, "key": args.key}
|
ssl = {
|
||||||
|
"cert": args.cert,
|
||||||
|
"key": args.key,
|
||||||
|
} # type: Optional[Dict[str, Any]]
|
||||||
else:
|
else:
|
||||||
ssl = None
|
ssl = None
|
||||||
|
|
||||||
|
|||||||
1
sanic/__version__.py
Normal file
1
sanic/__version__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "19.12.5"
|
||||||
600
sanic/app.py
600
sanic/app.py
@@ -4,25 +4,35 @@ import os
|
|||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from asyncio import CancelledError, ensure_future, get_event_loop
|
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import getmodulename, isawaitable, signature, stack
|
from inspect import getmodulename, isawaitable, signature, stack
|
||||||
from ssl import Purpose, create_default_context
|
from socket import socket
|
||||||
|
from ssl import Purpose, SSLContext, create_default_context
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
from typing import Any, Dict, Optional, Type, Union
|
||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
|
|
||||||
from sanic import reloader_helpers
|
from sanic import reloader_helpers
|
||||||
from sanic.config import Config
|
from sanic.asgi import ASGIApp
|
||||||
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
|
from sanic.config import BASE_LOGO, Config
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import SanicException, ServerError, URLBuildError
|
from sanic.exceptions import SanicException, ServerError, URLBuildError
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
|
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
|
||||||
from sanic.response import HTTPResponse, StreamingHTTPResponse
|
from sanic.response import HTTPResponse, StreamingHTTPResponse
|
||||||
from sanic.router import Router
|
from sanic.router import Router
|
||||||
from sanic.server import HttpProtocol, Signal, serve, serve_multiple
|
from sanic.server import (
|
||||||
|
AsyncioServer,
|
||||||
|
HttpProtocol,
|
||||||
|
Signal,
|
||||||
|
serve,
|
||||||
|
serve_multiple,
|
||||||
|
)
|
||||||
from sanic.static import register as static_register
|
from sanic.static import register as static_register
|
||||||
from sanic.testing import SanicTestClient
|
from sanic.testing import SanicASGITestClient, SanicTestClient
|
||||||
from sanic.views import CompositionView
|
from sanic.views import CompositionView
|
||||||
from sanic.websocket import ConnectionClosed, WebSocketProtocol
|
from sanic.websocket import ConnectionClosed, WebSocketProtocol
|
||||||
|
|
||||||
@@ -42,6 +52,13 @@ class Sanic:
|
|||||||
|
|
||||||
# Get name from previous stack frame
|
# Get name from previous stack frame
|
||||||
if name is None:
|
if name is None:
|
||||||
|
warnings.warn(
|
||||||
|
"Sanic(name=None) is deprecated and None value support "
|
||||||
|
"for `name` will be removed in the next release. "
|
||||||
|
"Please use Sanic(name='your_application_name') instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
frame_records = stack()[1]
|
frame_records = stack()[1]
|
||||||
name = getmodulename(frame_records[1])
|
name = getmodulename(frame_records[1])
|
||||||
|
|
||||||
@@ -50,6 +67,7 @@ class Sanic:
|
|||||||
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.asgi = False
|
||||||
self.router = router or Router()
|
self.router = router or Router()
|
||||||
self.request_class = request_class
|
self.request_class = request_class
|
||||||
self.error_handler = error_handler or ErrorHandler()
|
self.error_handler = error_handler or ErrorHandler()
|
||||||
@@ -67,7 +85,8 @@ class Sanic:
|
|||||||
self.is_request_stream = False
|
self.is_request_stream = False
|
||||||
self.websocket_enabled = False
|
self.websocket_enabled = False
|
||||||
self.websocket_tasks = set()
|
self.websocket_tasks = set()
|
||||||
|
self.named_request_middleware = {}
|
||||||
|
self.named_response_middleware = {}
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
|
|
||||||
@@ -77,7 +96,7 @@ class Sanic:
|
|||||||
|
|
||||||
Only supported when using the `app.run` method.
|
Only supported when using the `app.run` method.
|
||||||
"""
|
"""
|
||||||
if not self.is_running:
|
if not self.is_running and self.asgi is False:
|
||||||
raise SanicException(
|
raise SanicException(
|
||||||
"Loop can only be retrieved after the app has started "
|
"Loop can only be retrieved after the app has started "
|
||||||
"running. Not supported with `create_server` function"
|
"running. Not supported with `create_server` function"
|
||||||
@@ -133,11 +152,9 @@ class Sanic:
|
|||||||
"""
|
"""
|
||||||
Register the listener for a given event.
|
Register the listener for a given event.
|
||||||
|
|
||||||
Args:
|
:param listener: callable i.e. setup_db(app, loop)
|
||||||
listener: callable i.e. setup_db(app, loop)
|
:param event: when to register listener i.e. 'before_server_start'
|
||||||
event: when to register listener i.e. 'before_server_start'
|
:return: listener
|
||||||
|
|
||||||
Returns: listener
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.listener(event)(listener)
|
return self.listener(event)(listener)
|
||||||
@@ -162,7 +179,7 @@ class Sanic:
|
|||||||
:param stream:
|
:param stream:
|
||||||
:param version:
|
:param version:
|
||||||
:param name: user defined route name for url_for
|
:param name: user defined route name for url_for
|
||||||
:return: decorated function
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Fix case where the user did not prefix the URL with a /
|
# Fix case where the user did not prefix the URL with a /
|
||||||
@@ -177,11 +194,24 @@ class Sanic:
|
|||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
|
|
||||||
def response(handler):
|
def response(handler):
|
||||||
args = [key for key in signature(handler).parameters.keys()]
|
if isinstance(handler, tuple):
|
||||||
if args:
|
# if a handler fn is already wrapped in a route, the handler
|
||||||
if stream:
|
# variable will be a tuple of (existing routes, handler fn)
|
||||||
handler.is_stream = stream
|
routes, handler = handler
|
||||||
|
else:
|
||||||
|
routes = []
|
||||||
|
args = list(signature(handler).parameters.keys())
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
raise ValueError(
|
||||||
|
"Required parameter `request` missing "
|
||||||
|
"in the {0}() route?".format(handler.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
|
if stream:
|
||||||
|
handler.is_stream = stream
|
||||||
|
|
||||||
|
routes.extend(
|
||||||
self.router.add(
|
self.router.add(
|
||||||
uri=uri,
|
uri=uri,
|
||||||
methods=methods,
|
methods=methods,
|
||||||
@@ -191,12 +221,8 @@ class Sanic:
|
|||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
return handler
|
)
|
||||||
else:
|
return routes, handler
|
||||||
raise ValueError(
|
|
||||||
"Required parameter `request` missing "
|
|
||||||
"in the {0}() route?".format(handler.__name__)
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -204,6 +230,17 @@ class Sanic:
|
|||||||
def get(
|
def get(
|
||||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **GET** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **GET** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||||
|
URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=frozenset({"GET"}),
|
methods=frozenset({"GET"}),
|
||||||
@@ -222,6 +259,17 @@ class Sanic:
|
|||||||
version=None,
|
version=None,
|
||||||
name=None,
|
name=None,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **POST** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **POST** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||||
|
URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=frozenset({"POST"}),
|
methods=frozenset({"POST"}),
|
||||||
@@ -241,6 +289,17 @@ class Sanic:
|
|||||||
version=None,
|
version=None,
|
||||||
name=None,
|
name=None,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **PUT** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **PUT** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||||
|
URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=frozenset({"PUT"}),
|
methods=frozenset({"PUT"}),
|
||||||
@@ -266,6 +325,17 @@ class Sanic:
|
|||||||
def options(
|
def options(
|
||||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **OPTIONS** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **OPTIONS** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||||
|
URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=frozenset({"OPTIONS"}),
|
methods=frozenset({"OPTIONS"}),
|
||||||
@@ -284,6 +354,17 @@ class Sanic:
|
|||||||
version=None,
|
version=None,
|
||||||
name=None,
|
name=None,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **PATCH** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||||
|
URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=frozenset({"PATCH"}),
|
methods=frozenset({"PATCH"}),
|
||||||
@@ -297,6 +378,17 @@ class Sanic:
|
|||||||
def delete(
|
def delete(
|
||||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **DELETE** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **DELETE** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||||
|
URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=frozenset({"DELETE"}),
|
methods=frozenset({"DELETE"}),
|
||||||
@@ -369,11 +461,17 @@ class Sanic:
|
|||||||
def websocket(
|
def websocket(
|
||||||
self, uri, host=None, strict_slashes=None, subprotocols=None, name=None
|
self, uri, host=None, strict_slashes=None, subprotocols=None, name=None
|
||||||
):
|
):
|
||||||
"""Decorate a function to be registered as a websocket route
|
"""
|
||||||
|
Decorate a function to be registered as a websocket route
|
||||||
|
|
||||||
:param uri: path of the URL
|
:param uri: path of the URL
|
||||||
|
:param host: Host IP or FQDN details
|
||||||
|
:param strict_slashes: If the API endpoint needs to terminate
|
||||||
|
with a "/" or not
|
||||||
:param subprotocols: optional list of str with supported subprotocols
|
:param subprotocols: optional list of str with supported subprotocols
|
||||||
:param host:
|
:param name: A unique name assigned to the URL so that it can
|
||||||
:return: decorated function
|
be used with :func:`url_for`
|
||||||
|
:return: tuple of routes, decorated function
|
||||||
"""
|
"""
|
||||||
self.enable_websocket()
|
self.enable_websocket()
|
||||||
|
|
||||||
@@ -386,15 +484,39 @@ class Sanic:
|
|||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
|
|
||||||
def response(handler):
|
def response(handler):
|
||||||
|
if isinstance(handler, tuple):
|
||||||
|
# if a handler fn is already wrapped in a route, the handler
|
||||||
|
# variable will be a tuple of (existing routes, handler fn)
|
||||||
|
routes, handler = handler
|
||||||
|
else:
|
||||||
|
routes = []
|
||||||
|
|
||||||
async def websocket_handler(request, *args, **kwargs):
|
async def websocket_handler(request, *args, **kwargs):
|
||||||
request.app = self
|
request.app = self
|
||||||
try:
|
if not getattr(handler, "__blueprintname__", False):
|
||||||
protocol = request.transport.get_protocol()
|
request.endpoint = handler.__name__
|
||||||
except AttributeError:
|
else:
|
||||||
# On Python3.5 the Transport classes in asyncio do not
|
request.endpoint = (
|
||||||
# have a get_protocol() method as in uvloop
|
getattr(handler, "__blueprintname__", "")
|
||||||
protocol = request.transport._protocol
|
+ handler.__name__
|
||||||
ws = await protocol.websocket_handshake(request, subprotocols)
|
)
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.asgi:
|
||||||
|
ws = request.transport.get_websocket_connection()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
protocol = request.transport.get_protocol()
|
||||||
|
except AttributeError:
|
||||||
|
# On Python3.5 the Transport classes in asyncio do not
|
||||||
|
# have a get_protocol() method as in uvloop
|
||||||
|
protocol = request.transport._protocol
|
||||||
|
protocol.app = self
|
||||||
|
|
||||||
|
ws = await protocol.websocket_handshake(
|
||||||
|
request, subprotocols
|
||||||
|
)
|
||||||
|
|
||||||
# schedule the application handler
|
# schedule the application handler
|
||||||
# its future is kept in self.websocket_tasks in case it
|
# its future is kept in self.websocket_tasks in case it
|
||||||
@@ -409,15 +531,17 @@ class Sanic:
|
|||||||
self.websocket_tasks.remove(fut)
|
self.websocket_tasks.remove(fut)
|
||||||
await ws.close()
|
await ws.close()
|
||||||
|
|
||||||
self.router.add(
|
routes.extend(
|
||||||
uri=uri,
|
self.router.add(
|
||||||
handler=websocket_handler,
|
uri=uri,
|
||||||
methods=frozenset({"GET"}),
|
handler=websocket_handler,
|
||||||
host=host,
|
methods=frozenset({"GET"}),
|
||||||
strict_slashes=strict_slashes,
|
host=host,
|
||||||
name=name,
|
strict_slashes=strict_slashes,
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return handler
|
return routes, handler
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -430,7 +554,23 @@ class Sanic:
|
|||||||
subprotocols=None,
|
subprotocols=None,
|
||||||
name=None,
|
name=None,
|
||||||
):
|
):
|
||||||
"""A helper method to register a function as a websocket route."""
|
"""
|
||||||
|
A helper method to register a function as a websocket route.
|
||||||
|
|
||||||
|
:param handler: a callable function or instance of a class
|
||||||
|
that can handle the websocket request
|
||||||
|
:param host: Host IP or FQDN details
|
||||||
|
:param uri: URL path that will be mapped to the websocket
|
||||||
|
handler
|
||||||
|
handler
|
||||||
|
:param strict_slashes: If the API endpoint needs to terminate
|
||||||
|
with a "/" or not
|
||||||
|
:param subprotocols: Subprotocols to be used with websocket
|
||||||
|
handshake
|
||||||
|
:param name: A unique name assigned to the URL so that it can
|
||||||
|
be used with :func:`url_for`
|
||||||
|
:return: Objected decorated by :func:`websocket`
|
||||||
|
"""
|
||||||
if strict_slashes is None:
|
if strict_slashes is None:
|
||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
|
|
||||||
@@ -459,6 +599,26 @@ class Sanic:
|
|||||||
self.websocket_enabled = enable
|
self.websocket_enabled = enable
|
||||||
|
|
||||||
def remove_route(self, uri, clean_cache=True, host=None):
|
def remove_route(self, uri, clean_cache=True, host=None):
|
||||||
|
"""
|
||||||
|
This method provides the app user a mechanism by which an already
|
||||||
|
existing route can be removed from the :class:`Sanic` object
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
remove_route is deprecated in v19.06 and will be removed
|
||||||
|
from future versions.
|
||||||
|
|
||||||
|
:param uri: URL Path to be removed from the app
|
||||||
|
:param clean_cache: Instruct sanic if it needs to clean up the LRU
|
||||||
|
route cache
|
||||||
|
:param host: IP address or FQDN specific to the host
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"remove_route is deprecated and will be removed "
|
||||||
|
"from future versions.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
self.router.remove(uri, clean_cache, host)
|
self.router.remove(uri, clean_cache, host)
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
@@ -481,18 +641,55 @@ class Sanic:
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def register_middleware(self, middleware, attach_to="request"):
|
def register_middleware(self, middleware, attach_to="request"):
|
||||||
|
"""
|
||||||
|
Register an application level middleware that will be attached
|
||||||
|
to all the API URLs registered under this application.
|
||||||
|
|
||||||
|
This method is internally invoked by the :func:`middleware`
|
||||||
|
decorator provided at the app level.
|
||||||
|
|
||||||
|
:param middleware: Callback method to be attached to the
|
||||||
|
middleware
|
||||||
|
:param attach_to: The state at which the middleware needs to be
|
||||||
|
invoked in the lifecycle of an *HTTP Request*.
|
||||||
|
**request** - Invoke before the request is processed
|
||||||
|
**response** - Invoke before the response is returned back
|
||||||
|
:return: decorated method
|
||||||
|
"""
|
||||||
if attach_to == "request":
|
if attach_to == "request":
|
||||||
self.request_middleware.append(middleware)
|
if middleware not in self.request_middleware:
|
||||||
|
self.request_middleware.append(middleware)
|
||||||
if attach_to == "response":
|
if attach_to == "response":
|
||||||
self.response_middleware.appendleft(middleware)
|
if middleware not in self.response_middleware:
|
||||||
|
self.response_middleware.appendleft(middleware)
|
||||||
return middleware
|
return middleware
|
||||||
|
|
||||||
|
def register_named_middleware(
|
||||||
|
self, middleware, route_names, attach_to="request"
|
||||||
|
):
|
||||||
|
if attach_to == "request":
|
||||||
|
for _rn in route_names:
|
||||||
|
if _rn not in self.named_request_middleware:
|
||||||
|
self.named_request_middleware[_rn] = deque()
|
||||||
|
if middleware not in self.named_request_middleware[_rn]:
|
||||||
|
self.named_request_middleware[_rn].append(middleware)
|
||||||
|
if attach_to == "response":
|
||||||
|
for _rn in route_names:
|
||||||
|
if _rn not in self.named_response_middleware:
|
||||||
|
self.named_response_middleware[_rn] = deque()
|
||||||
|
if middleware not in self.named_response_middleware[_rn]:
|
||||||
|
self.named_response_middleware[_rn].append(middleware)
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def middleware(self, middleware_or_request):
|
def middleware(self, middleware_or_request):
|
||||||
"""Decorate and register middleware to be called before a request.
|
|
||||||
Can either be called as @app.middleware or @app.middleware('request')
|
|
||||||
"""
|
"""
|
||||||
|
Decorate and register middleware to be called before a request.
|
||||||
|
Can either be called as *@app.middleware* or
|
||||||
|
*@app.middleware('request')*
|
||||||
|
|
||||||
|
:param: middleware_or_request: Optional parameter to use for
|
||||||
|
identifying which type of middleware is being registered.
|
||||||
|
"""
|
||||||
# Detect which way this was called, @middleware or @middleware('AT')
|
# Detect which way this was called, @middleware or @middleware('AT')
|
||||||
if callable(middleware_or_request):
|
if callable(middleware_or_request):
|
||||||
return self.register_middleware(middleware_or_request)
|
return self.register_middleware(middleware_or_request)
|
||||||
@@ -516,8 +713,30 @@ class Sanic:
|
|||||||
strict_slashes=None,
|
strict_slashes=None,
|
||||||
content_type=None,
|
content_type=None,
|
||||||
):
|
):
|
||||||
"""Register a root to serve files from. The input can either be a
|
"""
|
||||||
file or a directory. See
|
Register a root to serve files from. The input can either be a
|
||||||
|
file or a directory. This method will enable an easy and simple way
|
||||||
|
to setup the :class:`Route` necessary to serve the static files.
|
||||||
|
|
||||||
|
:param uri: URL path to be used for serving static content
|
||||||
|
:param file_or_directory: Path for the Static file/directory with
|
||||||
|
static files
|
||||||
|
:param pattern: Regex Pattern identifying the valid static files
|
||||||
|
:param use_modified_since: If true, send file modified time, and return
|
||||||
|
not modified if the browser's matches the server's
|
||||||
|
:param use_content_range: If true, process header for range requests
|
||||||
|
and sends the file part that is requested
|
||||||
|
:param stream_large_files: If true, use the
|
||||||
|
:func:`StreamingHTTPResponse.file_stream` handler rather
|
||||||
|
than the :func:`HTTPResponse.file` handler to send the file.
|
||||||
|
If this is an integer, this represents the threshold size to
|
||||||
|
switch to :func:`StreamingHTTPResponse.file_stream`
|
||||||
|
:param name: user defined name used for url_for
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`Sanic` to check if the request
|
||||||
|
URLs need to terminate with a */*
|
||||||
|
:param content_type: user defined content type for header
|
||||||
|
:return: None
|
||||||
"""
|
"""
|
||||||
static_register(
|
static_register(
|
||||||
self,
|
self,
|
||||||
@@ -540,7 +759,7 @@ class Sanic:
|
|||||||
:param options: option dictionary with blueprint defaults
|
:param options: option dictionary with blueprint defaults
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if isinstance(blueprint, (list, tuple)):
|
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
||||||
for item in blueprint:
|
for item in blueprint:
|
||||||
self.blueprint(item, **options)
|
self.blueprint(item, **options)
|
||||||
return
|
return
|
||||||
@@ -555,7 +774,17 @@ class Sanic:
|
|||||||
blueprint.register(self, options)
|
blueprint.register(self, options)
|
||||||
|
|
||||||
def register_blueprint(self, *args, **kwargs):
|
def register_blueprint(self, *args, **kwargs):
|
||||||
# TODO: deprecate 1.0
|
"""
|
||||||
|
Proxy method provided for invoking the :func:`blueprint` method
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
To be deprecated in 1.0. Use :func:`blueprint` instead.
|
||||||
|
|
||||||
|
:param args: Blueprint object or (list, tuple) thereof
|
||||||
|
:param kwargs: option dictionary with blueprint defaults
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
warnings.simplefilter("default")
|
warnings.simplefilter("default")
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
@@ -587,7 +816,7 @@ class Sanic:
|
|||||||
URLBuildError
|
URLBuildError
|
||||||
"""
|
"""
|
||||||
# find the route by the supplied view name
|
# find the route by the supplied view name
|
||||||
kw = {}
|
kw: Dict[str, str] = {}
|
||||||
# special static files url_for
|
# special static files url_for
|
||||||
if view_name == "static":
|
if view_name == "static":
|
||||||
kw.update(name=kwargs.pop("name", "static"))
|
kw.update(name=kwargs.pop("name", "static"))
|
||||||
@@ -601,6 +830,14 @@ class Sanic:
|
|||||||
"Endpoint with name `{}` was not found".format(view_name)
|
"Endpoint with name `{}` was not found".format(view_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If the route has host defined, split that off
|
||||||
|
# TODO: Retain netloc and path separately in Route objects
|
||||||
|
host = uri.find("/")
|
||||||
|
if host > 0:
|
||||||
|
host, uri = uri[:host], uri[host:]
|
||||||
|
else:
|
||||||
|
host = None
|
||||||
|
|
||||||
if view_name == "static" or view_name.endswith(".static"):
|
if view_name == "static" or view_name.endswith(".static"):
|
||||||
filename = kwargs.pop("filename", None)
|
filename = kwargs.pop("filename", None)
|
||||||
# it's static folder
|
# it's static folder
|
||||||
@@ -633,7 +870,7 @@ class Sanic:
|
|||||||
|
|
||||||
netloc = kwargs.pop("_server", None)
|
netloc = kwargs.pop("_server", None)
|
||||||
if netloc is None and external:
|
if netloc is None and external:
|
||||||
netloc = self.config.get("SERVER_NAME", "")
|
netloc = host or self.config.get("SERVER_NAME", "")
|
||||||
|
|
||||||
if external:
|
if external:
|
||||||
if not scheme:
|
if not scheme:
|
||||||
@@ -700,6 +937,9 @@ class Sanic:
|
|||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def converted_response_type(self, response):
|
def converted_response_type(self, response):
|
||||||
|
"""
|
||||||
|
No implementation provided.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def handle_request(self, request, write_callback, stream_callback):
|
async def handle_request(self, request, write_callback, stream_callback):
|
||||||
@@ -719,22 +959,23 @@ class Sanic:
|
|||||||
# allocation before assignment below.
|
# allocation before assignment below.
|
||||||
response = None
|
response = None
|
||||||
cancelled = False
|
cancelled = False
|
||||||
|
name = None
|
||||||
try:
|
try:
|
||||||
|
# Fetch handler from router
|
||||||
|
handler, args, kwargs, uri, name = self.router.get(request)
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
response = await self._run_request_middleware(
|
||||||
request.app = self
|
request, request_name=name
|
||||||
response = await self._run_request_middleware(request)
|
)
|
||||||
# No middleware results
|
# No middleware results
|
||||||
if not response:
|
if not response:
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Execute Handler
|
# Execute Handler
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
|
||||||
# Fetch handler from router
|
|
||||||
handler, args, kwargs, uri = self.router.get(request)
|
|
||||||
|
|
||||||
request.uri_template = uri
|
request.uri_template = uri
|
||||||
if handler is None:
|
if handler is None:
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
@@ -743,6 +984,16 @@ class Sanic:
|
|||||||
"handler from the router"
|
"handler from the router"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
if not getattr(handler, "__blueprintname__", False):
|
||||||
|
request.endpoint = self._build_endpoint_name(
|
||||||
|
handler.__name__
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
request.endpoint = self._build_endpoint_name(
|
||||||
|
getattr(handler, "__blueprintname__", ""),
|
||||||
|
handler.__name__,
|
||||||
|
)
|
||||||
|
|
||||||
# Run response handler
|
# Run response handler
|
||||||
response = handler(request, *args, **kwargs)
|
response = handler(request, *args, **kwargs)
|
||||||
@@ -788,7 +1039,7 @@ class Sanic:
|
|||||||
if response is not None:
|
if response is not None:
|
||||||
try:
|
try:
|
||||||
response = await self._run_response_middleware(
|
response = await self._run_response_middleware(
|
||||||
request, response
|
request, response, request_name=name
|
||||||
)
|
)
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
# Response middleware can timeout too, as above.
|
# Response middleware can timeout too, as above.
|
||||||
@@ -803,8 +1054,16 @@ class Sanic:
|
|||||||
raise CancelledError()
|
raise CancelledError()
|
||||||
|
|
||||||
# pass the response to the correct callback
|
# pass the response to the correct callback
|
||||||
if isinstance(response, StreamingHTTPResponse):
|
if write_callback is None or isinstance(
|
||||||
await stream_callback(response)
|
response, StreamingHTTPResponse
|
||||||
|
):
|
||||||
|
if stream_callback:
|
||||||
|
await stream_callback(response)
|
||||||
|
else:
|
||||||
|
# Should only end here IF it is an ASGI websocket.
|
||||||
|
# TODO:
|
||||||
|
# - Add exception handling
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
write_callback(response)
|
write_callback(response)
|
||||||
|
|
||||||
@@ -816,25 +1075,59 @@ class Sanic:
|
|||||||
def test_client(self):
|
def test_client(self):
|
||||||
return SanicTestClient(self)
|
return SanicTestClient(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def asgi_client(self):
|
||||||
|
return SanicASGITestClient(self)
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Execution
|
# Execution
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
host=None,
|
host: Optional[str] = None,
|
||||||
port=None,
|
port: Optional[int] = None,
|
||||||
debug=False,
|
debug: bool = False,
|
||||||
ssl=None,
|
ssl: Union[dict, SSLContext, None] = None,
|
||||||
sock=None,
|
sock: Optional[socket] = None,
|
||||||
workers=1,
|
workers: int = 1,
|
||||||
protocol=None,
|
protocol: Type[Protocol] = None,
|
||||||
backlog=100,
|
backlog: int = 100,
|
||||||
stop_event=None,
|
stop_event: Any = None,
|
||||||
register_sys_signals=True,
|
register_sys_signals: bool = True,
|
||||||
access_log=True,
|
access_log: Optional[bool] = None,
|
||||||
**kwargs
|
**kwargs: Any
|
||||||
):
|
) -> None:
|
||||||
|
"""Run the HTTP Server and listen until keyboard interrupt or term
|
||||||
|
signal. On termination, drain connections before closing.
|
||||||
|
|
||||||
|
:param host: Address to host on
|
||||||
|
:type host: str
|
||||||
|
:param port: Port to host on
|
||||||
|
:type port: int
|
||||||
|
:param debug: Enables debug output (slows server)
|
||||||
|
:type debug: bool
|
||||||
|
:param ssl: SSLContext, or location of certificate and key
|
||||||
|
for SSL encryption of worker(s)
|
||||||
|
:type ssl: SSLContext or dict
|
||||||
|
:param sock: Socket for the server to accept connections from
|
||||||
|
:type sock: socket
|
||||||
|
:param workers: Number of processes received before it is respected
|
||||||
|
:type workers: int
|
||||||
|
:param protocol: Subclass of asyncio Protocol class
|
||||||
|
:type protocol: type[Protocol]
|
||||||
|
:param backlog: a number of unaccepted connections that the system
|
||||||
|
will allow before refusing new connections
|
||||||
|
:type backlog: int
|
||||||
|
:param stop_event: event to be triggered
|
||||||
|
before stopping the app - deprecated
|
||||||
|
:type stop_event: None
|
||||||
|
:param register_sys_signals: Register SIG* events
|
||||||
|
:type register_sys_signals: bool
|
||||||
|
:param access_log: Enables writing access logs (slows server)
|
||||||
|
:type access_log: bool
|
||||||
|
:return: Nothing
|
||||||
|
"""
|
||||||
if "loop" in kwargs:
|
if "loop" in kwargs:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"loop is not a valid argument. To use an existing loop, "
|
"loop is not a valid argument. To use an existing loop, "
|
||||||
@@ -843,23 +1136,6 @@ class Sanic:
|
|||||||
"#asynchronous-support"
|
"#asynchronous-support"
|
||||||
)
|
)
|
||||||
|
|
||||||
"""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 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 backlog:
|
|
||||||
:param stop_event:
|
|
||||||
:param register_sys_signals:
|
|
||||||
:param protocol: Subclass of asyncio protocol class
|
|
||||||
:return: Nothing
|
|
||||||
"""
|
|
||||||
# Default auto_reload to false
|
# Default auto_reload to false
|
||||||
auto_reload = False
|
auto_reload = False
|
||||||
# If debug is set, default it to true (unless on windows)
|
# If debug is set, default it to true (unless on windows)
|
||||||
@@ -882,8 +1158,10 @@ class Sanic:
|
|||||||
"stop_event will be removed from future versions.",
|
"stop_event will be removed from future versions.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
# compatibility old access_log params
|
# if access_log is passed explicitly change config.ACCESS_LOG
|
||||||
self.config.ACCESS_LOG = access_log
|
if access_log is not None:
|
||||||
|
self.config.ACCESS_LOG = access_log
|
||||||
|
|
||||||
server_settings = self._helper(
|
server_settings = self._helper(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
@@ -927,26 +1205,60 @@ class Sanic:
|
|||||||
"""This kills the Sanic"""
|
"""This kills the Sanic"""
|
||||||
get_event_loop().stop()
|
get_event_loop().stop()
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
"""gunicorn compatibility"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def create_server(
|
async def create_server(
|
||||||
self,
|
self,
|
||||||
host=None,
|
host: Optional[str] = None,
|
||||||
port=None,
|
port: Optional[int] = None,
|
||||||
debug=False,
|
debug: bool = False,
|
||||||
ssl=None,
|
ssl: Union[dict, SSLContext, None] = None,
|
||||||
sock=None,
|
sock: Optional[socket] = None,
|
||||||
protocol=None,
|
protocol: Type[Protocol] = None,
|
||||||
backlog=100,
|
backlog: int = 100,
|
||||||
stop_event=None,
|
stop_event: Any = None,
|
||||||
access_log=True,
|
access_log: Optional[bool] = None,
|
||||||
):
|
return_asyncio_server=False,
|
||||||
"""Asynchronous version of `run`.
|
asyncio_server_kwargs=None,
|
||||||
|
) -> Optional[AsyncioServer]:
|
||||||
|
"""
|
||||||
|
Asynchronous version of :func:`run`.
|
||||||
|
|
||||||
NOTE: This does not support multiprocessing and is not the preferred
|
This method will take care of the operations necessary to invoke
|
||||||
way to run a Sanic application.
|
the *before_start* events via :func:`trigger_events` method invocation
|
||||||
|
before starting the *sanic* app in Async mode.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
This does not support multiprocessing and is not the preferred
|
||||||
|
way to run a :class:`Sanic` application.
|
||||||
|
|
||||||
|
:param host: Address to host on
|
||||||
|
:type host: str
|
||||||
|
:param port: Port to host on
|
||||||
|
:type port: int
|
||||||
|
:param debug: Enables debug output (slows server)
|
||||||
|
:type debug: bool
|
||||||
|
:param ssl: SSLContext, or location of certificate and key
|
||||||
|
for SSL encryption of worker(s)
|
||||||
|
:type ssl: SSLContext or dict
|
||||||
|
:param sock: Socket for the server to accept connections from
|
||||||
|
:type sock: socket
|
||||||
|
:param protocol: Subclass of asyncio Protocol class
|
||||||
|
:type protocol: type[Protocol]
|
||||||
|
:param backlog: a number of unaccepted connections that the system
|
||||||
|
will allow before refusing new connections
|
||||||
|
:type backlog: int
|
||||||
|
:param stop_event: event to be triggered
|
||||||
|
before stopping the app - deprecated
|
||||||
|
:type stop_event: None
|
||||||
|
:param access_log: Enables writing access logs (slows server)
|
||||||
|
:type access_log: bool
|
||||||
|
:param return_asyncio_server: flag that defines whether there's a need
|
||||||
|
to return asyncio.Server or
|
||||||
|
start it serving right away
|
||||||
|
:type return_asyncio_server: bool
|
||||||
|
:param asyncio_server_kwargs: key-value arguments for
|
||||||
|
asyncio/uvloop create_server method
|
||||||
|
:type asyncio_server_kwargs: dict
|
||||||
|
:return: AsyncioServer if return_asyncio_server is true, else Nothing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if sock is None:
|
if sock is None:
|
||||||
@@ -963,8 +1275,10 @@ class Sanic:
|
|||||||
"stop_event will be removed from future versions.",
|
"stop_event will be removed from future versions.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
# compatibility old access_log params
|
# if access_log is passed explicitly change config.ACCESS_LOG
|
||||||
self.config.ACCESS_LOG = access_log
|
if access_log is not None:
|
||||||
|
self.config.ACCESS_LOG = access_log
|
||||||
|
|
||||||
server_settings = self._helper(
|
server_settings = self._helper(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
@@ -974,7 +1288,7 @@ class Sanic:
|
|||||||
loop=get_event_loop(),
|
loop=get_event_loop(),
|
||||||
protocol=protocol,
|
protocol=protocol,
|
||||||
backlog=backlog,
|
backlog=backlog,
|
||||||
run_async=True,
|
run_async=return_asyncio_server,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Trigger before_start events
|
# Trigger before_start events
|
||||||
@@ -983,7 +1297,9 @@ class Sanic:
|
|||||||
server_settings.get("loop"),
|
server_settings.get("loop"),
|
||||||
)
|
)
|
||||||
|
|
||||||
return await serve(**server_settings)
|
return await serve(
|
||||||
|
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
|
||||||
|
)
|
||||||
|
|
||||||
async def trigger_events(self, events, loop):
|
async def trigger_events(self, events, loop):
|
||||||
"""Trigger events (functions or async)
|
"""Trigger events (functions or async)
|
||||||
@@ -995,10 +1311,14 @@ class Sanic:
|
|||||||
if isawaitable(result):
|
if isawaitable(result):
|
||||||
await result
|
await result
|
||||||
|
|
||||||
async def _run_request_middleware(self, request):
|
async def _run_request_middleware(self, request, request_name=None):
|
||||||
# The if improves speed. I don't know why
|
# The if improves speed. I don't know why
|
||||||
if self.request_middleware:
|
named_middleware = self.named_request_middleware.get(
|
||||||
for middleware in self.request_middleware:
|
request_name, deque()
|
||||||
|
)
|
||||||
|
applicable_middleware = self.request_middleware + named_middleware
|
||||||
|
if applicable_middleware:
|
||||||
|
for middleware in applicable_middleware:
|
||||||
response = middleware(request)
|
response = middleware(request)
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
@@ -1006,9 +1326,15 @@ class Sanic:
|
|||||||
return response
|
return response
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _run_response_middleware(self, request, response):
|
async def _run_response_middleware(
|
||||||
if self.response_middleware:
|
self, request, response, request_name=None
|
||||||
for middleware in self.response_middleware:
|
):
|
||||||
|
named_middleware = self.named_response_middleware.get(
|
||||||
|
request_name, deque()
|
||||||
|
)
|
||||||
|
applicable_middleware = self.response_middleware + named_middleware
|
||||||
|
if applicable_middleware:
|
||||||
|
for middleware in applicable_middleware:
|
||||||
_response = middleware(request, response)
|
_response = middleware(request, response)
|
||||||
if isawaitable(_response):
|
if isawaitable(_response):
|
||||||
_response = await _response
|
_response = await _response
|
||||||
@@ -1050,6 +1376,12 @@ class Sanic:
|
|||||||
"stop_event will be removed from future versions.",
|
"stop_event will be removed from future versions.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
|
||||||
|
raise ValueError(
|
||||||
|
"PROXIES_COUNT cannot be negative. "
|
||||||
|
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
|
||||||
|
"#proxy-configuration"
|
||||||
|
)
|
||||||
|
|
||||||
self.error_handler.debug = debug
|
self.error_handler.debug = debug
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
@@ -1063,6 +1395,7 @@ class Sanic:
|
|||||||
"port": port,
|
"port": port,
|
||||||
"sock": sock,
|
"sock": sock,
|
||||||
"ssl": ssl,
|
"ssl": ssl,
|
||||||
|
"app": self,
|
||||||
"signal": Signal(),
|
"signal": Signal(),
|
||||||
"debug": debug,
|
"debug": debug,
|
||||||
"request_handler": self.handle_request,
|
"request_handler": self.handle_request,
|
||||||
@@ -1071,6 +1404,7 @@ class Sanic:
|
|||||||
"response_timeout": self.config.RESPONSE_TIMEOUT,
|
"response_timeout": self.config.RESPONSE_TIMEOUT,
|
||||||
"keep_alive_timeout": self.config.KEEP_ALIVE_TIMEOUT,
|
"keep_alive_timeout": self.config.KEEP_ALIVE_TIMEOUT,
|
||||||
"request_max_size": self.config.REQUEST_MAX_SIZE,
|
"request_max_size": self.config.REQUEST_MAX_SIZE,
|
||||||
|
"request_buffer_queue_size": self.config.REQUEST_BUFFER_QUEUE_SIZE,
|
||||||
"keep_alive": self.config.KEEP_ALIVE,
|
"keep_alive": self.config.KEEP_ALIVE,
|
||||||
"loop": loop,
|
"loop": loop,
|
||||||
"register_sys_signals": register_sys_signals,
|
"register_sys_signals": register_sys_signals,
|
||||||
@@ -1104,10 +1438,14 @@ class Sanic:
|
|||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.config.LOGO is not None
|
self.config.LOGO
|
||||||
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
|
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
|
||||||
):
|
):
|
||||||
logger.debug(self.config.LOGO)
|
logger.debug(
|
||||||
|
self.config.LOGO
|
||||||
|
if isinstance(self.config.LOGO, str)
|
||||||
|
else BASE_LOGO
|
||||||
|
)
|
||||||
|
|
||||||
if run_async:
|
if run_async:
|
||||||
server_settings["run_async"] = True
|
server_settings["run_async"] = True
|
||||||
@@ -1120,3 +1458,21 @@ class Sanic:
|
|||||||
logger.info("Goin' Fast @ {}://{}:{}".format(proto, host, port))
|
logger.info("Goin' Fast @ {}://{}:{}".format(proto, host, port))
|
||||||
|
|
||||||
return server_settings
|
return server_settings
|
||||||
|
|
||||||
|
def _build_endpoint_name(self, *parts):
|
||||||
|
parts = [self.name, *parts]
|
||||||
|
return ".".join(parts)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- #
|
||||||
|
# ASGI
|
||||||
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
async def __call__(self, scope, receive, send):
|
||||||
|
"""To be ASGI compliant, our instance must be a callable that accepts
|
||||||
|
three arguments: scope, receive, send. See the ASGI reference for more
|
||||||
|
details: https://asgi.readthedocs.io/en/latest/"""
|
||||||
|
self.asgi = True
|
||||||
|
asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||||
|
await asgi_app()
|
||||||
|
|
||||||
|
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
|
||||||
|
|||||||
412
sanic/asgi.py
Normal file
412
sanic/asgi.py
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
import asyncio
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from inspect import isawaitable
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Awaitable,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
MutableMapping,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import sanic.app # noqa
|
||||||
|
|
||||||
|
from sanic.compat import Header
|
||||||
|
from sanic.exceptions import InvalidUsage, ServerError
|
||||||
|
from sanic.log import logger
|
||||||
|
from sanic.request import Request
|
||||||
|
from sanic.response import HTTPResponse, StreamingHTTPResponse
|
||||||
|
from sanic.server import StreamBuffer
|
||||||
|
from sanic.websocket import WebSocketConnection
|
||||||
|
|
||||||
|
|
||||||
|
ASGIScope = MutableMapping[str, Any]
|
||||||
|
ASGIMessage = MutableMapping[str, Any]
|
||||||
|
ASGISend = Callable[[ASGIMessage], Awaitable[None]]
|
||||||
|
ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
|
||||||
|
|
||||||
|
|
||||||
|
class MockProtocol:
|
||||||
|
def __init__(self, transport: "MockTransport", loop):
|
||||||
|
self.transport = transport
|
||||||
|
self._not_paused = asyncio.Event(loop=loop)
|
||||||
|
self._not_paused.set()
|
||||||
|
self._complete = asyncio.Event(loop=loop)
|
||||||
|
|
||||||
|
def pause_writing(self) -> None:
|
||||||
|
self._not_paused.clear()
|
||||||
|
|
||||||
|
def resume_writing(self) -> None:
|
||||||
|
self._not_paused.set()
|
||||||
|
|
||||||
|
async def complete(self) -> None:
|
||||||
|
self._not_paused.set()
|
||||||
|
await self.transport.send(
|
||||||
|
{"type": "http.response.body", "body": b"", "more_body": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_complete(self) -> bool:
|
||||||
|
return self._complete.is_set()
|
||||||
|
|
||||||
|
async def push_data(self, data: bytes) -> None:
|
||||||
|
if not self.is_complete:
|
||||||
|
await self.transport.send(
|
||||||
|
{"type": "http.response.body", "body": data, "more_body": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def drain(self) -> None:
|
||||||
|
await self._not_paused.wait()
|
||||||
|
|
||||||
|
|
||||||
|
class MockTransport:
|
||||||
|
_protocol: Optional[MockProtocol]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||||
|
) -> None:
|
||||||
|
self.scope = scope
|
||||||
|
self._receive = receive
|
||||||
|
self._send = send
|
||||||
|
self._protocol = None
|
||||||
|
self.loop = None
|
||||||
|
|
||||||
|
def get_protocol(self) -> MockProtocol:
|
||||||
|
if not self._protocol:
|
||||||
|
self._protocol = MockProtocol(self, self.loop)
|
||||||
|
return self._protocol
|
||||||
|
|
||||||
|
def get_extra_info(self, info: str) -> Union[str, bool, None]:
|
||||||
|
if info == "peername":
|
||||||
|
return self.scope.get("server")
|
||||||
|
elif info == "sslcontext":
|
||||||
|
return self.scope.get("scheme") in ["https", "wss"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_websocket_connection(self) -> WebSocketConnection:
|
||||||
|
try:
|
||||||
|
return self._websocket_connection
|
||||||
|
except AttributeError:
|
||||||
|
raise InvalidUsage("Improper websocket connection.")
|
||||||
|
|
||||||
|
def create_websocket_connection(
|
||||||
|
self, send: ASGISend, receive: ASGIReceive
|
||||||
|
) -> WebSocketConnection:
|
||||||
|
self._websocket_connection = WebSocketConnection(send, receive)
|
||||||
|
return self._websocket_connection
|
||||||
|
|
||||||
|
def add_task(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def send(self, data) -> None:
|
||||||
|
# TODO:
|
||||||
|
# - Validation on data and that it is formatted properly and is valid
|
||||||
|
await self._send(data)
|
||||||
|
|
||||||
|
async def receive(self) -> ASGIMessage:
|
||||||
|
return await self._receive()
|
||||||
|
|
||||||
|
|
||||||
|
class Lifespan:
|
||||||
|
def __init__(self, asgi_app: "ASGIApp") -> None:
|
||||||
|
self.asgi_app = asgi_app
|
||||||
|
|
||||||
|
if "before_server_start" in self.asgi_app.sanic_app.listeners:
|
||||||
|
warnings.warn(
|
||||||
|
'You have set a listener for "before_server_start" '
|
||||||
|
"in ASGI mode. "
|
||||||
|
"It will be executed as early as possible, but not before "
|
||||||
|
"the ASGI server is started."
|
||||||
|
)
|
||||||
|
if "after_server_stop" in self.asgi_app.sanic_app.listeners:
|
||||||
|
warnings.warn(
|
||||||
|
'You have set a listener for "after_server_stop" '
|
||||||
|
"in ASGI mode. "
|
||||||
|
"It will be executed as late as possible, but not after "
|
||||||
|
"the ASGI server is stopped."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def startup(self) -> None:
|
||||||
|
"""
|
||||||
|
Gather the listeners to fire on server start.
|
||||||
|
Because we are using a third-party server and not Sanic server, we do
|
||||||
|
not have access to fire anything BEFORE the server starts.
|
||||||
|
Therefore, we fire before_server_start and after_server_start
|
||||||
|
in sequence since the ASGI lifespan protocol only supports a single
|
||||||
|
startup event.
|
||||||
|
"""
|
||||||
|
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||||
|
"before_server_start", []
|
||||||
|
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
|
||||||
|
|
||||||
|
for handler in listeners:
|
||||||
|
response = handler(
|
||||||
|
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||||
|
)
|
||||||
|
if isawaitable(response):
|
||||||
|
await response
|
||||||
|
|
||||||
|
async def shutdown(self) -> None:
|
||||||
|
"""
|
||||||
|
Gather the listeners to fire on server stop.
|
||||||
|
Because we are using a third-party server and not Sanic server, we do
|
||||||
|
not have access to fire anything AFTER the server stops.
|
||||||
|
Therefore, we fire before_server_stop and after_server_stop
|
||||||
|
in sequence since the ASGI lifespan protocol only supports a single
|
||||||
|
shutdown event.
|
||||||
|
"""
|
||||||
|
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||||
|
"before_server_stop", []
|
||||||
|
) + self.asgi_app.sanic_app.listeners.get("after_server_stop", [])
|
||||||
|
|
||||||
|
for handler in listeners:
|
||||||
|
response = handler(
|
||||||
|
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||||
|
)
|
||||||
|
if isawaitable(response):
|
||||||
|
await response
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||||
|
) -> None:
|
||||||
|
message = await receive()
|
||||||
|
if message["type"] == "lifespan.startup":
|
||||||
|
await self.startup()
|
||||||
|
await send({"type": "lifespan.startup.complete"})
|
||||||
|
|
||||||
|
message = await receive()
|
||||||
|
if message["type"] == "lifespan.shutdown":
|
||||||
|
await self.shutdown()
|
||||||
|
await send({"type": "lifespan.shutdown.complete"})
|
||||||
|
|
||||||
|
|
||||||
|
class ASGIApp:
|
||||||
|
sanic_app: "sanic.app.Sanic"
|
||||||
|
request: Request
|
||||||
|
transport: MockTransport
|
||||||
|
do_stream: bool
|
||||||
|
lifespan: Lifespan
|
||||||
|
ws: Optional[WebSocketConnection]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.ws = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(
|
||||||
|
cls, sanic_app, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||||
|
) -> "ASGIApp":
|
||||||
|
instance = cls()
|
||||||
|
instance.sanic_app = sanic_app
|
||||||
|
instance.transport = MockTransport(scope, receive, send)
|
||||||
|
instance.transport.loop = sanic_app.loop
|
||||||
|
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
|
||||||
|
|
||||||
|
headers = Header(
|
||||||
|
[
|
||||||
|
(key.decode("latin-1"), value.decode("latin-1"))
|
||||||
|
for key, value in scope.get("headers", [])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
instance.do_stream = (
|
||||||
|
True if headers.get("expect") == "100-continue" else False
|
||||||
|
)
|
||||||
|
instance.lifespan = Lifespan(instance)
|
||||||
|
|
||||||
|
if scope["type"] == "lifespan":
|
||||||
|
await instance.lifespan(scope, receive, send)
|
||||||
|
else:
|
||||||
|
path = (
|
||||||
|
scope["path"][1:]
|
||||||
|
if scope["path"].startswith("/")
|
||||||
|
else scope["path"]
|
||||||
|
)
|
||||||
|
url = "/".join([scope.get("root_path", ""), quote(path)])
|
||||||
|
url_bytes = url.encode("latin-1")
|
||||||
|
url_bytes += b"?" + scope["query_string"]
|
||||||
|
|
||||||
|
if scope["type"] == "http":
|
||||||
|
version = scope["http_version"]
|
||||||
|
method = scope["method"]
|
||||||
|
elif scope["type"] == "websocket":
|
||||||
|
version = "1.1"
|
||||||
|
method = "GET"
|
||||||
|
|
||||||
|
instance.ws = instance.transport.create_websocket_connection(
|
||||||
|
send, receive
|
||||||
|
)
|
||||||
|
await instance.ws.accept()
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
# TODO:
|
||||||
|
# - close connection
|
||||||
|
|
||||||
|
request_class = sanic_app.request_class or Request
|
||||||
|
instance.request = request_class(
|
||||||
|
url_bytes,
|
||||||
|
headers,
|
||||||
|
version,
|
||||||
|
method,
|
||||||
|
instance.transport,
|
||||||
|
sanic_app,
|
||||||
|
)
|
||||||
|
|
||||||
|
if sanic_app.is_request_stream:
|
||||||
|
is_stream_handler = sanic_app.router.is_stream_handler(
|
||||||
|
instance.request
|
||||||
|
)
|
||||||
|
if is_stream_handler:
|
||||||
|
instance.request.stream = StreamBuffer(
|
||||||
|
sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE
|
||||||
|
)
|
||||||
|
instance.do_stream = True
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
async def read_body(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Read and return the entire body from an incoming ASGI message.
|
||||||
|
"""
|
||||||
|
body = b""
|
||||||
|
more_body = True
|
||||||
|
while more_body:
|
||||||
|
message = await self.transport.receive()
|
||||||
|
body += message.get("body", b"")
|
||||||
|
more_body = message.get("more_body", False)
|
||||||
|
|
||||||
|
return body
|
||||||
|
|
||||||
|
async def stream_body(self) -> None:
|
||||||
|
"""
|
||||||
|
Read and stream the body in chunks from an incoming ASGI message.
|
||||||
|
"""
|
||||||
|
more_body = True
|
||||||
|
|
||||||
|
while more_body:
|
||||||
|
message = await self.transport.receive()
|
||||||
|
chunk = message.get("body", b"")
|
||||||
|
await self.request.stream.put(chunk)
|
||||||
|
|
||||||
|
more_body = message.get("more_body", False)
|
||||||
|
|
||||||
|
await self.request.stream.put(None)
|
||||||
|
|
||||||
|
async def __call__(self) -> None:
|
||||||
|
"""
|
||||||
|
Handle the incoming request.
|
||||||
|
"""
|
||||||
|
if not self.do_stream:
|
||||||
|
self.request.body = await self.read_body()
|
||||||
|
else:
|
||||||
|
self.sanic_app.loop.create_task(self.stream_body())
|
||||||
|
|
||||||
|
handler = self.sanic_app.handle_request
|
||||||
|
callback = None if self.ws else self.stream_callback
|
||||||
|
await handler(self.request, None, callback)
|
||||||
|
|
||||||
|
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
|
||||||
|
|
||||||
|
async def stream_callback(self, response: HTTPResponse) -> None:
|
||||||
|
"""
|
||||||
|
Write the response.
|
||||||
|
"""
|
||||||
|
headers: List[Tuple[bytes, bytes]] = []
|
||||||
|
cookies: Dict[str, str] = {}
|
||||||
|
content_length: List[str] = []
|
||||||
|
try:
|
||||||
|
content_length = response.headers.popall("content-length", [])
|
||||||
|
cookies = {
|
||||||
|
v.key: v
|
||||||
|
for _, v in list(
|
||||||
|
filter(
|
||||||
|
lambda item: item[0].lower() == "set-cookie",
|
||||||
|
response.headers.items(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
headers += [
|
||||||
|
(str(name).encode("latin-1"), str(value).encode("latin-1"))
|
||||||
|
for name, value in response.headers.items()
|
||||||
|
if name.lower() not in ["set-cookie"]
|
||||||
|
]
|
||||||
|
except AttributeError:
|
||||||
|
logger.error(
|
||||||
|
"Invalid response object for url %s, "
|
||||||
|
"Expected Type: HTTPResponse, Actual Type: %s",
|
||||||
|
self.request.url,
|
||||||
|
type(response),
|
||||||
|
)
|
||||||
|
exception = ServerError("Invalid response type")
|
||||||
|
response = self.sanic_app.error_handler.response(
|
||||||
|
self.request, exception
|
||||||
|
)
|
||||||
|
headers = [
|
||||||
|
(str(name).encode("latin-1"), str(value).encode("latin-1"))
|
||||||
|
for name, value in response.headers.items()
|
||||||
|
if name not in (b"Set-Cookie",)
|
||||||
|
]
|
||||||
|
|
||||||
|
response.asgi = True
|
||||||
|
is_streaming = isinstance(response, StreamingHTTPResponse)
|
||||||
|
if is_streaming and getattr(response, "chunked", False):
|
||||||
|
# disable sanic chunking, this is done at the ASGI-server level
|
||||||
|
setattr(response, "chunked", False)
|
||||||
|
# content-length header is removed to signal to the ASGI-server
|
||||||
|
# to use automatic-chunking if it supports it
|
||||||
|
elif len(content_length) > 0:
|
||||||
|
headers += [
|
||||||
|
(b"content-length", str(content_length[0]).encode("latin-1"))
|
||||||
|
]
|
||||||
|
elif not is_streaming:
|
||||||
|
headers += [
|
||||||
|
(
|
||||||
|
b"content-length",
|
||||||
|
str(len(getattr(response, "body", b""))).encode("latin-1"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
if "content-type" not in response.headers:
|
||||||
|
headers += [
|
||||||
|
(b"content-type", str(response.content_type).encode("latin-1"))
|
||||||
|
]
|
||||||
|
|
||||||
|
if response.cookies:
|
||||||
|
cookies.update(
|
||||||
|
{
|
||||||
|
v.key: v
|
||||||
|
for _, v in response.cookies.items()
|
||||||
|
if v.key not in cookies.keys()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
headers += [
|
||||||
|
(b"set-cookie", cookie.encode("utf-8"))
|
||||||
|
for k, cookie in cookies.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
await self.transport.send(
|
||||||
|
{
|
||||||
|
"type": "http.response.start",
|
||||||
|
"status": response.status,
|
||||||
|
"headers": headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(response, StreamingHTTPResponse):
|
||||||
|
response.protocol = self.transport.get_protocol()
|
||||||
|
await response.stream()
|
||||||
|
await response.protocol.complete()
|
||||||
|
|
||||||
|
else:
|
||||||
|
await self.transport.send(
|
||||||
|
{
|
||||||
|
"type": "http.response.body",
|
||||||
|
"body": response.body,
|
||||||
|
"more_body": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
121
sanic/blueprint_group.py
Normal file
121
sanic/blueprint_group.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
from collections.abc import MutableSequence
|
||||||
|
|
||||||
|
|
||||||
|
class BlueprintGroup(MutableSequence):
|
||||||
|
"""
|
||||||
|
This class provides a mechanism to implement a Blueprint Group
|
||||||
|
using the :meth:`~sanic.blueprints.Blueprint.group` method in
|
||||||
|
:class:`~sanic.blueprints.Blueprint`. To avoid having to re-write
|
||||||
|
some of the existing implementation, this class provides a custom
|
||||||
|
iterator implementation that will let you use the object of this
|
||||||
|
class as a list/tuple inside the existing implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_blueprints", "_url_prefix")
|
||||||
|
|
||||||
|
def __init__(self, url_prefix=None):
|
||||||
|
"""
|
||||||
|
Create a new Blueprint Group
|
||||||
|
|
||||||
|
:param url_prefix: URL: to be prefixed before all the Blueprint Prefix
|
||||||
|
"""
|
||||||
|
self._blueprints = []
|
||||||
|
self._url_prefix = url_prefix
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_prefix(self):
|
||||||
|
"""
|
||||||
|
Retrieve the URL prefix being used for the Current Blueprint Group
|
||||||
|
:return: string with url prefix
|
||||||
|
"""
|
||||||
|
return self._url_prefix
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blueprints(self):
|
||||||
|
"""
|
||||||
|
Retrieve a list of all the available blueprints under this group.
|
||||||
|
:return: List of Blueprint instance
|
||||||
|
"""
|
||||||
|
return self._blueprints
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Tun the class Blueprint Group into an Iterable item"""
|
||||||
|
return iter(self._blueprints)
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
"""
|
||||||
|
This method returns a blueprint inside the group specified by
|
||||||
|
an index value. This will enable indexing, splice and slicing
|
||||||
|
of the blueprint group like we can do with regular list/tuple.
|
||||||
|
|
||||||
|
This method is provided to ensure backward compatibility with
|
||||||
|
any of the pre-existing usage that might break.
|
||||||
|
|
||||||
|
:param item: Index of the Blueprint item in the group
|
||||||
|
:return: Blueprint object
|
||||||
|
"""
|
||||||
|
return self._blueprints[item]
|
||||||
|
|
||||||
|
def __setitem__(self, index, item) -> None:
|
||||||
|
"""
|
||||||
|
Abstract method implemented to turn the `BlueprintGroup` class
|
||||||
|
into a list like object to support all the existing behavior.
|
||||||
|
|
||||||
|
This method is used to perform the list's indexed setter operation.
|
||||||
|
|
||||||
|
:param index: Index to use for inserting a new Blueprint item
|
||||||
|
:param item: New `Blueprint` object.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self._blueprints[index] = item
|
||||||
|
|
||||||
|
def __delitem__(self, index) -> None:
|
||||||
|
"""
|
||||||
|
Abstract method implemented to turn the `BlueprintGroup` class
|
||||||
|
into a list like object to support all the existing behavior.
|
||||||
|
|
||||||
|
This method is used to delete an item from the list of blueprint
|
||||||
|
groups like it can be done on a regular list with index.
|
||||||
|
|
||||||
|
:param index: Index to use for removing a new Blueprint item
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
del self._blueprints[index]
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""
|
||||||
|
Get the Length of the blueprint group object.
|
||||||
|
:return: Length of Blueprint group object
|
||||||
|
"""
|
||||||
|
return len(self._blueprints)
|
||||||
|
|
||||||
|
def insert(self, index: int, item: object) -> None:
|
||||||
|
"""
|
||||||
|
The Abstract class `MutableSequence` leverages this insert method to
|
||||||
|
perform the `BlueprintGroup.append` operation.
|
||||||
|
|
||||||
|
:param index: Index to use for removing a new Blueprint item
|
||||||
|
:param item: New `Blueprint` object.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self._blueprints.insert(index, item)
|
||||||
|
|
||||||
|
def middleware(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
A decorator that can be used to implement a Middleware plugin to
|
||||||
|
all of the Blueprints that belongs to this specific Blueprint Group.
|
||||||
|
|
||||||
|
In case of nested Blueprint Groups, the same middleware is applied
|
||||||
|
across each of the Blueprints recursively.
|
||||||
|
|
||||||
|
:param args: Optional positional Parameters to be use middleware
|
||||||
|
:param kwargs: Optional Keyword arg to use with Middleware
|
||||||
|
:return: Partial function to apply the middleware
|
||||||
|
"""
|
||||||
|
kwargs["bp_group"] = True
|
||||||
|
|
||||||
|
def register_middleware_for_blueprints(fn):
|
||||||
|
for blueprint in self.blueprints:
|
||||||
|
blueprint.middleware(fn, *args, **kwargs)
|
||||||
|
|
||||||
|
return register_middleware_for_blueprints
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
|
|
||||||
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.views import CompositionView
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
@@ -36,13 +37,19 @@ class Blueprint:
|
|||||||
url_prefix=None,
|
url_prefix=None,
|
||||||
host=None,
|
host=None,
|
||||||
version=None,
|
version=None,
|
||||||
strict_slashes=False,
|
strict_slashes=None,
|
||||||
):
|
):
|
||||||
"""Create a new blueprint
|
"""
|
||||||
|
In *Sanic* terminology, a **Blueprint** is a logical collection of
|
||||||
|
URLs that perform a specific set of tasks which can be identified by
|
||||||
|
a unique name.
|
||||||
|
|
||||||
:param name: unique name of the blueprint
|
:param name: unique name of the blueprint
|
||||||
:param url_prefix: URL to be prefixed before all route URLs
|
:param url_prefix: URL to be prefixed before all route URLs
|
||||||
:param strict_slashes: strict to trailing slash
|
:param host: IP Address of FQDN for the sanic server to use.
|
||||||
|
:param version: Blueprint Version
|
||||||
|
:param strict_slashes: Enforce the API urls are requested with a
|
||||||
|
training */*
|
||||||
"""
|
"""
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url_prefix = url_prefix
|
self.url_prefix = url_prefix
|
||||||
@@ -59,8 +66,9 @@ class Blueprint:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def group(*blueprints, url_prefix=""):
|
def group(*blueprints, url_prefix=""):
|
||||||
"""Create a list of blueprints, optionally
|
"""
|
||||||
grouping them under a general URL prefix.
|
Create a list of blueprints, optionally grouping them under a
|
||||||
|
general URL prefix.
|
||||||
|
|
||||||
:param blueprints: blueprints to be registered as a group
|
:param blueprints: blueprints to be registered as a group
|
||||||
:param url_prefix: URL route to be prepended to all sub-prefixes
|
:param url_prefix: URL route to be prepended to all sub-prefixes
|
||||||
@@ -71,10 +79,12 @@ class Blueprint:
|
|||||||
for i in nested:
|
for i in nested:
|
||||||
if isinstance(i, (list, tuple)):
|
if isinstance(i, (list, tuple)):
|
||||||
yield from chain(i)
|
yield from chain(i)
|
||||||
|
elif isinstance(i, BlueprintGroup):
|
||||||
|
yield from i.blueprints
|
||||||
else:
|
else:
|
||||||
yield i
|
yield i
|
||||||
|
|
||||||
bps = []
|
bps = BlueprintGroup(url_prefix=url_prefix)
|
||||||
for bp in chain(blueprints):
|
for bp in chain(blueprints):
|
||||||
if bp.url_prefix is None:
|
if bp.url_prefix is None:
|
||||||
bp.url_prefix = ""
|
bp.url_prefix = ""
|
||||||
@@ -83,10 +93,19 @@ class Blueprint:
|
|||||||
return bps
|
return bps
|
||||||
|
|
||||||
def register(self, app, options):
|
def register(self, app, options):
|
||||||
"""Register the blueprint to the sanic app."""
|
"""
|
||||||
|
Register the blueprint to the sanic app.
|
||||||
|
|
||||||
|
:param app: Instance of :class:`sanic.app.Sanic` class
|
||||||
|
:param options: Options to be used while registering the
|
||||||
|
blueprint into the app.
|
||||||
|
*url_prefix* - URL Prefix to override the blueprint prefix
|
||||||
|
"""
|
||||||
|
|
||||||
url_prefix = options.get("url_prefix", self.url_prefix)
|
url_prefix = options.get("url_prefix", self.url_prefix)
|
||||||
|
|
||||||
|
routes = []
|
||||||
|
|
||||||
# Routes
|
# Routes
|
||||||
for future in self.routes:
|
for future in self.routes:
|
||||||
# attach the blueprint name to the handler so that it can be
|
# attach the blueprint name to the handler so that it can be
|
||||||
@@ -97,7 +116,7 @@ class Blueprint:
|
|||||||
|
|
||||||
version = future.version or self.version
|
version = future.version or self.version
|
||||||
|
|
||||||
app.route(
|
_routes, _ = app.route(
|
||||||
uri=uri[1:] if uri.startswith("//") else uri,
|
uri=uri[1:] if uri.startswith("//") else uri,
|
||||||
methods=future.methods,
|
methods=future.methods,
|
||||||
host=future.host or self.host,
|
host=future.host or self.host,
|
||||||
@@ -106,6 +125,8 @@ class Blueprint:
|
|||||||
version=version,
|
version=version,
|
||||||
name=future.name,
|
name=future.name,
|
||||||
)(future.handler)
|
)(future.handler)
|
||||||
|
if _routes:
|
||||||
|
routes += _routes
|
||||||
|
|
||||||
for future in self.websocket_routes:
|
for future in self.websocket_routes:
|
||||||
# attach the blueprint name to the handler so that it can be
|
# attach the blueprint name to the handler so that it can be
|
||||||
@@ -113,21 +134,27 @@ class Blueprint:
|
|||||||
future.handler.__blueprintname__ = self.name
|
future.handler.__blueprintname__ = self.name
|
||||||
# Prepend the blueprint URI prefix if available
|
# Prepend the blueprint URI prefix if available
|
||||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||||
app.websocket(
|
_routes, _ = app.websocket(
|
||||||
uri=uri,
|
uri=uri,
|
||||||
host=future.host or self.host,
|
host=future.host or self.host,
|
||||||
strict_slashes=future.strict_slashes,
|
strict_slashes=future.strict_slashes,
|
||||||
name=future.name,
|
name=future.name,
|
||||||
)(future.handler)
|
)(future.handler)
|
||||||
|
if _routes:
|
||||||
|
routes += _routes
|
||||||
|
|
||||||
|
route_names = [route.name for route in routes]
|
||||||
# Middleware
|
# Middleware
|
||||||
for future in self.middlewares:
|
for future in self.middlewares:
|
||||||
if future.args or future.kwargs:
|
if future.args or future.kwargs:
|
||||||
app.register_middleware(
|
app.register_named_middleware(
|
||||||
future.middleware, *future.args, **future.kwargs
|
future.middleware,
|
||||||
|
route_names,
|
||||||
|
*future.args,
|
||||||
|
**future.kwargs
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
app.register_middleware(future.middleware)
|
app.register_named_middleware(future.middleware, route_names)
|
||||||
|
|
||||||
# Exceptions
|
# Exceptions
|
||||||
for future in self.exceptions:
|
for future in self.exceptions:
|
||||||
@@ -160,6 +187,15 @@ class Blueprint:
|
|||||||
|
|
||||||
:param uri: endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
:param methods: list of acceptable HTTP methods.
|
:param methods: list of acceptable HTTP methods.
|
||||||
|
:param host: IP Address of FQDN for the sanic server to use.
|
||||||
|
:param strict_slashes: Enforce the API urls are requested with a
|
||||||
|
training */*
|
||||||
|
:param stream: If the route should provide a streaming support
|
||||||
|
:param version: Blueprint Version
|
||||||
|
:param name: Unique name to identify the Route
|
||||||
|
|
||||||
|
:return a decorated method that when invoked will return an object
|
||||||
|
of type :class:`FutureRoute`
|
||||||
"""
|
"""
|
||||||
if strict_slashes is None:
|
if strict_slashes is None:
|
||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
@@ -189,6 +225,7 @@ class Blueprint:
|
|||||||
strict_slashes=None,
|
strict_slashes=None,
|
||||||
version=None,
|
version=None,
|
||||||
name=None,
|
name=None,
|
||||||
|
stream=False,
|
||||||
):
|
):
|
||||||
"""Create a blueprint route from a function.
|
"""Create a blueprint route from a function.
|
||||||
|
|
||||||
@@ -196,10 +233,12 @@ class Blueprint:
|
|||||||
or class instance with a view_class method.
|
or class instance with a view_class method.
|
||||||
:param uri: endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
:param methods: list of acceptable HTTP methods.
|
:param methods: list of acceptable HTTP methods.
|
||||||
:param host:
|
:param host: IP Address of FQDN for the sanic server to use.
|
||||||
:param strict_slashes:
|
:param strict_slashes: Enforce the API urls are requested with a
|
||||||
:param version:
|
training */*
|
||||||
|
:param version: Blueprint Version
|
||||||
:param name: user defined route name for url_for
|
:param name: user defined route name for url_for
|
||||||
|
:param stream: boolean specifying if the handler is a stream handler
|
||||||
:return: function or class instance
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
# Handle HTTPMethodView differently
|
# Handle HTTPMethodView differently
|
||||||
@@ -222,6 +261,7 @@ class Blueprint:
|
|||||||
methods=methods,
|
methods=methods,
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
)(handler)
|
)(handler)
|
||||||
@@ -233,6 +273,11 @@ class Blueprint:
|
|||||||
"""Create a blueprint websocket route from a decorated function.
|
"""Create a blueprint websocket route from a decorated function.
|
||||||
|
|
||||||
:param uri: endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
|
:param host: IP Address of FQDN for the sanic server to use.
|
||||||
|
:param strict_slashes: Enforce the API urls are requested with a
|
||||||
|
training */*
|
||||||
|
:param version: Blueprint Version
|
||||||
|
:param name: Unique name to identify the Websocket Route
|
||||||
"""
|
"""
|
||||||
if strict_slashes is None:
|
if strict_slashes is None:
|
||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
@@ -254,6 +299,9 @@ class Blueprint:
|
|||||||
:param handler: function for handling uri requests. Accepts function,
|
:param handler: function for handling uri requests. Accepts function,
|
||||||
or class instance with a view_class method.
|
or class instance with a view_class method.
|
||||||
:param uri: endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
|
:param host: IP Address of FQDN for the sanic server to use.
|
||||||
|
:param version: Blueprint Version
|
||||||
|
:param name: Unique name to identify the Websocket Route
|
||||||
:return: function or class instance
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
self.websocket(uri=uri, host=host, version=version, name=name)(handler)
|
self.websocket(uri=uri, host=host, version=version, name=name)(handler)
|
||||||
@@ -272,7 +320,14 @@ class Blueprint:
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def middleware(self, *args, **kwargs):
|
def middleware(self, *args, **kwargs):
|
||||||
"""Create a blueprint middleware from a decorated function."""
|
"""
|
||||||
|
Create a blueprint middleware from a decorated function.
|
||||||
|
|
||||||
|
:param args: Positional arguments to be used while invoking the
|
||||||
|
middleware
|
||||||
|
:param kwargs: optional keyword args that can be used with the
|
||||||
|
middleware.
|
||||||
|
"""
|
||||||
|
|
||||||
def register_middleware(_middleware):
|
def register_middleware(_middleware):
|
||||||
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
||||||
@@ -285,10 +340,26 @@ class Blueprint:
|
|||||||
args = []
|
args = []
|
||||||
return register_middleware(middleware)
|
return register_middleware(middleware)
|
||||||
else:
|
else:
|
||||||
return register_middleware
|
if kwargs.get("bp_group") and callable(args[0]):
|
||||||
|
middleware = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
kwargs.pop("bp_group")
|
||||||
|
return register_middleware(middleware)
|
||||||
|
else:
|
||||||
|
return register_middleware
|
||||||
|
|
||||||
def exception(self, *args, **kwargs):
|
def exception(self, *args, **kwargs):
|
||||||
"""Create a blueprint exception from a decorated function."""
|
"""
|
||||||
|
This method enables the process of creating a global exception
|
||||||
|
handler for the current blueprint under question.
|
||||||
|
|
||||||
|
:param args: List of Python exceptions to be caught by the handler
|
||||||
|
:param kwargs: Additional optional arguments to be passed to the
|
||||||
|
exception handler
|
||||||
|
|
||||||
|
:return a decorated method to handle global exceptions for any
|
||||||
|
route registered under this blueprint.
|
||||||
|
"""
|
||||||
|
|
||||||
def decorator(handler):
|
def decorator(handler):
|
||||||
exception = FutureException(handler, args, kwargs)
|
exception = FutureException(handler, args, kwargs)
|
||||||
@@ -319,9 +390,20 @@ class Blueprint:
|
|||||||
def get(
|
def get(
|
||||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **GET** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **GET** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
|
||||||
|
if the request URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=["GET"],
|
methods=frozenset({"GET"}),
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -337,9 +419,20 @@ class Blueprint:
|
|||||||
version=None,
|
version=None,
|
||||||
name=None,
|
name=None,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **POST** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **POST** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
|
||||||
|
if the request URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=["POST"],
|
methods=frozenset({"POST"}),
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
@@ -356,9 +449,20 @@ class Blueprint:
|
|||||||
version=None,
|
version=None,
|
||||||
name=None,
|
name=None,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **PUT** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **PUT** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
|
||||||
|
if the request URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=["PUT"],
|
methods=frozenset({"PUT"}),
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
@@ -369,9 +473,20 @@ class Blueprint:
|
|||||||
def head(
|
def head(
|
||||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **HEAD** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **HEAD** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
|
||||||
|
if the request URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=["HEAD"],
|
methods=frozenset({"HEAD"}),
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -381,9 +496,20 @@ class Blueprint:
|
|||||||
def options(
|
def options(
|
||||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **OPTIONS** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **OPTIONS** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
|
||||||
|
if the request URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=["OPTIONS"],
|
methods=frozenset({"OPTIONS"}),
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -399,9 +525,20 @@ class Blueprint:
|
|||||||
version=None,
|
version=None,
|
||||||
name=None,
|
name=None,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **PATCH** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
|
||||||
|
if the request URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=["PATCH"],
|
methods=frozenset({"PATCH"}),
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
@@ -412,9 +549,20 @@ class Blueprint:
|
|||||||
def delete(
|
def delete(
|
||||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Add an API URL under the **DELETE** *HTTP* method
|
||||||
|
|
||||||
|
:param uri: URL to be tagged to **DELETE** method of *HTTP*
|
||||||
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
|
||||||
|
if the request URLs need to terminate with a */*
|
||||||
|
:param version: API Version
|
||||||
|
:param name: Unique name that can be used to identify the Route
|
||||||
|
:return: Object decorated with :func:`route` method
|
||||||
|
"""
|
||||||
return self.route(
|
return self.route(
|
||||||
uri,
|
uri,
|
||||||
methods=["DELETE"],
|
methods=frozenset({"DELETE"}),
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
version=version,
|
version=version,
|
||||||
|
|||||||
6
sanic/compat.py
Normal file
6
sanic/compat.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from multidict import CIMultiDict # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Header(CIMultiDict):
|
||||||
|
def get_all(self, key):
|
||||||
|
return self.getall(key, default=[])
|
||||||
@@ -2,46 +2,46 @@ import os
|
|||||||
import types
|
import types
|
||||||
|
|
||||||
from sanic.exceptions import PyFileError
|
from sanic.exceptions import PyFileError
|
||||||
|
from sanic.helpers import import_string
|
||||||
|
|
||||||
|
|
||||||
SANIC_PREFIX = "SANIC_"
|
SANIC_PREFIX = "SANIC_"
|
||||||
|
BASE_LOGO = """
|
||||||
|
|
||||||
|
Sanic
|
||||||
|
Build Fast. Run Fast.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||||
|
"REQUEST_BUFFER_QUEUE_SIZE": 100,
|
||||||
|
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||||
|
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||||
|
"KEEP_ALIVE": True,
|
||||||
|
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
||||||
|
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabytes
|
||||||
|
"WEBSOCKET_MAX_QUEUE": 32,
|
||||||
|
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
||||||
|
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||||
|
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||||
|
"ACCESS_LOG": True,
|
||||||
|
"FORWARDED_SECRET": None,
|
||||||
|
"REAL_IP_HEADER": None,
|
||||||
|
"PROXIES_COUNT": None,
|
||||||
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Config(dict):
|
class Config(dict):
|
||||||
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
||||||
super().__init__(defaults or {})
|
defaults = defaults or {}
|
||||||
self.LOGO = """
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
▄▄▄▄▄
|
|
||||||
▀▀▀██████▄▄▄ _______________
|
self.LOGO = BASE_LOGO
|
||||||
▄▄▄▄▄ █████████▄ / \\
|
|
||||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
if keep_alive is not None:
|
||||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
self.KEEP_ALIVE = keep_alive
|
||||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
|
||||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
|
||||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
|
||||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
|
||||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
|
||||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
|
||||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
|
||||||
▀▀█████████▀
|
|
||||||
▄▄██▀██████▀█
|
|
||||||
▄██▀ ▀▀▀ █
|
|
||||||
▄█ ▐▌
|
|
||||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
|
||||||
▌ ▐ ▀▀▄▄▄▀
|
|
||||||
▀▀▄▄▀
|
|
||||||
"""
|
|
||||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
|
|
||||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
|
||||||
self.RESPONSE_TIMEOUT = 60 # 60 seconds
|
|
||||||
self.KEEP_ALIVE = keep_alive
|
|
||||||
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
|
|
||||||
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
|
||||||
self.WEBSOCKET_MAX_QUEUE = 32
|
|
||||||
self.WEBSOCKET_READ_LIMIT = 2 ** 16
|
|
||||||
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
|
|
||||||
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
|
||||||
self.ACCESS_LOG = True
|
|
||||||
|
|
||||||
if load_env:
|
if load_env:
|
||||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||||
@@ -81,7 +81,7 @@ class Config(dict):
|
|||||||
module.__file__ = filename
|
module.__file__ = filename
|
||||||
try:
|
try:
|
||||||
with open(filename) as config_file:
|
with open(filename) as config_file:
|
||||||
exec(
|
exec( # nosec
|
||||||
compile(config_file.read(), filename, "exec"),
|
compile(config_file.read(), filename, "exec"),
|
||||||
module.__dict__,
|
module.__dict__,
|
||||||
)
|
)
|
||||||
@@ -104,6 +104,9 @@ class Config(dict):
|
|||||||
from yourapplication import default_config
|
from yourapplication import default_config
|
||||||
app.config.from_object(default_config)
|
app.config.from_object(default_config)
|
||||||
|
|
||||||
|
or also:
|
||||||
|
app.config.from_object('myproject.config.MyConfigClass')
|
||||||
|
|
||||||
You should not use this function to load the actual configuration but
|
You should not use this function to load the actual configuration but
|
||||||
rather configuration defaults. The actual config should be loaded
|
rather configuration defaults. The actual config should be loaded
|
||||||
with :meth:`from_pyfile` and ideally from a location not within the
|
with :meth:`from_pyfile` and ideally from a location not within the
|
||||||
@@ -111,6 +114,8 @@ class Config(dict):
|
|||||||
|
|
||||||
:param obj: an object holding the configuration
|
:param obj: an object holding the configuration
|
||||||
"""
|
"""
|
||||||
|
if isinstance(obj, str):
|
||||||
|
obj = import_string(obj)
|
||||||
for key in dir(obj):
|
for key in dir(obj):
|
||||||
if key.isupper():
|
if key.isupper():
|
||||||
self[key] = getattr(obj, key)
|
self[key] = getattr(obj, key)
|
||||||
@@ -129,4 +134,24 @@ class Config(dict):
|
|||||||
try:
|
try:
|
||||||
self[config_key] = float(v)
|
self[config_key] = float(v)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self[config_key] = v
|
try:
|
||||||
|
self[config_key] = strtobool(v)
|
||||||
|
except ValueError:
|
||||||
|
self[config_key] = v
|
||||||
|
|
||||||
|
|
||||||
|
def strtobool(val):
|
||||||
|
"""
|
||||||
|
This function was borrowed from distutils.utils. While distutils
|
||||||
|
is part of stdlib, it feels odd to use distutils in main application code.
|
||||||
|
|
||||||
|
The function was modified to walk its talk and actually return bool
|
||||||
|
and not int.
|
||||||
|
"""
|
||||||
|
val = val.lower()
|
||||||
|
if val in ("y", "yes", "t", "true", "on", "1"):
|
||||||
|
return True
|
||||||
|
elif val in ("n", "no", "f", "false", "off", "0"):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid truth value %r" % (val,))
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_MAX_AGE = 0
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# SimpleCookie
|
# SimpleCookie
|
||||||
@@ -103,9 +107,33 @@ class Cookie(dict):
|
|||||||
if key not in self._keys:
|
if key not in self._keys:
|
||||||
raise KeyError("Unknown cookie property")
|
raise KeyError("Unknown cookie property")
|
||||||
if value is not False:
|
if value is not False:
|
||||||
|
if key.lower() == "max-age":
|
||||||
|
if not str(value).isdigit():
|
||||||
|
value = DEFAULT_MAX_AGE
|
||||||
|
elif key.lower() == "expires":
|
||||||
|
if not isinstance(value, datetime):
|
||||||
|
raise TypeError(
|
||||||
|
"Cookie 'expires' property must be a datetime"
|
||||||
|
)
|
||||||
return super().__setitem__(key, value)
|
return super().__setitem__(key, value)
|
||||||
|
|
||||||
def encode(self, encoding):
|
def encode(self, encoding):
|
||||||
|
"""
|
||||||
|
Encode the cookie content in a specific type of encoding instructed
|
||||||
|
by the developer. Leverages the :func:`str.encode` method provided
|
||||||
|
by python.
|
||||||
|
|
||||||
|
This method can be used to encode and embed ``utf-8`` content into
|
||||||
|
the cookies.
|
||||||
|
|
||||||
|
:param encoding: Encoding to be used with the cookie
|
||||||
|
:return: Cookie encoded in a codec of choosing.
|
||||||
|
:except: UnicodeEncodeError
|
||||||
|
"""
|
||||||
|
return str(self).encode(encoding)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Format as a Set-Cookie header value."""
|
||||||
output = ["%s=%s" % (self.key, _quote(self.value))]
|
output = ["%s=%s" % (self.key, _quote(self.value))]
|
||||||
for key, value in self.items():
|
for key, value in self.items():
|
||||||
if key == "max-age":
|
if key == "max-age":
|
||||||
@@ -114,19 +142,13 @@ class Cookie(dict):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
output.append("%s=%s" % (self._keys[key], value))
|
output.append("%s=%s" % (self._keys[key], value))
|
||||||
elif key == "expires":
|
elif key == "expires":
|
||||||
try:
|
output.append(
|
||||||
output.append(
|
"%s=%s"
|
||||||
"%s=%s"
|
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
|
||||||
% (
|
)
|
||||||
self._keys[key],
|
|
||||||
value.strftime("%a, %d-%b-%Y %T GMT"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
output.append("%s=%s" % (self._keys[key], value))
|
|
||||||
elif key in self._flags and self[key]:
|
elif key in self._flags and self[key]:
|
||||||
output.append(self._keys[key])
|
output.append(self._keys[key])
|
||||||
else:
|
else:
|
||||||
output.append("%s=%s" % (self._keys[key], value))
|
output.append("%s=%s" % (self._keys[key], value))
|
||||||
|
|
||||||
return "; ".join(output).encode(encoding)
|
return "; ".join(output)
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ _sanic_exceptions = {}
|
|||||||
|
|
||||||
def add_status_code(code):
|
def add_status_code(code):
|
||||||
"""
|
"""
|
||||||
Decorator used for adding exceptions to _sanic_exceptions.
|
Decorator used for adding exceptions to :class:`SanicException`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def class_decorator(cls):
|
def class_decorator(cls):
|
||||||
@@ -218,6 +218,11 @@ class ContentRangeError(SanicException):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(417)
|
||||||
|
class HeaderExpectationFailed(SanicException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@add_status_code(403)
|
@add_status_code(403)
|
||||||
class Forbidden(SanicException):
|
class Forbidden(SanicException):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ from sanic.response import html, text
|
|||||||
|
|
||||||
|
|
||||||
class ErrorHandler:
|
class ErrorHandler:
|
||||||
|
"""
|
||||||
|
Provide :class:`sanic.app.Sanic` application with a mechanism to handle
|
||||||
|
and process any and all uncaught exceptions in a way the application
|
||||||
|
developer will set fit.
|
||||||
|
|
||||||
|
This error handling framework is built into the core that can be extended
|
||||||
|
by the developers to perform a wide range of tasks from recording the error
|
||||||
|
stats to reporting them to an external service that can be used for
|
||||||
|
realtime alerting system.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
handlers = None
|
handlers = None
|
||||||
cached_handlers = None
|
cached_handlers = None
|
||||||
_missing = object()
|
_missing = object()
|
||||||
@@ -58,9 +70,34 @@ class ErrorHandler:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def add(self, exception, handler):
|
def add(self, exception, handler):
|
||||||
|
"""
|
||||||
|
Add a new exception handler to an already existing handler object.
|
||||||
|
|
||||||
|
:param exception: Type of exception that need to be handled
|
||||||
|
:param handler: Reference to the method that will handle the exception
|
||||||
|
|
||||||
|
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||||
|
:class:`Exception`
|
||||||
|
:type handler: ``function``
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
self.handlers.append((exception, handler))
|
self.handlers.append((exception, handler))
|
||||||
|
|
||||||
def lookup(self, exception):
|
def lookup(self, exception):
|
||||||
|
"""
|
||||||
|
Lookup the existing instance of :class:`ErrorHandler` and fetch the
|
||||||
|
registered handler for a specific type of exception.
|
||||||
|
|
||||||
|
This method leverages a dict lookup to speedup the retrieval process.
|
||||||
|
|
||||||
|
:param exception: Type of exception
|
||||||
|
|
||||||
|
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||||
|
:class:`Exception`
|
||||||
|
|
||||||
|
:return: Registered function if found ``None`` otherwise
|
||||||
|
"""
|
||||||
handler = self.cached_handlers.get(type(exception), self._missing)
|
handler = self.cached_handlers.get(type(exception), self._missing)
|
||||||
if handler is self._missing:
|
if handler is self._missing:
|
||||||
for exception_class, handler in self.handlers:
|
for exception_class, handler in self.handlers:
|
||||||
@@ -75,9 +112,15 @@ class ErrorHandler:
|
|||||||
"""Fetches and executes an exception handler and returns a response
|
"""Fetches and executes an exception handler and returns a response
|
||||||
object
|
object
|
||||||
|
|
||||||
:param request: Request
|
:param request: Instance of :class:`sanic.request.Request`
|
||||||
:param exception: Exception to handle
|
:param exception: Exception to handle
|
||||||
:return: Response object
|
|
||||||
|
:type request: :class:`sanic.request.Request`
|
||||||
|
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||||
|
:class:`Exception`
|
||||||
|
|
||||||
|
:return: Wrap the return value obtained from :func:`default`
|
||||||
|
or registered handler for that type of exception.
|
||||||
"""
|
"""
|
||||||
handler = self.lookup(exception)
|
handler = self.lookup(exception)
|
||||||
response = None
|
response = None
|
||||||
@@ -109,6 +152,20 @@ class ErrorHandler:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
|
"""
|
||||||
|
Provide a default behavior for the objects of :class:`ErrorHandler`.
|
||||||
|
If a developer chooses to extent the :class:`ErrorHandler` they can
|
||||||
|
provide a custom implementation for this method to behave in a way
|
||||||
|
they see fit.
|
||||||
|
|
||||||
|
:param request: Incoming request
|
||||||
|
:param exception: Exception object
|
||||||
|
|
||||||
|
:type request: :class:`sanic.request.Request`
|
||||||
|
:type exception: :class:`sanic.exceptions.SanicException` or
|
||||||
|
:class:`Exception`
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
self.log(format_exc())
|
self.log(format_exc())
|
||||||
try:
|
try:
|
||||||
url = repr(request.url)
|
url = repr(request.url)
|
||||||
@@ -133,7 +190,23 @@ class ErrorHandler:
|
|||||||
|
|
||||||
|
|
||||||
class ContentRangeHandler:
|
class ContentRangeHandler:
|
||||||
"""Class responsible for parsing request header"""
|
"""
|
||||||
|
A mechanism to parse and process the incoming request headers to
|
||||||
|
extract the content range information.
|
||||||
|
|
||||||
|
:param request: Incoming api request
|
||||||
|
:param stats: Stats related to the content
|
||||||
|
|
||||||
|
:type request: :class:`sanic.request.Request`
|
||||||
|
:type stats: :class:`posix.stat_result`
|
||||||
|
|
||||||
|
:ivar start: Content Range start
|
||||||
|
:ivar end: Content Range end
|
||||||
|
:ivar size: Length of the content
|
||||||
|
:ivar total: Total size identified by the :class:`posix.stat_result`
|
||||||
|
instance
|
||||||
|
:ivar ContentRangeHandler.headers: Content range header ``dict``
|
||||||
|
"""
|
||||||
|
|
||||||
__slots__ = ("start", "end", "size", "total", "headers")
|
__slots__ = ("start", "end", "size", "total", "headers")
|
||||||
|
|
||||||
|
|||||||
182
sanic/headers.py
Normal file
182
sanic/headers.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
|
||||||
|
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
|
||||||
|
Options = Dict[str, Union[int, str]] # key=value fields in various headers
|
||||||
|
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
|
||||||
|
|
||||||
|
_token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
|
||||||
|
_param = re.compile(fr";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
|
||||||
|
_firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)')
|
||||||
|
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
|
||||||
|
_ipv6_re = re.compile(_ipv6)
|
||||||
|
_host_re = re.compile(
|
||||||
|
r"((?:\[" + _ipv6 + r"\])|[a-zA-Z0-9.\-]{1,253})(?::(\d{1,5}))?"
|
||||||
|
)
|
||||||
|
|
||||||
|
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
|
||||||
|
# curl all have different escaping, that we try to handle as well as possible,
|
||||||
|
# even though no client espaces in a way that would allow perfect handling.
|
||||||
|
|
||||||
|
# For more information, consult ../tests/test_requests.py
|
||||||
|
|
||||||
|
|
||||||
|
def parse_content_header(value: str) -> Tuple[str, Options]:
|
||||||
|
"""Parse content-type and content-disposition header values.
|
||||||
|
|
||||||
|
E.g. 'form-data; name=upload; filename=\"file.txt\"' to
|
||||||
|
('form-data', {'name': 'upload', 'filename': 'file.txt'})
|
||||||
|
|
||||||
|
Mostly identical to cgi.parse_header and werkzeug.parse_options_header
|
||||||
|
but runs faster and handles special characters better. Unescapes quotes.
|
||||||
|
"""
|
||||||
|
value = _firefox_quote_escape.sub("%22", value)
|
||||||
|
pos = value.find(";")
|
||||||
|
if pos == -1:
|
||||||
|
options: Dict[str, Union[int, str]] = {}
|
||||||
|
else:
|
||||||
|
options = {
|
||||||
|
m.group(1).lower(): m.group(2) or m.group(3).replace("%22", '"')
|
||||||
|
for m in _param.finditer(value[pos:])
|
||||||
|
}
|
||||||
|
value = value[:pos]
|
||||||
|
return value.strip().lower(), options
|
||||||
|
|
||||||
|
|
||||||
|
# https://tools.ietf.org/html/rfc7230#section-3.2.6 and
|
||||||
|
# https://tools.ietf.org/html/rfc7239#section-4
|
||||||
|
# This regex is for *reversed* strings because that works much faster for
|
||||||
|
# right-to-left matching than the other way around. Be wary that all things are
|
||||||
|
# a bit backwards! _rparam matches forwarded pairs alike ";key=value"
|
||||||
|
_rparam = re.compile(f"(?:{_token}|{_quoted})={_token}\\s*($|[;,])", re.ASCII)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_forwarded(headers, config) -> Optional[Options]:
|
||||||
|
"""Parse RFC 7239 Forwarded headers.
|
||||||
|
The value of `by` or `secret` must match `config.FORWARDED_SECRET`
|
||||||
|
:return: dict with keys and values, or None if nothing matched
|
||||||
|
"""
|
||||||
|
header = headers.getall("forwarded", None)
|
||||||
|
secret = config.FORWARDED_SECRET
|
||||||
|
if header is None or not secret:
|
||||||
|
return None
|
||||||
|
header = ",".join(header) # Join multiple header lines
|
||||||
|
if secret not in header:
|
||||||
|
return None
|
||||||
|
# Loop over <separator><key>=<value> elements from right to left
|
||||||
|
sep = pos = None
|
||||||
|
options: List[Tuple[str, str]] = []
|
||||||
|
found = False
|
||||||
|
for m in _rparam.finditer(header[::-1]):
|
||||||
|
# Start of new element? (on parser skips and non-semicolon right sep)
|
||||||
|
if m.start() != pos or sep != ";":
|
||||||
|
# Was the previous element (from right) what we wanted?
|
||||||
|
if found:
|
||||||
|
break
|
||||||
|
# Clear values and parse as new element
|
||||||
|
del options[:]
|
||||||
|
pos = m.end()
|
||||||
|
val_token, val_quoted, key, sep = m.groups()
|
||||||
|
key = key.lower()[::-1]
|
||||||
|
val = (val_token or val_quoted.replace('"\\', '"'))[::-1]
|
||||||
|
options.append((key, val))
|
||||||
|
if key in ("secret", "by") and val == secret:
|
||||||
|
found = True
|
||||||
|
# Check if we would return on next round, to avoid useless parse
|
||||||
|
if found and sep != ";":
|
||||||
|
break
|
||||||
|
# If secret was found, return the matching options in left-to-right order
|
||||||
|
return fwd_normalize(reversed(options)) if found else None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_xforwarded(headers, config) -> Optional[Options]:
|
||||||
|
"""Parse traditional proxy headers."""
|
||||||
|
real_ip_header = config.REAL_IP_HEADER
|
||||||
|
proxies_count = config.PROXIES_COUNT
|
||||||
|
addr = real_ip_header and headers.get(real_ip_header)
|
||||||
|
if not addr and proxies_count:
|
||||||
|
assert proxies_count > 0
|
||||||
|
try:
|
||||||
|
# Combine, split and filter multiple headers' entries
|
||||||
|
forwarded_for = headers.getall(config.FORWARDED_FOR_HEADER)
|
||||||
|
proxies = [
|
||||||
|
p
|
||||||
|
for p in (
|
||||||
|
p.strip() for h in forwarded_for for p in h.split(",")
|
||||||
|
)
|
||||||
|
if p
|
||||||
|
]
|
||||||
|
addr = proxies[-proxies_count]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
# No processing of other headers if no address is found
|
||||||
|
if not addr:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def options():
|
||||||
|
yield "for", addr
|
||||||
|
for key, header in (
|
||||||
|
("proto", "x-scheme"),
|
||||||
|
("proto", "x-forwarded-proto"), # Overrides X-Scheme if present
|
||||||
|
("host", "x-forwarded-host"),
|
||||||
|
("port", "x-forwarded-port"),
|
||||||
|
("path", "x-forwarded-path"),
|
||||||
|
):
|
||||||
|
yield key, headers.get(header)
|
||||||
|
|
||||||
|
return fwd_normalize(options())
|
||||||
|
|
||||||
|
|
||||||
|
def fwd_normalize(fwd: OptionsIterable) -> Options:
|
||||||
|
"""Normalize and convert values extracted from forwarded headers."""
|
||||||
|
ret: Dict[str, Union[int, str]] = {}
|
||||||
|
for key, val in fwd:
|
||||||
|
if val is not None:
|
||||||
|
try:
|
||||||
|
if key in ("by", "for"):
|
||||||
|
ret[key] = fwd_normalize_address(val)
|
||||||
|
elif key in ("host", "proto"):
|
||||||
|
ret[key] = val.lower()
|
||||||
|
elif key == "port":
|
||||||
|
ret[key] = int(val)
|
||||||
|
elif key == "path":
|
||||||
|
ret[key] = unquote(val)
|
||||||
|
else:
|
||||||
|
ret[key] = val
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def fwd_normalize_address(addr: str) -> str:
|
||||||
|
"""Normalize address fields of proxy headers."""
|
||||||
|
if addr == "unknown":
|
||||||
|
raise ValueError() # omit unknown value identifiers
|
||||||
|
if addr.startswith("_"):
|
||||||
|
return addr # do not lower-case obfuscated strings
|
||||||
|
if _ipv6_re.fullmatch(addr):
|
||||||
|
addr = f"[{addr}]" # bracket IPv6
|
||||||
|
return addr.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]:
|
||||||
|
"""Split host:port into hostname and port.
|
||||||
|
:return: None in place of missing elements
|
||||||
|
"""
|
||||||
|
m = _host_re.fullmatch(host)
|
||||||
|
if not m:
|
||||||
|
return None, None
|
||||||
|
host, port = m.groups()
|
||||||
|
return host.lower(), int(port) if port is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def format_http1(headers: HeaderIterable) -> bytes:
|
||||||
|
"""Convert a headers iterable into HTTP/1 header format.
|
||||||
|
|
||||||
|
- Outputs UTF-8 bytes where each header line ends with \\r\\n.
|
||||||
|
- Values are converted into strings if necessary.
|
||||||
|
"""
|
||||||
|
return "".join(f"{name}: {val}\r\n" for name, val in headers).encode()
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
"""Defines basics of HTTP standard."""
|
"""Defines basics of HTTP standard."""
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
from inspect import ismodule
|
||||||
|
|
||||||
|
|
||||||
STATUS_CODES = {
|
STATUS_CODES = {
|
||||||
100: b"Continue",
|
100: b"Continue",
|
||||||
101: b"Switching Protocols",
|
101: b"Switching Protocols",
|
||||||
102: b"Processing",
|
102: b"Processing",
|
||||||
|
103: b"Early Hints",
|
||||||
200: b"OK",
|
200: b"OK",
|
||||||
201: b"Created",
|
201: b"Created",
|
||||||
202: b"Accepted",
|
202: b"Accepted",
|
||||||
@@ -131,3 +136,21 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")):
|
|||||||
if not is_entity_header(header) or header.lower() in allowed
|
if not is_entity_header(header) or header.lower() in allowed
|
||||||
}
|
}
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def import_string(module_name, package=None):
|
||||||
|
"""
|
||||||
|
import a module or class by string path.
|
||||||
|
|
||||||
|
:module_name: str with path of module or path to import and
|
||||||
|
instanciate a class
|
||||||
|
:returns: a module object or one instance from class if
|
||||||
|
module_name is a valid path to class
|
||||||
|
|
||||||
|
"""
|
||||||
|
module, klass = module_name.rsplit(".", 1)
|
||||||
|
module = import_module(module, package=package)
|
||||||
|
obj = getattr(module, klass)
|
||||||
|
if ismodule(obj):
|
||||||
|
return obj
|
||||||
|
return obj()
|
||||||
|
|||||||
@@ -36,7 +36,15 @@ def _iter_module_files():
|
|||||||
def _get_args_for_reloading():
|
def _get_args_for_reloading():
|
||||||
"""Returns the executable."""
|
"""Returns the executable."""
|
||||||
rv = [sys.executable]
|
rv = [sys.executable]
|
||||||
rv.extend(sys.argv)
|
main_module = sys.modules["__main__"]
|
||||||
|
mod_spec = getattr(main_module, "__spec__", None)
|
||||||
|
if mod_spec:
|
||||||
|
# Parent exe was launched as a module rather than a script
|
||||||
|
rv.extend(["-m", mod_spec.name])
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
rv.extend(sys.argv[1:])
|
||||||
|
else:
|
||||||
|
rv.extend(sys.argv)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +52,7 @@ def restart_with_reloader():
|
|||||||
"""Create a new process and a subprocess in it with the same arguments as
|
"""Create a new process and a subprocess in it with the same arguments as
|
||||||
this one.
|
this one.
|
||||||
"""
|
"""
|
||||||
|
cwd = os.getcwd()
|
||||||
args = _get_args_for_reloading()
|
args = _get_args_for_reloading()
|
||||||
new_environ = os.environ.copy()
|
new_environ = os.environ.copy()
|
||||||
new_environ["SANIC_SERVER_RUNNING"] = "true"
|
new_environ["SANIC_SERVER_RUNNING"] = "true"
|
||||||
@@ -51,7 +60,7 @@ def restart_with_reloader():
|
|||||||
worker_process = Process(
|
worker_process = Process(
|
||||||
target=subprocess.call,
|
target=subprocess.call,
|
||||||
args=(cmd,),
|
args=(cmd,),
|
||||||
kwargs=dict(shell=True, env=new_environ),
|
kwargs={"cwd": cwd, "shell": True, "env": new_environ},
|
||||||
)
|
)
|
||||||
worker_process.start()
|
worker_process.start()
|
||||||
return worker_process
|
return worker_process
|
||||||
|
|||||||
398
sanic/request.py
398
sanic/request.py
@@ -1,32 +1,31 @@
|
|||||||
import json
|
import asyncio
|
||||||
import sys
|
import email.utils
|
||||||
|
import warnings
|
||||||
|
|
||||||
from cgi import parse_header
|
from collections import defaultdict, namedtuple
|
||||||
from collections import namedtuple
|
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
from urllib.parse import parse_qs, urlunparse
|
from types import SimpleNamespace
|
||||||
|
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
||||||
|
|
||||||
from httptools import parse_url
|
from httptools import parse_url # type: ignore
|
||||||
|
|
||||||
from sanic.exceptions import InvalidUsage
|
from sanic.exceptions import InvalidUsage
|
||||||
|
from sanic.headers import (
|
||||||
|
parse_content_header,
|
||||||
|
parse_forwarded,
|
||||||
|
parse_host,
|
||||||
|
parse_xforwarded,
|
||||||
|
)
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import error_logger, logger
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ujson import loads as json_loads
|
from ujson import loads as json_loads # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
if sys.version_info[:2] == (3, 5):
|
from json import loads as json_loads # type: ignore
|
||||||
|
|
||||||
def json_loads(data):
|
|
||||||
# on Python 3.5 json.loads only supports str not bytes
|
|
||||||
return json.loads(data.decode())
|
|
||||||
|
|
||||||
else:
|
|
||||||
json_loads = json.loads
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||||
|
EXPECT_HEADER = "EXPECT"
|
||||||
|
|
||||||
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
||||||
# > If the media type remains unknown, the recipient SHOULD treat it
|
# > If the media type remains unknown, the recipient SHOULD treat it
|
||||||
@@ -47,37 +46,62 @@ class RequestParameters(dict):
|
|||||||
return super().get(name, default)
|
return super().get(name, default)
|
||||||
|
|
||||||
|
|
||||||
class Request(dict):
|
class StreamBuffer:
|
||||||
|
def __init__(self, buffer_size=100):
|
||||||
|
self._queue = asyncio.Queue(buffer_size)
|
||||||
|
|
||||||
|
async def read(self):
|
||||||
|
""" Stop reading when gets None """
|
||||||
|
payload = await self._queue.get()
|
||||||
|
self._queue.task_done()
|
||||||
|
return payload
|
||||||
|
|
||||||
|
async def put(self, payload):
|
||||||
|
await self._queue.put(payload)
|
||||||
|
|
||||||
|
def is_full(self):
|
||||||
|
return self._queue.full()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def buffer_size(self):
|
||||||
|
return self._queue.maxsize
|
||||||
|
|
||||||
|
|
||||||
|
class Request:
|
||||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"app",
|
"__weakref__",
|
||||||
"headers",
|
|
||||||
"version",
|
|
||||||
"method",
|
|
||||||
"_cookies",
|
"_cookies",
|
||||||
"transport",
|
|
||||||
"body",
|
|
||||||
"parsed_json",
|
|
||||||
"parsed_args",
|
|
||||||
"parsed_form",
|
|
||||||
"parsed_files",
|
|
||||||
"_ip",
|
"_ip",
|
||||||
"_parsed_url",
|
"_parsed_url",
|
||||||
"uri_template",
|
"_port",
|
||||||
"stream",
|
|
||||||
"_remote_addr",
|
"_remote_addr",
|
||||||
"_socket",
|
"_socket",
|
||||||
"_port",
|
"app",
|
||||||
"__weakref__",
|
"body",
|
||||||
|
"ctx",
|
||||||
|
"endpoint",
|
||||||
|
"headers",
|
||||||
|
"method",
|
||||||
|
"parsed_args",
|
||||||
|
"parsed_not_grouped_args",
|
||||||
|
"parsed_files",
|
||||||
|
"parsed_form",
|
||||||
|
"parsed_json",
|
||||||
|
"parsed_forwarded",
|
||||||
"raw_url",
|
"raw_url",
|
||||||
|
"stream",
|
||||||
|
"transport",
|
||||||
|
"uri_template",
|
||||||
|
"version",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, url_bytes, headers, version, method, transport):
|
def __init__(self, url_bytes, headers, version, method, transport, app):
|
||||||
self.raw_url = url_bytes
|
self.raw_url = url_bytes
|
||||||
# TODO: Content-Encoding detection
|
# TODO: Content-Encoding detection
|
||||||
self._parsed_url = parse_url(url_bytes)
|
self._parsed_url = parse_url(url_bytes)
|
||||||
self.app = None
|
self.app = app
|
||||||
|
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
self.version = version
|
self.version = version
|
||||||
@@ -86,25 +110,47 @@ class Request(dict):
|
|||||||
|
|
||||||
# Init but do not inhale
|
# Init but do not inhale
|
||||||
self.body_init()
|
self.body_init()
|
||||||
|
self.ctx = SimpleNamespace()
|
||||||
|
self.parsed_forwarded = None
|
||||||
self.parsed_json = None
|
self.parsed_json = None
|
||||||
self.parsed_form = None
|
self.parsed_form = None
|
||||||
self.parsed_files = None
|
self.parsed_files = None
|
||||||
self.parsed_args = None
|
self.parsed_args = defaultdict(RequestParameters)
|
||||||
|
self.parsed_not_grouped_args = defaultdict(list)
|
||||||
self.uri_template = None
|
self.uri_template = None
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
self.stream = None
|
self.stream = None
|
||||||
|
self.endpoint = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.method is None or not self.path:
|
|
||||||
return "<{0}>".format(self.__class__.__name__)
|
|
||||||
return "<{0}: {1} {2}>".format(
|
return "<{0}: {1} {2}>".format(
|
||||||
self.__class__.__name__, self.method, self.path
|
self.__class__.__name__, self.method, self.path
|
||||||
)
|
)
|
||||||
|
|
||||||
def __bool__(self):
|
def get(self, key, default=None):
|
||||||
if self.transport:
|
""".. deprecated:: 19.9
|
||||||
return True
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
return False
|
return self.ctx.__dict__.get(key, default)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
""".. deprecated:: 19.9
|
||||||
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
|
return key in self.ctx.__dict__
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
""".. deprecated:: 19.9
|
||||||
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
|
return self.ctx.__dict__[key]
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
""".. deprecated:: 19.9
|
||||||
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
|
del self.ctx.__dict__[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
""".. deprecated:: 19.9
|
||||||
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
|
setattr(self.ctx, key, value)
|
||||||
|
|
||||||
def body_init(self):
|
def body_init(self):
|
||||||
self.body = []
|
self.body = []
|
||||||
@@ -156,7 +202,7 @@ class Request(dict):
|
|||||||
content_type = self.headers.get(
|
content_type = self.headers.get(
|
||||||
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
|
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
|
||||||
)
|
)
|
||||||
content_type, parameters = parse_header(content_type)
|
content_type, parameters = parse_content_header(content_type)
|
||||||
try:
|
try:
|
||||||
if content_type == "application/x-www-form-urlencoded":
|
if content_type == "application/x-www-form-urlencoded":
|
||||||
self.parsed_form = RequestParameters(
|
self.parsed_form = RequestParameters(
|
||||||
@@ -180,21 +226,127 @@ class Request(dict):
|
|||||||
|
|
||||||
return self.parsed_files
|
return self.parsed_files
|
||||||
|
|
||||||
@property
|
def get_args(
|
||||||
def args(self):
|
self,
|
||||||
if self.parsed_args is None:
|
keep_blank_values: bool = False,
|
||||||
|
strict_parsing: bool = False,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
errors: str = "replace",
|
||||||
|
) -> RequestParameters:
|
||||||
|
"""
|
||||||
|
Method to parse `query_string` using `urllib.parse.parse_qs`.
|
||||||
|
This methods is used by `args` property.
|
||||||
|
Can be used directly if you need to change default parameters.
|
||||||
|
|
||||||
|
:param keep_blank_values:
|
||||||
|
flag indicating whether blank values in
|
||||||
|
percent-encoded queries should be treated as blank strings.
|
||||||
|
A true value indicates that blanks should be retained as blank
|
||||||
|
strings. The default false value indicates that blank values
|
||||||
|
are to be ignored and treated as if they were not included.
|
||||||
|
:type keep_blank_values: bool
|
||||||
|
:param strict_parsing:
|
||||||
|
flag indicating what to do with parsing errors.
|
||||||
|
If false (the default), errors are silently ignored. If true,
|
||||||
|
errors raise a ValueError exception.
|
||||||
|
:type strict_parsing: bool
|
||||||
|
:param encoding:
|
||||||
|
specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type encoding: str
|
||||||
|
:param errors:
|
||||||
|
specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type errors: str
|
||||||
|
:return: RequestParameters
|
||||||
|
"""
|
||||||
|
if not self.parsed_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]:
|
||||||
if self.query_string:
|
if self.query_string:
|
||||||
self.parsed_args = RequestParameters(
|
self.parsed_args[
|
||||||
parse_qs(self.query_string)
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
] = RequestParameters(
|
||||||
|
parse_qs(
|
||||||
|
qs=self.query_string,
|
||||||
|
keep_blank_values=keep_blank_values,
|
||||||
|
strict_parsing=strict_parsing,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
self.parsed_args = RequestParameters()
|
return self.parsed_args[
|
||||||
return self.parsed_args
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]
|
||||||
|
|
||||||
|
args = property(get_args)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def raw_args(self):
|
def raw_args(self) -> dict:
|
||||||
|
if self.app.debug: # pragma: no cover
|
||||||
|
warnings.simplefilter("default")
|
||||||
|
warnings.warn(
|
||||||
|
"Use of raw_args will be deprecated in "
|
||||||
|
"the future versions. Please use args or query_args "
|
||||||
|
"properties instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
return {k: v[0] for k, v in self.args.items()}
|
return {k: v[0] for k, v in self.args.items()}
|
||||||
|
|
||||||
|
def get_query_args(
|
||||||
|
self,
|
||||||
|
keep_blank_values: bool = False,
|
||||||
|
strict_parsing: bool = False,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
errors: str = "replace",
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
Method to parse `query_string` using `urllib.parse.parse_qsl`.
|
||||||
|
This methods is used by `query_args` property.
|
||||||
|
Can be used directly if you need to change default parameters.
|
||||||
|
|
||||||
|
:param keep_blank_values:
|
||||||
|
flag indicating whether blank values in
|
||||||
|
percent-encoded queries should be treated as blank strings.
|
||||||
|
A true value indicates that blanks should be retained as blank
|
||||||
|
strings. The default false value indicates that blank values
|
||||||
|
are to be ignored and treated as if they were not included.
|
||||||
|
:type keep_blank_values: bool
|
||||||
|
:param strict_parsing:
|
||||||
|
flag indicating what to do with parsing errors.
|
||||||
|
If false (the default), errors are silently ignored. If true,
|
||||||
|
errors raise a ValueError exception.
|
||||||
|
:type strict_parsing: bool
|
||||||
|
:param encoding:
|
||||||
|
specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type encoding: str
|
||||||
|
:param errors:
|
||||||
|
specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type errors: str
|
||||||
|
:return: list
|
||||||
|
"""
|
||||||
|
if not self.parsed_not_grouped_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]:
|
||||||
|
if self.query_string:
|
||||||
|
self.parsed_not_grouped_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
] = parse_qsl(
|
||||||
|
qs=self.query_string,
|
||||||
|
keep_blank_values=keep_blank_values,
|
||||||
|
strict_parsing=strict_parsing,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
return self.parsed_not_grouped_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]
|
||||||
|
|
||||||
|
query_args = property(get_query_args)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookies(self):
|
def cookies(self):
|
||||||
if self._cookies is None:
|
if self._cookies is None:
|
||||||
@@ -211,12 +363,18 @@ class Request(dict):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ip(self):
|
def ip(self):
|
||||||
|
"""
|
||||||
|
:return: peer ip of the socket
|
||||||
|
"""
|
||||||
if not hasattr(self, "_socket"):
|
if not hasattr(self, "_socket"):
|
||||||
self._get_address()
|
self._get_address()
|
||||||
return self._ip
|
return self._ip
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self):
|
def port(self):
|
||||||
|
"""
|
||||||
|
:return: peer port of the socket
|
||||||
|
"""
|
||||||
if not hasattr(self, "_socket"):
|
if not hasattr(self, "_socket"):
|
||||||
self._get_address()
|
self._get_address()
|
||||||
return self._port
|
return self._port
|
||||||
@@ -235,27 +393,78 @@ class Request(dict):
|
|||||||
self._ip = self._socket[0]
|
self._ip = self._socket[0]
|
||||||
self._port = self._socket[1]
|
self._port = self._socket[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_name(self):
|
||||||
|
"""
|
||||||
|
Attempt to get the server's external hostname in this order:
|
||||||
|
`config.SERVER_NAME`, proxied or direct Host headers
|
||||||
|
:func:`Request.host`
|
||||||
|
|
||||||
|
:return: the server name without port number
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
server_name = self.app.config.get("SERVER_NAME")
|
||||||
|
if server_name:
|
||||||
|
host = server_name.split("//", 1)[-1].split("/", 1)[0]
|
||||||
|
return parse_host(host)[0]
|
||||||
|
return parse_host(self.host)[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forwarded(self):
|
||||||
|
if self.parsed_forwarded is None:
|
||||||
|
self.parsed_forwarded = (
|
||||||
|
parse_forwarded(self.headers, self.app.config)
|
||||||
|
or parse_xforwarded(self.headers, self.app.config)
|
||||||
|
or {}
|
||||||
|
)
|
||||||
|
return self.parsed_forwarded
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_port(self):
|
||||||
|
"""
|
||||||
|
Attempt to get the server's external port number in this order:
|
||||||
|
`config.SERVER_NAME`, proxied or direct Host headers
|
||||||
|
:func:`Request.host`,
|
||||||
|
actual port used by the transport layer socket.
|
||||||
|
:return: server port
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
if self.forwarded:
|
||||||
|
return self.forwarded.get("port") or (
|
||||||
|
80 if self.scheme in ("http", "ws") else 443
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
parse_host(self.host)[1]
|
||||||
|
or self.transport.get_extra_info("sockname")[1]
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def remote_addr(self):
|
def remote_addr(self):
|
||||||
"""Attempt to return the original client ip based on X-Forwarded-For.
|
"""Attempt to return the original client ip based on `forwarded`,
|
||||||
|
`x-forwarded-for` or `x-real-ip`. If HTTP headers are unavailable or
|
||||||
|
untrusted, returns an empty string.
|
||||||
|
|
||||||
:return: original client ip.
|
:return: original client ip.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, "_remote_addr"):
|
if not hasattr(self, "_remote_addr"):
|
||||||
forwarded_for = self.headers.get("X-Forwarded-For", "").split(",")
|
self._remote_addr = self.forwarded.get("for", "")
|
||||||
remote_addrs = [
|
|
||||||
addr
|
|
||||||
for addr in [addr.strip() for addr in forwarded_for]
|
|
||||||
if addr
|
|
||||||
]
|
|
||||||
if len(remote_addrs) > 0:
|
|
||||||
self._remote_addr = remote_addrs[0]
|
|
||||||
else:
|
|
||||||
self._remote_addr = ""
|
|
||||||
return self._remote_addr
|
return self._remote_addr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scheme(self):
|
def scheme(self):
|
||||||
|
"""
|
||||||
|
Attempt to get the request scheme.
|
||||||
|
Seeking the value in this order:
|
||||||
|
`forwarded` header, `x-forwarded-proto` header,
|
||||||
|
`x-scheme` header, the sanic app itself.
|
||||||
|
|
||||||
|
:return: http|https|ws|wss or arbitrary value given by the headers.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
forwarded_proto = self.forwarded.get("proto")
|
||||||
|
if forwarded_proto:
|
||||||
|
return forwarded_proto
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.app.websocket_enabled
|
self.app.websocket_enabled
|
||||||
and self.headers.get("upgrade") == "websocket"
|
and self.headers.get("upgrade") == "websocket"
|
||||||
@@ -271,9 +480,11 @@ class Request(dict):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def host(self):
|
def host(self):
|
||||||
# it appears that httptools doesn't return the host
|
"""
|
||||||
# so pull it from the headers
|
:return: proxied or direct Host header. Hostname and port number may be
|
||||||
return self.headers.get("Host", "")
|
separated by sanic.headers.parse_host(request.host).
|
||||||
|
"""
|
||||||
|
return self.forwarded.get("host", self.headers.get("Host", ""))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content_type(self):
|
def content_type(self):
|
||||||
@@ -301,6 +512,38 @@ class Request(dict):
|
|||||||
(self.scheme, self.host, self.path, None, self.query_string, None)
|
(self.scheme, self.host, self.path, None, self.query_string, None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def url_for(self, view_name, **kwargs):
|
||||||
|
"""
|
||||||
|
Same as :func:`sanic.Sanic.url_for`, but automatically determine
|
||||||
|
`scheme` and `netloc` base on the request. Since this method is aiming
|
||||||
|
to generate correct schema & netloc, `_external` is implied.
|
||||||
|
|
||||||
|
:param kwargs: takes same parameters as in :func:`sanic.Sanic.url_for`
|
||||||
|
:return: an absolute url to the given view
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
# Full URL SERVER_NAME can only be handled in app.url_for
|
||||||
|
try:
|
||||||
|
if "//" in self.app.config.SERVER_NAME:
|
||||||
|
return self.app.url_for(view_name, _external=True, **kwargs)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
scheme = self.scheme
|
||||||
|
host = self.server_name
|
||||||
|
port = self.server_port
|
||||||
|
|
||||||
|
if (scheme.lower() in ("http", "ws") and port == 80) or (
|
||||||
|
scheme.lower() in ("https", "wss") and port == 443
|
||||||
|
):
|
||||||
|
netloc = host
|
||||||
|
else:
|
||||||
|
netloc = "{}:{}".format(host, port)
|
||||||
|
|
||||||
|
return self.app.url_for(
|
||||||
|
view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
File = namedtuple("File", ["type", "body", "name"])
|
File = namedtuple("File", ["type", "body", "name"])
|
||||||
|
|
||||||
@@ -333,20 +576,33 @@ def parse_multipart_form(body, boundary):
|
|||||||
|
|
||||||
colon_index = form_line.index(":")
|
colon_index = form_line.index(":")
|
||||||
form_header_field = form_line[0:colon_index].lower()
|
form_header_field = form_line[0:colon_index].lower()
|
||||||
form_header_value, form_parameters = parse_header(
|
form_header_value, form_parameters = parse_content_header(
|
||||||
form_line[colon_index + 2 :]
|
form_line[colon_index + 2 :]
|
||||||
)
|
)
|
||||||
|
|
||||||
if form_header_field == "content-disposition":
|
if form_header_field == "content-disposition":
|
||||||
file_name = form_parameters.get("filename")
|
|
||||||
field_name = form_parameters.get("name")
|
field_name = form_parameters.get("name")
|
||||||
|
file_name = form_parameters.get("filename")
|
||||||
|
|
||||||
|
# non-ASCII filenames in RFC2231, "filename*" format
|
||||||
|
if file_name is None and form_parameters.get("filename*"):
|
||||||
|
encoding, _, value = email.utils.decode_rfc2231(
|
||||||
|
form_parameters["filename*"]
|
||||||
|
)
|
||||||
|
file_name = unquote(value, encoding=encoding)
|
||||||
elif form_header_field == "content-type":
|
elif form_header_field == "content-type":
|
||||||
content_type = form_header_value
|
content_type = form_header_value
|
||||||
content_charset = form_parameters.get("charset", "utf-8")
|
content_charset = form_parameters.get("charset", "utf-8")
|
||||||
|
|
||||||
if field_name:
|
if field_name:
|
||||||
post_data = form_part[line_index:-4]
|
post_data = form_part[line_index:-4]
|
||||||
if file_name:
|
if file_name is None:
|
||||||
|
value = post_data.decode(content_charset)
|
||||||
|
if field_name in fields:
|
||||||
|
fields[field_name].append(value)
|
||||||
|
else:
|
||||||
|
fields[field_name] = [value]
|
||||||
|
else:
|
||||||
form_file = File(
|
form_file = File(
|
||||||
type=content_type, name=file_name, body=post_data
|
type=content_type, name=file_name, body=post_data
|
||||||
)
|
)
|
||||||
@@ -354,12 +610,6 @@ def parse_multipart_form(body, boundary):
|
|||||||
files[field_name].append(form_file)
|
files[field_name].append(form_file)
|
||||||
else:
|
else:
|
||||||
files[field_name] = [form_file]
|
files[field_name] = [form_file]
|
||||||
else:
|
|
||||||
value = post_data.decode(content_charset)
|
|
||||||
if field_name in fields:
|
|
||||||
fields[field_name].append(value)
|
|
||||||
else:
|
|
||||||
fields[field_name] = [value]
|
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Form-data field does not have a 'name' parameter "
|
"Form-data field does not have a 'name' parameter "
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ from mimetypes import guess_type
|
|||||||
from os import path
|
from os import path
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from aiofiles import open as open_async
|
from aiofiles import open as open_async # type: ignore
|
||||||
from multidict import CIMultiDict
|
|
||||||
|
|
||||||
|
from sanic.compat import Header
|
||||||
from sanic.cookies import CookieJar
|
from sanic.cookies import CookieJar
|
||||||
|
from sanic.headers import format_http1
|
||||||
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
|
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ujson import dumps as json_dumps
|
from ujson import dumps as json_dumps
|
||||||
except BaseException:
|
except ImportError:
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
# This is done in order to ensure that the JSON response is
|
# This is done in order to ensure that the JSON response is
|
||||||
@@ -21,6 +22,9 @@ except BaseException:
|
|||||||
|
|
||||||
|
|
||||||
class BaseHTTPResponse:
|
class BaseHTTPResponse:
|
||||||
|
def __init__(self):
|
||||||
|
self.asgi = False
|
||||||
|
|
||||||
def _encode_body(self, data):
|
def _encode_body(self, data):
|
||||||
try:
|
try:
|
||||||
# Try to encode it regularly
|
# Try to encode it regularly
|
||||||
@@ -30,20 +34,7 @@ class BaseHTTPResponse:
|
|||||||
return str(data).encode()
|
return str(data).encode()
|
||||||
|
|
||||||
def _parse_headers(self):
|
def _parse_headers(self):
|
||||||
headers = b""
|
return format_http1(self.headers.items())
|
||||||
for name, value in self.headers.items():
|
|
||||||
try:
|
|
||||||
headers += b"%b: %b\r\n" % (
|
|
||||||
name.encode(),
|
|
||||||
value.encode("utf-8"),
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
headers += b"%b: %b\r\n" % (
|
|
||||||
str(name).encode(),
|
|
||||||
str(value).encode("utf-8"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return headers
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookies(self):
|
def cookies(self):
|
||||||
@@ -59,17 +50,27 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
"status",
|
"status",
|
||||||
"content_type",
|
"content_type",
|
||||||
"headers",
|
"headers",
|
||||||
|
"chunked",
|
||||||
"_cookies",
|
"_cookies",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, streaming_fn, status=200, headers=None, content_type="text/plain"
|
self,
|
||||||
|
streaming_fn,
|
||||||
|
status=200,
|
||||||
|
headers=None,
|
||||||
|
content_type="text/plain",
|
||||||
|
chunked=True,
|
||||||
):
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
self.streaming_fn = streaming_fn
|
self.streaming_fn = streaming_fn
|
||||||
self.status = status
|
self.status = status
|
||||||
self.headers = CIMultiDict(headers or {})
|
self.headers = Header(headers or {})
|
||||||
|
self.chunked = chunked
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
|
self.protocol = None
|
||||||
|
|
||||||
async def write(self, data):
|
async def write(self, data):
|
||||||
"""Writes a chunk of data to the streaming response.
|
"""Writes a chunk of data to the streaming response.
|
||||||
@@ -79,7 +80,12 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
if type(data) != bytes:
|
if type(data) != bytes:
|
||||||
data = self._encode_body(data)
|
data = self._encode_body(data)
|
||||||
|
|
||||||
self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
# `chunked` will always be False in ASGI-mode, even if the underlying
|
||||||
|
# ASGI Transport implements Chunked transport. That does it itself.
|
||||||
|
if self.chunked:
|
||||||
|
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
||||||
|
else:
|
||||||
|
await self.protocol.push_data(data)
|
||||||
await self.protocol.drain()
|
await self.protocol.drain()
|
||||||
|
|
||||||
async def stream(
|
async def stream(
|
||||||
@@ -88,15 +94,19 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
"""Streams headers, runs the `streaming_fn` callback that writes
|
"""Streams headers, runs the `streaming_fn` callback that writes
|
||||||
content to the response body, then finalizes the response body.
|
content to the response body, then finalizes the response body.
|
||||||
"""
|
"""
|
||||||
|
if version != "1.1":
|
||||||
|
self.chunked = False
|
||||||
headers = self.get_headers(
|
headers = self.get_headers(
|
||||||
version,
|
version,
|
||||||
keep_alive=keep_alive,
|
keep_alive=keep_alive,
|
||||||
keep_alive_timeout=keep_alive_timeout,
|
keep_alive_timeout=keep_alive_timeout,
|
||||||
)
|
)
|
||||||
self.protocol.push_data(headers)
|
if not getattr(self, "asgi", False):
|
||||||
await self.protocol.drain()
|
await self.protocol.push_data(headers)
|
||||||
|
await self.protocol.drain()
|
||||||
await self.streaming_fn(self)
|
await self.streaming_fn(self)
|
||||||
self.protocol.push_data(b"0\r\n\r\n")
|
if self.chunked:
|
||||||
|
await self.protocol.push_data(b"0\r\n\r\n")
|
||||||
# no need to await drain here after this write, because it is the
|
# no need to await drain here after this write, because it is the
|
||||||
# very last thing we write and nothing needs to wait for it.
|
# very last thing we write and nothing needs to wait for it.
|
||||||
|
|
||||||
@@ -109,15 +119,16 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||||||
if keep_alive and keep_alive_timeout is not None:
|
if keep_alive and keep_alive_timeout is not None:
|
||||||
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
|
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
|
||||||
|
|
||||||
self.headers["Transfer-Encoding"] = "chunked"
|
if self.chunked and version == "1.1":
|
||||||
self.headers.pop("Content-Length", None)
|
self.headers["Transfer-Encoding"] = "chunked"
|
||||||
|
self.headers.pop("Content-Length", None)
|
||||||
self.headers["Content-Type"] = self.headers.get(
|
self.headers["Content-Type"] = self.headers.get(
|
||||||
"Content-Type", self.content_type
|
"Content-Type", self.content_type
|
||||||
)
|
)
|
||||||
|
|
||||||
headers = self._parse_headers()
|
headers = self._parse_headers()
|
||||||
|
|
||||||
if self.status is 200:
|
if self.status == 200:
|
||||||
status = b"OK"
|
status = b"OK"
|
||||||
else:
|
else:
|
||||||
status = STATUS_CODES.get(self.status)
|
status = STATUS_CODES.get(self.status)
|
||||||
@@ -139,9 +150,11 @@ class HTTPResponse(BaseHTTPResponse):
|
|||||||
body=None,
|
body=None,
|
||||||
status=200,
|
status=200,
|
||||||
headers=None,
|
headers=None,
|
||||||
content_type="text/plain",
|
content_type=None,
|
||||||
body_bytes=b"",
|
body_bytes=b"",
|
||||||
):
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
|
|
||||||
if body is not None:
|
if body is not None:
|
||||||
@@ -150,7 +163,7 @@ class HTTPResponse(BaseHTTPResponse):
|
|||||||
self.body = body_bytes
|
self.body = body_bytes
|
||||||
|
|
||||||
self.status = status
|
self.status = status
|
||||||
self.headers = CIMultiDict(headers or {})
|
self.headers = Header(headers or {})
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
|
|
||||||
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||||
@@ -167,16 +180,16 @@ class HTTPResponse(BaseHTTPResponse):
|
|||||||
"Content-Length", len(self.body)
|
"Content-Length", len(self.body)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.headers["Content-Type"] = self.headers.get(
|
# self.headers get priority over content_type
|
||||||
"Content-Type", self.content_type
|
if self.content_type and "Content-Type" not in self.headers:
|
||||||
)
|
self.headers["Content-Type"] = self.content_type
|
||||||
|
|
||||||
if self.status in (304, 412):
|
if self.status in (304, 412):
|
||||||
self.headers = remove_entity_headers(self.headers)
|
self.headers = remove_entity_headers(self.headers)
|
||||||
|
|
||||||
headers = self._parse_headers()
|
headers = self._parse_headers()
|
||||||
|
|
||||||
if self.status is 200:
|
if self.status == 200:
|
||||||
status = b"OK"
|
status = b"OK"
|
||||||
else:
|
else:
|
||||||
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
|
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
|
||||||
@@ -200,6 +213,16 @@ class HTTPResponse(BaseHTTPResponse):
|
|||||||
return self._cookies
|
return self._cookies
|
||||||
|
|
||||||
|
|
||||||
|
def empty(status=204, headers=None):
|
||||||
|
"""
|
||||||
|
Returns an empty response to the client.
|
||||||
|
|
||||||
|
:param status Response code.
|
||||||
|
:param headers Custom Headers.
|
||||||
|
"""
|
||||||
|
return HTTPResponse(body_bytes=b"", status=status, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
def json(
|
def json(
|
||||||
body,
|
body,
|
||||||
status=200,
|
status=200,
|
||||||
@@ -327,6 +350,7 @@ async def file_stream(
|
|||||||
mime_type=None,
|
mime_type=None,
|
||||||
headers=None,
|
headers=None,
|
||||||
filename=None,
|
filename=None,
|
||||||
|
chunked=True,
|
||||||
_range=None,
|
_range=None,
|
||||||
):
|
):
|
||||||
"""Return a streaming response object with file data.
|
"""Return a streaming response object with file data.
|
||||||
@@ -336,6 +360,7 @@ async def file_stream(
|
|||||||
:param mime_type: Specific mime_type.
|
:param mime_type: Specific mime_type.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param filename: Override filename.
|
:param filename: Override filename.
|
||||||
|
:param chunked: Enable or disable chunked transfer-encoding
|
||||||
:param _range:
|
:param _range:
|
||||||
"""
|
"""
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
@@ -383,6 +408,7 @@ async def file_stream(
|
|||||||
status=status,
|
status=status,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=mime_type,
|
content_type=mime_type,
|
||||||
|
chunked=chunked,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -391,6 +417,7 @@ def stream(
|
|||||||
status=200,
|
status=200,
|
||||||
headers=None,
|
headers=None,
|
||||||
content_type="text/plain; charset=utf-8",
|
content_type="text/plain; charset=utf-8",
|
||||||
|
chunked=True,
|
||||||
):
|
):
|
||||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||||
@@ -409,9 +436,14 @@ def stream(
|
|||||||
writes content to that response.
|
writes content to that response.
|
||||||
:param mime_type: Specific mime_type.
|
:param mime_type: Specific mime_type.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
|
:param chunked: Enable or disable chunked transfer-encoding
|
||||||
"""
|
"""
|
||||||
return StreamingHTTPResponse(
|
return StreamingHTTPResponse(
|
||||||
streaming_fn, headers=headers, content_type=content_type, status=status
|
streaming_fn,
|
||||||
|
headers=headers,
|
||||||
|
content_type=content_type,
|
||||||
|
status=status,
|
||||||
|
chunked=chunked,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ Parameter = namedtuple("Parameter", ["name", "cast"])
|
|||||||
|
|
||||||
REGEX_TYPES = {
|
REGEX_TYPES = {
|
||||||
"string": (str, r"[^/]+"),
|
"string": (str, r"[^/]+"),
|
||||||
"int": (int, r"\d+"),
|
"int": (int, r"-?\d+"),
|
||||||
"number": (float, r"[0-9\\.]+"),
|
"number": (float, r"-?(?:\d+(?:\.\d*)?|\.\d+)"),
|
||||||
"alpha": (str, r"[A-Za-z]+"),
|
"alpha": (str, r"[A-Za-z]+"),
|
||||||
"path": (str, r"[^/].*?"),
|
"path": (str, r"[^/].*?"),
|
||||||
"uuid": (
|
"uuid": (
|
||||||
@@ -140,21 +140,22 @@ class Router:
|
|||||||
docs for further details.
|
docs for further details.
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
routes = []
|
||||||
if version is not None:
|
if version is not None:
|
||||||
version = re.escape(str(version).strip("/").lstrip("v"))
|
version = re.escape(str(version).strip("/").lstrip("v"))
|
||||||
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
|
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
|
||||||
# add regular version
|
# add regular version
|
||||||
self._add(uri, methods, handler, host, name)
|
routes.append(self._add(uri, methods, handler, host, name))
|
||||||
|
|
||||||
if strict_slashes:
|
if strict_slashes:
|
||||||
return
|
return routes
|
||||||
|
|
||||||
if not isinstance(host, str) and host is not None:
|
if not isinstance(host, str) and host is not None:
|
||||||
# we have gotten back to the top of the recursion tree where the
|
# we have gotten back to the top of the recursion tree where the
|
||||||
# host was originally a list. By now, we've processed the strict
|
# host was originally a list. By now, we've processed the strict
|
||||||
# slashes logic on the leaf nodes (the individual host strings in
|
# slashes logic on the leaf nodes (the individual host strings in
|
||||||
# the list of host)
|
# the list of host)
|
||||||
return
|
return routes
|
||||||
|
|
||||||
# Add versions with and without trailing /
|
# Add versions with and without trailing /
|
||||||
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
|
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
|
||||||
@@ -176,10 +177,12 @@ class Router:
|
|||||||
)
|
)
|
||||||
# add version with trailing slash
|
# add version with trailing slash
|
||||||
if slash_is_missing:
|
if slash_is_missing:
|
||||||
self._add(uri + "/", methods, handler, host, name)
|
routes.append(self._add(uri + "/", methods, handler, host, name))
|
||||||
# add version without trailing slash
|
# add version without trailing slash
|
||||||
elif without_slash_is_missing:
|
elif without_slash_is_missing:
|
||||||
self._add(uri[:-1], methods, handler, host, name)
|
routes.append(self._add(uri[:-1], methods, handler, host, name))
|
||||||
|
|
||||||
|
return routes
|
||||||
|
|
||||||
def _add(self, uri, methods, handler, host=None, name=None):
|
def _add(self, uri, methods, handler, host=None, name=None):
|
||||||
"""Add a handler to the route list
|
"""Add a handler to the route list
|
||||||
@@ -328,9 +331,21 @@ class Router:
|
|||||||
self.routes_dynamic[url_hash(uri)].append(route)
|
self.routes_dynamic[url_hash(uri)].append(route)
|
||||||
else:
|
else:
|
||||||
self.routes_static[uri] = route
|
self.routes_static[uri] = route
|
||||||
|
return route
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
|
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
|
||||||
|
"""
|
||||||
|
Check if a URL pattern exists in a list of routes provided based on
|
||||||
|
the comparison of URL pattern and the parameters.
|
||||||
|
|
||||||
|
:param pattern: URL parameter pattern
|
||||||
|
:param routes_to_check: list of dynamic routes either hashable or
|
||||||
|
unhashable routes.
|
||||||
|
:param parameters: List of :class:`Parameter` items
|
||||||
|
:return: Tuple of index and route if matching route exists else
|
||||||
|
-1 for index and None for route
|
||||||
|
"""
|
||||||
for ndx, route in enumerate(routes_to_check):
|
for ndx, route in enumerate(routes_to_check):
|
||||||
if route.pattern == pattern and route.parameters == parameters:
|
if route.pattern == pattern and route.parameters == parameters:
|
||||||
return ndx, route
|
return ndx, route
|
||||||
@@ -395,6 +410,7 @@ class Router:
|
|||||||
if not self.hosts:
|
if not self.hosts:
|
||||||
return self._get(request.path, request.method, "")
|
return self._get(request.path, request.method, "")
|
||||||
# virtual hosts specified; try to match route to the host header
|
# virtual hosts specified; try to match route to the host header
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self._get(
|
return self._get(
|
||||||
request.path, request.method, request.headers.get("Host", "")
|
request.path, request.method, request.headers.get("Host", "")
|
||||||
@@ -430,6 +446,7 @@ class Router:
|
|||||||
method=method,
|
method=method,
|
||||||
allowed_methods=self.get_supported_methods(url),
|
allowed_methods=self.get_supported_methods(url),
|
||||||
)
|
)
|
||||||
|
|
||||||
if route:
|
if route:
|
||||||
if route.methods and method not in route.methods:
|
if route.methods and method not in route.methods:
|
||||||
raise method_not_supported
|
raise method_not_supported
|
||||||
@@ -464,10 +481,10 @@ class Router:
|
|||||||
route_handler = route.handler
|
route_handler = route.handler
|
||||||
if hasattr(route_handler, "handlers"):
|
if hasattr(route_handler, "handlers"):
|
||||||
route_handler = route_handler.handlers[method]
|
route_handler = route_handler.handlers[method]
|
||||||
return route_handler, [], kwargs, route.uri
|
return route_handler, [], kwargs, route.uri, route.name
|
||||||
|
|
||||||
def is_stream_handler(self, request):
|
def is_stream_handler(self, request):
|
||||||
""" Handler for request is stream or not.
|
"""Handler for request is stream or not.
|
||||||
:param request: Request object
|
:param request: Request object
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
|
|||||||
323
sanic/server.py
323
sanic/server.py
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
@@ -10,11 +11,12 @@ from signal import signal as signal_func
|
|||||||
from socket import SO_REUSEADDR, SOL_SOCKET, socket
|
from socket import SO_REUSEADDR, SOL_SOCKET, socket
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from httptools import HttpRequestParser
|
from httptools import HttpRequestParser # type: ignore
|
||||||
from httptools.parser.errors import HttpParserError
|
from httptools.parser.errors import HttpParserError # type: ignore
|
||||||
from multidict import CIMultiDict
|
|
||||||
|
|
||||||
|
from sanic.compat import Header
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
|
HeaderExpectationFailed,
|
||||||
InvalidUsage,
|
InvalidUsage,
|
||||||
PayloadTooLarge,
|
PayloadTooLarge,
|
||||||
RequestTimeout,
|
RequestTimeout,
|
||||||
@@ -22,27 +24,31 @@ from sanic.exceptions import (
|
|||||||
ServiceUnavailable,
|
ServiceUnavailable,
|
||||||
)
|
)
|
||||||
from sanic.log import access_logger, logger
|
from sanic.log import access_logger, logger
|
||||||
from sanic.request import Request
|
from sanic.request import EXPECT_HEADER, Request, StreamBuffer
|
||||||
from sanic.response import HTTPResponse
|
from sanic.response import HTTPResponse
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uvloop
|
import uvloop # type: ignore
|
||||||
|
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
current_time = None
|
|
||||||
|
|
||||||
|
|
||||||
class Signal:
|
class Signal:
|
||||||
stopped = False
|
stopped = False
|
||||||
|
|
||||||
|
|
||||||
class HttpProtocol(asyncio.Protocol):
|
class HttpProtocol(asyncio.Protocol):
|
||||||
|
"""
|
||||||
|
This class provides a basic HTTP implementation of the sanic framework.
|
||||||
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
|
# app
|
||||||
|
"app",
|
||||||
# event loop, connection
|
# event loop, connection
|
||||||
"loop",
|
"loop",
|
||||||
"transport",
|
"transport",
|
||||||
@@ -59,6 +65,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
"response_timeout",
|
"response_timeout",
|
||||||
"keep_alive_timeout",
|
"keep_alive_timeout",
|
||||||
"request_max_size",
|
"request_max_size",
|
||||||
|
"request_buffer_queue_size",
|
||||||
"request_class",
|
"request_class",
|
||||||
"is_request_stream",
|
"is_request_stream",
|
||||||
"router",
|
"router",
|
||||||
@@ -80,20 +87,23 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
"_header_fragment",
|
"_header_fragment",
|
||||||
"state",
|
"state",
|
||||||
"_debug",
|
"_debug",
|
||||||
|
"_body_chunks",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
loop,
|
loop,
|
||||||
|
app,
|
||||||
request_handler,
|
request_handler,
|
||||||
error_handler,
|
error_handler,
|
||||||
signal=Signal(),
|
signal=Signal(),
|
||||||
connections=set(),
|
connections=None,
|
||||||
request_timeout=60,
|
request_timeout=60,
|
||||||
response_timeout=60,
|
response_timeout=60,
|
||||||
keep_alive_timeout=5,
|
keep_alive_timeout=5,
|
||||||
request_max_size=None,
|
request_max_size=None,
|
||||||
|
request_buffer_queue_size=100,
|
||||||
request_class=None,
|
request_class=None,
|
||||||
access_log=True,
|
access_log=True,
|
||||||
keep_alive=True,
|
keep_alive=True,
|
||||||
@@ -104,6 +114,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
|
self.app = app
|
||||||
self.transport = None
|
self.transport = None
|
||||||
self.request = None
|
self.request = None
|
||||||
self.parser = None
|
self.parser = None
|
||||||
@@ -112,10 +123,11 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self.router = router
|
self.router = router
|
||||||
self.signal = signal
|
self.signal = signal
|
||||||
self.access_log = access_log
|
self.access_log = access_log
|
||||||
self.connections = connections
|
self.connections = connections if connections is not None else set()
|
||||||
self.request_handler = request_handler
|
self.request_handler = request_handler
|
||||||
self.error_handler = error_handler
|
self.error_handler = error_handler
|
||||||
self.request_timeout = request_timeout
|
self.request_timeout = request_timeout
|
||||||
|
self.request_buffer_queue_size = request_buffer_queue_size
|
||||||
self.response_timeout = response_timeout
|
self.response_timeout = response_timeout
|
||||||
self.keep_alive_timeout = keep_alive_timeout
|
self.keep_alive_timeout = keep_alive_timeout
|
||||||
self.request_max_size = request_max_size
|
self.request_max_size = request_max_size
|
||||||
@@ -138,9 +150,17 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self.state["requests_count"] = 0
|
self.state["requests_count"] = 0
|
||||||
self._debug = debug
|
self._debug = debug
|
||||||
self._not_paused.set()
|
self._not_paused.set()
|
||||||
|
self._body_chunks = deque()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keep_alive(self):
|
def keep_alive(self):
|
||||||
|
"""
|
||||||
|
Check if the connection needs to be kept alive based on the params
|
||||||
|
attached to the `_keep_alive` attribute, :attr:`Signal.stopped`
|
||||||
|
and :func:`HttpProtocol.parser.should_keep_alive`
|
||||||
|
|
||||||
|
:return: ``True`` if connection is to be kept alive ``False`` else
|
||||||
|
"""
|
||||||
return (
|
return (
|
||||||
self._keep_alive
|
self._keep_alive
|
||||||
and not self.signal.stopped
|
and not self.signal.stopped
|
||||||
@@ -157,7 +177,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self.request_timeout, self.request_timeout_callback
|
self.request_timeout, self.request_timeout_callback
|
||||||
)
|
)
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
self._last_request_time = current_time
|
self._last_request_time = time()
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
self.connections.discard(self)
|
self.connections.discard(self)
|
||||||
@@ -183,7 +203,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
# exactly what this timeout is checking for.
|
# exactly what this timeout is checking for.
|
||||||
# Check if elapsed time since request initiated exceeds our
|
# Check if elapsed time since request initiated exceeds our
|
||||||
# configured maximum request timeout value
|
# configured maximum request timeout value
|
||||||
time_elapsed = current_time - self._last_request_time
|
time_elapsed = time() - self._last_request_time
|
||||||
if time_elapsed < self.request_timeout:
|
if time_elapsed < self.request_timeout:
|
||||||
time_left = self.request_timeout - time_elapsed
|
time_left = self.request_timeout - time_elapsed
|
||||||
self._request_timeout_handler = self.loop.call_later(
|
self._request_timeout_handler = self.loop.call_later(
|
||||||
@@ -199,7 +219,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
def response_timeout_callback(self):
|
def response_timeout_callback(self):
|
||||||
# Check if elapsed time since response was initiated exceeds our
|
# Check if elapsed time since response was initiated exceeds our
|
||||||
# configured maximum request timeout value
|
# configured maximum request timeout value
|
||||||
time_elapsed = current_time - self._last_request_time
|
time_elapsed = time() - self._last_request_time
|
||||||
if time_elapsed < self.response_timeout:
|
if time_elapsed < self.response_timeout:
|
||||||
time_left = self.response_timeout - time_elapsed
|
time_left = self.response_timeout - time_elapsed
|
||||||
self._response_timeout_handler = self.loop.call_later(
|
self._response_timeout_handler = self.loop.call_later(
|
||||||
@@ -213,9 +233,14 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self.write_error(ServiceUnavailable("Response Timeout"))
|
self.write_error(ServiceUnavailable("Response Timeout"))
|
||||||
|
|
||||||
def keep_alive_timeout_callback(self):
|
def keep_alive_timeout_callback(self):
|
||||||
# Check if elapsed time since last response exceeds our configured
|
"""
|
||||||
# maximum keep alive timeout value
|
Check if elapsed time since last response exceeds our configured
|
||||||
time_elapsed = current_time - self._last_response_time
|
maximum keep alive timeout value and if so, close the transport
|
||||||
|
pipe and let the response writer handle the error.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
time_elapsed = time() - self._last_response_time
|
||||||
if time_elapsed < self.keep_alive_timeout:
|
if time_elapsed < self.keep_alive_timeout:
|
||||||
time_left = self.keep_alive_timeout - time_elapsed
|
time_left = self.keep_alive_timeout - time_elapsed
|
||||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||||
@@ -283,31 +308,87 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
def on_headers_complete(self):
|
def on_headers_complete(self):
|
||||||
self.request = self.request_class(
|
self.request = self.request_class(
|
||||||
url_bytes=self.url,
|
url_bytes=self.url,
|
||||||
headers=CIMultiDict(self.headers),
|
headers=Header(self.headers),
|
||||||
version=self.parser.get_http_version(),
|
version=self.parser.get_http_version(),
|
||||||
method=self.parser.get_method().decode(),
|
method=self.parser.get_method().decode(),
|
||||||
transport=self.transport,
|
transport=self.transport,
|
||||||
|
app=self.app,
|
||||||
)
|
)
|
||||||
# Remove any existing KeepAlive handler here,
|
# Remove any existing KeepAlive handler here,
|
||||||
# It will be recreated if required on the new request.
|
# It will be recreated if required on the new request.
|
||||||
if self._keep_alive_timeout_handler:
|
if self._keep_alive_timeout_handler:
|
||||||
self._keep_alive_timeout_handler.cancel()
|
self._keep_alive_timeout_handler.cancel()
|
||||||
self._keep_alive_timeout_handler = None
|
self._keep_alive_timeout_handler = None
|
||||||
|
|
||||||
|
if self.request.headers.get(EXPECT_HEADER):
|
||||||
|
self.expect_handler()
|
||||||
|
|
||||||
if self.is_request_stream:
|
if self.is_request_stream:
|
||||||
self._is_stream_handler = self.router.is_stream_handler(
|
self._is_stream_handler = self.router.is_stream_handler(
|
||||||
self.request
|
self.request
|
||||||
)
|
)
|
||||||
if self._is_stream_handler:
|
if self._is_stream_handler:
|
||||||
self.request.stream = asyncio.Queue()
|
self.request.stream = StreamBuffer(
|
||||||
|
self.request_buffer_queue_size
|
||||||
|
)
|
||||||
self.execute_request_handler()
|
self.execute_request_handler()
|
||||||
|
|
||||||
|
def expect_handler(self):
|
||||||
|
"""
|
||||||
|
Handler for Expect Header.
|
||||||
|
"""
|
||||||
|
expect = self.request.headers.get(EXPECT_HEADER)
|
||||||
|
if self.request.version == "1.1":
|
||||||
|
if expect.lower() == "100-continue":
|
||||||
|
self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
|
||||||
|
else:
|
||||||
|
self.write_error(
|
||||||
|
HeaderExpectationFailed(
|
||||||
|
"Unknown Expect: {expect}".format(expect=expect)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def on_body(self, body):
|
def on_body(self, body):
|
||||||
if self.is_request_stream and self._is_stream_handler:
|
if self.is_request_stream and self._is_stream_handler:
|
||||||
self._request_stream_task = self.loop.create_task(
|
# body chunks can be put into asyncio.Queue out of order if
|
||||||
self.request.stream.put(body)
|
# multiple tasks put concurrently and the queue is full in python
|
||||||
)
|
# 3.7. so we should not create more than one task putting into the
|
||||||
|
# queue simultaneously.
|
||||||
|
self._body_chunks.append(body)
|
||||||
|
if (
|
||||||
|
not self._request_stream_task
|
||||||
|
or self._request_stream_task.done()
|
||||||
|
):
|
||||||
|
self._request_stream_task = self.loop.create_task(
|
||||||
|
self.stream_append()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.request.body_push(body)
|
||||||
|
|
||||||
|
async def body_append(self, body):
|
||||||
|
if (
|
||||||
|
self.request is None
|
||||||
|
or self._request_stream_task is None
|
||||||
|
or self._request_stream_task.cancelled()
|
||||||
|
):
|
||||||
return
|
return
|
||||||
self.request.body_push(body)
|
|
||||||
|
if self.request.stream.is_full():
|
||||||
|
self.transport.pause_reading()
|
||||||
|
await self.request.stream.put(body)
|
||||||
|
self.transport.resume_reading()
|
||||||
|
else:
|
||||||
|
await self.request.stream.put(body)
|
||||||
|
|
||||||
|
async def stream_append(self):
|
||||||
|
while self._body_chunks:
|
||||||
|
body = self._body_chunks.popleft()
|
||||||
|
if self.request.stream.is_full():
|
||||||
|
self.transport.pause_reading()
|
||||||
|
await self.request.stream.put(body)
|
||||||
|
self.transport.resume_reading()
|
||||||
|
else:
|
||||||
|
await self.request.stream.put(body)
|
||||||
|
|
||||||
def on_message_complete(self):
|
def on_message_complete(self):
|
||||||
# Entire request (headers and whole body) is received.
|
# Entire request (headers and whole body) is received.
|
||||||
@@ -316,18 +397,29 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self._request_timeout_handler.cancel()
|
self._request_timeout_handler.cancel()
|
||||||
self._request_timeout_handler = None
|
self._request_timeout_handler = None
|
||||||
if self.is_request_stream and self._is_stream_handler:
|
if self.is_request_stream and self._is_stream_handler:
|
||||||
self._request_stream_task = self.loop.create_task(
|
self._body_chunks.append(None)
|
||||||
self.request.stream.put(None)
|
if (
|
||||||
)
|
not self._request_stream_task
|
||||||
|
or self._request_stream_task.done()
|
||||||
|
):
|
||||||
|
self._request_stream_task = self.loop.create_task(
|
||||||
|
self.stream_append()
|
||||||
|
)
|
||||||
return
|
return
|
||||||
self.request.body_finish()
|
self.request.body_finish()
|
||||||
self.execute_request_handler()
|
self.execute_request_handler()
|
||||||
|
|
||||||
def execute_request_handler(self):
|
def execute_request_handler(self):
|
||||||
|
"""
|
||||||
|
Invoke the request handler defined by the
|
||||||
|
:func:`sanic.app.Sanic.handle_request` method
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
self._response_timeout_handler = self.loop.call_later(
|
self._response_timeout_handler = self.loop.call_later(
|
||||||
self.response_timeout, self.response_timeout_callback
|
self.response_timeout, self.response_timeout_callback
|
||||||
)
|
)
|
||||||
self._last_request_time = current_time
|
self._last_request_time = time()
|
||||||
self._request_handler_task = self.loop.create_task(
|
self._request_handler_task = self.loop.create_task(
|
||||||
self.request_handler(
|
self.request_handler(
|
||||||
self.request, self.write_response, self.stream_response
|
self.request, self.write_response, self.stream_response
|
||||||
@@ -338,6 +430,17 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
# Responding
|
# Responding
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
def log_response(self, response):
|
def log_response(self, response):
|
||||||
|
"""
|
||||||
|
Helper method provided to enable the logging of responses in case if
|
||||||
|
the :attr:`HttpProtocol.access_log` is enabled.
|
||||||
|
|
||||||
|
:param response: Response generated for the current request
|
||||||
|
|
||||||
|
:type response: :class:`sanic.response.HTTPResponse` or
|
||||||
|
:class:`sanic.response.StreamingHTTPResponse`
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
if self.access_log:
|
if self.access_log:
|
||||||
extra = {"status": getattr(response, "status", 0)}
|
extra = {"status": getattr(response, "status", 0)}
|
||||||
|
|
||||||
@@ -403,13 +506,13 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||||
)
|
)
|
||||||
self._last_response_time = current_time
|
self._last_response_time = time()
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
async def drain(self):
|
async def drain(self):
|
||||||
await self._not_paused.wait()
|
await self._not_paused.wait()
|
||||||
|
|
||||||
def push_data(self, data):
|
async def push_data(self, data):
|
||||||
self.transport.write(data)
|
self.transport.write(data)
|
||||||
|
|
||||||
async def stream_response(self, response):
|
async def stream_response(self, response):
|
||||||
@@ -456,7 +559,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||||
)
|
)
|
||||||
self._last_response_time = current_time
|
self._last_response_time = time()
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
def write_error(self, exception):
|
def write_error(self, exception):
|
||||||
@@ -492,11 +595,29 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
logger.debug("Connection lost before server could close it.")
|
logger.debug("Connection lost before server could close it.")
|
||||||
|
|
||||||
def bail_out(self, message, from_error=False):
|
def bail_out(self, message, from_error=False):
|
||||||
if from_error or self.transport.is_closing():
|
"""
|
||||||
|
In case if the transport pipes are closed and the sanic app encounters
|
||||||
|
an error while writing data to the transport pipe, we log the error
|
||||||
|
with proper details.
|
||||||
|
|
||||||
|
:param message: Error message to display
|
||||||
|
:param from_error: If the bail out was invoked while handling an
|
||||||
|
exception scenario.
|
||||||
|
|
||||||
|
:type message: str
|
||||||
|
:type from_error: bool
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if from_error or self.transport is None or self.transport.is_closing():
|
||||||
logger.error(
|
logger.error(
|
||||||
"Transport closed @ %s and exception "
|
"Transport closed @ %s and exception "
|
||||||
"experienced during error handling",
|
"experienced during error handling",
|
||||||
self.transport.get_extra_info("peername"),
|
(
|
||||||
|
self.transport.get_extra_info("peername")
|
||||||
|
if self.transport is not None
|
||||||
|
else "N/A"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
logger.debug("Exception:", exc_info=True)
|
logger.debug("Exception:", exc_info=True)
|
||||||
else:
|
else:
|
||||||
@@ -535,18 +656,6 @@ class HttpProtocol(asyncio.Protocol):
|
|||||||
self.transport = None
|
self.transport = None
|
||||||
|
|
||||||
|
|
||||||
def update_current_time(loop):
|
|
||||||
"""Cache the current time, since it is needed at the end of every
|
|
||||||
keep-alive request to update the request timeout time
|
|
||||||
|
|
||||||
:param loop:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
global current_time
|
|
||||||
current_time = time()
|
|
||||||
loop.call_later(1, partial(update_current_time, loop))
|
|
||||||
|
|
||||||
|
|
||||||
def trigger_events(events, loop):
|
def trigger_events(events, loop):
|
||||||
"""Trigger event callbacks (functions or async)
|
"""Trigger event callbacks (functions or async)
|
||||||
|
|
||||||
@@ -559,9 +668,102 @@ def trigger_events(events, loop):
|
|||||||
loop.run_until_complete(result)
|
loop.run_until_complete(result)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncioServer:
|
||||||
|
"""
|
||||||
|
Wraps an asyncio server with functionality that might be useful to
|
||||||
|
a user who needs to manage the server lifecycle manually.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"loop",
|
||||||
|
"serve_coro",
|
||||||
|
"_after_start",
|
||||||
|
"_before_stop",
|
||||||
|
"_after_stop",
|
||||||
|
"server",
|
||||||
|
"connections",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
loop,
|
||||||
|
serve_coro,
|
||||||
|
connections,
|
||||||
|
after_start,
|
||||||
|
before_stop,
|
||||||
|
after_stop,
|
||||||
|
):
|
||||||
|
# Note, Sanic already called "before_server_start" events
|
||||||
|
# before this helper was even created. So we don't need it here.
|
||||||
|
self.loop = loop
|
||||||
|
self.serve_coro = serve_coro
|
||||||
|
self._after_start = after_start
|
||||||
|
self._before_stop = before_stop
|
||||||
|
self._after_stop = after_stop
|
||||||
|
self.server = None
|
||||||
|
self.connections = connections
|
||||||
|
|
||||||
|
def after_start(self):
|
||||||
|
"""Trigger "after_server_start" events"""
|
||||||
|
trigger_events(self._after_start, self.loop)
|
||||||
|
|
||||||
|
def before_stop(self):
|
||||||
|
"""Trigger "before_server_stop" events"""
|
||||||
|
trigger_events(self._before_stop, self.loop)
|
||||||
|
|
||||||
|
def after_stop(self):
|
||||||
|
"""Trigger "after_server_stop" events"""
|
||||||
|
trigger_events(self._after_stop, self.loop)
|
||||||
|
|
||||||
|
def is_serving(self):
|
||||||
|
if self.server:
|
||||||
|
return self.server.is_serving()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wait_closed(self):
|
||||||
|
if self.server:
|
||||||
|
return self.server.wait_closed()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.server:
|
||||||
|
self.server.close()
|
||||||
|
coro = self.wait_closed()
|
||||||
|
task = asyncio.ensure_future(coro, loop=self.loop)
|
||||||
|
return task
|
||||||
|
|
||||||
|
def start_serving(self):
|
||||||
|
if self.server:
|
||||||
|
try:
|
||||||
|
return self.server.start_serving()
|
||||||
|
except AttributeError:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"server.start_serving not available in this version "
|
||||||
|
"of asyncio or uvloop."
|
||||||
|
)
|
||||||
|
|
||||||
|
def serve_forever(self):
|
||||||
|
if self.server:
|
||||||
|
try:
|
||||||
|
return self.server.serve_forever()
|
||||||
|
except AttributeError:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"server.serve_forever not available in this version "
|
||||||
|
"of asyncio or uvloop."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __await__(self):
|
||||||
|
"""Starts the asyncio server, returns AsyncServerCoro"""
|
||||||
|
task = asyncio.ensure_future(self.serve_coro)
|
||||||
|
while not task.done():
|
||||||
|
yield
|
||||||
|
self.server = task.result()
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
def serve(
|
def serve(
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
|
app,
|
||||||
request_handler,
|
request_handler,
|
||||||
error_handler,
|
error_handler,
|
||||||
before_start=None,
|
before_start=None,
|
||||||
@@ -575,6 +777,7 @@ def serve(
|
|||||||
ssl=None,
|
ssl=None,
|
||||||
sock=None,
|
sock=None,
|
||||||
request_max_size=None,
|
request_max_size=None,
|
||||||
|
request_buffer_queue_size=100,
|
||||||
reuse_port=False,
|
reuse_port=False,
|
||||||
loop=None,
|
loop=None,
|
||||||
protocol=HttpProtocol,
|
protocol=HttpProtocol,
|
||||||
@@ -595,6 +798,7 @@ def serve(
|
|||||||
websocket_write_limit=2 ** 16,
|
websocket_write_limit=2 ** 16,
|
||||||
state=None,
|
state=None,
|
||||||
graceful_shutdown_timeout=15.0,
|
graceful_shutdown_timeout=15.0,
|
||||||
|
asyncio_server_kwargs=None,
|
||||||
):
|
):
|
||||||
"""Start asynchronous HTTP Server on an individual process.
|
"""Start asynchronous HTTP Server on an individual process.
|
||||||
|
|
||||||
@@ -622,6 +826,8 @@ def serve(
|
|||||||
:param reuse_port: `True` for multiple workers
|
:param reuse_port: `True` for multiple workers
|
||||||
:param loop: asyncio compatible event loop
|
:param loop: asyncio compatible event loop
|
||||||
:param protocol: subclass of asyncio protocol class
|
:param protocol: subclass of asyncio protocol class
|
||||||
|
:param run_async: bool: Do not create a new event loop for the server,
|
||||||
|
and return an AsyncServer object rather than running it
|
||||||
:param request_class: Request class to use
|
:param request_class: Request class to use
|
||||||
:param access_log: disable/enable access log
|
:param access_log: disable/enable access log
|
||||||
:param websocket_max_size: enforces the maximum size for
|
:param websocket_max_size: enforces the maximum size for
|
||||||
@@ -635,9 +841,12 @@ def serve(
|
|||||||
outgoing bytes, the low-water limit is a
|
outgoing bytes, the low-water limit is a
|
||||||
quarter of the high-water limit.
|
quarter of the high-water limit.
|
||||||
:param is_request_stream: disable/enable Request.stream
|
:param is_request_stream: disable/enable Request.stream
|
||||||
|
:param request_buffer_queue_size: streaming request buffer queue size
|
||||||
:param router: Router object
|
:param router: Router object
|
||||||
:param graceful_shutdown_timeout: How long take to Force close non-idle
|
:param graceful_shutdown_timeout: How long take to Force close non-idle
|
||||||
connection
|
connection
|
||||||
|
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
|
||||||
|
create_server method
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if not run_async:
|
if not run_async:
|
||||||
@@ -648,18 +857,22 @@ def serve(
|
|||||||
if debug:
|
if debug:
|
||||||
loop.set_debug(debug)
|
loop.set_debug(debug)
|
||||||
|
|
||||||
|
app.asgi = False
|
||||||
|
|
||||||
connections = connections if connections is not None else set()
|
connections = connections if connections is not None else set()
|
||||||
server = partial(
|
server = partial(
|
||||||
protocol,
|
protocol,
|
||||||
loop=loop,
|
loop=loop,
|
||||||
connections=connections,
|
connections=connections,
|
||||||
signal=signal,
|
signal=signal,
|
||||||
|
app=app,
|
||||||
request_handler=request_handler,
|
request_handler=request_handler,
|
||||||
error_handler=error_handler,
|
error_handler=error_handler,
|
||||||
request_timeout=request_timeout,
|
request_timeout=request_timeout,
|
||||||
response_timeout=response_timeout,
|
response_timeout=response_timeout,
|
||||||
keep_alive_timeout=keep_alive_timeout,
|
keep_alive_timeout=keep_alive_timeout,
|
||||||
request_max_size=request_max_size,
|
request_max_size=request_max_size,
|
||||||
|
request_buffer_queue_size=request_buffer_queue_size,
|
||||||
request_class=request_class,
|
request_class=request_class,
|
||||||
access_log=access_log,
|
access_log=access_log,
|
||||||
keep_alive=keep_alive,
|
keep_alive=keep_alive,
|
||||||
@@ -672,7 +885,9 @@ def serve(
|
|||||||
state=state,
|
state=state,
|
||||||
debug=debug,
|
debug=debug,
|
||||||
)
|
)
|
||||||
|
asyncio_server_kwargs = (
|
||||||
|
asyncio_server_kwargs if asyncio_server_kwargs else {}
|
||||||
|
)
|
||||||
server_coroutine = loop.create_server(
|
server_coroutine = loop.create_server(
|
||||||
server,
|
server,
|
||||||
host,
|
host,
|
||||||
@@ -681,14 +896,18 @@ def serve(
|
|||||||
reuse_port=reuse_port,
|
reuse_port=reuse_port,
|
||||||
sock=sock,
|
sock=sock,
|
||||||
backlog=backlog,
|
backlog=backlog,
|
||||||
|
**asyncio_server_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
# Instead of pulling time at the end of every request,
|
|
||||||
# pull it once per minute
|
|
||||||
loop.call_soon(partial(update_current_time, loop))
|
|
||||||
|
|
||||||
if run_async:
|
if run_async:
|
||||||
return server_coroutine
|
return AsyncioServer(
|
||||||
|
loop,
|
||||||
|
server_coroutine,
|
||||||
|
connections,
|
||||||
|
after_start,
|
||||||
|
before_stop,
|
||||||
|
after_stop,
|
||||||
|
)
|
||||||
|
|
||||||
trigger_events(before_start, loop)
|
trigger_events(before_start, loop)
|
||||||
|
|
||||||
@@ -782,6 +1001,8 @@ def serve_multiple(server_settings, workers):
|
|||||||
server_settings["host"] = None
|
server_settings["host"] = None
|
||||||
server_settings["port"] = None
|
server_settings["port"] = None
|
||||||
|
|
||||||
|
processes = []
|
||||||
|
|
||||||
def sig_handler(signal, frame):
|
def sig_handler(signal, frame):
|
||||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||||
for process in processes:
|
for process in processes:
|
||||||
@@ -790,8 +1011,6 @@ def serve_multiple(server_settings, workers):
|
|||||||
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
|
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
|
||||||
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
|
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
|
||||||
|
|
||||||
processes = []
|
|
||||||
|
|
||||||
for _ in range(workers):
|
for _ in range(workers):
|
||||||
process = Process(target=serve, kwargs=server_settings)
|
process = Process(target=serve, kwargs=server_settings)
|
||||||
process.daemon = True
|
process.daemon = True
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from re import sub
|
|||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from aiofiles.os import stat
|
from aiofiles.os import stat # type: ignore
|
||||||
|
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
ContentRangeError,
|
ContentRangeError,
|
||||||
|
|||||||
184
sanic/testing.py
184
sanic/testing.py
@@ -1,52 +1,63 @@
|
|||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
|
from socket import socket
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
from sanic.asgi import ASGIApp
|
||||||
from sanic.exceptions import MethodNotSupported
|
from sanic.exceptions import MethodNotSupported
|
||||||
from sanic.log import logger
|
from sanic.log import logger
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
|
ASGI_HOST = "mockserver"
|
||||||
HOST = "127.0.0.1"
|
HOST = "127.0.0.1"
|
||||||
PORT = 42101
|
PORT = 42101
|
||||||
|
|
||||||
|
|
||||||
class SanicTestClient:
|
class SanicTestClient:
|
||||||
def __init__(self, app, port=PORT):
|
def __init__(self, app, port=PORT, host=HOST):
|
||||||
|
"""Use port=None to bind to a random port"""
|
||||||
self.app = app
|
self.app = app
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.host = host
|
||||||
|
|
||||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
def get_new_session(self):
|
||||||
import aiohttp
|
return httpx.Client()
|
||||||
|
|
||||||
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
|
|
||||||
url = uri
|
|
||||||
else:
|
|
||||||
url = "http://{host}:{port}{uri}".format(
|
|
||||||
host=HOST, port=self.port, uri=uri
|
|
||||||
)
|
|
||||||
|
|
||||||
|
async def _local_request(self, method, url, *args, **kwargs):
|
||||||
logger.info(url)
|
logger.info(url)
|
||||||
conn = aiohttp.TCPConnector(ssl=False)
|
raw_cookies = kwargs.pop("raw_cookies", None)
|
||||||
async with aiohttp.ClientSession(
|
|
||||||
cookies=cookies, connector=conn
|
if method == "websocket":
|
||||||
) as session:
|
async with websockets.connect(url, *args, **kwargs) as websocket:
|
||||||
async with getattr(session, method.lower())(
|
websocket.opened = websocket.open
|
||||||
url, *args, **kwargs
|
return websocket
|
||||||
) as response:
|
else:
|
||||||
try:
|
async with self.get_new_session() as session:
|
||||||
response.text = await response.text()
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
response.text = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response.json = await response.json()
|
response = await getattr(session, method.lower())(
|
||||||
except (
|
url, verify=False, *args, **kwargs
|
||||||
JSONDecodeError,
|
)
|
||||||
UnicodeDecodeError,
|
except NameError:
|
||||||
aiohttp.ClientResponseError,
|
raise Exception(response.status_code)
|
||||||
):
|
|
||||||
|
try:
|
||||||
|
response.json = response.json()
|
||||||
|
except (JSONDecodeError, UnicodeDecodeError):
|
||||||
response.json = None
|
response.json = None
|
||||||
|
|
||||||
response.body = await response.read()
|
response.body = await response.read()
|
||||||
|
response.status = response.status_code
|
||||||
|
response.content_type = response.headers.get("content-type")
|
||||||
|
|
||||||
|
if raw_cookies:
|
||||||
|
response.raw_cookies = {}
|
||||||
|
|
||||||
|
for cookie in response.cookies.jar:
|
||||||
|
response.raw_cookies[cookie.name] = cookie
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _sanic_endpoint_test(
|
def _sanic_endpoint_test(
|
||||||
@@ -56,8 +67,9 @@ class SanicTestClient:
|
|||||||
gather_request=True,
|
gather_request=True,
|
||||||
debug=False,
|
debug=False,
|
||||||
server_kwargs={"auto_reload": False},
|
server_kwargs={"auto_reload": False},
|
||||||
|
host=None,
|
||||||
*request_args,
|
*request_args,
|
||||||
**request_kwargs
|
**request_kwargs,
|
||||||
):
|
):
|
||||||
results = [None, None]
|
results = [None, None]
|
||||||
exceptions = []
|
exceptions = []
|
||||||
@@ -79,11 +91,33 @@ class SanicTestClient:
|
|||||||
else:
|
else:
|
||||||
return self.app.error_handler.default(request, exception)
|
return self.app.error_handler.default(request, exception)
|
||||||
|
|
||||||
|
if self.port:
|
||||||
|
server_kwargs = dict(
|
||||||
|
host=host or self.host, port=self.port, **server_kwargs
|
||||||
|
)
|
||||||
|
host, port = host or self.host, self.port
|
||||||
|
else:
|
||||||
|
sock = socket()
|
||||||
|
sock.bind((host or self.host, 0))
|
||||||
|
server_kwargs = dict(sock=sock, **server_kwargs)
|
||||||
|
host, port = sock.getsockname()
|
||||||
|
|
||||||
|
if uri.startswith(
|
||||||
|
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
|
||||||
|
):
|
||||||
|
url = uri
|
||||||
|
else:
|
||||||
|
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
|
||||||
|
scheme = "ws" if method == "websocket" else "http"
|
||||||
|
url = "{scheme}://{host}:{port}{uri}".format(
|
||||||
|
scheme=scheme, host=host, port=port, uri=uri
|
||||||
|
)
|
||||||
|
|
||||||
@self.app.listener("after_server_start")
|
@self.app.listener("after_server_start")
|
||||||
async def _collect_response(sanic, loop):
|
async def _collect_response(sanic, loop):
|
||||||
try:
|
try:
|
||||||
response = await self._local_request(
|
response = await self._local_request(
|
||||||
method, uri, *request_args, **request_kwargs
|
method, url, *request_args, **request_kwargs
|
||||||
)
|
)
|
||||||
results[-1] = response
|
results[-1] = response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -91,7 +125,7 @@ class SanicTestClient:
|
|||||||
exceptions.append(e)
|
exceptions.append(e)
|
||||||
self.app.stop()
|
self.app.stop()
|
||||||
|
|
||||||
self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
|
self.app.run(debug=debug, **server_kwargs)
|
||||||
self.app.listeners["after_server_start"].pop()
|
self.app.listeners["after_server_start"].pop()
|
||||||
|
|
||||||
if exceptions:
|
if exceptions:
|
||||||
@@ -101,7 +135,7 @@ class SanicTestClient:
|
|||||||
try:
|
try:
|
||||||
request, response = results
|
request, response = results
|
||||||
return request, response
|
return request, response
|
||||||
except BaseException:
|
except BaseException: # noqa
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Request and response object expected, got ({})".format(
|
"Request and response object expected, got ({})".format(
|
||||||
results
|
results
|
||||||
@@ -110,7 +144,7 @@ class SanicTestClient:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return results[-1]
|
return results[-1]
|
||||||
except BaseException:
|
except BaseException: # noqa
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Request object expected, got ({})".format(results)
|
"Request object expected, got ({})".format(results)
|
||||||
)
|
)
|
||||||
@@ -135,3 +169,89 @@ class SanicTestClient:
|
|||||||
|
|
||||||
def head(self, *args, **kwargs):
|
def head(self, *args, **kwargs):
|
||||||
return self._sanic_endpoint_test("head", *args, **kwargs)
|
return self._sanic_endpoint_test("head", *args, **kwargs)
|
||||||
|
|
||||||
|
def websocket(self, *args, **kwargs):
|
||||||
|
return self._sanic_endpoint_test("websocket", *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TestASGIApp(ASGIApp):
|
||||||
|
async def __call__(self):
|
||||||
|
await super().__call__()
|
||||||
|
return self.request
|
||||||
|
|
||||||
|
|
||||||
|
async def app_call_with_return(self, scope, receive, send):
|
||||||
|
asgi_app = await TestASGIApp.create(self, scope, receive, send)
|
||||||
|
return await asgi_app()
|
||||||
|
|
||||||
|
|
||||||
|
class SanicASGIDispatch(httpx.dispatch.ASGIDispatch):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SanicASGITestClient(httpx.Client):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
app,
|
||||||
|
base_url: str = "http://{}".format(ASGI_HOST),
|
||||||
|
suppress_exceptions: bool = False,
|
||||||
|
) -> None:
|
||||||
|
app.__class__.__call__ = app_call_with_return
|
||||||
|
app.asgi = True
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT))
|
||||||
|
super().__init__(dispatch=dispatch, base_url=base_url)
|
||||||
|
|
||||||
|
self.last_request = None
|
||||||
|
|
||||||
|
def _collect_request(request):
|
||||||
|
self.last_request = request
|
||||||
|
|
||||||
|
app.request_middleware.appendleft(_collect_request)
|
||||||
|
|
||||||
|
async def request(self, method, url, gather_request=True, *args, **kwargs):
|
||||||
|
|
||||||
|
self.gather_request = gather_request
|
||||||
|
response = await super().request(method, url, *args, **kwargs)
|
||||||
|
response.status = response.status_code
|
||||||
|
response.body = response.content
|
||||||
|
response.content_type = response.headers.get("content-type")
|
||||||
|
|
||||||
|
return self.last_request, response
|
||||||
|
|
||||||
|
async def websocket(self, uri, subprotocols=None, *args, **kwargs):
|
||||||
|
scheme = "ws"
|
||||||
|
path = uri
|
||||||
|
root_path = "{}://{}".format(scheme, ASGI_HOST)
|
||||||
|
|
||||||
|
headers = kwargs.get("headers", {})
|
||||||
|
headers.setdefault("connection", "upgrade")
|
||||||
|
headers.setdefault("sec-websocket-key", "testserver==")
|
||||||
|
headers.setdefault("sec-websocket-version", "13")
|
||||||
|
if subprotocols is not None:
|
||||||
|
headers.setdefault(
|
||||||
|
"sec-websocket-protocol", ", ".join(subprotocols)
|
||||||
|
)
|
||||||
|
|
||||||
|
scope = {
|
||||||
|
"type": "websocket",
|
||||||
|
"asgi": {"version": "3.0"},
|
||||||
|
"http_version": "1.1",
|
||||||
|
"headers": [map(lambda y: y.encode(), x) for x in headers.items()],
|
||||||
|
"scheme": scheme,
|
||||||
|
"root_path": root_path,
|
||||||
|
"path": path,
|
||||||
|
"query_string": b"",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def receive():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def send(message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
return None, {}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Any, Callable, List
|
||||||
|
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import InvalidUsage
|
from sanic.exceptions import InvalidUsage
|
||||||
|
|
||||||
@@ -37,7 +39,7 @@ class HTTPMethodView:
|
|||||||
To add any decorator you could set it into decorators variable
|
To add any decorator you could set it into decorators variable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
decorators = []
|
decorators: List[Callable[[Callable[..., Any]], Callable[..., Any]]] = []
|
||||||
|
|
||||||
def dispatch_request(self, request, *args, **kwargs):
|
def dispatch_request(self, request, *args, **kwargs):
|
||||||
handler = getattr(self, request.method.lower(), None)
|
handler = getattr(self, request.method.lower(), None)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user