Compare commits
273 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abf8534ea9 | ||
|
|
773a66bc5b | ||
|
|
269100eac1 | ||
|
|
2a15583b87 | ||
|
|
d5813152ab | ||
|
|
348964fe12 | ||
|
|
e5c7589fc0 | ||
|
|
4260528645 | ||
|
|
34fe26e51b | ||
|
|
8a59907319 | ||
|
|
52deebaf65 | ||
|
|
1e05b22fbc | ||
|
|
ab56af5d15 | ||
|
|
123f00eee6 | ||
|
|
42bf103269 | ||
|
|
c8d2a462e3 | ||
|
|
08794ae1cf | ||
|
|
4f70dba935 | ||
|
|
b926a2c9b0 | ||
|
|
52bdd1d5a2 | ||
|
|
bc7d0f0da5 | ||
|
|
6a8e9c9e95 | ||
|
|
211a922f3c | ||
|
|
2758a3ade6 | ||
|
|
ef3c9eae73 | ||
|
|
9cf2e1b519 | ||
|
|
51c2f7a599 | ||
|
|
5bdd046b11 | ||
|
|
af7ad0a621 | ||
|
|
1473753d43 | ||
|
|
b36bd21813 | ||
|
|
f8f0241c27 | ||
|
|
1af16836d4 | ||
|
|
757974714e | ||
|
|
eed22a7a24 | ||
|
|
0242bc999f | ||
|
|
b89c533818 | ||
|
|
2cb05ab865 | ||
|
|
391639210e | ||
|
|
99f34c9f50 | ||
|
|
d418cc9950 | ||
|
|
6dfafb0787 | ||
|
|
7067295e67 | ||
|
|
2af229eb1a | ||
|
|
8dd8e9916e | ||
|
|
96af1fe7cf | ||
|
|
cb3a03356b | ||
|
|
68aa2ae3ce | ||
|
|
52de354e24 | ||
|
|
f4f90cada4 | ||
|
|
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 | ||
|
|
15b1c875f5 | ||
|
|
13804dc380 | ||
|
|
9bea23da29 | ||
|
|
7005fabd4d | ||
|
|
de8c37ad00 | ||
|
|
a80499c4b7 | ||
|
|
82f7f847ba | ||
|
|
4880761fe0 | ||
|
|
87ab0b386d | ||
|
|
c42c274002 | ||
|
|
2d82b8951f | ||
|
|
b7702bc3e8 | ||
|
|
9c6b83501f | ||
|
|
5189d8b14c | ||
|
|
e13053ed89 | ||
|
|
efa77cf5ec | ||
|
|
f6355bd075 | ||
|
|
e3dfce88ff | ||
|
|
939b5ea095 | ||
|
|
e6fba01682 | ||
|
|
1623d397be | ||
|
|
09678d601d | ||
|
|
67d51f7e1b | ||
|
|
aa7f2759a6 | ||
|
|
9b9dd67797 | ||
|
|
3f73bc075a | ||
|
|
56989a017b | ||
|
|
ada5918bc8 | ||
|
|
4efd450b32 | ||
|
|
d2670664ba | ||
|
|
fa7405fe9c | ||
|
|
33297f48a5 | ||
|
|
956793e066 | ||
|
|
1bfbc67c62 | ||
|
|
b5287184e9 | ||
|
|
7c9957e058 | ||
|
|
fca7cb9fb0 | ||
|
|
268d254d85 | ||
|
|
181adebf82 | ||
|
|
06297a1918 | ||
|
|
aa0874b6d8 | ||
|
|
822ced6294 | ||
|
|
1a59614f79 | ||
|
|
f2d528e52a | ||
|
|
f7adc5f84c | ||
|
|
e955e833c4 | ||
|
|
096c44b910 | ||
|
|
efb9a42045 | ||
|
|
296cda7801 | ||
|
|
90b9d73244 | ||
|
|
c8b0e7f2a7 | ||
|
|
6ce88ab5a4 | ||
|
|
e13ab805df | ||
|
|
e58ea8c7b4 | ||
|
|
dd5bac61cb | ||
|
|
6270b27a97 | ||
|
|
f89ba1d39f | ||
|
|
8b5d137d8f | ||
|
|
2629fab649 | ||
|
|
92cd10c6a8 | ||
|
|
cc3edb90dc | ||
|
|
c60ba81984 | ||
|
|
ece3cdaa2e | ||
|
|
4cb40f2042 | ||
|
|
0e9f350982 | ||
|
|
cf439f01f8 | ||
|
|
f1f1b8a630 | ||
|
|
d4d1df03c9 | ||
|
|
92b73a6f4f | ||
|
|
b63c06c75a | ||
|
|
3e3bce422e | ||
|
|
e3a27c2cc4 | ||
|
|
f13f451084 | ||
|
|
df0e3de911 | ||
|
|
8466be8728 | ||
|
|
5cf2144b3f | ||
|
|
7c182f63c8 | ||
|
|
056180782c | ||
|
|
ff0d5870e9 | ||
|
|
b70176f8c7 | ||
|
|
e3655b525d | ||
|
|
e63d0091af | ||
|
|
7b0af2d80d | ||
|
|
7d79a86d4d | ||
|
|
ba46aff069 | ||
|
|
7a65471ba5 | ||
|
|
c7c46da975 | ||
|
|
c708e8425f | ||
|
|
905c51bef0 | ||
|
|
bd87098b7e | ||
|
|
5f486cc25f | ||
|
|
f79fb72a33 | ||
|
|
0505aa2dda | ||
|
|
485ff32e42 | ||
|
|
5ead67972f | ||
|
|
9c860dbff3 | ||
|
|
a20ad99638 | ||
|
|
8ef7bf8e7b | ||
|
|
0d5be1969a | ||
|
|
d06ea9bfc3 | ||
|
|
57e79882e1 | ||
|
|
20d1ab60c7 | ||
|
|
277c2ce2d2 | ||
|
|
34e51f01d1 | ||
|
|
f4b4e3a58c | ||
|
|
def2e033c8 | ||
|
|
dfec18278b | ||
|
|
cd5bdecda3 | ||
|
|
9b6217ba41 | ||
|
|
272f6e195d | ||
|
|
aa9bf04dfe | ||
|
|
9ae6dfb6d2 | ||
|
|
619bb79a2f | ||
|
|
0cad831eca | ||
|
|
f15a7fb588 | ||
|
|
1bdf9ca057 | ||
|
|
c8c370b784 | ||
|
|
63182f55f7 | ||
|
|
41759248e2 | ||
|
|
3149d5a66d | ||
|
|
8b13597099 | ||
|
|
36032cc26e | ||
|
|
4cb107aedc | ||
|
|
176f8d1981 | ||
|
|
9a26030bd5 | ||
|
|
6778f4d9e0 | ||
|
|
fd61b9e3e2 | ||
|
|
298d5cdf24 | ||
|
|
1bf1c9d006 | ||
|
|
7dc62be5cf | ||
|
|
be580a6a5b | ||
|
|
8ce519668b | ||
|
|
801258c46a | ||
|
|
32a1db3622 | ||
|
|
ed1f3daacc | ||
|
|
b7d74c82ba | ||
|
|
c3b31a6fb0 | ||
|
|
f4c55bbc07 | ||
|
|
a16842f7bc | ||
|
|
439a38664f | ||
|
|
5cc12fd945 | ||
|
|
fe116fff5a | ||
|
|
06aaaf4727 | ||
|
|
6deb9b49b2 | ||
|
|
d59e92d3e5 | ||
|
|
cc83c1f0cf | ||
|
|
1fe7306af8 | ||
|
|
c796d73fc3 | ||
|
|
eb93f884f3 | ||
|
|
3673feb256 | ||
|
|
7c9c783e9d | ||
|
|
74a4b9efaa | ||
|
|
4466e8cce1 | ||
|
|
b689037984 | ||
|
|
db1ba21d88 | ||
|
|
50d270ef7c | ||
|
|
d1a578b555 | ||
|
|
76e9859cf8 | ||
|
|
add9d363c5 | ||
|
|
1498baab0f | ||
|
|
df7f63d45d | ||
|
|
f7425126a1 | ||
|
|
790047e450 | ||
|
|
9198b5b0be | ||
|
|
d534acb79d | ||
|
|
d100f54551 | ||
|
|
7a9e100b0f | ||
|
|
fafe23d7c2 | ||
|
|
9a08bdae4a | ||
|
|
bcc11fa7fe | ||
|
|
7d0c0fdf7c | ||
|
|
0e33d46ead | ||
|
|
efbacc17cf | ||
|
|
bd6dbd9090 | ||
|
|
076cf51fb2 | ||
|
|
f8a6af1e28 | ||
|
|
96912f436d | ||
|
|
f0e162442f | ||
|
|
04b8dd989f | ||
|
|
5851c8bd91 | ||
|
|
78efcf93f8 | ||
|
|
bb35bc3898 | ||
|
|
f38783bdef | ||
|
|
d8f9986089 | ||
|
|
3e616b599a | ||
|
|
d38fc17191 | ||
|
|
7ae0eb0dc3 | ||
|
|
c578974246 | ||
|
|
fec81ffe73 | ||
|
|
a7dd73c657 | ||
|
|
f770e16f6d | ||
|
|
c1222175b3 | ||
|
|
7928b9b3a2 | ||
|
|
63bbcb5152 | ||
|
|
9150767574 | ||
|
|
75f2180cb1 |
32
.appveyor.yml
Normal file
32
.appveyor.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: "{branch}.{build}"
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- TOXENV: py35-no-ext
|
||||
PYTHON: "C:\\Python35-x64"
|
||||
PYTHON_VERSION: "3.5.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
- TOXENV: py36-no-ext
|
||||
PYTHON: "C:\\Python36-x64"
|
||||
PYTHON_VERSION: "3.6.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
- TOXENV: py37-no-ext
|
||||
PYTHON: "C:\\Python37-x64"
|
||||
PYTHON_VERSION: "3.7.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||
|
||||
install:
|
||||
- pip install tox
|
||||
|
||||
build: off
|
||||
|
||||
test_script: tox
|
||||
|
||||
notifications:
|
||||
- provider: Email
|
||||
on_build_success: false
|
||||
on_build_status_changed: false
|
||||
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks.
|
||||
|
||||
|
||||
**Code snippet**
|
||||
Relevant source code, make sure to remove what is not necessary.
|
||||
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 0.8.3]
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for Sanic
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context or sample code about the feature request here.
|
||||
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: Help wanted
|
||||
about: Do you need help? Try community.sanicframework.org
|
||||
|
||||
---
|
||||
|
||||
*DELETE ALL BEFORE POSTING*
|
||||
*Post your HELP WANTED questions on [the community forum](https://community.sanicframework.org/)*.
|
||||
|
||||
Checkout the community forum before posting any question here.
|
||||
We prefer if you put these kinds of questions here:
|
||||
|
||||
https://community.sanicframework.org/c/questions-and-help
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,3 +15,4 @@ docs/_build/
|
||||
docs/_api/
|
||||
build/*
|
||||
.DS_Store
|
||||
dist/*
|
||||
10
.travis.yml
10
.travis.yml
@@ -21,17 +21,21 @@ matrix:
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- env: TOX_ENV=flake8
|
||||
- env: TOX_ENV=lint
|
||||
python: 3.6
|
||||
- env: TOX_ENV=check
|
||||
python: 3.6
|
||||
install: pip install -U tox
|
||||
install:
|
||||
- pip install -U tox
|
||||
- pip install codecov
|
||||
script: travis_retry tox -e $TOX_ENV
|
||||
after_success:
|
||||
- codecov
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
password:
|
||||
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
|
||||
secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
|
||||
on:
|
||||
tags: true
|
||||
distributions: "sdist bdist_wheel"
|
||||
|
||||
123
CHANGELOG.md
123
CHANGELOG.md
@@ -1,3 +1,116 @@
|
||||
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
|
||||
@@ -5,18 +118,18 @@ Version 0.1
|
||||
- 0.1.6
|
||||
- Static files
|
||||
- Lazy Cookie Loading
|
||||
- 0.1.5
|
||||
- 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
|
||||
- 0.1.4
|
||||
- Multiprocessing
|
||||
- 0.1.3
|
||||
- Blueprint support
|
||||
- Faster Response processing
|
||||
- 0.1.1 - 0.1.2
|
||||
- 0.1.1 - 0.1.2
|
||||
- Struggling to update pypi via CI
|
||||
- 0.1.0
|
||||
- Released to public
|
||||
- 0.1.0
|
||||
- Released to public
|
||||
|
||||
@@ -18,9 +18,22 @@ 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
|
||||
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.
|
||||
|
||||
| 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 tests
|
||||
|
||||
To run the tests for sanic it is recommended to use tox like so:
|
||||
|
||||
18
MANIFEST.in
18
MANIFEST.in
@@ -1,7 +1,15 @@
|
||||
include README.rst
|
||||
include MANIFEST.in
|
||||
# Non Code related contents
|
||||
include LICENSE
|
||||
include setup.py
|
||||
include README.rst
|
||||
include pyproject.toml
|
||||
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
# Setup
|
||||
include setup.py
|
||||
include Makefile
|
||||
|
||||
# Tests
|
||||
include .coveragerc
|
||||
graft tests
|
||||
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co]
|
||||
58
Makefile
58
Makefile
@@ -1,4 +1,58 @@
|
||||
test:
|
||||
find . -name "*.pyc" -delete
|
||||
.PHONY: help test test-coverage install docker-test black fix-import beautify
|
||||
|
||||
.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 ""
|
||||
|
||||
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 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
|
||||
endif
|
||||
endif
|
||||
|
||||
black:
|
||||
black --config ./pyproject.toml sanic tests
|
||||
|
||||
fix-import: black
|
||||
isort -rc sanic
|
||||
|
||||
154
README.rst
154
README.rst
@@ -1,15 +1,71 @@
|
||||
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| |PyPI| |PyPI version|
|
||||
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/channelcat/sanic/>`_. 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|
|
||||
|
||||
.. |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
|
||||
|
||||
.. end-badges
|
||||
|
||||
Sanic is a Python 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 sanic
|
||||
|
||||
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/channelcat/sanic/issues/396>`_ that we use to track those projects!
|
||||
|
||||
Hello World Example
|
||||
-------------------
|
||||
@@ -27,17 +83,27 @@ Hello World Example
|
||||
|
||||
if __name__ == '__main__':
|
||||
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
|
||||
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
|
||||
installation.
|
||||
And, we can verify it is working: ``curl localhost:8000 -i``
|
||||
|
||||
- ``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
|
||||
@@ -45,56 +111,18 @@ Documentation
|
||||
|
||||
`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
|
||||
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master
|
||||
:target: https://travis-ci.org/channelcat/sanic
|
||||
.. |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/
|
||||
Changelog
|
||||
---------
|
||||
|
||||
`Release Changelogs <https://github.com/huge-success/sanic/blob/master/CHANGELOG.md>`_.
|
||||
|
||||
|
||||
Questions and Discussion
|
||||
------------------------
|
||||
|
||||
Examples
|
||||
--------
|
||||
`Non-Core examples <https://github.com/channelcat/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
|
||||
`Ask a question or join the conversation <https://community.sanicframework.org/>`_.
|
||||
|
||||
`Extensions <https://github.com/channelcat/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
|
||||
Contribution
|
||||
------------
|
||||
|
||||
`Projects <https://github.com/channelcat/sanic/wiki/Projects/>`_. Sanic in production use.
|
||||
|
||||
|
||||
TODO
|
||||
----
|
||||
* http2
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
* No wheels for uvloop and httptools on Windows :(
|
||||
|
||||
Final Thoughts
|
||||
--------------
|
||||
|
||||
::
|
||||
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
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://github.com/huge-success/sanic/blob/master/CONTRIBUTING.md>`_.
|
||||
|
||||
0
docs/_static/.gitkeep
vendored
Normal file
0
docs/_static/.gitkeep
vendored
Normal file
@@ -38,7 +38,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Sanic'
|
||||
copyright = '2016, Sanic contributors'
|
||||
copyright = '2018, Sanic contributors'
|
||||
author = 'Sanic contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
||||
@@ -7,27 +7,33 @@ Guides
|
||||
:maxdepth: 2
|
||||
|
||||
sanic/getting_started
|
||||
sanic/routing
|
||||
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/blueprints
|
||||
sanic/websocket
|
||||
sanic/config
|
||||
sanic/cookies
|
||||
sanic/decorators
|
||||
sanic/streaming
|
||||
sanic/class_based_views
|
||||
sanic/custom_protocol
|
||||
sanic/sockets
|
||||
sanic/ssl
|
||||
sanic/logging
|
||||
sanic/debug_mode
|
||||
sanic/testing
|
||||
sanic/deploying
|
||||
sanic/extensions
|
||||
sanic/examples
|
||||
sanic/changelog
|
||||
sanic/contributing
|
||||
sanic/api_reference
|
||||
sanic/asyncio_python37
|
||||
|
||||
|
||||
Module Documentation
|
||||
|
||||
@@ -20,6 +20,15 @@ sanic.blueprints module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.blueprint_group module
|
||||
----------------------------
|
||||
|
||||
.. automodule:: sanic.blueprint_group
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
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.AnstractServer
|
||||
|
||||
|
||||
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.
|
||||
@@ -48,7 +48,7 @@ by that blueprint. In this example, the registered routes in the `app.router`
|
||||
will look like:
|
||||
|
||||
```python
|
||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
|
||||
```
|
||||
|
||||
## Blueprint groups and nesting
|
||||
@@ -87,7 +87,7 @@ from sanic import Blueprint
|
||||
from .static import static
|
||||
from .authors import authors
|
||||
|
||||
content = Blueprint.group(assets, authors, url_prefix='/content')
|
||||
content = Blueprint.group(static, authors, url_prefix='/content')
|
||||
```
|
||||
```python
|
||||
# api/info.py
|
||||
@@ -118,16 +118,16 @@ app = Sanic(__name__)
|
||||
app.blueprint(api)
|
||||
```
|
||||
|
||||
## Using blueprints
|
||||
## Using Blueprints
|
||||
|
||||
Blueprints have much the same functionality as an application instance.
|
||||
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.
|
||||
|
||||
### Middleware
|
||||
### Blueprint Middleware
|
||||
|
||||
Using blueprints allows you to also register middleware globally.
|
||||
|
||||
@@ -145,6 +145,36 @@ 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.
|
||||
|
||||
```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.
|
||||
@@ -201,7 +231,7 @@ async def close_connection(app, loop):
|
||||
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,
|
||||
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.
|
||||
|
||||
@@ -210,8 +240,8 @@ can be used to implement our API versioning scheme.
|
||||
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 = 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):
|
||||
@@ -222,7 +252,7 @@ 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
|
||||
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.
|
||||
|
||||
@@ -232,8 +262,8 @@ 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.blueprint(blueprint_v1)
|
||||
app.blueprint(blueprint_v2)
|
||||
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
```
|
||||
@@ -246,7 +276,7 @@ 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'
|
||||
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/api/post/5'
|
||||
return redirect(url)
|
||||
|
||||
|
||||
@@ -254,5 +284,3 @@ async def root(request):
|
||||
async def post_handler(request, post_id):
|
||||
return text('Post {} in Blueprint V1'.format(post_id))
|
||||
```
|
||||
|
||||
|
||||
|
||||
135
docs/sanic/changelog.md
Normal file
135
docs/sanic/changelog.md
Normal file
@@ -0,0 +1,135 @@
|
||||
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
|
||||
@@ -85,27 +85,53 @@ DB_USER = 'appuser'
|
||||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | --------- | --------------------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take 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) |
|
||||
| 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 |
|
||||
|
||||
### 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.
|
||||
#### `REQUEST_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 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.
|
||||
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.
|
||||
|
||||
### What is Keep Alive? And what does the Keep Alive Timeout value do?
|
||||
#### `RESPONSE_TIMEOUT`
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
#### `KEEP_ALIVE_TIMEOUT`
|
||||
|
||||
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.
|
||||
##### 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:
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
1. All pull requests must pass unit tests
|
||||
* All pull requests must be reviewed and approved by at least
|
||||
one current collaborator on the project
|
||||
* All pull requests must pass flake8 checks
|
||||
* If you decide to remove/change anything from any common interface
|
||||
a deprecation message should accompany it.
|
||||
* If you implement a new feature you should have at least one unit
|
||||
test to accompany it.
|
||||
|
||||
## Documentation
|
||||
|
||||
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.
|
||||
89
docs/sanic/contributing.rst
Normal file
89
docs/sanic/contributing.rst
Normal file
@@ -0,0 +1,89 @@
|
||||
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:
|
||||
|
||||
.. code:: 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.
|
||||
|
||||
+------------------------+-----------------------------------------------+--------------------------------+
|
||||
| Dependency Type | Usage | Installation |
|
||||
+========================+===============================================+================================+
|
||||
| requirements | Bare minimum dependencies required for sanic | ``pip3 install -e .`` |
|
||||
| | to function | |
|
||||
+------------------------+-----------------------------------------------+--------------------------------+
|
||||
| tests_require / | Dependencies required to run the Unit Tests | ``pip3 install -e '.[test]'`` |
|
||||
| extras_require['test'] | for ``sanic`` | |
|
||||
+------------------------+-----------------------------------------------+--------------------------------+
|
||||
| extras_require['dev'] | Additional Development requirements to add | ``pip3 install -e '.[dev]'`` |
|
||||
| | for contributing | |
|
||||
+------------------------+-----------------------------------------------+--------------------------------+
|
||||
| extras_require['docs'] | Dependencies required to enable building and | ``pip3 install -e '.[docs]'`` |
|
||||
| | enhancing sanic documentation | |
|
||||
+------------------------+-----------------------------------------------+--------------------------------+
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
|
||||
To run the tests for sanic it is recommended to use tox like so:
|
||||
|
||||
.. code:: 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`_. 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:: 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.
|
||||
|
||||
.. _sphinx: http://www.sphinx-doc.org/en/1.5.1/
|
||||
@@ -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)
|
||||
|
||||
@@ -34,6 +34,6 @@ def authorized():
|
||||
@app.route("/")
|
||||
@authorized()
|
||||
async def test(request):
|
||||
return json({status: 'authorized'})
|
||||
return json({'status': 'authorized'})
|
||||
```
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ keyword arguments:
|
||||
- `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).
|
||||
|
||||
## Workers
|
||||
|
||||
@@ -63,6 +64,26 @@ of the memory leak.
|
||||
|
||||
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
|
||||
|
||||
## Disable debug logging
|
||||
|
||||
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
|
||||
|
||||
```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
|
||||
|
||||
```python
|
||||
app.config.ACCESS_LOG = False
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
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 ``reqeust`` 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>`_
|
||||
@@ -47,6 +47,36 @@ 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 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:
|
||||
|
||||
@@ -1,34 +1,75 @@
|
||||
# Extensions
|
||||
|
||||
A list of Sanic extensions created by the community.
|
||||
|
||||
|
||||
## Extension and Plugin Development
|
||||
|
||||
- [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-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
|
||||
|
||||
## Security
|
||||
|
||||
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping 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.
|
||||
- [Secure](https://github.com/cakinney/secure): Secure 🔒 is a lightweight package that adds optional security headers and cookie attributes for Python web frameworks.
|
||||
- [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.
|
||||
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for
|
||||
- [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.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||
- [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.
|
||||
|
||||
## ORM and Database Integration
|
||||
|
||||
- [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.
|
||||
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
|
||||
- [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/))
|
||||
- [Databases](https://github.com/encode/databases): Async database access for SQLAlchemy core, with support for PostgreSQL, MySQL, and SQLite.
|
||||
|
||||
## Unit Testing
|
||||
|
||||
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
|
||||
|
||||
## Project Creation Template
|
||||
|
||||
- [cookiecutter-sanic](https://github.com/harshanarayana/cookiecutter-sanic): Get your sanic application up and running in a matter of second in a well defined project structure.
|
||||
Batteries included for deployment, unit testing, automated release management and changelog generation.
|
||||
|
||||
## Templating
|
||||
|
||||
- [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.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
|
||||
|
||||
## API Helper Utilities
|
||||
|
||||
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.
|
||||
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
|
||||
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
||||
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
|
||||
|
||||
## i18n/l10n Support
|
||||
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the `Babel` library
|
||||
|
||||
## Custom Middlewares
|
||||
|
||||
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
|
||||
|
||||
## Monitoring and Reporting
|
||||
|
||||
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
||||
- [sanic-zipkin](https://github.com/kevinqqnj/sanic-zipkin): Easily report request/function/RPC traces to zipkin/jaeger, through aiozipkin.
|
||||
|
||||
|
||||
## Sample Applications
|
||||
|
||||
- [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.
|
||||
|
||||
@@ -4,8 +4,21 @@ Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
|
||||
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
|
||||
syntax, so earlier versions of python won't work.
|
||||
|
||||
1. Install Sanic: `python3 -m pip install sanic`
|
||||
2. Create a file called `main.py` with the following code:
|
||||
## 1. Install Sanic
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
```bash
|
||||
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic
|
||||
```
|
||||
|
||||
## 2. Create a file called `main.py`
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
@@ -20,9 +33,16 @@ syntax, so earlier versions of python won't work.
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
3. Run the server: `python3 main.py`
|
||||
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||
the message *Hello world!*.
|
||||
|
||||
## 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!
|
||||
|
||||
@@ -1,85 +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
|
||||
|
||||
app = Sanic('test')
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
return response.text('Hello World!')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, access_log=True)
|
||||
```
|
||||
|
||||
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**:
|
||||
|
||||
- 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
|
||||
@@ -17,7 +17,7 @@ string representing its type: `'request'` or `'response'`.
|
||||
|
||||
The simplest middleware doesn't modify the request or response at all:
|
||||
|
||||
```python
|
||||
```
|
||||
@app.middleware('request')
|
||||
async def print_on_request(request):
|
||||
print("I print when a request is received by the server")
|
||||
@@ -33,23 +33,34 @@ Middleware can modify the request or response parameter it is given, *as long
|
||||
as it does not return it*. The following example shows a practical use-case for
|
||||
this.
|
||||
|
||||
```python
|
||||
```
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.middleware('request')
|
||||
async def add_key(request):
|
||||
# Add a key to request object like dict object
|
||||
request['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.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
The above code will apply the two middleware in order. First, the middleware
|
||||
The above code will apply the three middleware in order. The first middleware
|
||||
**add_key** will add a new key `foo` into `request` object. This worked because
|
||||
`request` object can be manipulated like `dict` object. Then, the second middleware
|
||||
**custom_banner** will change the HTTP response header *Server* to
|
||||
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
|
||||
*Fake-Server*, and the last 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.
|
||||
|
||||
@@ -60,7 +71,7 @@ and the response will be returned. If this occurs to a request before the
|
||||
relevant user route handler is reached, the handler will never be called.
|
||||
Returning a response will also prevent any further middleware from running.
|
||||
|
||||
```python
|
||||
```
|
||||
@app.middleware('request')
|
||||
async def halt_request(request):
|
||||
return text('I halted the request')
|
||||
@@ -79,11 +90,11 @@ If you want to execute startup/teardown code as your server starts or closes, yo
|
||||
- `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.
|
||||
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
```
|
||||
@app.listener('before_server_start')
|
||||
async def setup_db(app, loop):
|
||||
app.db = await db_setup()
|
||||
@@ -101,16 +112,16 @@ async def close_db(app, loop):
|
||||
await app.db.close()
|
||||
```
|
||||
|
||||
It's also possible to register a listener using the `register_listener` method.
|
||||
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.
|
||||
|
||||
```python
|
||||
```
|
||||
app = Sanic()
|
||||
|
||||
|
||||
async def setup_db(app, loop):
|
||||
app.db = await db_setup()
|
||||
|
||||
|
||||
app.register_listener(setup_db, 'before_server_start')
|
||||
|
||||
```
|
||||
@@ -118,7 +129,7 @@ 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.
|
||||
|
||||
```python
|
||||
```
|
||||
async def notify_server_started_after_five_seconds():
|
||||
await asyncio.sleep(5)
|
||||
print('Server successfully started!')
|
||||
@@ -128,7 +139,7 @@ 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:
|
||||
|
||||
```python
|
||||
```
|
||||
async def notify_server_started_after_five_seconds(app):
|
||||
await asyncio.sleep(5)
|
||||
print(app.name)
|
||||
@@ -138,7 +149,7 @@ app.add_task(notify_server_started_after_five_seconds)
|
||||
|
||||
Or you can pass the app explicitly for the same effect:
|
||||
|
||||
```python
|
||||
```
|
||||
async def notify_server_started_after_five_seconds(app):
|
||||
await asyncio.sleep(5)
|
||||
print(app.name)
|
||||
|
||||
@@ -19,6 +19,8 @@ The following variables are accessible as properties on `Request` objects:
|
||||
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`).
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
@@ -28,9 +30,54 @@ The following variables are accessible as properties on `Request` objects:
|
||||
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'}`.
|
||||
- `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`
|
||||
|
||||
```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
|
||||
|
||||
```
|
||||
{
|
||||
"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
|
||||
|
||||
@@ -106,6 +153,51 @@ The following variables are accessible as properties on `Request` objects:
|
||||
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
||||
|
||||
|
||||
## 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`:
|
||||
|
||||
```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:
|
||||
|
||||
```
|
||||
{
|
||||
"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 properties which return a dictionary actually return a subclass of
|
||||
@@ -126,3 +218,40 @@ args.get('titles') # => 'Post 1'
|
||||
|
||||
args.getlist('titles') # => ['Post 1', 'Post 2']
|
||||
```
|
||||
|
||||
## 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".
|
||||
|
||||
```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:
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
@@ -164,24 +164,24 @@ url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
|
||||
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:
|
||||
- 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:
|
||||
```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
|
||||
# _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 one time
|
||||
# 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 thrown.
|
||||
- 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
|
||||
|
||||
@@ -209,7 +209,7 @@ async def feed(request, ws):
|
||||
app.add_websocket_route(my_websocket_handler, '/feed')
|
||||
```
|
||||
|
||||
Handlers for a WebSocket route are passed the request as first argument, and a
|
||||
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.
|
||||
|
||||
@@ -243,7 +243,8 @@ app.blueprint(bp)
|
||||
|
||||
## User defined route name
|
||||
|
||||
You can pass `name` to change the route name to avoid using the default name (`handler.__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.
|
||||
|
||||
```python
|
||||
|
||||
@@ -305,8 +306,8 @@ def handler(request):
|
||||
|
||||
## 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.
|
||||
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
|
||||
|
||||
```python
|
||||
|
||||
|
||||
66
docs/sanic/sockets.rst
Normal file
66
docs/sanic/sockets.rst
Normal file
@@ -0,0 +1,66 @@
|
||||
Sockets
|
||||
=======
|
||||
|
||||
Sanic can use the python
|
||||
`socket module <https://docs.python.org/3/library/socket.html>`_ to accommodate
|
||||
non IPv4 sockets.
|
||||
|
||||
IPv6 example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
import socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
sock.bind(('::', 7777))
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(sock=sock)
|
||||
|
||||
to test IPv6 ``curl -g -6 "http://[::1]:7777/"``
|
||||
|
||||
|
||||
UNIX socket example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import signal
|
||||
import sys
|
||||
import socket
|
||||
import os
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
server_socket = '/tmp/sanic.sock'
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.bind(server_socket)
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print('Exiting')
|
||||
os.unlink(server_socket)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(sock=sock)
|
||||
|
||||
to test UNIX: ``curl -v --unix-socket /tmp/sanic.sock http://localhost/hello``
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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
|
||||
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
|
||||
@@ -43,3 +43,41 @@ 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:
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
@@ -22,7 +22,7 @@ class SimpleView(HTTPMethodView):
|
||||
async def post(self, request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
@@ -33,7 +33,7 @@ class SimpleView(HTTPMethodView):
|
||||
async def handler(request):
|
||||
async def streaming(response):
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
body = body.decode('utf-8').replace('1', 'A')
|
||||
@@ -42,20 +42,33 @@ async def handler(request):
|
||||
|
||||
|
||||
@bp.put('/bp_stream', stream=True)
|
||||
async def bp_handler(request):
|
||||
async def bp_put_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
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.get()
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
|
||||
@@ -59,6 +59,23 @@ the available arguments to aiohttp can be found
|
||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
```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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
WebSocket
|
||||
=========
|
||||
|
||||
Sanic supports websockets, to setup a WebSocket:
|
||||
Sanic provides an easy to user abstraction on top of `websockets`. To setup a WebSocket:
|
||||
|
||||
.. code:: python
|
||||
|
||||
@@ -35,7 +35,7 @@ decorator:
|
||||
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``
|
||||
and ``recv`` methods to send and receive data respectively.
|
||||
|
||||
@@ -43,6 +43,7 @@ and ``recv`` methods to send and receive data respectively.
|
||||
You could setup your own WebSocket configuration through ``app.config``, like
|
||||
|
||||
.. code:: python
|
||||
|
||||
app.config.WEBSOCKET_MAX_SIZE = 2 ** 20
|
||||
app.config.WEBSOCKET_MAX_QUEUE = 32
|
||||
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
name: py35
|
||||
name: py36
|
||||
dependencies:
|
||||
- openssl=1.0.2g=0
|
||||
- pip=8.1.1=py35_0
|
||||
- python=3.5.1=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=18.1=py36_0
|
||||
- python=3.6=0
|
||||
- setuptools=40.4.3=py36_0
|
||||
- pip:
|
||||
- httptools>=0.0.10
|
||||
- uvloop>=0.5.3
|
||||
- httptools>=0.0.9
|
||||
- ujson>=1.35
|
||||
- aiofiles>=0.3.0
|
||||
- websockets>=3.2
|
||||
- websockets>=6.0,<7.0
|
||||
- multidict>=4.0,<5.0
|
||||
- sphinx==1.8.3
|
||||
- sphinx_rtd_theme==0.4.2
|
||||
- recommonmark==0.5.0
|
||||
- sphinxcontrib-asyncio>=0.2.0
|
||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
||||
- 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)
|
||||
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)
|
||||
)
|
||||
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)
|
||||
)
|
||||
2
pyproject.toml
Normal file
2
pyproject.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
304
release.py
Executable file
304
release.py
Executable file
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from collections import OrderedDict
|
||||
from configparser import RawConfigParser
|
||||
from datetime import datetime
|
||||
from json import dumps
|
||||
from os import path
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from jinja2 import Environment, BaseLoader
|
||||
from requests import patch
|
||||
|
||||
GIT_COMMANDS = {
|
||||
"get_tag": ["git describe --tags --abbrev=0"],
|
||||
"commit_version_change": [
|
||||
"git add . && git commit -m 'Bumping up version from "
|
||||
"{current_version} to {new_version}'"
|
||||
],
|
||||
"create_new_tag": [
|
||||
"git tag -a {new_version} -m 'Bumping up version from "
|
||||
"{current_version} to {new_version}'"
|
||||
],
|
||||
"push_tag": ["git push origin {new_version}"],
|
||||
"get_change_log": [
|
||||
'git log --no-merges --pretty=format:"%h::: %cn::: %s" '
|
||||
"{current_version}.."
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
RELEASE_NOTE_TEMPLATE = """
|
||||
# {{ release_name }} - {% now 'utc', '%Y-%m-%d' %}
|
||||
|
||||
To see the exhaustive list of pull requests included in this release see:
|
||||
https://github.com/huge-success/sanic/milestone/{{milestone}}?closed=1
|
||||
|
||||
# Changelog
|
||||
{% for row in changelogs %}
|
||||
* {{ row -}}
|
||||
{% endfor %}
|
||||
|
||||
# Credits
|
||||
{% for author in authors %}
|
||||
* {{ author -}}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
JINJA_RELEASE_NOTE_TEMPLATE = Environment(
|
||||
loader=BaseLoader, extensions=["jinja2_time.TimeExtension"]
|
||||
).from_string(RELEASE_NOTE_TEMPLATE)
|
||||
|
||||
RELEASE_NOTE_UPDATE_URL = (
|
||||
"https://api.github.com/repos/huge-success/sanic/releases/tags/"
|
||||
"{new_version}?access_token={token}"
|
||||
)
|
||||
|
||||
|
||||
def _run_shell_command(command: list):
|
||||
try:
|
||||
process = Popen(
|
||||
command, stderr=PIPE, stdout=PIPE, stdin=PIPE, shell=True
|
||||
)
|
||||
output, error = process.communicate()
|
||||
return_code = process.returncode
|
||||
return output.decode("utf-8"), error, return_code
|
||||
except:
|
||||
return None, None, -1
|
||||
|
||||
|
||||
def _fetch_default_calendar_release_version():
|
||||
return datetime.now().strftime("%y.%m.0")
|
||||
|
||||
|
||||
def _fetch_current_version(config_file: str) -> str:
|
||||
if path.isfile(config_file):
|
||||
config_parser = RawConfigParser()
|
||||
with open(config_file) as cfg:
|
||||
config_parser.read_file(cfg)
|
||||
return (
|
||||
config_parser.get("version", "current_version")
|
||||
or _fetch_default_calendar_release_version()
|
||||
)
|
||||
else:
|
||||
return _fetch_default_calendar_release_version()
|
||||
|
||||
|
||||
def _change_micro_version(current_version: str):
|
||||
version_string = current_version.split(".")
|
||||
version_string[-1] = str((int(version_string[-1]) + 1))
|
||||
return ".".join(version_string)
|
||||
|
||||
|
||||
def _get_new_version(
|
||||
config_file: str = "./setup.cfg",
|
||||
current_version: str = None,
|
||||
micro_release: bool = False,
|
||||
):
|
||||
if micro_release:
|
||||
if current_version:
|
||||
return _change_micro_version(current_version)
|
||||
elif config_file:
|
||||
return _change_micro_version(_fetch_current_version(config_file))
|
||||
else:
|
||||
return _fetch_default_calendar_release_version()
|
||||
else:
|
||||
return _fetch_default_calendar_release_version()
|
||||
|
||||
|
||||
def _get_current_tag(git_command_name="get_tag"):
|
||||
global GIT_COMMANDS
|
||||
command = GIT_COMMANDS.get(git_command_name)
|
||||
out, err, ret = _run_shell_command(command)
|
||||
if len(str(out)):
|
||||
return str(out).split("\n")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _update_release_version_for_sanic(
|
||||
current_version, new_version, config_file
|
||||
):
|
||||
config_parser = RawConfigParser()
|
||||
with open(config_file) as cfg:
|
||||
config_parser.read_file(cfg)
|
||||
config_parser.set("version", "current_version", new_version)
|
||||
|
||||
version_file = config_parser.get("version", "file")
|
||||
current_version_line = config_parser.get(
|
||||
"version", "current_version_pattern"
|
||||
).format(current_version=current_version)
|
||||
new_version_line = config_parser.get(
|
||||
"version", "new_version_pattern"
|
||||
).format(new_version=new_version)
|
||||
|
||||
with open(version_file) as init_file:
|
||||
data = init_file.read()
|
||||
|
||||
new_data = data.replace(current_version_line, new_version_line)
|
||||
with open(version_file, "w") as init_file:
|
||||
init_file.write(new_data)
|
||||
|
||||
with open(config_file, "w") as config:
|
||||
config_parser.write(config)
|
||||
|
||||
command = GIT_COMMANDS.get("commit_version_change")
|
||||
command[0] = command[0].format(
|
||||
new_version=new_version, current_version=current_version
|
||||
)
|
||||
_, err, ret = _run_shell_command(command)
|
||||
if int(ret) != 0:
|
||||
print(
|
||||
"Failed to Commit Version upgrade changes to Sanic: {}".format(
|
||||
err.decode("utf-8")
|
||||
)
|
||||
)
|
||||
exit(1)
|
||||
|
||||
|
||||
def _generate_change_log(current_version: str = None):
|
||||
global GIT_COMMANDS
|
||||
command = GIT_COMMANDS.get("get_change_log")
|
||||
command[0] = command[0].format(current_version=current_version)
|
||||
output, error, ret = _run_shell_command(command=command)
|
||||
if not len(str(output)):
|
||||
print("Unable to Fetch Change log details to update the Release Note")
|
||||
exit(1)
|
||||
|
||||
commit_details = OrderedDict()
|
||||
commit_details["authors"] = dict()
|
||||
commit_details["commits"] = list()
|
||||
|
||||
for line in str(output).split("\n"):
|
||||
commit, author, description = line.split(":::")
|
||||
if "GitHub" not in author:
|
||||
commit_details["authors"][author] = 1
|
||||
commit_details["commits"].append(" - ".join([commit, description]))
|
||||
|
||||
return commit_details
|
||||
|
||||
|
||||
def _generate_markdown_document(
|
||||
milestone, release_name, current_version, release_version
|
||||
):
|
||||
global JINJA_RELEASE_NOTE_TEMPLATE
|
||||
release_name = release_name or release_version
|
||||
change_log = _generate_change_log(current_version=current_version)
|
||||
return JINJA_RELEASE_NOTE_TEMPLATE.render(
|
||||
release_name=release_name,
|
||||
milestone=milestone,
|
||||
changelogs=change_log["commits"],
|
||||
authors=change_log["authors"].keys(),
|
||||
)
|
||||
|
||||
|
||||
def _tag_release(new_version, current_version, milestone, release_name, token):
|
||||
global GIT_COMMANDS
|
||||
global RELEASE_NOTE_UPDATE_URL
|
||||
for command_name in ["create_new_tag", "push_tag"]:
|
||||
command = GIT_COMMANDS.get(command_name)
|
||||
command[0] = command[0].format(
|
||||
new_version=new_version, current_version=current_version
|
||||
)
|
||||
out, error, ret = _run_shell_command(command=command)
|
||||
if int(ret) != 0:
|
||||
print("Failed to execute the command: {}".format(command[0]))
|
||||
exit(1)
|
||||
|
||||
change_log = _generate_markdown_document(
|
||||
milestone, release_name, current_version, new_version
|
||||
)
|
||||
|
||||
body = {"name": release_name or new_version, "body": change_log}
|
||||
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
response = patch(
|
||||
RELEASE_NOTE_UPDATE_URL.format(new_version=new_version, token=token),
|
||||
data=dumps(body),
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
def release(args: Namespace):
|
||||
current_tag = _get_current_tag()
|
||||
current_version = _fetch_current_version(args.config)
|
||||
if current_tag and current_version not in current_tag:
|
||||
print(
|
||||
"Tag mismatch between what's in git and what was provided by "
|
||||
"--current-version. Existing: {}, Give: {}".format(
|
||||
current_tag, current_version
|
||||
)
|
||||
)
|
||||
exit(1)
|
||||
new_version = args.release_version or _get_new_version(
|
||||
args.config, current_version, args.micro_release
|
||||
)
|
||||
_update_release_version_for_sanic(
|
||||
current_version=current_version,
|
||||
new_version=new_version,
|
||||
config_file=args.config,
|
||||
)
|
||||
_tag_release(
|
||||
current_version=current_version,
|
||||
new_version=new_version,
|
||||
milestone=args.milestone,
|
||||
release_name=args.release_name,
|
||||
token=args.token,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli = ArgumentParser(description="Sanic Release Manager")
|
||||
cli.add_argument(
|
||||
"--release-version",
|
||||
"-r",
|
||||
help="New Version to use for Release",
|
||||
default=_fetch_default_calendar_release_version(),
|
||||
required=False,
|
||||
)
|
||||
cli.add_argument(
|
||||
"--current-version",
|
||||
"-cv",
|
||||
help="Current Version to default in case if you don't want to "
|
||||
"use the version configuration files",
|
||||
default=None,
|
||||
required=False,
|
||||
)
|
||||
cli.add_argument(
|
||||
"--config",
|
||||
"-c",
|
||||
help="Configuration file used for release",
|
||||
default="./setup.cfg",
|
||||
required=False,
|
||||
)
|
||||
cli.add_argument(
|
||||
"--token",
|
||||
"-t",
|
||||
help="Git access token with necessary access to Huge Sanic Org",
|
||||
required=True,
|
||||
)
|
||||
cli.add_argument(
|
||||
"--milestone",
|
||||
"-ms",
|
||||
help="Git Release milestone information to include in relase note",
|
||||
required=True,
|
||||
)
|
||||
cli.add_argument(
|
||||
"--release-name",
|
||||
"-n",
|
||||
help="Release Name to use if any",
|
||||
required=False,
|
||||
)
|
||||
cli.add_argument(
|
||||
"--micro-release",
|
||||
"-m",
|
||||
help="Micro Release with patches only",
|
||||
default=False,
|
||||
action="store_true",
|
||||
required=False,
|
||||
)
|
||||
args = cli.parse_args()
|
||||
release(args)
|
||||
@@ -1,13 +0,0 @@
|
||||
aiofiles
|
||||
aiohttp>=2.3.0,<=3.2.1
|
||||
chardet<=2.3.0
|
||||
beautifulsoup4
|
||||
coverage
|
||||
httptools
|
||||
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
|
||||
ujson; sys_platform != "win32" and implementation_name == "cpython"
|
||||
uvloop; sys_platform != "win32" and implementation_name == "cpython"
|
||||
websockets>=5.0,<6.0
|
||||
multidict>=4.0,<5.0
|
||||
@@ -1,6 +1,7 @@
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
__version__ = '0.8.1'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
__version__ = "18.12.0"
|
||||
|
||||
__all__ = ["Sanic", "Blueprint"]
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
from argparse import ArgumentParser
|
||||
from importlib import import_module
|
||||
|
||||
from sanic.log import logger
|
||||
from sanic.app import Sanic
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser(prog='sanic')
|
||||
parser.add_argument('--host', dest='host', type=str, default='127.0.0.1')
|
||||
parser.add_argument('--port', dest='port', type=int, default=8000)
|
||||
parser.add_argument('--cert', dest='cert', type=str,
|
||||
help='location of certificate for SSL')
|
||||
parser.add_argument('--key', dest='key', type=str,
|
||||
help='location of keyfile for SSL.')
|
||||
parser.add_argument('--workers', dest='workers', type=int, default=1, )
|
||||
parser.add_argument('--debug', dest='debug', action="store_true")
|
||||
parser.add_argument('module')
|
||||
parser = ArgumentParser(prog="sanic")
|
||||
parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
|
||||
parser.add_argument("--port", dest="port", type=int, default=8000)
|
||||
parser.add_argument(
|
||||
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--key", dest="key", type=str, help="location of keyfile for SSL."
|
||||
)
|
||||
parser.add_argument("--workers", dest="workers", type=int, default=1)
|
||||
parser.add_argument("--debug", dest="debug", action="store_true")
|
||||
parser.add_argument("module")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
@@ -25,20 +28,29 @@ if __name__ == "__main__":
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
if not isinstance(app, Sanic):
|
||||
raise ValueError("Module is not a Sanic app, it is a {}. "
|
||||
"Perhaps you meant {}.app?"
|
||||
.format(type(app).__name__, args.module))
|
||||
raise ValueError(
|
||||
"Module is not a Sanic app, it is a {}. "
|
||||
"Perhaps you meant {}.app?".format(
|
||||
type(app).__name__, args.module
|
||||
)
|
||||
)
|
||||
if args.cert is not None or args.key is not None:
|
||||
ssl = {'cert': args.cert, 'key': args.key}
|
||||
ssl = {"cert": args.cert, "key": args.key}
|
||||
else:
|
||||
ssl = None
|
||||
|
||||
app.run(host=args.host, port=args.port,
|
||||
workers=args.workers, debug=args.debug, ssl=ssl)
|
||||
app.run(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
workers=args.workers,
|
||||
debug=args.debug,
|
||||
ssl=ssl,
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(e.name))
|
||||
except ValueError as e:
|
||||
logger.error("{}".format(e))
|
||||
logger.error(
|
||||
"No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app".format(e.name)
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception("Failed to run app")
|
||||
|
||||
974
sanic/app.py
974
sanic/app.py
File diff suppressed because it is too large
Load Diff
120
sanic/blueprint_group.py
Normal file
120
sanic/blueprint_group.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from collections import MutableSequence
|
||||
|
||||
|
||||
class BlueprintGroup(MutableSequence):
|
||||
"""
|
||||
This class provides a mechanism to implement a Blueprint Group
|
||||
using the `Blueprint.group` method. 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: int, item: object) -> 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: int) -> 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,28 +1,55 @@
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
FutureRoute = namedtuple('Route',
|
||||
['handler', 'uri', 'methods', 'host',
|
||||
'strict_slashes', 'stream', 'version', 'name'])
|
||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
||||
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
||||
FutureStatic = namedtuple('Route',
|
||||
['uri', 'file_or_directory', 'args', 'kwargs'])
|
||||
|
||||
FutureRoute = namedtuple(
|
||||
"FutureRoute",
|
||||
[
|
||||
"handler",
|
||||
"uri",
|
||||
"methods",
|
||||
"host",
|
||||
"strict_slashes",
|
||||
"stream",
|
||||
"version",
|
||||
"name",
|
||||
],
|
||||
)
|
||||
FutureListener = namedtuple(
|
||||
"FutureListener", ["handler", "uri", "methods", "host"]
|
||||
)
|
||||
FutureMiddleware = namedtuple(
|
||||
"FutureMiddleware", ["middleware", "args", "kwargs"]
|
||||
)
|
||||
FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"])
|
||||
FutureStatic = namedtuple(
|
||||
"FutureStatic", ["uri", "file_or_directory", "args", "kwargs"]
|
||||
)
|
||||
|
||||
|
||||
class Blueprint:
|
||||
def __init__(self, name,
|
||||
url_prefix=None,
|
||||
host=None, version=None,
|
||||
strict_slashes=False):
|
||||
"""Create a new blueprint
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
url_prefix=None,
|
||||
host=None,
|
||||
version=None,
|
||||
strict_slashes=False,
|
||||
):
|
||||
"""
|
||||
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 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.url_prefix = url_prefix
|
||||
@@ -38,30 +65,44 @@ class Blueprint:
|
||||
self.strict_slashes = strict_slashes
|
||||
|
||||
@staticmethod
|
||||
def group(*blueprints, url_prefix=''):
|
||||
"""Create a list of blueprints, optionally
|
||||
grouping them under a general URL prefix.
|
||||
def group(*blueprints, 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 url_prefix: URL route to be prepended to all sub-prefixes
|
||||
"""
|
||||
|
||||
def chain(nested):
|
||||
"""itertools.chain() but leaves strings untouched"""
|
||||
for i in nested:
|
||||
if isinstance(i, (list, tuple)):
|
||||
yield from chain(i)
|
||||
elif isinstance(i, BlueprintGroup):
|
||||
yield from i.blueprints
|
||||
else:
|
||||
yield i
|
||||
bps = []
|
||||
|
||||
bps = BlueprintGroup(url_prefix=url_prefix)
|
||||
for bp in chain(blueprints):
|
||||
if bp.url_prefix is None:
|
||||
bp.url_prefix = ""
|
||||
bp.url_prefix = url_prefix + bp.url_prefix
|
||||
bps.append(bp)
|
||||
return bps
|
||||
|
||||
def register(self, app, options):
|
||||
"""Register the blueprint to the sanic app."""
|
||||
"""
|
||||
Register the blueprint to the sanic app.
|
||||
|
||||
url_prefix = options.get('url_prefix', self.url_prefix)
|
||||
: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)
|
||||
|
||||
# Routes
|
||||
for future in self.routes:
|
||||
@@ -73,14 +114,15 @@ class Blueprint:
|
||||
|
||||
version = future.version or self.version
|
||||
|
||||
app.route(uri=uri[1:] if uri.startswith('//') else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
app.route(
|
||||
uri=uri[1:] if uri.startswith("//") else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
for future in self.websocket_routes:
|
||||
# attach the blueprint name to the handler so that it can be
|
||||
@@ -88,18 +130,19 @@ class Blueprint:
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.websocket(uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
app.websocket(
|
||||
uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
# Middleware
|
||||
for future in self.middlewares:
|
||||
if future.args or future.kwargs:
|
||||
app.register_middleware(future.middleware,
|
||||
*future.args,
|
||||
**future.kwargs)
|
||||
app.register_middleware(
|
||||
future.middleware, *future.args, **future.kwargs
|
||||
)
|
||||
else:
|
||||
app.register_middleware(future.middleware)
|
||||
|
||||
@@ -111,48 +154,85 @@ class Blueprint:
|
||||
for future in self.statics:
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.static(uri, future.file_or_directory,
|
||||
*future.args, **future.kwargs)
|
||||
app.static(
|
||||
uri, future.file_or_directory, *future.args, **future.kwargs
|
||||
)
|
||||
|
||||
# Event listeners
|
||||
for event, listeners in self.listeners.items():
|
||||
for listener in listeners:
|
||||
app.listener(event)(listener)
|
||||
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=None, stream=False, version=None, name=None):
|
||||
def route(
|
||||
self,
|
||||
uri,
|
||||
methods=frozenset({"GET"}),
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""Create a blueprint route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
: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:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(
|
||||
handler, uri, methods, host, strict_slashes, stream, version,
|
||||
name)
|
||||
handler,
|
||||
uri,
|
||||
methods,
|
||||
host,
|
||||
strict_slashes,
|
||||
stream,
|
||||
version,
|
||||
name,
|
||||
)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=None, version=None, name=None):
|
||||
def add_route(
|
||||
self,
|
||||
handler,
|
||||
uri,
|
||||
methods=frozenset({"GET"}),
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
version=None,
|
||||
name=None,
|
||||
stream=False,
|
||||
):
|
||||
"""Create a blueprint route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param version:
|
||||
: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: user defined route name for url_for
|
||||
:param stream: boolean specifying if the handler is a stream handler
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, 'view_class'):
|
||||
if hasattr(handler, "view_class"):
|
||||
methods = set()
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
@@ -166,34 +246,52 @@ class Blueprint:
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)(handler)
|
||||
self.route(
|
||||
uri=uri,
|
||||
methods=methods,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)(handler)
|
||||
return handler
|
||||
|
||||
def websocket(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
def websocket(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
"""Create a blueprint websocket route from a decorated function.
|
||||
|
||||
: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:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(handler, uri, [], host, strict_slashes,
|
||||
False, version, name)
|
||||
route = FutureRoute(
|
||||
handler, uri, [], host, strict_slashes, False, version, name
|
||||
)
|
||||
self.websocket_routes.append(route)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None, version=None,
|
||||
name=None):
|
||||
def add_websocket_route(
|
||||
self, handler, uri, host=None, version=None, name=None
|
||||
):
|
||||
"""Create a blueprint websocket route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
: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
|
||||
"""
|
||||
self.websocket(uri=uri, host=host, version=version, name=name)(handler)
|
||||
@@ -204,13 +302,23 @@ class Blueprint:
|
||||
|
||||
:param event: Event to listen to.
|
||||
"""
|
||||
|
||||
def decorator(listener):
|
||||
self.listeners[event].append(listener)
|
||||
return listener
|
||||
|
||||
return decorator
|
||||
|
||||
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):
|
||||
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
||||
self.middlewares.append(future_middleware)
|
||||
@@ -222,14 +330,32 @@ class Blueprint:
|
||||
args = []
|
||||
return register_middleware(middleware)
|
||||
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):
|
||||
"""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):
|
||||
exception = FutureException(handler, args, kwargs)
|
||||
self.exceptions.append(exception)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||
@@ -238,12 +364,12 @@ class Blueprint:
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
name = kwargs.pop('name', 'static')
|
||||
if not name.startswith(self.name + '.'):
|
||||
name = '{}.{}'.format(self.name, name)
|
||||
name = kwargs.pop("name", "static")
|
||||
if not name.startswith(self.name + "."):
|
||||
name = "{}.{}".format(self.name, name)
|
||||
kwargs.update(name=name)
|
||||
|
||||
strict_slashes = kwargs.get('strict_slashes')
|
||||
strict_slashes = kwargs.get("strict_slashes")
|
||||
if strict_slashes is None and self.strict_slashes is not None:
|
||||
kwargs.update(strict_slashes=self.strict_slashes)
|
||||
|
||||
@@ -251,44 +377,184 @@ class Blueprint:
|
||||
self.statics.append(static)
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["GET"], host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
def get(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
"""
|
||||
Add an API URL under the **GET** *HTTP* method
|
||||
|
||||
def post(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["POST"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
: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(
|
||||
uri,
|
||||
methods=frozenset({"GET"}),
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def put(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["PUT"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
def post(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""
|
||||
Add an API URL under the **POST** *HTTP* method
|
||||
|
||||
def head(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
: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(
|
||||
uri,
|
||||
methods=frozenset({"POST"}),
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def options(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
def put(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""
|
||||
Add an API URL under the **PUT** *HTTP* method
|
||||
|
||||
def patch(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
: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(
|
||||
uri,
|
||||
methods=frozenset({"PUT"}),
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def delete(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host,
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
def head(
|
||||
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(
|
||||
uri,
|
||||
methods=frozenset({"HEAD"}),
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def options(
|
||||
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(
|
||||
uri,
|
||||
methods=frozenset({"OPTIONS"}),
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def patch(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=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(
|
||||
uri,
|
||||
methods=frozenset({"PATCH"}),
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def delete(
|
||||
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(
|
||||
uri,
|
||||
methods=frozenset({"DELETE"}),
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
import os
|
||||
import types
|
||||
|
||||
from distutils.util import strtobool
|
||||
|
||||
SANIC_PREFIX = 'SANIC_'
|
||||
from sanic.exceptions import PyFileError
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
||||
super().__init__(defaults or {})
|
||||
self.LOGO = """
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \\
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
"""
|
||||
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
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
||||
defaults = defaults or {}
|
||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||
|
||||
self.LOGO = BASE_LOGO
|
||||
|
||||
if keep_alive is not None:
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
|
||||
if load_env:
|
||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||
@@ -63,9 +62,10 @@ class Config(dict):
|
||||
"""
|
||||
config_file = os.environ.get(variable_name)
|
||||
if not config_file:
|
||||
raise RuntimeError('The environment variable %r is not set and '
|
||||
'thus configuration could not be loaded.' %
|
||||
variable_name)
|
||||
raise RuntimeError(
|
||||
"The environment variable %r is not set and "
|
||||
"thus configuration could not be loaded." % variable_name
|
||||
)
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
@@ -74,15 +74,20 @@ class Config(dict):
|
||||
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
module = types.ModuleType('config')
|
||||
module = types.ModuleType("config")
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec(compile(config_file.read(), filename, 'exec'),
|
||||
module.__dict__)
|
||||
exec(
|
||||
compile(config_file.read(), filename, "exec"),
|
||||
module.__dict__,
|
||||
)
|
||||
except IOError as e:
|
||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
||||
e.strerror = "Unable to load configuration file (%s)" % e.strerror
|
||||
raise
|
||||
except Exception as e:
|
||||
raise PyFileError(filename) from e
|
||||
|
||||
self.from_object(module)
|
||||
return True
|
||||
|
||||
@@ -121,4 +126,7 @@ class Config(dict):
|
||||
try:
|
||||
self[config_key] = float(v)
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
try:
|
||||
self[config_key] = bool(strtobool(v))
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
|
||||
@@ -1 +1 @@
|
||||
HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE')
|
||||
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import re
|
||||
import string
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
DEFAULT_MAX_AGE = 0
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# SimpleCookie
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -8,18 +13,16 @@ import string
|
||||
# Straight up copied this section of dark magic from SimpleCookie
|
||||
|
||||
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
|
||||
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
|
||||
_UnescapedChars = _LegalChars + " ()/<=>?@[]{}"
|
||||
|
||||
_Translator = {n: '\\%03o' % n
|
||||
for n in set(range(256)) - set(map(ord, _UnescapedChars))}
|
||||
_Translator.update({
|
||||
ord('"'): '\\"',
|
||||
ord('\\'): '\\\\',
|
||||
})
|
||||
_Translator = {
|
||||
n: "\\%03o" % n for n in set(range(256)) - set(map(ord, _UnescapedChars))
|
||||
}
|
||||
_Translator.update({ord('"'): '\\"', ord("\\"): "\\\\"})
|
||||
|
||||
|
||||
def _quote(str):
|
||||
"""Quote a string for use in a cookie header.
|
||||
r"""Quote a string for use in a cookie header.
|
||||
If the string does not need to be double-quoted, then just return the
|
||||
string. Otherwise, surround the string in doublequotes and quote
|
||||
(with a \) special characters.
|
||||
@@ -30,7 +33,7 @@ def _quote(str):
|
||||
return '"' + str.translate(_Translator) + '"'
|
||||
|
||||
|
||||
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||
_is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# Custom SimpleCookie
|
||||
@@ -53,7 +56,7 @@ class CookieJar(dict):
|
||||
# If this cookie doesn't exist, add it to the header keys
|
||||
if not self.cookie_headers.get(key):
|
||||
cookie = Cookie(key, value)
|
||||
cookie['path'] = '/'
|
||||
cookie["path"] = "/"
|
||||
self.cookie_headers[key] = self.header_key
|
||||
self.headers.add(self.header_key, cookie)
|
||||
return super().__setitem__(key, cookie)
|
||||
@@ -62,8 +65,8 @@ class CookieJar(dict):
|
||||
|
||||
def __delitem__(self, key):
|
||||
if key not in self.cookie_headers:
|
||||
self[key] = ''
|
||||
self[key]['max-age'] = 0
|
||||
self[key] = ""
|
||||
self[key]["max-age"] = 0
|
||||
else:
|
||||
cookie_header = self.cookie_headers[key]
|
||||
# remove it from header
|
||||
@@ -77,6 +80,7 @@ class CookieJar(dict):
|
||||
|
||||
class Cookie(dict):
|
||||
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
|
||||
|
||||
_keys = {
|
||||
"expires": "expires",
|
||||
"path": "Path",
|
||||
@@ -88,7 +92,7 @@ class Cookie(dict):
|
||||
"version": "Version",
|
||||
"samesite": "SameSite",
|
||||
}
|
||||
_flags = {'secure', 'httponly'}
|
||||
_flags = {"secure", "httponly"}
|
||||
|
||||
def __init__(self, key, value):
|
||||
if key in self._keys:
|
||||
@@ -103,27 +107,44 @@ class Cookie(dict):
|
||||
if key not in self._keys:
|
||||
raise KeyError("Unknown cookie property")
|
||||
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)
|
||||
|
||||
def encode(self, encoding):
|
||||
output = ['%s=%s' % (self.key, _quote(self.value))]
|
||||
"""
|
||||
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
|
||||
"""
|
||||
output = ["%s=%s" % (self.key, _quote(self.value))]
|
||||
for key, value in self.items():
|
||||
if key == 'max-age':
|
||||
if key == "max-age":
|
||||
try:
|
||||
output.append('%s=%d' % (self._keys[key], value))
|
||||
output.append("%s=%d" % (self._keys[key], value))
|
||||
except TypeError:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
elif key == 'expires':
|
||||
try:
|
||||
output.append('%s=%s' % (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT")
|
||||
))
|
||||
except AttributeError:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
elif key == "expires":
|
||||
output.append(
|
||||
"%s=%s"
|
||||
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
|
||||
)
|
||||
elif key in self._flags and self[key]:
|
||||
output.append(self._keys[key])
|
||||
else:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
|
||||
return "; ".join(output).encode(encoding)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from sanic.http import STATUS_CODES
|
||||
from sanic.helpers import STATUS_CODES
|
||||
|
||||
TRACEBACK_STYLE = '''
|
||||
|
||||
TRACEBACK_STYLE = """
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
@@ -61,9 +62,9 @@ TRACEBACK_STYLE = '''
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
'''
|
||||
"""
|
||||
|
||||
TRACEBACK_WRAPPER_HTML = '''
|
||||
TRACEBACK_WRAPPER_HTML = """
|
||||
<html>
|
||||
<head>
|
||||
{style}
|
||||
@@ -78,27 +79,27 @@ TRACEBACK_WRAPPER_HTML = '''
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
"""
|
||||
|
||||
TRACEBACK_WRAPPER_INNER_HTML = '''
|
||||
TRACEBACK_WRAPPER_INNER_HTML = """
|
||||
<h1>{exc_name}</h1>
|
||||
<h3><code>{exc_value}</code></h3>
|
||||
<div class="tb-wrapper">
|
||||
<p class="tb-header">Traceback (most recent call last):</p>
|
||||
{frame_html}
|
||||
</div>
|
||||
'''
|
||||
"""
|
||||
|
||||
TRACEBACK_BORDER = '''
|
||||
TRACEBACK_BORDER = """
|
||||
<div class="tb-border">
|
||||
<b><i>
|
||||
The above exception was the direct cause of the
|
||||
following exception:
|
||||
</i></b>
|
||||
</div>
|
||||
'''
|
||||
"""
|
||||
|
||||
TRACEBACK_LINE_HTML = '''
|
||||
TRACEBACK_LINE_HTML = """
|
||||
<div class="frame-line">
|
||||
<p class="frame-descriptor">
|
||||
File {0.filename}, line <i>{0.lineno}</i>,
|
||||
@@ -106,15 +107,15 @@ TRACEBACK_LINE_HTML = '''
|
||||
</p>
|
||||
<p class="frame-code"><code>{0.line}</code></p>
|
||||
</div>
|
||||
'''
|
||||
"""
|
||||
|
||||
INTERNAL_SERVER_ERROR_HTML = '''
|
||||
INTERNAL_SERVER_ERROR_HTML = """
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>
|
||||
The server encountered an internal error and cannot complete
|
||||
your request.
|
||||
</p>
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
_sanic_exceptions = {}
|
||||
@@ -122,17 +123,18 @@ _sanic_exceptions = {}
|
||||
|
||||
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):
|
||||
cls.status_code = code
|
||||
_sanic_exceptions[code] = cls
|
||||
return cls
|
||||
|
||||
return class_decorator
|
||||
|
||||
|
||||
class SanicException(Exception):
|
||||
|
||||
def __init__(self, message, status_code=None):
|
||||
super().__init__(message)
|
||||
|
||||
@@ -156,8 +158,8 @@ class MethodNotSupported(SanicException):
|
||||
super().__init__(message)
|
||||
self.headers = dict()
|
||||
self.headers["Allow"] = ", ".join(allowed_methods)
|
||||
if method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
|
||||
self.headers['Content-Length'] = 0
|
||||
if method in ["HEAD", "PATCH", "PUT", "DELETE"]:
|
||||
self.headers["Content-Length"] = 0
|
||||
|
||||
|
||||
@add_status_code(500)
|
||||
@@ -169,6 +171,7 @@ class ServerError(SanicException):
|
||||
class ServiceUnavailable(SanicException):
|
||||
"""The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -192,6 +195,7 @@ class RequestTimeout(SanicException):
|
||||
the connection. The socket connection has actually been lost - the Web
|
||||
server has 'timed out' on that particular socket connection.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -209,8 +213,8 @@ class ContentRangeError(SanicException):
|
||||
def __init__(self, message, content_range):
|
||||
super().__init__(message)
|
||||
self.headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
"Content-Range": "bytes */%s" % (content_range.total,)
|
||||
"Content-Type": "text/plain",
|
||||
"Content-Range": "bytes */%s" % (content_range.total,),
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +227,11 @@ class InvalidRangeType(ContentRangeError):
|
||||
pass
|
||||
|
||||
|
||||
class PyFileError(Exception):
|
||||
def __init__(self, file):
|
||||
super().__init__("could not execute config file %s", file)
|
||||
|
||||
|
||||
@add_status_code(401)
|
||||
class Unauthorized(SanicException):
|
||||
"""
|
||||
@@ -258,13 +267,14 @@ class Unauthorized(SanicException):
|
||||
scheme="Bearer",
|
||||
realm="Restricted Area")
|
||||
"""
|
||||
|
||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||
super().__init__(message, status_code)
|
||||
|
||||
# if auth-scheme is specified, set "WWW-Authenticate" header
|
||||
if scheme is not None:
|
||||
values = ['{!s}="{!s}"'.format(k, v) for k, v in kwargs.items()]
|
||||
challenge = ', '.join(values)
|
||||
challenge = ", ".join(values)
|
||||
|
||||
self.headers = {
|
||||
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
|
||||
@@ -283,6 +293,6 @@ def abort(status_code, message=None):
|
||||
if message is None:
|
||||
message = STATUS_CODES.get(status_code)
|
||||
# These are stored as bytes in the STATUS_CODES dict
|
||||
message = message.decode('utf8')
|
||||
message = message.decode("utf8")
|
||||
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
||||
raise sanic_exception(message=message, status_code=status_code)
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import sys
|
||||
from traceback import format_exc, extract_tb
|
||||
|
||||
from traceback import extract_tb, format_exc
|
||||
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
INTERNAL_SERVER_ERROR_HTML,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
TRACEBACK_BORDER,
|
||||
TRACEBACK_LINE_HTML,
|
||||
TRACEBACK_STYLE,
|
||||
TRACEBACK_WRAPPER_HTML,
|
||||
TRACEBACK_WRAPPER_INNER_HTML,
|
||||
TRACEBACK_BORDER)
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
)
|
||||
from sanic.log import logger
|
||||
from sanic.response import text, html
|
||||
from sanic.response import html, text
|
||||
|
||||
|
||||
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
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
@@ -36,7 +50,8 @@ class ErrorHandler:
|
||||
return TRACEBACK_WRAPPER_INNER_HTML.format(
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
frame_html=''.join(frame_html))
|
||||
frame_html="".join(frame_html),
|
||||
)
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
@@ -51,13 +66,39 @@ class ErrorHandler:
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
|
||||
path=request.path)
|
||||
path=request.path,
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
def lookup(self, exception):
|
||||
handler = self.cached_handlers.get(exception, self._missing)
|
||||
"""
|
||||
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)
|
||||
if handler is self._missing:
|
||||
for exception_class, handler in self.handlers:
|
||||
if isinstance(exception, exception_class):
|
||||
@@ -71,9 +112,15 @@ class ErrorHandler:
|
||||
"""Fetches and executes an exception handler and returns a response
|
||||
object
|
||||
|
||||
:param request: Request
|
||||
:param request: Instance of :class:`sanic.request.Request`
|
||||
: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)
|
||||
response = None
|
||||
@@ -84,88 +131,130 @@ class ErrorHandler:
|
||||
response = self.default(request, exception)
|
||||
except Exception:
|
||||
self.log(format_exc())
|
||||
if self.debug:
|
||||
url = getattr(request, 'url', 'unknown')
|
||||
response_message = ('Exception raised in exception handler '
|
||||
'"%s" for uri: "%s"\n%s')
|
||||
logger.error(response_message,
|
||||
handler.__name__, url, format_exc())
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
url = "unknown"
|
||||
response_message = (
|
||||
"Exception raised in exception handler " '"%s" for uri: %s'
|
||||
)
|
||||
logger.exception(response_message, handler.__name__, url)
|
||||
|
||||
return text(response_message % (
|
||||
handler.__name__, url, format_exc()), 500)
|
||||
if self.debug:
|
||||
return text(response_message % (handler.__name__, url), 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return text("An error occurred while handling an error", 500)
|
||||
return response
|
||||
|
||||
def log(self, message, level='error'):
|
||||
def log(self, message, level="error"):
|
||||
"""
|
||||
Override this method in an ErrorHandler subclass to prevent
|
||||
logging exceptions.
|
||||
Deprecated, do not use.
|
||||
"""
|
||||
getattr(logger, level)(message)
|
||||
|
||||
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())
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
url = "unknown"
|
||||
|
||||
response_message = "Exception occurred while handling uri: %s"
|
||||
logger.exception(response_message, url)
|
||||
|
||||
if issubclass(type(exception), SanicException):
|
||||
return text(
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500),
|
||||
headers=getattr(exception, 'headers', dict())
|
||||
"Error: {}".format(exception),
|
||||
status=getattr(exception, "status_code", 500),
|
||||
headers=getattr(exception, "headers", dict()),
|
||||
)
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
response_message = ('Exception occurred while handling uri: '
|
||||
'"%s"\n%s')
|
||||
logger.error(response_message, request.url, format_exc())
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
|
||||
|
||||
class ContentRangeHandler:
|
||||
"""Class responsible for parsing request header"""
|
||||
__slots__ = ('start', 'end', 'size', 'total', 'headers')
|
||||
"""
|
||||
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")
|
||||
|
||||
def __init__(self, request, stats):
|
||||
self.total = stats.st_size
|
||||
_range = request.headers.get('Range')
|
||||
_range = request.headers.get("Range")
|
||||
if _range is None:
|
||||
raise HeaderNotFound('Range Header Not Found')
|
||||
unit, _, value = tuple(map(str.strip, _range.partition('=')))
|
||||
if unit != 'bytes':
|
||||
raise HeaderNotFound("Range Header Not Found")
|
||||
unit, _, value = tuple(map(str.strip, _range.partition("=")))
|
||||
if unit != "bytes":
|
||||
raise InvalidRangeType(
|
||||
'%s is not a valid Range Type' % (unit,), self)
|
||||
start_b, _, end_b = tuple(map(str.strip, value.partition('-')))
|
||||
"%s is not a valid Range Type" % (unit,), self
|
||||
)
|
||||
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
|
||||
try:
|
||||
self.start = int(start_b) if start_b else None
|
||||
except ValueError:
|
||||
raise ContentRangeError(
|
||||
'\'%s\' is invalid for Content Range' % (start_b,), self)
|
||||
"'%s' is invalid for Content Range" % (start_b,), self
|
||||
)
|
||||
try:
|
||||
self.end = int(end_b) if end_b else None
|
||||
except ValueError:
|
||||
raise ContentRangeError(
|
||||
'\'%s\' is invalid for Content Range' % (end_b,), self)
|
||||
"'%s' is invalid for Content Range" % (end_b,), self
|
||||
)
|
||||
if self.end is None:
|
||||
if self.start is None:
|
||||
raise ContentRangeError(
|
||||
'Invalid for Content Range parameters', self)
|
||||
"Invalid for Content Range parameters", self
|
||||
)
|
||||
else:
|
||||
# this case represents `Content-Range: bytes 5-`
|
||||
self.end = self.total
|
||||
self.end = self.total - 1
|
||||
else:
|
||||
if self.start is None:
|
||||
# this case represents `Content-Range: bytes -5`
|
||||
self.start = self.total - self.end
|
||||
self.end = self.total
|
||||
self.end = self.total - 1
|
||||
if self.start >= self.end:
|
||||
raise ContentRangeError(
|
||||
'Invalid for Content Range parameters', self)
|
||||
self.size = self.end - self.start
|
||||
"Invalid for Content Range parameters", self
|
||||
)
|
||||
self.size = self.end - self.start + 1
|
||||
self.headers = {
|
||||
'Content-Range': "bytes %s-%s/%s" % (
|
||||
self.start, self.end, self.total)}
|
||||
"Content-Range": "bytes %s-%s/%s"
|
||||
% (self.start, self.end, self.total)
|
||||
}
|
||||
|
||||
def __bool__(self):
|
||||
return self.size > 0
|
||||
|
||||
133
sanic/helpers.py
Normal file
133
sanic/helpers.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Defines basics of HTTP standard."""
|
||||
|
||||
STATUS_CODES = {
|
||||
100: b"Continue",
|
||||
101: b"Switching Protocols",
|
||||
102: b"Processing",
|
||||
200: b"OK",
|
||||
201: b"Created",
|
||||
202: b"Accepted",
|
||||
203: b"Non-Authoritative Information",
|
||||
204: b"No Content",
|
||||
205: b"Reset Content",
|
||||
206: b"Partial Content",
|
||||
207: b"Multi-Status",
|
||||
208: b"Already Reported",
|
||||
226: b"IM Used",
|
||||
300: b"Multiple Choices",
|
||||
301: b"Moved Permanently",
|
||||
302: b"Found",
|
||||
303: b"See Other",
|
||||
304: b"Not Modified",
|
||||
305: b"Use Proxy",
|
||||
307: b"Temporary Redirect",
|
||||
308: b"Permanent Redirect",
|
||||
400: b"Bad Request",
|
||||
401: b"Unauthorized",
|
||||
402: b"Payment Required",
|
||||
403: b"Forbidden",
|
||||
404: b"Not Found",
|
||||
405: b"Method Not Allowed",
|
||||
406: b"Not Acceptable",
|
||||
407: b"Proxy Authentication Required",
|
||||
408: b"Request Timeout",
|
||||
409: b"Conflict",
|
||||
410: b"Gone",
|
||||
411: b"Length Required",
|
||||
412: b"Precondition Failed",
|
||||
413: b"Request Entity Too Large",
|
||||
414: b"Request-URI Too Long",
|
||||
415: b"Unsupported Media Type",
|
||||
416: b"Requested Range Not Satisfiable",
|
||||
417: b"Expectation Failed",
|
||||
418: b"I'm a teapot",
|
||||
422: b"Unprocessable Entity",
|
||||
423: b"Locked",
|
||||
424: b"Failed Dependency",
|
||||
426: b"Upgrade Required",
|
||||
428: b"Precondition Required",
|
||||
429: b"Too Many Requests",
|
||||
431: b"Request Header Fields Too Large",
|
||||
451: b"Unavailable For Legal Reasons",
|
||||
500: b"Internal Server Error",
|
||||
501: b"Not Implemented",
|
||||
502: b"Bad Gateway",
|
||||
503: b"Service Unavailable",
|
||||
504: b"Gateway Timeout",
|
||||
505: b"HTTP Version Not Supported",
|
||||
506: b"Variant Also Negotiates",
|
||||
507: b"Insufficient Storage",
|
||||
508: b"Loop Detected",
|
||||
510: b"Not Extended",
|
||||
511: b"Network Authentication Required",
|
||||
}
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-7.1
|
||||
_ENTITY_HEADERS = frozenset(
|
||||
[
|
||||
"allow",
|
||||
"content-encoding",
|
||||
"content-language",
|
||||
"content-length",
|
||||
"content-location",
|
||||
"content-md5",
|
||||
"content-range",
|
||||
"content-type",
|
||||
"expires",
|
||||
"last-modified",
|
||||
"extension-header",
|
||||
]
|
||||
)
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-13.5.1
|
||||
_HOP_BY_HOP_HEADERS = frozenset(
|
||||
[
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailers",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def has_message_body(status):
|
||||
"""
|
||||
According to the following RFC message body and length SHOULD NOT
|
||||
be included in responses status 1XX, 204 and 304.
|
||||
https://tools.ietf.org/html/rfc2616#section-4.4
|
||||
https://tools.ietf.org/html/rfc2616#section-4.3
|
||||
"""
|
||||
return status not in (204, 304) and not (100 <= status < 200)
|
||||
|
||||
|
||||
def is_entity_header(header):
|
||||
"""Checks if the given header is an Entity Header"""
|
||||
return header.lower() in _ENTITY_HEADERS
|
||||
|
||||
|
||||
def is_hop_by_hop_header(header):
|
||||
"""Checks if the given header is a Hop By Hop header"""
|
||||
return header.lower() in _HOP_BY_HOP_HEADERS
|
||||
|
||||
|
||||
def remove_entity_headers(headers, allowed=("content-location", "expires")):
|
||||
"""
|
||||
Removes all the entity headers present in the headers given.
|
||||
According to RFC 2616 Section 10.3.5,
|
||||
Content-Location and Expires are allowed as for the
|
||||
"strong cache validator".
|
||||
https://tools.ietf.org/html/rfc2616#section-10.3.5
|
||||
|
||||
returns the headers without the entity headers
|
||||
"""
|
||||
allowed = set([h.lower() for h in allowed])
|
||||
headers = {
|
||||
header: value
|
||||
for header, value in headers.items()
|
||||
if not is_entity_header(header) or header.lower() in allowed
|
||||
}
|
||||
return headers
|
||||
128
sanic/http.py
128
sanic/http.py
@@ -1,128 +0,0 @@
|
||||
"""Defines basics of HTTP standard."""
|
||||
|
||||
STATUS_CODES = {
|
||||
100: b'Continue',
|
||||
101: b'Switching Protocols',
|
||||
102: b'Processing',
|
||||
200: b'OK',
|
||||
201: b'Created',
|
||||
202: b'Accepted',
|
||||
203: b'Non-Authoritative Information',
|
||||
204: b'No Content',
|
||||
205: b'Reset Content',
|
||||
206: b'Partial Content',
|
||||
207: b'Multi-Status',
|
||||
208: b'Already Reported',
|
||||
226: b'IM Used',
|
||||
300: b'Multiple Choices',
|
||||
301: b'Moved Permanently',
|
||||
302: b'Found',
|
||||
303: b'See Other',
|
||||
304: b'Not Modified',
|
||||
305: b'Use Proxy',
|
||||
307: b'Temporary Redirect',
|
||||
308: b'Permanent Redirect',
|
||||
400: b'Bad Request',
|
||||
401: b'Unauthorized',
|
||||
402: b'Payment Required',
|
||||
403: b'Forbidden',
|
||||
404: b'Not Found',
|
||||
405: b'Method Not Allowed',
|
||||
406: b'Not Acceptable',
|
||||
407: b'Proxy Authentication Required',
|
||||
408: b'Request Timeout',
|
||||
409: b'Conflict',
|
||||
410: b'Gone',
|
||||
411: b'Length Required',
|
||||
412: b'Precondition Failed',
|
||||
413: b'Request Entity Too Large',
|
||||
414: b'Request-URI Too Long',
|
||||
415: b'Unsupported Media Type',
|
||||
416: b'Requested Range Not Satisfiable',
|
||||
417: b'Expectation Failed',
|
||||
418: b'I\'m a teapot',
|
||||
422: b'Unprocessable Entity',
|
||||
423: b'Locked',
|
||||
424: b'Failed Dependency',
|
||||
426: b'Upgrade Required',
|
||||
428: b'Precondition Required',
|
||||
429: b'Too Many Requests',
|
||||
431: b'Request Header Fields Too Large',
|
||||
451: b'Unavailable For Legal Reasons',
|
||||
500: b'Internal Server Error',
|
||||
501: b'Not Implemented',
|
||||
502: b'Bad Gateway',
|
||||
503: b'Service Unavailable',
|
||||
504: b'Gateway Timeout',
|
||||
505: b'HTTP Version Not Supported',
|
||||
506: b'Variant Also Negotiates',
|
||||
507: b'Insufficient Storage',
|
||||
508: b'Loop Detected',
|
||||
510: b'Not Extended',
|
||||
511: b'Network Authentication Required'
|
||||
}
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-7.1
|
||||
_ENTITY_HEADERS = frozenset([
|
||||
'allow',
|
||||
'content-encoding',
|
||||
'content-language',
|
||||
'content-length',
|
||||
'content-location',
|
||||
'content-md5',
|
||||
'content-range',
|
||||
'content-type',
|
||||
'expires',
|
||||
'last-modified',
|
||||
'extension-header'
|
||||
])
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-13.5.1
|
||||
_HOP_BY_HOP_HEADERS = frozenset([
|
||||
'connection',
|
||||
'keep-alive',
|
||||
'proxy-authenticate',
|
||||
'proxy-authorization',
|
||||
'te',
|
||||
'trailers',
|
||||
'transfer-encoding',
|
||||
'upgrade'
|
||||
])
|
||||
|
||||
|
||||
def has_message_body(status):
|
||||
"""
|
||||
According to the following RFC message body and length SHOULD NOT
|
||||
be included in responses status 1XX, 204 and 304.
|
||||
https://tools.ietf.org/html/rfc2616#section-4.4
|
||||
https://tools.ietf.org/html/rfc2616#section-4.3
|
||||
"""
|
||||
return status not in (204, 304) and not (100 <= status < 200)
|
||||
|
||||
|
||||
def is_entity_header(header):
|
||||
"""Checks if the given header is an Entity Header"""
|
||||
return header.lower() in _ENTITY_HEADERS
|
||||
|
||||
|
||||
def is_hop_by_hop_header(header):
|
||||
"""Checks if the given header is a Hop By Hop header"""
|
||||
return header.lower() in _HOP_BY_HOP_HEADERS
|
||||
|
||||
|
||||
def remove_entity_headers(headers,
|
||||
allowed=('content-location', 'expires')):
|
||||
"""
|
||||
Removes all the entity headers present in the headers given.
|
||||
According to RFC 2616 Section 10.3.5,
|
||||
Content-Location and Expires are allowed as for the
|
||||
"strong cache validator".
|
||||
https://tools.ietf.org/html/rfc2616#section-10.3.5
|
||||
|
||||
returns the headers without the entity headers
|
||||
"""
|
||||
allowed = set([h.lower() for h in allowed])
|
||||
headers = {header: value for header, value in headers.items()
|
||||
if not is_entity_header(header)
|
||||
and header.lower() not in allowed}
|
||||
return headers
|
||||
35
sanic/log.py
35
sanic/log.py
@@ -5,59 +5,54 @@ import sys
|
||||
LOGGING_CONFIG_DEFAULTS = dict(
|
||||
version=1,
|
||||
disable_existing_loggers=False,
|
||||
|
||||
loggers={
|
||||
"root": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"]
|
||||
},
|
||||
"sanic.root": {"level": "INFO", "handlers": ["console"]},
|
||||
"sanic.error": {
|
||||
"level": "INFO",
|
||||
"handlers": ["error_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.error"
|
||||
"qualname": "sanic.error",
|
||||
},
|
||||
|
||||
"sanic.access": {
|
||||
"level": "INFO",
|
||||
"handlers": ["access_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.access"
|
||||
}
|
||||
"qualname": "sanic.access",
|
||||
},
|
||||
},
|
||||
handlers={
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stdout
|
||||
"stream": sys.stdout,
|
||||
},
|
||||
"error_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stderr
|
||||
"stream": sys.stderr,
|
||||
},
|
||||
"access_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "access",
|
||||
"stream": sys.stdout
|
||||
"stream": sys.stdout,
|
||||
},
|
||||
},
|
||||
formatters={
|
||||
"generic": {
|
||||
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
"class": "logging.Formatter"
|
||||
"class": "logging.Formatter",
|
||||
},
|
||||
"access": {
|
||||
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " +
|
||||
"%(request)s %(message)s %(status)d %(byte)d",
|
||||
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
|
||||
+ "%(request)s %(message)s %(status)d %(byte)d",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
"class": "logging.Formatter"
|
||||
"class": "logging.Formatter",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger('root')
|
||||
error_logger = logging.getLogger('sanic.error')
|
||||
access_logger = logging.getLogger('sanic.access')
|
||||
logger = logging.getLogger("sanic.root")
|
||||
error_logger = logging.getLogger("sanic.error")
|
||||
access_logger = logging.getLogger("sanic.access")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import subprocess
|
||||
from time import sleep
|
||||
import sys
|
||||
|
||||
from multiprocessing import Process
|
||||
from time import sleep
|
||||
|
||||
|
||||
def _iter_module_files():
|
||||
@@ -18,7 +19,7 @@ def _iter_module_files():
|
||||
for module in list(sys.modules.values()):
|
||||
if module is None:
|
||||
continue
|
||||
filename = getattr(module, '__file__', None)
|
||||
filename = getattr(module, "__file__", None)
|
||||
if filename:
|
||||
old = None
|
||||
while not os.path.isfile(filename):
|
||||
@@ -27,7 +28,7 @@ def _iter_module_files():
|
||||
if filename == old:
|
||||
break
|
||||
else:
|
||||
if filename[-4:] in ('.pyc', '.pyo'):
|
||||
if filename[-4:] in (".pyc", ".pyo"):
|
||||
filename = filename[:-1]
|
||||
yield filename
|
||||
|
||||
@@ -35,7 +36,15 @@ def _iter_module_files():
|
||||
def _get_args_for_reloading():
|
||||
"""Returns the 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
|
||||
|
||||
|
||||
@@ -43,13 +52,16 @@ def restart_with_reloader():
|
||||
"""Create a new process and a subprocess in it with the same arguments as
|
||||
this one.
|
||||
"""
|
||||
cwd = os.getcwd()
|
||||
args = _get_args_for_reloading()
|
||||
new_environ = os.environ.copy()
|
||||
new_environ['SANIC_SERVER_RUNNING'] = 'true'
|
||||
cmd = ' '.join(args)
|
||||
new_environ["SANIC_SERVER_RUNNING"] = "true"
|
||||
cmd = " ".join(args)
|
||||
worker_process = Process(
|
||||
target=subprocess.call, args=(cmd,),
|
||||
kwargs=dict(shell=True, env=new_environ))
|
||||
target=subprocess.call,
|
||||
args=(cmd,),
|
||||
kwargs={"cwd": cwd, "shell": True, "env": new_environ},
|
||||
)
|
||||
worker_process.start()
|
||||
return worker_process
|
||||
|
||||
@@ -67,8 +79,10 @@ def kill_process_children_unix(pid):
|
||||
children_list_pid = children_list_file.read().split()
|
||||
|
||||
for child_pid in children_list_pid:
|
||||
children_proc_path = "/proc/%s/task/%s/children" % \
|
||||
(child_pid, child_pid)
|
||||
children_proc_path = "/proc/%s/task/%s/children" % (
|
||||
child_pid,
|
||||
child_pid,
|
||||
)
|
||||
if not os.path.isfile(children_proc_path):
|
||||
continue
|
||||
with open(children_proc_path) as children_list_file_2:
|
||||
@@ -90,7 +104,7 @@ def kill_process_children_osx(pid):
|
||||
:param pid: PID of parent process (process ID)
|
||||
:return: Nothing
|
||||
"""
|
||||
subprocess.run(['pkill', '-P', str(pid)])
|
||||
subprocess.run(["pkill", "-P", str(pid)])
|
||||
|
||||
|
||||
def kill_process_children(pid):
|
||||
@@ -99,12 +113,12 @@ def kill_process_children(pid):
|
||||
:param pid: PID of parent process (process ID)
|
||||
:return: Nothing
|
||||
"""
|
||||
if sys.platform == 'darwin':
|
||||
if sys.platform == "darwin":
|
||||
kill_process_children_osx(pid)
|
||||
elif sys.platform == 'linux':
|
||||
elif sys.platform == "linux":
|
||||
kill_process_children_unix(pid)
|
||||
else:
|
||||
pass # should signal error here
|
||||
pass # should signal error here
|
||||
|
||||
|
||||
def kill_program_completly(proc):
|
||||
@@ -127,9 +141,11 @@ def watchdog(sleep_interval):
|
||||
mtimes = {}
|
||||
worker_process = restart_with_reloader()
|
||||
signal.signal(
|
||||
signal.SIGTERM, lambda *args: kill_program_completly(worker_process))
|
||||
signal.SIGTERM, lambda *args: kill_program_completly(worker_process)
|
||||
)
|
||||
signal.signal(
|
||||
signal.SIGINT, lambda *args: kill_program_completly(worker_process))
|
||||
signal.SIGINT, lambda *args: kill_program_completly(worker_process)
|
||||
)
|
||||
while True:
|
||||
for filename in _iter_module_files():
|
||||
try:
|
||||
|
||||
358
sanic/request.py
358
sanic/request.py
@@ -1,24 +1,32 @@
|
||||
import sys
|
||||
import asyncio
|
||||
import email.utils
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from cgi import parse_header
|
||||
from collections import namedtuple
|
||||
from collections import defaultdict, namedtuple
|
||||
from http.cookies import SimpleCookie
|
||||
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
||||
|
||||
from httptools import parse_url
|
||||
from urllib.parse import parse_qs, urlunparse
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import error_logger, logger
|
||||
|
||||
|
||||
try:
|
||||
from ujson import loads as json_loads
|
||||
except ImportError:
|
||||
if sys.version_info[:2] == (3, 5):
|
||||
|
||||
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
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import error_logger, logger
|
||||
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
@@ -42,13 +50,49 @@ class RequestParameters(dict):
|
||||
return super().get(name, default)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class Request(dict):
|
||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||
|
||||
__slots__ = (
|
||||
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr',
|
||||
'_socket', '_port', '__weakref__', 'raw_url'
|
||||
"__weakref__",
|
||||
"_cookies",
|
||||
"_ip",
|
||||
"_parsed_url",
|
||||
"_port",
|
||||
"_remote_addr",
|
||||
"_socket",
|
||||
"app",
|
||||
"body",
|
||||
"endpoint",
|
||||
"headers",
|
||||
"method",
|
||||
"parsed_args",
|
||||
"parsed_not_grouped_args",
|
||||
"parsed_files",
|
||||
"parsed_form",
|
||||
"parsed_json",
|
||||
"raw_url",
|
||||
"stream",
|
||||
"transport",
|
||||
"uri_template",
|
||||
"version",
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
@@ -63,27 +107,36 @@ class Request(dict):
|
||||
self.transport = transport
|
||||
|
||||
# Init but do not inhale
|
||||
self.body = []
|
||||
self.body_init()
|
||||
self.parsed_json = None
|
||||
self.parsed_form = 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._cookies = None
|
||||
self.stream = None
|
||||
self.endpoint = None
|
||||
|
||||
def __repr__(self):
|
||||
if self.method is None or not self.path:
|
||||
return '<{0}>'.format(self.__class__.__name__)
|
||||
return '<{0}: {1} {2}>'.format(self.__class__.__name__,
|
||||
self.method,
|
||||
self.path)
|
||||
return "<{0}: {1} {2}>".format(
|
||||
self.__class__.__name__, self.method, self.path
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
if self.transport:
|
||||
return True
|
||||
return False
|
||||
|
||||
def body_init(self):
|
||||
self.body = []
|
||||
|
||||
def body_push(self, data):
|
||||
self.body.append(data)
|
||||
|
||||
def body_finish(self):
|
||||
self.body = b"".join(self.body)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
if self.parsed_json is None:
|
||||
@@ -107,8 +160,8 @@ class Request(dict):
|
||||
|
||||
:return: token related to request
|
||||
"""
|
||||
prefixes = ('Bearer', 'Token')
|
||||
auth_header = self.headers.get('Authorization')
|
||||
prefixes = ("Bearer", "Token")
|
||||
auth_header = self.headers.get("Authorization")
|
||||
|
||||
if auth_header is not None:
|
||||
for prefix in prefixes:
|
||||
@@ -123,17 +176,20 @@ class Request(dict):
|
||||
self.parsed_form = RequestParameters()
|
||||
self.parsed_files = RequestParameters()
|
||||
content_type = self.headers.get(
|
||||
'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
|
||||
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
|
||||
)
|
||||
content_type, parameters = parse_header(content_type)
|
||||
try:
|
||||
if content_type == 'application/x-www-form-urlencoded':
|
||||
if content_type == "application/x-www-form-urlencoded":
|
||||
self.parsed_form = RequestParameters(
|
||||
parse_qs(self.body.decode('utf-8')))
|
||||
elif content_type == 'multipart/form-data':
|
||||
parse_qs(self.body.decode("utf-8"))
|
||||
)
|
||||
elif content_type == "multipart/form-data":
|
||||
# TODO: Stream this instead of reading to/from memory
|
||||
boundary = parameters['boundary'].encode('utf-8')
|
||||
self.parsed_form, self.parsed_files = (
|
||||
parse_multipart_form(self.body, boundary))
|
||||
boundary = parameters["boundary"].encode("utf-8")
|
||||
self.parsed_form, self.parsed_files = parse_multipart_form(
|
||||
self.body, boundary
|
||||
)
|
||||
except Exception:
|
||||
error_logger.exception("Failed when parsing form")
|
||||
|
||||
@@ -146,64 +202,156 @@ class Request(dict):
|
||||
|
||||
return self.parsed_files
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
if self.parsed_args is None:
|
||||
def get_args(
|
||||
self,
|
||||
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:
|
||||
self.parsed_args = RequestParameters(
|
||||
parse_qs(self.query_string))
|
||||
else:
|
||||
self.parsed_args = RequestParameters()
|
||||
return self.parsed_args
|
||||
self.parsed_args[
|
||||
(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,
|
||||
)
|
||||
)
|
||||
|
||||
return self.parsed_args[
|
||||
(keep_blank_values, strict_parsing, encoding, errors)
|
||||
]
|
||||
|
||||
args = property(get_args)
|
||||
|
||||
@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()}
|
||||
|
||||
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
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
cookie = self.headers.get('Cookie')
|
||||
cookie = self.headers.get("Cookie")
|
||||
if cookie is not None:
|
||||
cookies = SimpleCookie()
|
||||
cookies.load(cookie)
|
||||
self._cookies = {name: cookie.value
|
||||
for name, cookie in cookies.items()}
|
||||
self._cookies = {
|
||||
name: cookie.value for name, cookie in cookies.items()
|
||||
}
|
||||
else:
|
||||
self._cookies = {}
|
||||
return self._cookies
|
||||
|
||||
@property
|
||||
def ip(self):
|
||||
if not hasattr(self, '_socket'):
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._ip
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
if not hasattr(self, '_socket'):
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._port
|
||||
|
||||
@property
|
||||
def socket(self):
|
||||
if not hasattr(self, '_socket'):
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._socket
|
||||
|
||||
def _get_address(self):
|
||||
sock = self.transport.get_extra_info('socket')
|
||||
|
||||
if sock.family == socket.AF_INET:
|
||||
self._socket = (self.transport.get_extra_info('peername') or
|
||||
(None, None))
|
||||
self._ip, self._port = self._socket
|
||||
elif sock.family == socket.AF_INET6:
|
||||
self._socket = (self.transport.get_extra_info('peername') or
|
||||
(None, None, None, None))
|
||||
self._ip, self._port, *_ = self._socket
|
||||
else:
|
||||
self._ip, self._port = (None, None)
|
||||
self._socket = self.transport.get_extra_info("peername") or (
|
||||
None,
|
||||
None,
|
||||
)
|
||||
self._ip = self._socket[0]
|
||||
self._port = self._socket[1]
|
||||
|
||||
@property
|
||||
def remote_addr(self):
|
||||
@@ -211,29 +359,31 @@ class Request(dict):
|
||||
|
||||
:return: original client ip.
|
||||
"""
|
||||
if not hasattr(self, '_remote_addr'):
|
||||
forwarded_for = self.headers.get('X-Forwarded-For', '').split(',')
|
||||
if not hasattr(self, "_remote_addr"):
|
||||
forwarded_for = self.headers.get("X-Forwarded-For", "").split(",")
|
||||
remote_addrs = [
|
||||
addr for addr in [
|
||||
addr.strip() for addr in forwarded_for
|
||||
] if addr
|
||||
]
|
||||
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 = ''
|
||||
self._remote_addr = ""
|
||||
return self._remote_addr
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
if self.app.websocket_enabled \
|
||||
and self.headers.get('upgrade') == 'websocket':
|
||||
scheme = 'ws'
|
||||
if (
|
||||
self.app.websocket_enabled
|
||||
and self.headers.get("upgrade") == "websocket"
|
||||
):
|
||||
scheme = "ws"
|
||||
else:
|
||||
scheme = 'http'
|
||||
scheme = "http"
|
||||
|
||||
if self.transport.get_extra_info('sslcontext'):
|
||||
scheme += 's'
|
||||
if self.transport.get_extra_info("sslcontext"):
|
||||
scheme += "s"
|
||||
|
||||
return scheme
|
||||
|
||||
@@ -241,11 +391,11 @@ class Request(dict):
|
||||
def host(self):
|
||||
# it appears that httptools doesn't return the host
|
||||
# so pull it from the headers
|
||||
return self.headers.get('Host', '')
|
||||
return self.headers.get("Host", "")
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return self.headers.get('Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
|
||||
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def match_info(self):
|
||||
@@ -254,27 +404,23 @@ class Request(dict):
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._parsed_url.path.decode('utf-8')
|
||||
return self._parsed_url.path.decode("utf-8")
|
||||
|
||||
@property
|
||||
def query_string(self):
|
||||
if self._parsed_url.query:
|
||||
return self._parsed_url.query.decode('utf-8')
|
||||
return self._parsed_url.query.decode("utf-8")
|
||||
else:
|
||||
return ''
|
||||
return ""
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return urlunparse((
|
||||
self.scheme,
|
||||
self.host,
|
||||
self.path,
|
||||
None,
|
||||
self.query_string,
|
||||
None))
|
||||
return urlunparse(
|
||||
(self.scheme, self.host, self.path, None, self.query_string, None)
|
||||
)
|
||||
|
||||
|
||||
File = namedtuple('File', ['type', 'body', 'name'])
|
||||
File = namedtuple("File", ["type", "body", "name"])
|
||||
|
||||
|
||||
def parse_multipart_form(body, boundary):
|
||||
@@ -290,49 +436,59 @@ def parse_multipart_form(body, boundary):
|
||||
form_parts = body.split(boundary)
|
||||
for form_part in form_parts[1:-1]:
|
||||
file_name = None
|
||||
content_type = 'text/plain'
|
||||
content_charset = 'utf-8'
|
||||
content_type = "text/plain"
|
||||
content_charset = "utf-8"
|
||||
field_name = None
|
||||
line_index = 2
|
||||
line_end_index = 0
|
||||
while not line_end_index == -1:
|
||||
line_end_index = form_part.find(b'\r\n', line_index)
|
||||
form_line = form_part[line_index:line_end_index].decode('utf-8')
|
||||
line_end_index = form_part.find(b"\r\n", line_index)
|
||||
form_line = form_part[line_index:line_end_index].decode("utf-8")
|
||||
line_index = line_end_index + 2
|
||||
|
||||
if not form_line:
|
||||
break
|
||||
|
||||
colon_index = form_line.index(':')
|
||||
colon_index = form_line.index(":")
|
||||
form_header_field = form_line[0:colon_index].lower()
|
||||
form_header_value, form_parameters = parse_header(
|
||||
form_line[colon_index + 2:])
|
||||
form_line[colon_index + 2 :]
|
||||
)
|
||||
|
||||
if form_header_field == 'content-disposition':
|
||||
file_name = form_parameters.get('filename')
|
||||
field_name = form_parameters.get('name')
|
||||
elif form_header_field == 'content-type':
|
||||
if form_header_field == "content-disposition":
|
||||
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":
|
||||
content_type = form_header_value
|
||||
content_charset = form_parameters.get('charset', 'utf-8')
|
||||
content_charset = form_parameters.get("charset", "utf-8")
|
||||
|
||||
if field_name:
|
||||
post_data = form_part[line_index:-4]
|
||||
if file_name:
|
||||
form_file = File(type=content_type,
|
||||
name=file_name,
|
||||
body=post_data)
|
||||
if field_name in files:
|
||||
files[field_name].append(form_file)
|
||||
else:
|
||||
files[field_name] = [form_file]
|
||||
else:
|
||||
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(
|
||||
type=content_type, name=file_name, body=post_data
|
||||
)
|
||||
if field_name in files:
|
||||
files[field_name].append(form_file)
|
||||
else:
|
||||
files[field_name] = [form_file]
|
||||
else:
|
||||
logger.debug('Form-data field does not have a \'name\' parameter \
|
||||
in the Content-Disposition header')
|
||||
logger.debug(
|
||||
"Form-data field does not have a 'name' parameter "
|
||||
"in the Content-Disposition header"
|
||||
)
|
||||
|
||||
return fields, files
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
from functools import partial
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except BaseException:
|
||||
from json import dumps as json_dumps
|
||||
|
||||
from aiofiles import open as open_async
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from sanic import http
|
||||
from sanic.cookies import CookieJar
|
||||
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
|
||||
|
||||
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except BaseException:
|
||||
from json import dumps
|
||||
|
||||
# This is done in order to ensure that the JSON response is
|
||||
# kept consistent across both ujson and inbuilt json usage.
|
||||
json_dumps = partial(dumps, separators=(",", ":"))
|
||||
|
||||
|
||||
class BaseHTTPResponse:
|
||||
@@ -24,16 +30,18 @@ class BaseHTTPResponse:
|
||||
return str(data).encode()
|
||||
|
||||
def _parse_headers(self):
|
||||
headers = b''
|
||||
headers = b""
|
||||
for name, value in self.headers.items():
|
||||
try:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (
|
||||
name.encode(), value.encode('utf-8')))
|
||||
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')))
|
||||
headers += b"%b: %b\r\n" % (
|
||||
str(name).encode(),
|
||||
str(value).encode("utf-8"),
|
||||
)
|
||||
|
||||
return headers
|
||||
|
||||
@@ -46,12 +54,17 @@ class BaseHTTPResponse:
|
||||
|
||||
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = (
|
||||
'protocol', 'streaming_fn', 'status',
|
||||
'content_type', 'headers', '_cookies'
|
||||
"protocol",
|
||||
"streaming_fn",
|
||||
"status",
|
||||
"content_type",
|
||||
"headers",
|
||||
"_cookies",
|
||||
)
|
||||
|
||||
def __init__(self, streaming_fn, status=200, headers=None,
|
||||
content_type='text/plain'):
|
||||
def __init__(
|
||||
self, streaming_fn, status=200, headers=None, content_type="text/plain"
|
||||
):
|
||||
self.content_type = content_type
|
||||
self.streaming_fn = streaming_fn
|
||||
self.status = status
|
||||
@@ -66,61 +79,69 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
if type(data) != bytes:
|
||||
data = self._encode_body(data)
|
||||
|
||||
self.protocol.push_data(
|
||||
b"%x\r\n%b\r\n" % (len(data), data))
|
||||
self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
||||
await self.protocol.drain()
|
||||
|
||||
async def stream(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None
|
||||
):
|
||||
"""Streams headers, runs the `streaming_fn` callback that writes
|
||||
content to the response body, then finalizes the response body.
|
||||
"""
|
||||
headers = self.get_headers(
|
||||
version, keep_alive=keep_alive,
|
||||
keep_alive_timeout=keep_alive_timeout)
|
||||
version,
|
||||
keep_alive=keep_alive,
|
||||
keep_alive_timeout=keep_alive_timeout,
|
||||
)
|
||||
self.protocol.push_data(headers)
|
||||
await self.protocol.drain()
|
||||
await self.streaming_fn(self)
|
||||
self.protocol.push_data(b'0\r\n\r\n')
|
||||
self.protocol.push_data(b"0\r\n\r\n")
|
||||
# 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.
|
||||
|
||||
def get_headers(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None
|
||||
):
|
||||
# This is all returned in a kind-of funky way
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
timeout_header = b""
|
||||
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'
|
||||
self.headers.pop('Content-Length', None)
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
self.headers["Transfer-Encoding"] = "chunked"
|
||||
self.headers.pop("Content-Length", None)
|
||||
self.headers["Content-Type"] = self.headers.get(
|
||||
"Content-Type", self.content_type
|
||||
)
|
||||
|
||||
headers = self._parse_headers()
|
||||
|
||||
if self.status is 200:
|
||||
status = b'OK'
|
||||
if self.status == 200:
|
||||
status = b"OK"
|
||||
else:
|
||||
status = http.STATUS_CODES.get(self.status)
|
||||
status = STATUS_CODES.get(self.status)
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'%b'
|
||||
b'%b\r\n') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
timeout_header,
|
||||
headers
|
||||
)
|
||||
return (b"HTTP/%b %d %b\r\n" b"%b" b"%b\r\n") % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
timeout_header,
|
||||
headers,
|
||||
)
|
||||
|
||||
|
||||
class HTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||
__slots__ = ("body", "status", "content_type", "headers", "_cookies")
|
||||
|
||||
def __init__(self, body=None, status=200, headers=None,
|
||||
content_type='text/plain', body_bytes=b''):
|
||||
def __init__(
|
||||
self,
|
||||
body=None,
|
||||
status=200,
|
||||
headers=None,
|
||||
content_type="text/plain",
|
||||
body_bytes=b"",
|
||||
):
|
||||
self.content_type = content_type
|
||||
|
||||
if body is not None:
|
||||
@@ -132,46 +153,45 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
self.headers = CIMultiDict(headers or {})
|
||||
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):
|
||||
# This is all returned in a kind-of funky way
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
timeout_header = b""
|
||||
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
|
||||
|
||||
body = b''
|
||||
if http.has_message_body(self.status):
|
||||
body = b""
|
||||
if has_message_body(self.status):
|
||||
body = self.body
|
||||
self.headers['Content-Length'] = self.headers.get(
|
||||
'Content-Length', len(self.body))
|
||||
self.headers["Content-Length"] = self.headers.get(
|
||||
"Content-Length", len(self.body)
|
||||
)
|
||||
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
self.headers["Content-Type"] = self.headers.get(
|
||||
"Content-Type", self.content_type
|
||||
)
|
||||
|
||||
if self.status in (304, 412):
|
||||
self.headers = http.remove_entity_headers(self.headers)
|
||||
self.headers = remove_entity_headers(self.headers)
|
||||
|
||||
headers = self._parse_headers()
|
||||
|
||||
if self.status is 200:
|
||||
status = b'OK'
|
||||
if self.status == 200:
|
||||
status = b"OK"
|
||||
else:
|
||||
status = http.STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
|
||||
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'Connection: %b\r\n'
|
||||
b'%b'
|
||||
b'%b\r\n'
|
||||
b'%b') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
b'keep-alive' if keep_alive else b'close',
|
||||
timeout_header,
|
||||
headers,
|
||||
body
|
||||
)
|
||||
return (
|
||||
b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b"
|
||||
) % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
b"keep-alive" if keep_alive else b"close",
|
||||
timeout_header,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
@@ -180,9 +200,14 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
return self._cookies
|
||||
|
||||
|
||||
def json(body, status=200, headers=None,
|
||||
content_type="application/json", dumps=json_dumps,
|
||||
**kwargs):
|
||||
def json(
|
||||
body,
|
||||
status=200,
|
||||
headers=None,
|
||||
content_type="application/json",
|
||||
dumps=json_dumps,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Returns response object with body in json format.
|
||||
|
||||
@@ -191,12 +216,17 @@ def json(body, status=200, headers=None,
|
||||
:param headers: Custom Headers.
|
||||
:param kwargs: Remaining arguments that are passed to the json encoder.
|
||||
"""
|
||||
return HTTPResponse(dumps(body, **kwargs), headers=headers,
|
||||
status=status, content_type=content_type)
|
||||
return HTTPResponse(
|
||||
dumps(body, **kwargs),
|
||||
headers=headers,
|
||||
status=status,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
def text(body, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
def text(
|
||||
body, status=200, headers=None, content_type="text/plain; charset=utf-8"
|
||||
):
|
||||
"""
|
||||
Returns response object with body in text format.
|
||||
|
||||
@@ -206,12 +236,13 @@ def text(body, status=200, headers=None,
|
||||
:param content_type: the content type (string) of the response
|
||||
"""
|
||||
return HTTPResponse(
|
||||
body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
body, status=status, headers=headers, content_type=content_type
|
||||
)
|
||||
|
||||
|
||||
def raw(body, status=200, headers=None,
|
||||
content_type="application/octet-stream"):
|
||||
def raw(
|
||||
body, status=200, headers=None, content_type="application/octet-stream"
|
||||
):
|
||||
"""
|
||||
Returns response object without encoding the body.
|
||||
|
||||
@@ -220,8 +251,12 @@ def raw(body, status=200, headers=None,
|
||||
:param headers: Custom Headers.
|
||||
:param content_type: the content type (string) of the response.
|
||||
"""
|
||||
return HTTPResponse(body_bytes=body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
return HTTPResponse(
|
||||
body_bytes=body,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
def html(body, status=200, headers=None):
|
||||
@@ -232,12 +267,22 @@ def html(body, status=200, headers=None):
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/html; charset=utf-8")
|
||||
return HTTPResponse(
|
||||
body,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type="text/html; charset=utf-8",
|
||||
)
|
||||
|
||||
|
||||
async def file(location, status=200, mime_type=None, headers=None,
|
||||
filename=None, _range=None):
|
||||
async def file(
|
||||
location,
|
||||
status=200,
|
||||
mime_type=None,
|
||||
headers=None,
|
||||
filename=None,
|
||||
_range=None,
|
||||
):
|
||||
"""Return a response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
@@ -249,28 +294,41 @@ async def file(location, status=200, mime_type=None, headers=None,
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(filename))
|
||||
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
|
||||
)
|
||||
filename = filename or path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
async with open_async(location, mode="rb") as _file:
|
||||
if _range:
|
||||
await _file.seek(_range.start)
|
||||
out_stream = await _file.read(_range.size)
|
||||
headers['Content-Range'] = 'bytes %s-%s/%s' % (
|
||||
_range.start, _range.end, _range.total)
|
||||
headers["Content-Range"] = "bytes %s-%s/%s" % (
|
||||
_range.start,
|
||||
_range.end,
|
||||
_range.total,
|
||||
)
|
||||
status = 206
|
||||
else:
|
||||
out_stream = await _file.read()
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
return HTTPResponse(status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream)
|
||||
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
|
||||
return HTTPResponse(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream,
|
||||
)
|
||||
|
||||
|
||||
async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
|
||||
headers=None, filename=None, _range=None):
|
||||
async def file_stream(
|
||||
location,
|
||||
status=200,
|
||||
chunk_size=4096,
|
||||
mime_type=None,
|
||||
headers=None,
|
||||
filename=None,
|
||||
_range=None,
|
||||
):
|
||||
"""Return a streaming response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
@@ -283,11 +341,11 @@ async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(filename))
|
||||
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
|
||||
)
|
||||
filename = filename or path.split(location)[-1]
|
||||
|
||||
_file = await open_async(location, mode='rb')
|
||||
_file = await open_async(location, mode="rb")
|
||||
|
||||
async def _streaming_fn(response):
|
||||
nonlocal _file, chunk_size
|
||||
@@ -312,19 +370,28 @@ async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
|
||||
await _file.close()
|
||||
return # Returning from this fn closes the stream
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
|
||||
if _range:
|
||||
headers['Content-Range'] = 'bytes %s-%s/%s' % (
|
||||
_range.start, _range.end, _range.total)
|
||||
return StreamingHTTPResponse(streaming_fn=_streaming_fn,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type)
|
||||
headers["Content-Range"] = "bytes %s-%s/%s" % (
|
||||
_range.start,
|
||||
_range.end,
|
||||
_range.total,
|
||||
)
|
||||
status = 206
|
||||
return StreamingHTTPResponse(
|
||||
streaming_fn=_streaming_fn,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
)
|
||||
|
||||
|
||||
def stream(
|
||||
streaming_fn, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
streaming_fn,
|
||||
status=200,
|
||||
headers=None,
|
||||
content_type="text/plain; charset=utf-8",
|
||||
):
|
||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||
|
||||
@@ -344,15 +411,13 @@ def stream(
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return StreamingHTTPResponse(
|
||||
streaming_fn,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
status=status
|
||||
streaming_fn, headers=headers, content_type=content_type, status=status
|
||||
)
|
||||
|
||||
|
||||
def redirect(to, headers=None, status=302,
|
||||
content_type="text/html; charset=utf-8"):
|
||||
def redirect(
|
||||
to, headers=None, status=302, content_type="text/html; charset=utf-8"
|
||||
):
|
||||
"""Abort execution and cause a 302 redirect (by default).
|
||||
|
||||
:param to: path or fully qualified URL to redirect to
|
||||
@@ -364,12 +429,11 @@ def redirect(to, headers=None, status=302,
|
||||
headers = headers or {}
|
||||
|
||||
# URL Quote the URL before redirecting
|
||||
safe_to = quote_plus(to, safe=":/#?&=@[]!$&'()*+,;")
|
||||
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
|
||||
|
||||
# According to RFC 7231, a relative URI is now permitted.
|
||||
headers['Location'] = safe_to
|
||||
headers["Location"] = safe_to
|
||||
|
||||
return HTTPResponse(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type)
|
||||
status=status, headers=headers, content_type=content_type
|
||||
)
|
||||
|
||||
208
sanic/router.py
208
sanic/router.py
@@ -1,33 +1,38 @@
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections.abc import Iterable
|
||||
from functools import lru_cache
|
||||
from urllib.parse import unquote
|
||||
|
||||
from sanic.exceptions import NotFound, MethodNotSupported
|
||||
from sanic.exceptions import MethodNotSupported, NotFound
|
||||
from sanic.views import CompositionView
|
||||
|
||||
|
||||
Route = namedtuple(
|
||||
'Route',
|
||||
['handler', 'methods', 'pattern', 'parameters', 'name', 'uri'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
"Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
|
||||
)
|
||||
Parameter = namedtuple("Parameter", ["name", "cast"])
|
||||
|
||||
REGEX_TYPES = {
|
||||
'string': (str, r'[^/]+'),
|
||||
'int': (int, r'\d+'),
|
||||
'number': (float, r'[0-9\\.]+'),
|
||||
'alpha': (str, r'[A-Za-z]+'),
|
||||
'path': (str, r'[^/].*?'),
|
||||
'uuid': (uuid.UUID, r'[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-'
|
||||
r'[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}')
|
||||
"string": (str, r"[^/]+"),
|
||||
"int": (int, r"-?\d+"),
|
||||
"number": (float, r"-?[0-9\\.]+"),
|
||||
"alpha": (str, r"[A-Za-z]+"),
|
||||
"path": (str, r"[^/].*?"),
|
||||
"uuid": (
|
||||
uuid.UUID,
|
||||
r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-"
|
||||
r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}",
|
||||
),
|
||||
}
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
|
||||
|
||||
def url_hash(url):
|
||||
return url.count('/')
|
||||
return url.count("/")
|
||||
|
||||
|
||||
class RouteExists(Exception):
|
||||
@@ -38,6 +43,10 @@ class RouteDoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ParameterNameConflicts(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Router:
|
||||
"""Router supports basic routing with parameters and method checks
|
||||
|
||||
@@ -64,10 +73,11 @@ class Router:
|
||||
also be passed in as the type. The argument given to the function will
|
||||
always be a string, independent of the type.
|
||||
"""
|
||||
|
||||
routes_static = None
|
||||
routes_dynamic = None
|
||||
routes_always_check = None
|
||||
parameter_pattern = re.compile(r'<(.+?)>')
|
||||
parameter_pattern = re.compile(r"<(.+?)>")
|
||||
|
||||
def __init__(self):
|
||||
self.routes_all = {}
|
||||
@@ -94,9 +104,9 @@ class Router:
|
||||
"""
|
||||
# We could receive NAME or NAME:PATTERN
|
||||
name = parameter_string
|
||||
pattern = 'string'
|
||||
if ':' in parameter_string:
|
||||
name, pattern = parameter_string.split(':', 1)
|
||||
pattern = "string"
|
||||
if ":" in parameter_string:
|
||||
name, pattern = parameter_string.split(":", 1)
|
||||
if not name:
|
||||
raise ValueError(
|
||||
"Invalid parameter syntax: {}".format(parameter_string)
|
||||
@@ -108,8 +118,16 @@ class Router:
|
||||
|
||||
return name, _type, pattern
|
||||
|
||||
def add(self, uri, methods, handler, host=None, strict_slashes=False,
|
||||
version=None, name=None):
|
||||
def add(
|
||||
self,
|
||||
uri,
|
||||
methods,
|
||||
handler,
|
||||
host=None,
|
||||
strict_slashes=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: path to match
|
||||
@@ -123,8 +141,8 @@ class Router:
|
||||
:return: Nothing
|
||||
"""
|
||||
if version is not None:
|
||||
version = re.escape(str(version).strip('/').lstrip('v'))
|
||||
uri = "/".join(["/v{}".format(version), uri.lstrip('/')])
|
||||
version = re.escape(str(version).strip("/").lstrip("v"))
|
||||
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host, name)
|
||||
|
||||
@@ -139,28 +157,26 @@ class Router:
|
||||
return
|
||||
|
||||
# Add versions with and without trailing /
|
||||
slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
|
||||
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
|
||||
unslashed_methods = self.routes_all.get(uri[:-1], frozenset({}))
|
||||
if isinstance(methods, Iterable):
|
||||
_slash_is_missing = all(method in slashed_methods for
|
||||
method in methods)
|
||||
_without_slash_is_missing = all(method in unslashed_methods for
|
||||
method in methods)
|
||||
_slash_is_missing = all(
|
||||
method in slashed_methods for method in methods
|
||||
)
|
||||
_without_slash_is_missing = all(
|
||||
method in unslashed_methods for method in methods
|
||||
)
|
||||
else:
|
||||
_slash_is_missing = methods in slashed_methods
|
||||
_without_slash_is_missing = methods in unslashed_methods
|
||||
|
||||
slash_is_missing = (
|
||||
not uri[-1] == '/' and not _slash_is_missing
|
||||
)
|
||||
slash_is_missing = not uri[-1] == "/" and not _slash_is_missing
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == '/' and not
|
||||
_without_slash_is_missing and not
|
||||
uri == '/'
|
||||
uri[-1] == "/" and not _without_slash_is_missing and not uri == "/"
|
||||
)
|
||||
# add version with trailing slash
|
||||
if slash_is_missing:
|
||||
self._add(uri + '/', methods, handler, host, name)
|
||||
self._add(uri + "/", methods, handler, host, name)
|
||||
# add version without trailing slash
|
||||
elif without_slash_is_missing:
|
||||
self._add(uri[:-1], methods, handler, host, name)
|
||||
@@ -183,8 +199,10 @@ class Router:
|
||||
|
||||
else:
|
||||
if not isinstance(host, Iterable):
|
||||
raise ValueError("Expected either string or Iterable of "
|
||||
"host strings, not {!r}".format(host))
|
||||
raise ValueError(
|
||||
"Expected either string or Iterable of "
|
||||
"host strings, not {!r}".format(host)
|
||||
)
|
||||
|
||||
for host_ in host:
|
||||
self.add(uri, methods, handler, host_, name)
|
||||
@@ -195,40 +213,48 @@ class Router:
|
||||
methods = frozenset(methods)
|
||||
|
||||
parameters = []
|
||||
parameter_names = set()
|
||||
properties = {"unhashable": None}
|
||||
|
||||
def add_parameter(match):
|
||||
name = match.group(1)
|
||||
name, _type, pattern = self.parse_parameter_string(name)
|
||||
|
||||
parameter = Parameter(
|
||||
name=name, cast=_type)
|
||||
if name in parameter_names:
|
||||
raise ParameterNameConflicts(
|
||||
"Multiple parameter named <{name}> "
|
||||
"in route uri {uri}".format(name=name, uri=uri)
|
||||
)
|
||||
parameter_names.add(name)
|
||||
|
||||
parameter = Parameter(name=name, cast=_type)
|
||||
parameters.append(parameter)
|
||||
|
||||
# Mark the whole route as unhashable if it has the hash key in it
|
||||
if re.search(r'(^|[^^]){1}/', pattern):
|
||||
properties['unhashable'] = True
|
||||
if re.search(r"(^|[^^]){1}/", pattern):
|
||||
properties["unhashable"] = True
|
||||
# Mark the route as unhashable if it matches the hash key
|
||||
elif re.search(r'/', pattern):
|
||||
properties['unhashable'] = True
|
||||
elif re.search(r"/", pattern):
|
||||
properties["unhashable"] = True
|
||||
|
||||
return '({})'.format(pattern)
|
||||
return "({})".format(pattern)
|
||||
|
||||
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
|
||||
pattern = re.compile(r'^{}$'.format(pattern_string))
|
||||
pattern = re.compile(r"^{}$".format(pattern_string))
|
||||
|
||||
def merge_route(route, methods, handler):
|
||||
# merge to the existing route when possible.
|
||||
if not route.methods or not methods:
|
||||
# method-unspecified routes are not mergeable.
|
||||
raise RouteExists(
|
||||
"Route already registered: {}".format(uri))
|
||||
raise RouteExists("Route already registered: {}".format(uri))
|
||||
elif route.methods.intersection(methods):
|
||||
# already existing method is not overloadable.
|
||||
duplicated = methods.intersection(route.methods)
|
||||
raise RouteExists(
|
||||
"Route already registered: {} [{}]".format(
|
||||
uri, ','.join(list(duplicated))))
|
||||
uri, ",".join(list(duplicated))
|
||||
)
|
||||
)
|
||||
if isinstance(route.handler, CompositionView):
|
||||
view = route.handler
|
||||
else:
|
||||
@@ -236,19 +262,22 @@ class Router:
|
||||
view.add(route.methods, route.handler)
|
||||
view.add(methods, handler)
|
||||
route = route._replace(
|
||||
handler=view, methods=methods.union(route.methods))
|
||||
handler=view, methods=methods.union(route.methods)
|
||||
)
|
||||
return route
|
||||
|
||||
if parameters:
|
||||
# TODO: This is too complex, we need to reduce the complexity
|
||||
if properties['unhashable']:
|
||||
if properties["unhashable"]:
|
||||
routes_to_check = self.routes_always_check
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check, parameters)
|
||||
pattern, routes_to_check, parameters
|
||||
)
|
||||
else:
|
||||
routes_to_check = self.routes_dynamic[url_hash(uri)]
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check, parameters)
|
||||
pattern, routes_to_check, parameters
|
||||
)
|
||||
if ndx != -1:
|
||||
# Pop the ndx of the route, no dups of the same route
|
||||
routes_to_check.pop(ndx)
|
||||
@@ -259,35 +288,41 @@ class Router:
|
||||
# if available
|
||||
# special prefix for static files
|
||||
is_static = False
|
||||
if name and name.startswith('_static_'):
|
||||
if name and name.startswith("_static_"):
|
||||
is_static = True
|
||||
name = name.split('_static_', 1)[-1]
|
||||
name = name.split("_static_", 1)[-1]
|
||||
|
||||
if hasattr(handler, '__blueprintname__'):
|
||||
handler_name = '{}.{}'.format(
|
||||
handler.__blueprintname__, name or handler.__name__)
|
||||
if hasattr(handler, "__blueprintname__"):
|
||||
handler_name = "{}.{}".format(
|
||||
handler.__blueprintname__, name or handler.__name__
|
||||
)
|
||||
else:
|
||||
handler_name = name or getattr(handler, '__name__', None)
|
||||
handler_name = name or getattr(handler, "__name__", None)
|
||||
|
||||
if route:
|
||||
route = merge_route(route, methods, handler)
|
||||
else:
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters, name=handler_name, uri=uri)
|
||||
handler=handler,
|
||||
methods=methods,
|
||||
pattern=pattern,
|
||||
parameters=parameters,
|
||||
name=handler_name,
|
||||
uri=uri,
|
||||
)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if is_static:
|
||||
pair = self.routes_static_files.get(handler_name)
|
||||
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
|
||||
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
|
||||
self.routes_static_files[handler_name] = (uri, route)
|
||||
|
||||
else:
|
||||
pair = self.routes_names.get(handler_name)
|
||||
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
|
||||
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
|
||||
self.routes_names[handler_name] = (uri, route)
|
||||
|
||||
if properties['unhashable']:
|
||||
if properties["unhashable"]:
|
||||
self.routes_always_check.append(route)
|
||||
elif parameters:
|
||||
self.routes_dynamic[url_hash(uri)].append(route)
|
||||
@@ -296,6 +331,17 @@ class Router:
|
||||
|
||||
@staticmethod
|
||||
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):
|
||||
if route.pattern == pattern and route.parameters == parameters:
|
||||
return ndx, route
|
||||
@@ -322,8 +368,10 @@ class Router:
|
||||
|
||||
if route in self.routes_always_check:
|
||||
self.routes_always_check.remove(route)
|
||||
elif url_hash(uri) in self.routes_dynamic \
|
||||
and route in self.routes_dynamic[url_hash(uri)]:
|
||||
elif (
|
||||
url_hash(uri) in self.routes_dynamic
|
||||
and route in self.routes_dynamic[url_hash(uri)]
|
||||
):
|
||||
self.routes_dynamic[url_hash(uri)].remove(route)
|
||||
else:
|
||||
self.routes_static.pop(uri)
|
||||
@@ -342,7 +390,7 @@ class Router:
|
||||
if not view_name:
|
||||
return (None, None)
|
||||
|
||||
if view_name == 'static' or view_name.endswith('.static'):
|
||||
if view_name == "static" or view_name.endswith(".static"):
|
||||
return self.routes_static_files.get(name, (None, None))
|
||||
|
||||
return self.routes_names.get(view_name, (None, None))
|
||||
@@ -356,14 +404,15 @@ class Router:
|
||||
"""
|
||||
# No virtual hosts specified; default behavior
|
||||
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
|
||||
try:
|
||||
return self._get(request.path, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
return self._get(
|
||||
request.path, request.method, request.headers.get("Host", "")
|
||||
)
|
||||
# try default hosts
|
||||
except NotFound:
|
||||
return self._get(request.path, request.method, '')
|
||||
return self._get(request.path, request.method, "")
|
||||
|
||||
def get_supported_methods(self, url):
|
||||
"""Get a list of supported methods for a url and optional host.
|
||||
@@ -373,7 +422,7 @@ class Router:
|
||||
"""
|
||||
route = self.routes_all.get(url)
|
||||
# if methods are None then this logic will prevent an error
|
||||
return getattr(route, 'methods', None) or frozenset()
|
||||
return getattr(route, "methods", None) or frozenset()
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(self, url, method, host):
|
||||
@@ -388,9 +437,10 @@ class Router:
|
||||
# Check against known static routes
|
||||
route = self.routes_static.get(url)
|
||||
method_not_supported = MethodNotSupported(
|
||||
'Method {} not allowed for URL {}'.format(method, url),
|
||||
"Method {} not allowed for URL {}".format(method, url),
|
||||
method=method,
|
||||
allowed_methods=self.get_supported_methods(url))
|
||||
allowed_methods=self.get_supported_methods(url),
|
||||
)
|
||||
if route:
|
||||
if route.methods and method not in route.methods:
|
||||
raise method_not_supported
|
||||
@@ -416,13 +466,14 @@ class Router:
|
||||
# Route was found but the methods didn't match
|
||||
if route_found:
|
||||
raise method_not_supported
|
||||
raise NotFound('Requested URL {} not found'.format(url))
|
||||
raise NotFound("Requested URL {} not found".format(url))
|
||||
|
||||
kwargs = {p.name: p.cast(value)
|
||||
for value, p
|
||||
in zip(match.groups(1), route.parameters)}
|
||||
kwargs = {
|
||||
p.name: p.cast(value)
|
||||
for value, p in zip(match.groups(1), route.parameters)
|
||||
}
|
||||
route_handler = route.handler
|
||||
if hasattr(route_handler, 'handlers'):
|
||||
if hasattr(route_handler, "handlers"):
|
||||
route_handler = route_handler.handlers[method]
|
||||
return route_handler, [], kwargs, route.uri
|
||||
|
||||
@@ -435,7 +486,8 @@ class Router:
|
||||
handler = self.get(request)[0]
|
||||
except (NotFound, MethodNotSupported):
|
||||
return False
|
||||
if (hasattr(handler, 'view_class') and
|
||||
hasattr(handler.view_class, request.method.lower())):
|
||||
if hasattr(handler, "view_class") and hasattr(
|
||||
handler.view_class, request.method.lower()
|
||||
):
|
||||
handler = getattr(handler.view_class, request.method.lower())
|
||||
return hasattr(handler, 'is_stream')
|
||||
return hasattr(handler, "is_stream")
|
||||
|
||||
511
sanic/server.py
511
sanic/server.py
@@ -1,69 +1,111 @@
|
||||
import asyncio
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from multiprocessing import Process
|
||||
from signal import (
|
||||
SIGTERM, SIGINT, SIG_IGN,
|
||||
signal as signal_func,
|
||||
Signals
|
||||
)
|
||||
from socket import (
|
||||
socket,
|
||||
SOL_SOCKET,
|
||||
SO_REUSEADDR,
|
||||
)
|
||||
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
|
||||
from signal import signal as signal_func
|
||||
from socket import SO_REUSEADDR, SOL_SOCKET, socket
|
||||
from time import time
|
||||
|
||||
from httptools import HttpRequestParser
|
||||
from httptools.parser.errors import HttpParserError
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from sanic.exceptions import (
|
||||
InvalidUsage,
|
||||
PayloadTooLarge,
|
||||
RequestTimeout,
|
||||
ServerError,
|
||||
ServiceUnavailable,
|
||||
)
|
||||
from sanic.log import access_logger, logger
|
||||
from sanic.request import Request, StreamBuffer
|
||||
from sanic.response import HTTPResponse
|
||||
|
||||
|
||||
try:
|
||||
import uvloop
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from sanic.log import logger, access_logger
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.request import Request
|
||||
from sanic.exceptions import (
|
||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError,
|
||||
ServiceUnavailable)
|
||||
|
||||
current_time = None
|
||||
|
||||
|
||||
class Signal:
|
||||
stopped = False
|
||||
|
||||
|
||||
class HttpProtocol(asyncio.Protocol):
|
||||
"""
|
||||
This class provides a basic HTTP implementation of the sanic framework.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
# event loop, connection
|
||||
'loop', 'transport', 'connections', 'signal',
|
||||
"loop",
|
||||
"transport",
|
||||
"connections",
|
||||
"signal",
|
||||
# request params
|
||||
'parser', 'request', 'url', 'headers',
|
||||
"parser",
|
||||
"request",
|
||||
"url",
|
||||
"headers",
|
||||
# request config
|
||||
'request_handler', 'request_timeout', 'response_timeout',
|
||||
'keep_alive_timeout', 'request_max_size', 'request_class',
|
||||
'is_request_stream', 'router',
|
||||
"request_handler",
|
||||
"request_timeout",
|
||||
"response_timeout",
|
||||
"keep_alive_timeout",
|
||||
"request_max_size",
|
||||
"request_buffer_queue_size",
|
||||
"request_class",
|
||||
"is_request_stream",
|
||||
"router",
|
||||
"error_handler",
|
||||
# enable or disable access log purpose
|
||||
'access_log',
|
||||
"access_log",
|
||||
# connection management
|
||||
'_total_request_size', '_request_timeout_handler',
|
||||
'_response_timeout_handler', '_keep_alive_timeout_handler',
|
||||
'_last_request_time', '_last_response_time', '_is_stream_handler',
|
||||
'_not_paused')
|
||||
"_total_request_size",
|
||||
"_request_timeout_handler",
|
||||
"_response_timeout_handler",
|
||||
"_keep_alive_timeout_handler",
|
||||
"_last_request_time",
|
||||
"_last_response_time",
|
||||
"_is_stream_handler",
|
||||
"_not_paused",
|
||||
"_request_handler_task",
|
||||
"_request_stream_task",
|
||||
"_keep_alive",
|
||||
"_header_fragment",
|
||||
"state",
|
||||
"_debug",
|
||||
)
|
||||
|
||||
def __init__(self, *, loop, request_handler, error_handler,
|
||||
signal=Signal(), connections=set(), request_timeout=60,
|
||||
response_timeout=60, keep_alive_timeout=5,
|
||||
request_max_size=None, request_class=None, access_log=True,
|
||||
keep_alive=True, is_request_stream=False, router=None,
|
||||
state=None, debug=False, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
loop,
|
||||
request_handler,
|
||||
error_handler,
|
||||
signal=Signal(),
|
||||
connections=None,
|
||||
request_timeout=60,
|
||||
response_timeout=60,
|
||||
keep_alive_timeout=5,
|
||||
request_max_size=None,
|
||||
request_buffer_queue_size=100,
|
||||
request_class=None,
|
||||
access_log=True,
|
||||
keep_alive=True,
|
||||
is_request_stream=False,
|
||||
router=None,
|
||||
state=None,
|
||||
debug=False,
|
||||
**kwargs
|
||||
):
|
||||
self.loop = loop
|
||||
self.transport = None
|
||||
self.request = None
|
||||
@@ -73,10 +115,11 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.router = router
|
||||
self.signal = signal
|
||||
self.access_log = access_log
|
||||
self.connections = connections
|
||||
self.connections = connections or set()
|
||||
self.request_handler = request_handler
|
||||
self.error_handler = error_handler
|
||||
self.request_timeout = request_timeout
|
||||
self.request_buffer_queue_size = request_buffer_queue_size
|
||||
self.response_timeout = response_timeout
|
||||
self.keep_alive_timeout = keep_alive_timeout
|
||||
self.request_max_size = request_max_size
|
||||
@@ -93,19 +136,27 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._request_handler_task = None
|
||||
self._request_stream_task = None
|
||||
self._keep_alive = keep_alive
|
||||
self._header_fragment = b''
|
||||
self._header_fragment = b""
|
||||
self.state = state if state else {}
|
||||
if 'requests_count' not in self.state:
|
||||
self.state['requests_count'] = 0
|
||||
if "requests_count" not in self.state:
|
||||
self.state["requests_count"] = 0
|
||||
self._debug = debug
|
||||
self._not_paused.set()
|
||||
|
||||
@property
|
||||
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 (
|
||||
self._keep_alive and
|
||||
not self.signal.stopped and
|
||||
self.parser.should_keep_alive())
|
||||
self._keep_alive
|
||||
and not self.signal.stopped
|
||||
and self.parser.should_keep_alive()
|
||||
)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Connection
|
||||
@@ -114,12 +165,17 @@ class HttpProtocol(asyncio.Protocol):
|
||||
def connection_made(self, transport):
|
||||
self.connections.add(self)
|
||||
self._request_timeout_handler = self.loop.call_later(
|
||||
self.request_timeout, self.request_timeout_callback)
|
||||
self.request_timeout, self.request_timeout_callback
|
||||
)
|
||||
self.transport = transport
|
||||
self._last_request_time = current_time
|
||||
self._last_request_time = time()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.connections.discard(self)
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
if self._request_timeout_handler:
|
||||
self._request_timeout_handler.cancel()
|
||||
if self._response_timeout_handler:
|
||||
@@ -138,55 +194,51 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# exactly what this timeout is checking for.
|
||||
# Check if elapsed time since request initiated exceeds our
|
||||
# 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:
|
||||
time_left = self.request_timeout - time_elapsed
|
||||
self._request_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.request_timeout_callback)
|
||||
self._request_timeout_handler = self.loop.call_later(
|
||||
time_left, self.request_timeout_callback
|
||||
)
|
||||
else:
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
try:
|
||||
raise RequestTimeout('Request Timeout')
|
||||
except RequestTimeout as exception:
|
||||
self.write_error(exception)
|
||||
self.write_error(RequestTimeout("Request Timeout"))
|
||||
|
||||
def response_timeout_callback(self):
|
||||
# Check if elapsed time since response was initiated exceeds our
|
||||
# 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:
|
||||
time_left = self.response_timeout - time_elapsed
|
||||
self._response_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.response_timeout_callback)
|
||||
self._response_timeout_handler = self.loop.call_later(
|
||||
time_left, self.response_timeout_callback
|
||||
)
|
||||
else:
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
try:
|
||||
raise ServiceUnavailable('Response Timeout')
|
||||
except ServiceUnavailable as exception:
|
||||
self.write_error(exception)
|
||||
self.write_error(ServiceUnavailable("Response Timeout"))
|
||||
|
||||
def keep_alive_timeout_callback(self):
|
||||
# Check if elapsed time since last response exceeds our configured
|
||||
# maximum keep alive timeout value
|
||||
time_elapsed = current_time - self._last_response_time
|
||||
"""
|
||||
Check if elapsed time since last response exceeds our configured
|
||||
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:
|
||||
time_left = self.keep_alive_timeout - time_elapsed
|
||||
self._keep_alive_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
time_left, self.keep_alive_timeout_callback
|
||||
)
|
||||
else:
|
||||
logger.debug('KeepAlive Timeout. Closing connection.')
|
||||
logger.debug("KeepAlive Timeout. Closing connection.")
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
|
||||
@@ -199,8 +251,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# memory limits
|
||||
self._total_request_size += len(data)
|
||||
if self._total_request_size > self.request_max_size:
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
self.write_error(PayloadTooLarge("Payload Too Large"))
|
||||
|
||||
# Create parser if this is the first time we're receiving data
|
||||
if self.parser is None:
|
||||
@@ -209,17 +260,16 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.parser = HttpRequestParser(self)
|
||||
|
||||
# requests count
|
||||
self.state['requests_count'] = self.state['requests_count'] + 1
|
||||
self.state["requests_count"] = self.state["requests_count"] + 1
|
||||
|
||||
# Parse request chunk or close connection
|
||||
try:
|
||||
self.parser.feed_data(data)
|
||||
except HttpParserError:
|
||||
message = 'Bad Request'
|
||||
message = "Bad Request"
|
||||
if self._debug:
|
||||
message += '\n' + traceback.format_exc()
|
||||
exception = InvalidUsage(message)
|
||||
self.write_error(exception)
|
||||
message += "\n" + traceback.format_exc()
|
||||
self.write_error(InvalidUsage(message))
|
||||
|
||||
def on_url(self, url):
|
||||
if not self.url:
|
||||
@@ -231,18 +281,20 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._header_fragment += name
|
||||
|
||||
if value is not None:
|
||||
if self._header_fragment == b'Content-Length' \
|
||||
and int(value) > self.request_max_size:
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
if (
|
||||
self._header_fragment == b"Content-Length"
|
||||
and int(value) > self.request_max_size
|
||||
):
|
||||
self.write_error(PayloadTooLarge("Payload Too Large"))
|
||||
try:
|
||||
value = value.decode()
|
||||
except UnicodeDecodeError:
|
||||
value = value.decode('latin_1')
|
||||
value = value.decode("latin_1")
|
||||
self.headers.append(
|
||||
(self._header_fragment.decode().casefold(), value))
|
||||
(self._header_fragment.decode().casefold(), value)
|
||||
)
|
||||
|
||||
self._header_fragment = b''
|
||||
self._header_fragment = b""
|
||||
|
||||
def on_headers_complete(self):
|
||||
self.request = self.request_class(
|
||||
@@ -250,7 +302,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
headers=CIMultiDict(self.headers),
|
||||
version=self.parser.get_http_version(),
|
||||
method=self.parser.get_method().decode(),
|
||||
transport=self.transport
|
||||
transport=self.transport,
|
||||
)
|
||||
# Remove any existing KeepAlive handler here,
|
||||
# It will be recreated if required on the new request.
|
||||
@@ -259,17 +311,29 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._keep_alive_timeout_handler = None
|
||||
if self.is_request_stream:
|
||||
self._is_stream_handler = self.router.is_stream_handler(
|
||||
self.request)
|
||||
self.request
|
||||
)
|
||||
if self._is_stream_handler:
|
||||
self.request.stream = asyncio.Queue()
|
||||
self.request.stream = StreamBuffer(
|
||||
self.request_buffer_queue_size
|
||||
)
|
||||
self.execute_request_handler()
|
||||
|
||||
def on_body(self, body):
|
||||
if self.is_request_stream and self._is_stream_handler:
|
||||
self._request_stream_task = self.loop.create_task(
|
||||
self.request.stream.put(body))
|
||||
return
|
||||
self.request.body.append(body)
|
||||
self.body_append(body)
|
||||
)
|
||||
else:
|
||||
self.request.body_push(body)
|
||||
|
||||
async def body_append(self, 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)
|
||||
|
||||
def on_message_complete(self):
|
||||
# Entire request (headers and whole body) is received.
|
||||
@@ -279,47 +343,66 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._request_timeout_handler = None
|
||||
if self.is_request_stream and self._is_stream_handler:
|
||||
self._request_stream_task = self.loop.create_task(
|
||||
self.request.stream.put(None))
|
||||
self.request.stream.put(None)
|
||||
)
|
||||
return
|
||||
self.request.body = b''.join(self.request.body)
|
||||
self.request.body_finish()
|
||||
self.execute_request_handler()
|
||||
|
||||
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, self.response_timeout_callback)
|
||||
self._last_request_time = current_time
|
||||
self.response_timeout, self.response_timeout_callback
|
||||
)
|
||||
self._last_request_time = time()
|
||||
self._request_handler_task = self.loop.create_task(
|
||||
self.request_handler(
|
||||
self.request,
|
||||
self.write_response,
|
||||
self.stream_response))
|
||||
self.request, self.write_response, self.stream_response
|
||||
)
|
||||
)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Responding
|
||||
# -------------------------------------------- #
|
||||
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:
|
||||
extra = {
|
||||
'status': getattr(response, 'status', 0),
|
||||
}
|
||||
extra = {"status": getattr(response, "status", 0)}
|
||||
|
||||
if isinstance(response, HTTPResponse):
|
||||
extra['byte'] = len(response.body)
|
||||
extra["byte"] = len(response.body)
|
||||
else:
|
||||
extra['byte'] = -1
|
||||
extra["byte"] = -1
|
||||
|
||||
extra['host'] = 'UNKNOWN'
|
||||
extra["host"] = "UNKNOWN"
|
||||
if self.request is not None:
|
||||
if self.request.ip:
|
||||
extra['host'] = '{0}:{1}'.format(self.request.ip,
|
||||
self.request.port)
|
||||
extra["host"] = "{0}:{1}".format(
|
||||
self.request.ip, self.request.port
|
||||
)
|
||||
|
||||
extra['request'] = '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
extra["request"] = "{0} {1}".format(
|
||||
self.request.method, self.request.url
|
||||
)
|
||||
else:
|
||||
extra['request'] = 'nil'
|
||||
extra["request"] = "nil"
|
||||
|
||||
access_logger.info('', extra=extra)
|
||||
access_logger.info("", extra=extra)
|
||||
|
||||
def write_response(self, response):
|
||||
"""
|
||||
@@ -332,32 +415,38 @@ class HttpProtocol(asyncio.Protocol):
|
||||
keep_alive = self.keep_alive
|
||||
self.transport.write(
|
||||
response.output(
|
||||
self.request.version, keep_alive,
|
||||
self.keep_alive_timeout))
|
||||
self.request.version, keep_alive, self.keep_alive_timeout
|
||||
)
|
||||
)
|
||||
self.log_response(response)
|
||||
except AttributeError:
|
||||
logger.error('Invalid response object for url %s, '
|
||||
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
logger.error(
|
||||
"Invalid response object for url %s, "
|
||||
"Expected Type: HTTPResponse, Actual Type: %s",
|
||||
self.url,
|
||||
type(response),
|
||||
)
|
||||
self.write_error(ServerError("Invalid response type"))
|
||||
except RuntimeError:
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
logger.error(
|
||||
"Connection lost before response written @ %s",
|
||||
self.request.ip,
|
||||
)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
repr(e)))
|
||||
"Writing response failed, connection closed {}".format(repr(e))
|
||||
)
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._last_response_time = current_time
|
||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||
)
|
||||
self._last_response_time = time()
|
||||
self.cleanup()
|
||||
|
||||
async def drain(self):
|
||||
@@ -380,31 +469,37 @@ class HttpProtocol(asyncio.Protocol):
|
||||
keep_alive = self.keep_alive
|
||||
response.protocol = self
|
||||
await response.stream(
|
||||
self.request.version, keep_alive, self.keep_alive_timeout)
|
||||
self.request.version, keep_alive, self.keep_alive_timeout
|
||||
)
|
||||
self.log_response(response)
|
||||
except AttributeError:
|
||||
logger.error('Invalid response object for url %s, '
|
||||
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
logger.error(
|
||||
"Invalid response object for url %s, "
|
||||
"Expected Type: HTTPResponse, Actual Type: %s",
|
||||
self.url,
|
||||
type(response),
|
||||
)
|
||||
self.write_error(ServerError("Invalid response type"))
|
||||
except RuntimeError:
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
logger.error(
|
||||
"Connection lost before response written @ %s",
|
||||
self.request.ip,
|
||||
)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
repr(e)))
|
||||
"Writing response failed, connection closed {}".format(repr(e))
|
||||
)
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._last_response_time = current_time
|
||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||
)
|
||||
self._last_response_time = time()
|
||||
self.cleanup()
|
||||
|
||||
def write_error(self, exception):
|
||||
@@ -416,35 +511,53 @@ class HttpProtocol(asyncio.Protocol):
|
||||
response = None
|
||||
try:
|
||||
response = self.error_handler.response(self.request, exception)
|
||||
version = self.request.version if self.request else '1.1'
|
||||
version = self.request.version if self.request else "1.1"
|
||||
self.transport.write(response.output(version))
|
||||
except RuntimeError:
|
||||
if self._debug:
|
||||
logger.error('Connection lost before error written @ %s',
|
||||
self.request.ip if self.request else 'Unknown')
|
||||
logger.error(
|
||||
"Connection lost before error written @ %s",
|
||||
self.request.ip if self.request else "Unknown",
|
||||
)
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing error failed, connection closed {}".format(
|
||||
repr(e)), from_error=True
|
||||
"Writing error failed, connection closed {}".format(repr(e)),
|
||||
from_error=True,
|
||||
)
|
||||
finally:
|
||||
if self.parser and (self.keep_alive
|
||||
or getattr(response, 'status', 0) == 408):
|
||||
if self.parser and (
|
||||
self.keep_alive or getattr(response, "status", 0) == 408
|
||||
):
|
||||
self.log_response(response)
|
||||
try:
|
||||
self.transport.close()
|
||||
except AttributeError as e:
|
||||
logger.debug('Connection lost before server could close it.')
|
||||
except AttributeError:
|
||||
logger.debug("Connection lost before server could close it.")
|
||||
|
||||
def bail_out(self, message, from_error=False):
|
||||
"""
|
||||
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_closing():
|
||||
logger.error("Transport closed @ %s and exception "
|
||||
"experienced during error handling",
|
||||
self.transport.get_extra_info('peername'))
|
||||
logger.debug('Exception:\n%s', traceback.format_exc())
|
||||
logger.error(
|
||||
"Transport closed @ %s and exception "
|
||||
"experienced during error handling",
|
||||
self.transport.get_extra_info("peername"),
|
||||
)
|
||||
logger.debug("Exception:", exc_info=True)
|
||||
else:
|
||||
exception = ServerError(message)
|
||||
self.write_error(exception)
|
||||
self.write_error(ServerError(message))
|
||||
logger.error(message)
|
||||
|
||||
def cleanup(self):
|
||||
@@ -479,18 +592,6 @@ class HttpProtocol(asyncio.Protocol):
|
||||
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):
|
||||
"""Trigger event callbacks (functions or async)
|
||||
|
||||
@@ -503,17 +604,45 @@ def trigger_events(events, loop):
|
||||
loop.run_until_complete(result)
|
||||
|
||||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, response_timeout=60, keep_alive_timeout=5,
|
||||
ssl=None, sock=None, request_max_size=None, reuse_port=False,
|
||||
loop=None, protocol=HttpProtocol, backlog=100,
|
||||
register_sys_signals=True, run_multiple=False, run_async=False,
|
||||
connections=None, signal=Signal(), request_class=None,
|
||||
access_log=True, keep_alive=True, is_request_stream=False,
|
||||
router=None, websocket_max_size=None, websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16, websocket_write_limit=2 ** 16,
|
||||
state=None, graceful_shutdown_timeout=15.0):
|
||||
def serve(
|
||||
host,
|
||||
port,
|
||||
request_handler,
|
||||
error_handler,
|
||||
before_start=None,
|
||||
after_start=None,
|
||||
before_stop=None,
|
||||
after_stop=None,
|
||||
debug=False,
|
||||
request_timeout=60,
|
||||
response_timeout=60,
|
||||
keep_alive_timeout=5,
|
||||
ssl=None,
|
||||
sock=None,
|
||||
request_max_size=None,
|
||||
request_buffer_queue_size=100,
|
||||
reuse_port=False,
|
||||
loop=None,
|
||||
protocol=HttpProtocol,
|
||||
backlog=100,
|
||||
register_sys_signals=True,
|
||||
run_multiple=False,
|
||||
run_async=False,
|
||||
connections=None,
|
||||
signal=Signal(),
|
||||
request_class=None,
|
||||
access_log=True,
|
||||
keep_alive=True,
|
||||
is_request_stream=False,
|
||||
router=None,
|
||||
websocket_max_size=None,
|
||||
websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16,
|
||||
websocket_write_limit=2 ** 16,
|
||||
state=None,
|
||||
graceful_shutdown_timeout=15.0,
|
||||
asyncio_server_kwargs=None,
|
||||
):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
@@ -553,7 +682,12 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
outgoing bytes, the low-water limit is a
|
||||
quarter of the high-water limit.
|
||||
:param is_request_stream: disable/enable Request.stream
|
||||
:param request_buffer_queue_size: streaming request buffer queue size
|
||||
:param router: Router object
|
||||
:param graceful_shutdown_timeout: How long take to Force close non-idle
|
||||
connection
|
||||
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
|
||||
create_server method
|
||||
:return: Nothing
|
||||
"""
|
||||
if not run_async:
|
||||
@@ -588,7 +722,9 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
state=state,
|
||||
debug=debug,
|
||||
)
|
||||
|
||||
asyncio_server_kwargs = (
|
||||
asyncio_server_kwargs if asyncio_server_kwargs else {}
|
||||
)
|
||||
server_coroutine = loop.create_server(
|
||||
server,
|
||||
host,
|
||||
@@ -596,13 +732,10 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
ssl=ssl,
|
||||
reuse_port=reuse_port,
|
||||
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:
|
||||
return server_coroutine
|
||||
|
||||
@@ -627,11 +760,13 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
try:
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
except NotImplementedError:
|
||||
logger.warning('Sanic tried to use loop.add_signal_handler '
|
||||
'but it is not implemented on this platform.')
|
||||
logger.warning(
|
||||
"Sanic tried to use loop.add_signal_handler "
|
||||
"but it is not implemented on this platform."
|
||||
)
|
||||
pid = os.getpid()
|
||||
try:
|
||||
logger.info('Starting worker [%s]', pid)
|
||||
logger.info("Starting worker [%s]", pid)
|
||||
loop.run_forever()
|
||||
finally:
|
||||
logger.info("Stopping worker [%s]", pid)
|
||||
@@ -662,9 +797,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
coros = []
|
||||
for conn in connections:
|
||||
if hasattr(conn, "websocket") and conn.websocket:
|
||||
coros.append(
|
||||
conn.websocket.close_connection()
|
||||
)
|
||||
coros.append(conn.websocket.close_connection())
|
||||
else:
|
||||
conn.close()
|
||||
|
||||
@@ -685,18 +818,18 @@ def serve_multiple(server_settings, workers):
|
||||
:param stop_event: if provided, is used as a stop signal
|
||||
:return:
|
||||
"""
|
||||
server_settings['reuse_port'] = True
|
||||
server_settings['run_multiple'] = True
|
||||
server_settings["reuse_port"] = True
|
||||
server_settings["run_multiple"] = True
|
||||
|
||||
# Handling when custom socket is not provided.
|
||||
if server_settings.get('sock') is None:
|
||||
if server_settings.get("sock") is None:
|
||||
sock = socket()
|
||||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
sock.bind((server_settings['host'], server_settings['port']))
|
||||
sock.bind((server_settings["host"], server_settings["port"]))
|
||||
sock.set_inheritable(True)
|
||||
server_settings['sock'] = sock
|
||||
server_settings['host'] = None
|
||||
server_settings['port'] = None
|
||||
server_settings["sock"] = sock
|
||||
server_settings["host"] = None
|
||||
server_settings["port"] = None
|
||||
|
||||
def sig_handler(signal, frame):
|
||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||
@@ -720,4 +853,4 @@ def serve_multiple(server_settings, workers):
|
||||
# the above processes will block this until they're stopped
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
server_settings.get('sock').close()
|
||||
server_settings.get("sock").close()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from re import sub
|
||||
from time import strftime, gmtime
|
||||
from time import gmtime, strftime
|
||||
from urllib.parse import unquote
|
||||
|
||||
from aiofiles.os import stat
|
||||
@@ -13,13 +13,22 @@ from sanic.exceptions import (
|
||||
InvalidUsage,
|
||||
)
|
||||
from sanic.handlers import ContentRangeHandler
|
||||
from sanic.response import file, file_stream, HTTPResponse
|
||||
from sanic.response import HTTPResponse, file, file_stream
|
||||
|
||||
|
||||
def register(app, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range,
|
||||
stream_large_files, name='static', host=None,
|
||||
strict_slashes=None, content_type=None):
|
||||
def register(
|
||||
app,
|
||||
uri,
|
||||
file_or_directory,
|
||||
pattern,
|
||||
use_modified_since,
|
||||
use_content_range,
|
||||
stream_large_files,
|
||||
name="static",
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
content_type=None,
|
||||
):
|
||||
# TODO: Though sanic is not a file server, I feel like we should at least
|
||||
# make a good effort here. Modified-since is nice, but we could
|
||||
# also look into etags, expires, and caching
|
||||
@@ -46,12 +55,12 @@ def register(app, uri, file_or_directory, pattern,
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
if not path.isfile(file_or_directory):
|
||||
uri += '<file_uri:' + pattern + '>'
|
||||
uri += "<file_uri:" + pattern + ">"
|
||||
|
||||
async def _handler(request, file_uri=None):
|
||||
# Using this to determine if the URL is trying to break out of the path
|
||||
# served. os.path.realpath seems to be very slow
|
||||
if file_uri and '../' in file_uri:
|
||||
if file_uri and "../" in file_uri:
|
||||
raise InvalidUsage("Invalid URL")
|
||||
# Merge served directory and requested file if provided
|
||||
# Strip all / that in the beginning of the URL to help prevent python
|
||||
@@ -59,15 +68,16 @@ def register(app, uri, file_or_directory, pattern,
|
||||
root_path = file_path = file_or_directory
|
||||
if file_uri:
|
||||
file_path = path.join(
|
||||
file_or_directory, sub('^[/]*', '', file_uri))
|
||||
file_or_directory, sub("^[/]*", "", file_uri)
|
||||
)
|
||||
|
||||
# URL decode the path sent by the browser otherwise we won't be able to
|
||||
# match filenames which got encoded (filenames with spaces etc)
|
||||
file_path = path.abspath(unquote(file_path))
|
||||
if not file_path.startswith(path.abspath(unquote(root_path))):
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
raise FileNotFound(
|
||||
"File not found", path=file_or_directory, relative_url=file_uri
|
||||
)
|
||||
try:
|
||||
headers = {}
|
||||
# Check if the client has been sent this file before
|
||||
@@ -76,33 +86,35 @@ def register(app, uri, file_or_directory, pattern,
|
||||
if use_modified_since:
|
||||
stats = await stat(file_path)
|
||||
modified_since = strftime(
|
||||
'%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
|
||||
if request.headers.get('If-Modified-Since') == modified_since:
|
||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
||||
)
|
||||
if request.headers.get("If-Modified-Since") == modified_since:
|
||||
return HTTPResponse(status=304)
|
||||
headers['Last-Modified'] = modified_since
|
||||
headers["Last-Modified"] = modified_since
|
||||
_range = None
|
||||
if use_content_range:
|
||||
_range = None
|
||||
if not stats:
|
||||
stats = await stat(file_path)
|
||||
headers['Accept-Ranges'] = 'bytes'
|
||||
headers['Content-Length'] = str(stats.st_size)
|
||||
if request.method != 'HEAD':
|
||||
headers["Accept-Ranges"] = "bytes"
|
||||
headers["Content-Length"] = str(stats.st_size)
|
||||
if request.method != "HEAD":
|
||||
try:
|
||||
_range = ContentRangeHandler(request, stats)
|
||||
except HeaderNotFound:
|
||||
pass
|
||||
else:
|
||||
del headers['Content-Length']
|
||||
del headers["Content-Length"]
|
||||
for key, value in _range.headers.items():
|
||||
headers[key] = value
|
||||
headers['Content-Type'] = content_type \
|
||||
or guess_type(file_path)[0] or 'text/plain'
|
||||
if request.method == 'HEAD':
|
||||
headers["Content-Type"] = (
|
||||
content_type or guess_type(file_path)[0] or "text/plain"
|
||||
)
|
||||
if request.method == "HEAD":
|
||||
return HTTPResponse(headers=headers)
|
||||
else:
|
||||
if stream_large_files:
|
||||
if isinstance(stream_large_files, int):
|
||||
if type(stream_large_files) == int:
|
||||
threshold = stream_large_files
|
||||
else:
|
||||
threshold = 1024 * 1024
|
||||
@@ -110,19 +122,25 @@ def register(app, uri, file_or_directory, pattern,
|
||||
if not stats:
|
||||
stats = await stat(file_path)
|
||||
if stats.st_size >= threshold:
|
||||
return await file_stream(file_path, headers=headers,
|
||||
_range=_range)
|
||||
return await file_stream(
|
||||
file_path, headers=headers, _range=_range
|
||||
)
|
||||
return await file(file_path, headers=headers, _range=_range)
|
||||
except ContentRangeError:
|
||||
raise
|
||||
except Exception:
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
raise FileNotFound(
|
||||
"File not found", path=file_or_directory, relative_url=file_uri
|
||||
)
|
||||
|
||||
# special prefix for static files
|
||||
if not name.startswith('_static_'):
|
||||
name = '_static_{}'.format(name)
|
||||
if not name.startswith("_static_"):
|
||||
name = "_static_{}".format(name)
|
||||
|
||||
app.route(uri, methods=['GET', 'HEAD'], name=name, host=host,
|
||||
strict_slashes=strict_slashes)(_handler)
|
||||
app.route(
|
||||
uri,
|
||||
methods=["GET", "HEAD"],
|
||||
name=name,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
)(_handler)
|
||||
|
||||
103
sanic/testing.py
103
sanic/testing.py
@@ -1,85 +1,109 @@
|
||||
import traceback
|
||||
from json import JSONDecodeError
|
||||
from sanic.log import logger
|
||||
from socket import socket
|
||||
|
||||
from sanic.exceptions import MethodNotSupported
|
||||
from sanic.log import logger
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 42101
|
||||
|
||||
|
||||
class SanicTestClient:
|
||||
def __init__(self, app, port=PORT):
|
||||
"""Use port=None to bind to a random port"""
|
||||
self.app = app
|
||||
self.port = port
|
||||
|
||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
||||
async def _local_request(self, method, url, cookies=None, *args, **kwargs):
|
||||
import aiohttp
|
||||
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
|
||||
url = uri
|
||||
else:
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=self.port, uri=uri)
|
||||
|
||||
logger.info(url)
|
||||
conn = aiohttp.TCPConnector(verify_ssl=False)
|
||||
conn = aiohttp.TCPConnector(ssl=False)
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, connector=conn) as session:
|
||||
async with getattr(
|
||||
session, method.lower())(url, *args, **kwargs) as response:
|
||||
cookies=cookies, connector=conn
|
||||
) as session:
|
||||
async with getattr(session, method.lower())(
|
||||
url, *args, **kwargs
|
||||
) as response:
|
||||
try:
|
||||
response.text = await response.text()
|
||||
except UnicodeDecodeError as e:
|
||||
except UnicodeDecodeError:
|
||||
response.text = None
|
||||
|
||||
try:
|
||||
response.json = await response.json()
|
||||
except (JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError):
|
||||
except (
|
||||
JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError,
|
||||
):
|
||||
response.json = None
|
||||
|
||||
response.body = await response.read()
|
||||
return response
|
||||
|
||||
def _sanic_endpoint_test(
|
||||
self, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={"auto_reload": False},
|
||||
*request_args, **request_kwargs):
|
||||
self,
|
||||
method="get",
|
||||
uri="/",
|
||||
gather_request=True,
|
||||
debug=False,
|
||||
server_kwargs={"auto_reload": False},
|
||||
*request_args,
|
||||
**request_kwargs
|
||||
):
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
|
||||
if gather_request:
|
||||
|
||||
def _collect_request(request):
|
||||
if results[0] is None:
|
||||
results[0] = request
|
||||
|
||||
self.app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
@self.app.exception(MethodNotSupported)
|
||||
async def error_handler(request, exception):
|
||||
if request.method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
|
||||
if request.method in ["HEAD", "PATCH", "PUT", "DELETE"]:
|
||||
return text(
|
||||
'', exception.status_code, headers=exception.headers
|
||||
"", exception.status_code, headers=exception.headers
|
||||
)
|
||||
else:
|
||||
return self.app.error_handler.default(request, exception)
|
||||
|
||||
@self.app.listener('after_server_start')
|
||||
if self.port:
|
||||
server_kwargs = dict(host=HOST, port=self.port, **server_kwargs)
|
||||
host, port = HOST, self.port
|
||||
else:
|
||||
sock = socket()
|
||||
sock.bind((HOST, 0))
|
||||
server_kwargs = dict(sock=sock, **server_kwargs)
|
||||
host, port = sock.getsockname()
|
||||
|
||||
if uri.startswith(("http:", "https:", "ftp:", "ftps://", "//")):
|
||||
url = uri
|
||||
else:
|
||||
url = "http://{host}:{port}{uri}".format(
|
||||
host=host, port=port, uri=uri
|
||||
)
|
||||
|
||||
@self.app.listener("after_server_start")
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
response = await self._local_request(
|
||||
method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
method, url, *request_args, **request_kwargs
|
||||
)
|
||||
results[-1] = response
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
logger.exception("Exception")
|
||||
exceptions.append(e)
|
||||
self.app.stop()
|
||||
|
||||
self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
|
||||
self.app.listeners['after_server_start'].pop()
|
||||
self.app.run(debug=debug, **server_kwargs)
|
||||
self.app.listeners["after_server_start"].pop()
|
||||
|
||||
if exceptions:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
@@ -91,31 +115,34 @@ class SanicTestClient:
|
||||
except BaseException:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
results
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except BaseException:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
"Request object expected, got ({})".format(results)
|
||||
)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('get', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("get", *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('post', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("post", *args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('put', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("put", *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('delete', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("delete", *args, **kwargs)
|
||||
|
||||
def patch(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('patch', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("patch", *args, **kwargs)
|
||||
|
||||
def options(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('options', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("options", *args, **kwargs)
|
||||
|
||||
def head(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('head', *args, **kwargs)
|
||||
return self._sanic_endpoint_test("head", *args, **kwargs)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import InvalidUsage
|
||||
|
||||
|
||||
class HTTPMethodView:
|
||||
@@ -48,6 +48,7 @@ class HTTPMethodView:
|
||||
"""Return view function for use with the routing system, that
|
||||
dispatches request to appropriate handler method.
|
||||
"""
|
||||
|
||||
def view(*args, **kwargs):
|
||||
self = view.view_class(*class_args, **class_kwargs)
|
||||
return self.dispatch_request(*args, **kwargs)
|
||||
@@ -94,11 +95,13 @@ class CompositionView:
|
||||
for method in methods:
|
||||
if method not in HTTP_METHODS:
|
||||
raise InvalidUsage(
|
||||
'{} is not a valid HTTP method.'.format(method))
|
||||
"{} is not a valid HTTP method.".format(method)
|
||||
)
|
||||
|
||||
if method in self.handlers:
|
||||
raise InvalidUsage(
|
||||
'Method {} is already registered.'.format(method))
|
||||
"Method {} is already registered.".format(method)
|
||||
)
|
||||
self.handlers[method] = handler
|
||||
|
||||
def __call__(self, request, *args, **kwargs):
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
from httptools import HttpParserUpgrade
|
||||
from websockets import ConnectionClosed # noqa
|
||||
from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.server import HttpProtocol
|
||||
from httptools import HttpParserUpgrade
|
||||
from websockets import handshake, WebSocketCommonProtocol, InvalidHandshake
|
||||
from websockets import ConnectionClosed # noqa
|
||||
|
||||
|
||||
class WebSocketProtocol(HttpProtocol):
|
||||
def __init__(self, *args, websocket_timeout=10,
|
||||
websocket_max_size=None,
|
||||
websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16,
|
||||
websocket_write_limit=2 ** 16, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
websocket_timeout=10,
|
||||
websocket_max_size=None,
|
||||
websocket_max_queue=None,
|
||||
websocket_read_limit=2 ** 16,
|
||||
websocket_write_limit=2 ** 16,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.websocket = None
|
||||
self.websocket_timeout = websocket_timeout
|
||||
@@ -57,36 +63,32 @@ class WebSocketProtocol(HttpProtocol):
|
||||
|
||||
async def websocket_handshake(self, request, subprotocols=None):
|
||||
# let the websockets package do the handshake with the client
|
||||
headers = []
|
||||
|
||||
def get_header(k):
|
||||
return request.headers.get(k, '')
|
||||
|
||||
def set_header(k, v):
|
||||
headers.append((k, v))
|
||||
headers = {}
|
||||
|
||||
try:
|
||||
key = handshake.check_request(get_header)
|
||||
handshake.build_response(set_header, key)
|
||||
key = handshake.check_request(request.headers)
|
||||
handshake.build_response(headers, key)
|
||||
except InvalidHandshake:
|
||||
raise InvalidUsage('Invalid websocket request')
|
||||
raise InvalidUsage("Invalid websocket request")
|
||||
|
||||
subprotocol = None
|
||||
if subprotocols and 'Sec-Websocket-Protocol' in request.headers:
|
||||
if subprotocols and "Sec-Websocket-Protocol" in request.headers:
|
||||
# select a subprotocol
|
||||
client_subprotocols = [p.strip() for p in request.headers[
|
||||
'Sec-Websocket-Protocol'].split(',')]
|
||||
client_subprotocols = [
|
||||
p.strip()
|
||||
for p in request.headers["Sec-Websocket-Protocol"].split(",")
|
||||
]
|
||||
for p in client_subprotocols:
|
||||
if p in subprotocols:
|
||||
subprotocol = p
|
||||
set_header('Sec-Websocket-Protocol', subprotocol)
|
||||
headers["Sec-Websocket-Protocol"] = subprotocol
|
||||
break
|
||||
|
||||
# write the 101 response back to the client
|
||||
rv = b'HTTP/1.1 101 Switching Protocols\r\n'
|
||||
for k, v in headers:
|
||||
rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n'
|
||||
rv += b'\r\n'
|
||||
rv = b"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
for k, v in headers.items():
|
||||
rv += k.encode("utf-8") + b": " + v.encode("utf-8") + b"\r\n"
|
||||
rv += b"\r\n"
|
||||
request.transport.write(rv)
|
||||
|
||||
# hook up the websocket protocol
|
||||
@@ -95,7 +97,7 @@ class WebSocketProtocol(HttpProtocol):
|
||||
max_size=self.websocket_max_size,
|
||||
max_queue=self.websocket_max_queue,
|
||||
read_limit=self.websocket_read_limit,
|
||||
write_limit=self.websocket_write_limit
|
||||
write_limit=self.websocket_write_limit,
|
||||
)
|
||||
self.websocket.subprotocol = subprotocol
|
||||
self.websocket.connection_made(request.transport)
|
||||
|
||||
104
sanic/worker.py
104
sanic/worker.py
@@ -1,10 +1,16 @@
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import gunicorn.workers.base as base
|
||||
|
||||
from sanic.server import HttpProtocol, Signal, serve, trigger_events
|
||||
from sanic.websocket import WebSocketProtocol
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
@@ -12,13 +18,10 @@ except ImportError:
|
||||
|
||||
try:
|
||||
import uvloop
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
import gunicorn.workers.base as base
|
||||
|
||||
from sanic.server import trigger_events, serve, HttpProtocol, Signal
|
||||
from sanic.websocket import WebSocketProtocol
|
||||
|
||||
|
||||
class GunicornWorker(base.Worker):
|
||||
@@ -50,36 +53,44 @@ class GunicornWorker(base.Worker):
|
||||
def run(self):
|
||||
is_debug = self.log.loglevel == logging.DEBUG
|
||||
protocol = (
|
||||
self.websocket_protocol if self.app.callable.websocket_enabled
|
||||
else self.http_protocol)
|
||||
self.websocket_protocol
|
||||
if self.app.callable.websocket_enabled
|
||||
else self.http_protocol
|
||||
)
|
||||
|
||||
self._server_settings = self.app.callable._helper(
|
||||
loop=self.loop,
|
||||
debug=is_debug,
|
||||
protocol=protocol,
|
||||
ssl=self.ssl_context,
|
||||
run_async=True)
|
||||
self._server_settings['signal'] = self.signal
|
||||
self._server_settings.pop('sock')
|
||||
trigger_events(self._server_settings.get('before_start', []),
|
||||
self.loop)
|
||||
self._server_settings['before_start'] = ()
|
||||
run_async=True,
|
||||
)
|
||||
self._server_settings["signal"] = self.signal
|
||||
self._server_settings.pop("sock")
|
||||
trigger_events(
|
||||
self._server_settings.get("before_start", []), self.loop
|
||||
)
|
||||
self._server_settings["before_start"] = ()
|
||||
|
||||
self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
|
||||
try:
|
||||
self.loop.run_until_complete(self._runner)
|
||||
self.app.callable.is_running = True
|
||||
trigger_events(self._server_settings.get('after_start', []),
|
||||
self.loop)
|
||||
trigger_events(
|
||||
self._server_settings.get("after_start", []), self.loop
|
||||
)
|
||||
self.loop.run_until_complete(self._check_alive())
|
||||
trigger_events(self._server_settings.get('before_stop', []),
|
||||
self.loop)
|
||||
trigger_events(
|
||||
self._server_settings.get("before_stop", []), self.loop
|
||||
)
|
||||
self.loop.run_until_complete(self.close())
|
||||
except BaseException:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
try:
|
||||
trigger_events(self._server_settings.get('after_stop', []),
|
||||
self.loop)
|
||||
trigger_events(
|
||||
self._server_settings.get("after_stop", []), self.loop
|
||||
)
|
||||
except BaseException:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
@@ -90,8 +101,11 @@ class GunicornWorker(base.Worker):
|
||||
async def close(self):
|
||||
if self.servers:
|
||||
# stop accepting connections
|
||||
self.log.info("Stopping server: %s, connections: %s",
|
||||
self.pid, len(self.connections))
|
||||
self.log.info(
|
||||
"Stopping server: %s, connections: %s",
|
||||
self.pid,
|
||||
len(self.connections),
|
||||
)
|
||||
for server in self.servers:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
@@ -105,8 +119,9 @@ class GunicornWorker(base.Worker):
|
||||
# gracefully shutdown timeout
|
||||
start_shutdown = 0
|
||||
graceful_shutdown_timeout = self.cfg.graceful_timeout
|
||||
while self.connections and \
|
||||
(start_shutdown < graceful_shutdown_timeout):
|
||||
while self.connections and (
|
||||
start_shutdown < graceful_shutdown_timeout
|
||||
):
|
||||
await asyncio.sleep(0.1)
|
||||
start_shutdown = start_shutdown + 0.1
|
||||
|
||||
@@ -115,9 +130,7 @@ class GunicornWorker(base.Worker):
|
||||
coros = []
|
||||
for conn in self.connections:
|
||||
if hasattr(conn, "websocket") and conn.websocket:
|
||||
coros.append(
|
||||
conn.websocket.close_connection()
|
||||
)
|
||||
coros.append(conn.websocket.close_connection())
|
||||
else:
|
||||
conn.close()
|
||||
_shutdown = asyncio.gather(*coros, loop=self.loop)
|
||||
@@ -148,8 +161,9 @@ class GunicornWorker(base.Worker):
|
||||
)
|
||||
if self.max_requests and req_count > self.max_requests:
|
||||
self.alive = False
|
||||
self.log.info("Max requests exceeded, shutting down: %s",
|
||||
self)
|
||||
self.log.info(
|
||||
"Max requests exceeded, shutting down: %s", self
|
||||
)
|
||||
elif pid == os.getpid() and self.ppid != os.getppid():
|
||||
self.alive = False
|
||||
self.log.info("Parent changed, shutting down: %s", self)
|
||||
@@ -175,23 +189,29 @@ class GunicornWorker(base.Worker):
|
||||
def init_signals(self):
|
||||
# Set up signals through the event loop API.
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit,
|
||||
signal.SIGQUIT, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGQUIT, self.handle_quit, signal.SIGQUIT, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit,
|
||||
signal.SIGTERM, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGTERM, self.handle_exit, signal.SIGTERM, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGINT, self.handle_quit,
|
||||
signal.SIGINT, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGINT, self.handle_quit, signal.SIGINT, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch,
|
||||
signal.SIGWINCH, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGWINCH, self.handle_winch, signal.SIGWINCH, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1,
|
||||
signal.SIGUSR1, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGUSR1, self.handle_usr1, signal.SIGUSR1, None
|
||||
)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort,
|
||||
signal.SIGABRT, None)
|
||||
self.loop.add_signal_handler(
|
||||
signal.SIGABRT, self.handle_abort, signal.SIGABRT, None
|
||||
)
|
||||
|
||||
# Don't let SIGTERM and SIGUSR1 disturb active requests
|
||||
# by interrupting system calls
|
||||
|
||||
20
setup.cfg
Normal file
20
setup.cfg
Normal file
@@ -0,0 +1,20 @@
|
||||
[flake8]
|
||||
ignore = E203, W503
|
||||
|
||||
[isort]
|
||||
atomic = true
|
||||
default_section = THIRDPARTY
|
||||
include_trailing_comma = true
|
||||
known_first_party = sanic
|
||||
known_third_party = pytest
|
||||
line_length = 79
|
||||
lines_after_imports = 2
|
||||
lines_between_types = 1
|
||||
multi_line_output = 3
|
||||
not_skip = __init__.py
|
||||
|
||||
[version]
|
||||
current_version = 0.8.3
|
||||
file = sanic/__init__.py
|
||||
current_version_pattern = __version__ = "{current_version}"
|
||||
new_version_pattern = __version__ = "{new_version}"
|
||||
127
setup.py
127
setup.py
@@ -4,73 +4,126 @@ Sanic
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
from distutils.errors import DistutilsPlatformError
|
||||
import sys
|
||||
from distutils.util import strtobool
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
def open_local(paths, mode='r', encoding='utf8'):
|
||||
path = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
*paths
|
||||
)
|
||||
class PyTest(TestCommand):
|
||||
"""
|
||||
Provide a Test runner to be used from setup.py to run unit tests
|
||||
"""
|
||||
|
||||
user_options = [("pytest-args=", "a", "Arguments to pass to pytest")]
|
||||
|
||||
def initialize_options(self):
|
||||
TestCommand.initialize_options(self)
|
||||
self.pytest_args = ""
|
||||
|
||||
def run_tests(self):
|
||||
import shlex
|
||||
import pytest
|
||||
|
||||
errno = pytest.main(shlex.split(self.pytest_args))
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
def open_local(paths, mode="r", encoding="utf8"):
|
||||
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), *paths)
|
||||
|
||||
return codecs.open(path, mode, encoding)
|
||||
|
||||
|
||||
with open_local(['sanic', '__init__.py'], encoding='latin1') as fp:
|
||||
with open_local(["sanic", "__init__.py"], encoding="latin1") as fp:
|
||||
try:
|
||||
version = re.findall(r"^__version__ = '([^']+)'\r?$",
|
||||
fp.read(), re.M)[0]
|
||||
version = re.findall(
|
||||
r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M
|
||||
)[0]
|
||||
except IndexError:
|
||||
raise RuntimeError('Unable to determine version.')
|
||||
raise RuntimeError("Unable to determine version.")
|
||||
|
||||
|
||||
with open_local(['README.rst']) as rm:
|
||||
with open_local(["README.rst"]) as rm:
|
||||
long_description = rm.read()
|
||||
|
||||
setup_kwargs = {
|
||||
'name': 'sanic',
|
||||
'version': version,
|
||||
'url': 'http://github.com/channelcat/sanic/',
|
||||
'license': 'MIT',
|
||||
'author': 'Channel Cat',
|
||||
'author_email': 'channelcat@gmail.com',
|
||||
'description': (
|
||||
'A microframework based on uvloop, httptools, and learnings of flask'),
|
||||
'long_description': long_description,
|
||||
'packages': ['sanic'],
|
||||
'platforms': 'any',
|
||||
'classifiers': [
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
"name": "sanic",
|
||||
"version": version,
|
||||
"url": "http://github.com/channelcat/sanic/",
|
||||
"license": "MIT",
|
||||
"author": "Channel Cat",
|
||||
"author_email": "channelcat@gmail.com",
|
||||
"description": (
|
||||
"A microframework based on uvloop, httptools, and learnings of flask"
|
||||
),
|
||||
"long_description": long_description,
|
||||
"packages": ["sanic"],
|
||||
"platforms": "any",
|
||||
"classifiers": [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
],
|
||||
}
|
||||
|
||||
env_dependency = '; sys_platform != "win32" and implementation_name == "cpython"'
|
||||
ujson = 'ujson>=1.35' + env_dependency
|
||||
uvloop = 'uvloop>=0.5.3' + env_dependency
|
||||
env_dependency = (
|
||||
'; sys_platform != "win32" ' 'and implementation_name == "cpython"'
|
||||
)
|
||||
ujson = "ujson>=1.35" + env_dependency
|
||||
uvloop = "uvloop>=0.5.3" + env_dependency
|
||||
|
||||
requirements = [
|
||||
'httptools>=0.0.9',
|
||||
"httptools>=0.0.10",
|
||||
uvloop,
|
||||
ujson,
|
||||
'aiofiles>=0.3.0',
|
||||
'websockets>=5.0,<6.0',
|
||||
'multidict>=4.0,<5.0',
|
||||
"aiofiles>=0.3.0",
|
||||
"websockets>=6.0,<7.0",
|
||||
"multidict>=4.0,<5.0",
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
"pytest==4.1.0",
|
||||
"multidict>=4.0,<5.0",
|
||||
"gunicorn",
|
||||
"pytest-cov",
|
||||
"aiohttp>=2.3.0,<=3.2.1",
|
||||
"beautifulsoup4",
|
||||
uvloop,
|
||||
ujson,
|
||||
"pytest-sanic",
|
||||
"pytest-sugar",
|
||||
]
|
||||
|
||||
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
||||
print("Installing without uJSON")
|
||||
requirements.remove(ujson)
|
||||
tests_require.remove(ujson)
|
||||
|
||||
# 'nt' means windows OS
|
||||
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
|
||||
print("Installing without uvLoop")
|
||||
requirements.remove(uvloop)
|
||||
tests_require.remove(uvloop)
|
||||
|
||||
setup_kwargs['install_requires'] = requirements
|
||||
extras_require = {
|
||||
"test": tests_require,
|
||||
"dev": tests_require + ["aiofiles", "tox", "black", "flake8"],
|
||||
"docs": [
|
||||
"sphinx",
|
||||
"sphinx_rtd_theme",
|
||||
"recommonmark",
|
||||
"sphinxcontrib-asyncio",
|
||||
"docutils",
|
||||
"pygments"
|
||||
],
|
||||
}
|
||||
|
||||
setup_kwargs["install_requires"] = requirements
|
||||
setup_kwargs["tests_require"] = tests_require
|
||||
setup_kwargs["extras_require"] = extras_require
|
||||
setup_kwargs["cmdclass"] = {"test": PyTest}
|
||||
setup(**setup_kwargs)
|
||||
|
||||
53
tests/benchmark/test_route_resolution_benchmark.py
Normal file
53
tests/benchmark/test_route_resolution_benchmark.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from random import choice, seed
|
||||
from pytest import mark
|
||||
|
||||
import sanic.router
|
||||
|
||||
seed("Pack my box with five dozen liquor jugs.")
|
||||
|
||||
# Disable Caching for testing purpose
|
||||
sanic.router.ROUTER_CACHE_SIZE = 0
|
||||
|
||||
|
||||
class TestSanicRouteResolution:
|
||||
@mark.asyncio
|
||||
async def test_resolve_route_no_arg_string_path(
|
||||
self, sanic_router, route_generator, benchmark
|
||||
):
|
||||
simple_routes = route_generator.generate_random_direct_route(
|
||||
max_route_depth=4
|
||||
)
|
||||
router, simple_routes = sanic_router(route_details=simple_routes)
|
||||
route_to_call = choice(simple_routes)
|
||||
|
||||
result = benchmark.pedantic(
|
||||
router._get,
|
||||
("/{}".format(route_to_call[-1]), route_to_call[0], "localhost"),
|
||||
iterations=1000,
|
||||
rounds=1000,
|
||||
)
|
||||
assert await result[0](None) == 1
|
||||
|
||||
@mark.asyncio
|
||||
async def test_resolve_route_with_typed_args(
|
||||
self, sanic_router, route_generator, benchmark
|
||||
):
|
||||
typed_routes = route_generator.add_typed_parameters(
|
||||
route_generator.generate_random_direct_route(max_route_depth=4),
|
||||
max_route_depth=8,
|
||||
)
|
||||
router, typed_routes = sanic_router(route_details=typed_routes)
|
||||
route_to_call = choice(typed_routes)
|
||||
url = route_generator.generate_url_for_template(
|
||||
template=route_to_call[-1]
|
||||
)
|
||||
|
||||
print("{} -> {}".format(route_to_call[-1], url))
|
||||
|
||||
result = benchmark.pedantic(
|
||||
router._get,
|
||||
("/{}".format(url), route_to_call[0], "localhost"),
|
||||
iterations=1000,
|
||||
rounds=1000,
|
||||
)
|
||||
assert await result[0](None) == 1
|
||||
130
tests/conftest.py
Normal file
130
tests/conftest.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.router import RouteExists, Router
|
||||
|
||||
random.seed("Pack my box with five dozen liquor jugs.")
|
||||
|
||||
if sys.platform in ["win32", "cygwin"]:
|
||||
collect_ignore = ["test_worker.py"]
|
||||
|
||||
|
||||
async def _handler(request):
|
||||
"""
|
||||
Dummy placeholder method used for route resolver when creating a new
|
||||
route into the sanic router. This router is not actually called by the
|
||||
sanic app. So do not worry about the arguments to this method.
|
||||
|
||||
If you change the return value of this method, make sure to propagate the
|
||||
change to any test case that leverages RouteStringGenerator.
|
||||
"""
|
||||
return 1
|
||||
|
||||
|
||||
TYPE_TO_GENERATOR_MAP = {
|
||||
"string": lambda: "".join(
|
||||
[random.choice(string.ascii_letters + string.digits) for _ in range(4)]
|
||||
),
|
||||
"int": lambda: random.choice(range(1000000)),
|
||||
"number": lambda: random.random(),
|
||||
"alpha": lambda: "".join(
|
||||
[random.choice(string.ascii_letters) for _ in range(4)]
|
||||
),
|
||||
"uuid": lambda: str(uuid.uuid1()),
|
||||
}
|
||||
|
||||
|
||||
class RouteStringGenerator:
|
||||
|
||||
ROUTE_COUNT_PER_DEPTH = 100
|
||||
HTTP_METHODS = ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTION"]
|
||||
ROUTE_PARAM_TYPES = ["string", "int", "number", "alpha", "uuid"]
|
||||
|
||||
def generate_random_direct_route(self, max_route_depth=4):
|
||||
routes = []
|
||||
for depth in range(1, max_route_depth + 1):
|
||||
for _ in range(self.ROUTE_COUNT_PER_DEPTH):
|
||||
route = "/".join(
|
||||
[
|
||||
TYPE_TO_GENERATOR_MAP.get("string")()
|
||||
for _ in range(depth)
|
||||
]
|
||||
)
|
||||
route = route.replace(".", "", -1)
|
||||
route_detail = (random.choice(self.HTTP_METHODS), route)
|
||||
|
||||
if route_detail not in routes:
|
||||
routes.append(route_detail)
|
||||
return routes
|
||||
|
||||
def add_typed_parameters(self, current_routes, max_route_depth=8):
|
||||
routes = []
|
||||
for method, route in current_routes:
|
||||
current_length = len(route.split("/"))
|
||||
new_route_part = "/".join(
|
||||
[
|
||||
"<{}:{}>".format(
|
||||
TYPE_TO_GENERATOR_MAP.get("string")(),
|
||||
random.choice(self.ROUTE_PARAM_TYPES),
|
||||
)
|
||||
for _ in range(max_route_depth - current_length)
|
||||
]
|
||||
)
|
||||
route = "/".join([route, new_route_part])
|
||||
route = route.replace(".", "", -1)
|
||||
routes.append((method, route))
|
||||
return routes
|
||||
|
||||
@staticmethod
|
||||
def generate_url_for_template(template):
|
||||
url = template
|
||||
for pattern, param_type in re.findall(
|
||||
re.compile(r"((?:<\w+:(string|int|number|alpha|uuid)>)+)"),
|
||||
template,
|
||||
):
|
||||
value = TYPE_TO_GENERATOR_MAP.get(param_type)()
|
||||
url = url.replace(pattern, str(value), -1)
|
||||
return url
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def sanic_router():
|
||||
# noinspection PyProtectedMember
|
||||
def _setup(route_details: tuple) -> (Router, tuple):
|
||||
router = Router()
|
||||
added_router = []
|
||||
for method, route in route_details:
|
||||
try:
|
||||
router._add(
|
||||
uri="/{}".format(route),
|
||||
methods=frozenset({method}),
|
||||
host="localhost",
|
||||
handler=_handler,
|
||||
)
|
||||
added_router.append((method, route))
|
||||
except RouteExists:
|
||||
pass
|
||||
return router, added_router
|
||||
|
||||
return _setup
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def route_generator() -> RouteStringGenerator:
|
||||
return RouteStringGenerator()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def url_param_generator():
|
||||
return TYPE_TO_GENERATOR_MAP
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(request):
|
||||
return Sanic(request.node.name)
|
||||
@@ -9,10 +9,15 @@ import ujson as json
|
||||
loop = uvloop.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
|
||||
async def handle(request):
|
||||
return web.Response(body=json.dumps({"test":True}).encode('utf-8'), content_type='application/json')
|
||||
return web.Response(
|
||||
body=json.dumps({"test": True}).encode("utf-8"),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
app.router.add_route('GET', '/', handle)
|
||||
app.router.add_route("GET", "/", handle)
|
||||
|
||||
web.run_app(app, port=sys.argv[1], access_log=None)
|
||||
|
||||
@@ -4,8 +4,9 @@ from bottle import route, run
|
||||
import ujson
|
||||
|
||||
|
||||
@route('/')
|
||||
@route("/")
|
||||
def index():
|
||||
return ujson.dumps({'test': True})
|
||||
return ujson.dumps({"test": True})
|
||||
|
||||
|
||||
app = bottle.default_app()
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
import falcon
|
||||
import ujson as json
|
||||
|
||||
|
||||
class TestResource:
|
||||
def on_get(self, req, resp):
|
||||
resp.body = json.dumps({"test": True})
|
||||
|
||||
|
||||
app = falcon.API()
|
||||
app.add_route('/', TestResource())
|
||||
app.add_route("/", TestResource())
|
||||
|
||||
@@ -13,8 +13,14 @@ kyk = Kyoukai("example_app")
|
||||
logger = logging.getLogger("Kyoukai")
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
@kyk.route("/")
|
||||
async def index(ctx: HTTPRequestContext):
|
||||
return ujson.dumps({"test":True}), 200, {"Content-Type": "application/json"}
|
||||
return (
|
||||
ujson.dumps({"test": True}),
|
||||
200,
|
||||
{"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
kyk.run()
|
||||
|
||||
kyk.run()
|
||||
|
||||
@@ -3,8 +3,10 @@ import sys
|
||||
import os
|
||||
import inspect
|
||||
|
||||
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||
sys.path.insert(0, currentdir + '/../../../')
|
||||
currentdir = os.path.dirname(
|
||||
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||
)
|
||||
sys.path.insert(0, currentdir + "/../../../")
|
||||
|
||||
import timeit
|
||||
|
||||
@@ -16,7 +18,11 @@ print("Running New 100,000 times")
|
||||
times = 0
|
||||
total_time = 0
|
||||
for n in range(6):
|
||||
time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000)
|
||||
time = timeit.timeit(
|
||||
'json({ "test":True }).output()',
|
||||
setup="from sanic.response import json",
|
||||
number=100000,
|
||||
)
|
||||
print("Took {} seconds".format(time))
|
||||
total_time += time
|
||||
times += 1
|
||||
@@ -26,7 +32,11 @@ print("Running Old 100,000 times")
|
||||
times = 0
|
||||
total_time = 0
|
||||
for n in range(6):
|
||||
time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000)
|
||||
time = timeit.timeit(
|
||||
'json({ "test":True }).output_old()',
|
||||
setup="from sanic.response import json",
|
||||
number=100000,
|
||||
)
|
||||
print("Took {} seconds".format(time))
|
||||
total_time += time
|
||||
times += 1
|
||||
|
||||
@@ -2,8 +2,10 @@ import sys
|
||||
import os
|
||||
import inspect
|
||||
|
||||
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||
sys.path.insert(0, currentdir + '/../../../')
|
||||
currentdir = os.path.dirname(
|
||||
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||
)
|
||||
sys.path.insert(0, currentdir + "/../../../")
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
@@ -15,5 +17,6 @@ app = Sanic("test")
|
||||
async def test(request):
|
||||
return json({"test": True})
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=sys.argv[1])
|
||||
|
||||
@@ -2,8 +2,10 @@ import sys
|
||||
import os
|
||||
import inspect
|
||||
|
||||
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||
sys.path.insert(0, currentdir + '/../../../')
|
||||
currentdir = os.path.dirname(
|
||||
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||
)
|
||||
sys.path.insert(0, currentdir + "/../../../")
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
@@ -17,7 +19,7 @@ async def test(request):
|
||||
return json({"test": True})
|
||||
|
||||
|
||||
@app.route("/sync", methods=['GET', 'POST'])
|
||||
@app.route("/sync", methods=["GET", "POST"])
|
||||
def test(request):
|
||||
return json({"test": True})
|
||||
|
||||
@@ -44,7 +46,14 @@ def post_json(request):
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
|
||||
return json(
|
||||
{
|
||||
"parsed": True,
|
||||
"args": request.args,
|
||||
"url": request.url,
|
||||
"query_string": request.query_string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
import sys
|
||||
@@ -52,7 +61,6 @@ import sys
|
||||
app.run(host="0.0.0.0", port=sys.argv[1])
|
||||
|
||||
|
||||
|
||||
# import asyncio_redis
|
||||
# import asyncpg
|
||||
# async def setup(sanic, loop):
|
||||
|
||||
@@ -5,14 +5,14 @@ from tornado import ioloop, web
|
||||
|
||||
class MainHandler(web.RequestHandler):
|
||||
def get(self):
|
||||
self.write(ujson.dumps({'test': True}))
|
||||
self.write(ujson.dumps({"test": True}))
|
||||
|
||||
|
||||
app = web.Application([
|
||||
(r'/', MainHandler)
|
||||
], debug=False,
|
||||
app = web.Application(
|
||||
[(r"/", MainHandler)],
|
||||
debug=False,
|
||||
compress_response=False,
|
||||
static_hash_cache=True
|
||||
static_hash_cache=True,
|
||||
)
|
||||
|
||||
app.listen(8000)
|
||||
|
||||
@@ -12,16 +12,17 @@ from wheezy.web.middleware import path_routing_middleware_factory
|
||||
|
||||
import ujson
|
||||
|
||||
class WelcomeHandler(BaseHandler):
|
||||
|
||||
class WelcomeHandler(BaseHandler):
|
||||
def get(self):
|
||||
response = HTTPResponse(content_type='application/json; charset=UTF-8')
|
||||
response.write(ujson.dumps({"test":True}))
|
||||
response = HTTPResponse(content_type="application/json; charset=UTF-8")
|
||||
response.write(ujson.dumps({"test": True}))
|
||||
return response
|
||||
|
||||
|
||||
all_urls = [
|
||||
url('', WelcomeHandler, name='default'),
|
||||
# url('', welcome, name='welcome')
|
||||
url("", WelcomeHandler, name="default"),
|
||||
# url('', welcome, name='welcome')
|
||||
]
|
||||
|
||||
|
||||
@@ -29,18 +30,19 @@ options = {}
|
||||
main = WSGIApplication(
|
||||
middleware=[
|
||||
bootstrap_defaults(url_mapping=all_urls),
|
||||
path_routing_middleware_factory
|
||||
path_routing_middleware_factory,
|
||||
],
|
||||
options=options
|
||||
options=options,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from wsgiref.simple_server import make_server
|
||||
|
||||
try:
|
||||
print('Visit http://localhost:{}/'.format(sys.argv[-1]))
|
||||
make_server('', int(sys.argv[-1]), main).serve_forever()
|
||||
print("Visit http://localhost:{}/".format(sys.argv[-1]))
|
||||
make_server("", int(sys.argv[-1]), main).serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print('\nThanks!')
|
||||
print("\nThanks!")
|
||||
|
||||
196
tests/test_app.py
Normal file
196
tests/test_app.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from inspect import isawaitable
|
||||
import pytest
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
def uvloop_installed():
|
||||
try:
|
||||
import uvloop
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def test_app_loop_running(app):
|
||||
@app.get("/test")
|
||||
async def handler(request):
|
||||
assert isinstance(app.loop, asyncio.AbstractEventLoop)
|
||||
return text("pass")
|
||||
|
||||
request, response = app.test_client.get("/test")
|
||||
assert response.text == "pass"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 7), reason="requires python3.7 or higher"
|
||||
)
|
||||
def test_create_asyncio_server(app):
|
||||
if not uvloop_installed():
|
||||
loop = asyncio.get_event_loop()
|
||||
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
|
||||
assert isawaitable(asyncio_srv_coro)
|
||||
srv = loop.run_until_complete(asyncio_srv_coro)
|
||||
assert srv.is_serving() is True
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 7), reason="requires python3.7 or higher"
|
||||
)
|
||||
def test_asyncio_server_start_serving(app):
|
||||
if not uvloop_installed():
|
||||
loop = asyncio.get_event_loop()
|
||||
asyncio_srv_coro = app.create_server(
|
||||
return_asyncio_server=True,
|
||||
asyncio_server_kwargs=dict(start_serving=False),
|
||||
)
|
||||
srv = loop.run_until_complete(asyncio_srv_coro)
|
||||
assert srv.is_serving() is False
|
||||
|
||||
|
||||
def test_app_loop_not_running(app):
|
||||
with pytest.raises(SanicException) as excinfo:
|
||||
_ = app.loop
|
||||
|
||||
assert str(excinfo.value) == (
|
||||
"Loop can only be retrieved after the app has started "
|
||||
"running. Not supported with `create_server` function"
|
||||
)
|
||||
|
||||
|
||||
def test_app_run_raise_type_error(app):
|
||||
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
app.run(loop="loop")
|
||||
|
||||
assert str(excinfo.value) == (
|
||||
"loop is not a valid argument. To use an existing loop, "
|
||||
"change to create_server().\nSee more: "
|
||||
"https://sanic.readthedocs.io/en/latest/sanic/deploying.html"
|
||||
"#asynchronous-support"
|
||||
)
|
||||
|
||||
|
||||
def test_app_route_raise_value_error(app):
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
|
||||
@app.route("/test")
|
||||
async def handler():
|
||||
return text("test")
|
||||
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== "Required parameter `request` missing in the handler() route?"
|
||||
)
|
||||
|
||||
|
||||
def test_app_handle_request_handler_is_none(app, monkeypatch):
|
||||
def mockreturn(*args, **kwargs):
|
||||
return None, [], {}, ""
|
||||
|
||||
# Not sure how to make app.router.get() return None, so use mock here.
|
||||
monkeypatch.setattr(app.router, "get", mockreturn)
|
||||
|
||||
@app.get("/test")
|
||||
def handler(request):
|
||||
return text("test")
|
||||
|
||||
request, response = app.test_client.get("/test")
|
||||
|
||||
assert (
|
||||
response.text
|
||||
== "Error: 'None' was returned while requesting a handler from the router"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("websocket_enabled", [True, False])
|
||||
@pytest.mark.parametrize("enable", [True, False])
|
||||
def test_app_enable_websocket(app, websocket_enabled, enable):
|
||||
app.websocket_enabled = websocket_enabled
|
||||
app.enable_websocket(enable=enable)
|
||||
|
||||
assert app.websocket_enabled == enable
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def handler(request, ws):
|
||||
await ws.send("test")
|
||||
|
||||
assert app.websocket_enabled == True
|
||||
|
||||
|
||||
def test_handle_request_with_nested_exception(app, monkeypatch):
|
||||
|
||||
err_msg = "Mock Exception"
|
||||
|
||||
# Not sure how to raise an exception in app.error_handler.response(), use mock here
|
||||
def mock_error_handler_response(*args, **kwargs):
|
||||
raise Exception(err_msg)
|
||||
|
||||
monkeypatch.setattr(
|
||||
app.error_handler, "response", mock_error_handler_response
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
def handler(request):
|
||||
raise Exception
|
||||
|
||||
request, response = app.test_client.get("/")
|
||||
assert response.status == 500
|
||||
assert response.text == "An error occurred while handling an error"
|
||||
|
||||
|
||||
def test_handle_request_with_nested_exception_debug(app, monkeypatch):
|
||||
|
||||
err_msg = "Mock Exception"
|
||||
|
||||
# Not sure how to raise an exception in app.error_handler.response(), use mock here
|
||||
def mock_error_handler_response(*args, **kwargs):
|
||||
raise Exception(err_msg)
|
||||
|
||||
monkeypatch.setattr(
|
||||
app.error_handler, "response", mock_error_handler_response
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
def handler(request):
|
||||
raise Exception
|
||||
|
||||
request, response = app.test_client.get("/", debug=True)
|
||||
assert response.status == 500
|
||||
assert response.text.startswith(
|
||||
"Error while handling error: {}\nStack: Traceback (most recent call last):\n".format(
|
||||
err_msg
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
|
||||
|
||||
# Not sure how to raise an exception in app.error_handler.response(), use mock here
|
||||
def mock_error_handler_response(*args, **kwargs):
|
||||
raise SanicException("Mock SanicException")
|
||||
|
||||
monkeypatch.setattr(
|
||||
app.error_handler, "response", mock_error_handler_response
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
def handler(request):
|
||||
raise Exception
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
request, response = app.test_client.get("/")
|
||||
assert response.status == 500
|
||||
assert response.text == "Error: Mock SanicException"
|
||||
assert (
|
||||
"sanic.root",
|
||||
logging.ERROR,
|
||||
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
||||
) in caplog.record_tuples
|
||||
@@ -1,21 +1,21 @@
|
||||
import asyncio
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def test_bad_request_response():
|
||||
app = Sanic('test_bad_request_response')
|
||||
def test_bad_request_response(app):
|
||||
lines = []
|
||||
@app.listener('after_server_start')
|
||||
|
||||
@app.listener("after_server_start")
|
||||
async def _request(sanic, loop):
|
||||
connect = asyncio.open_connection('127.0.0.1', 42101)
|
||||
connect = asyncio.open_connection("127.0.0.1", 42101)
|
||||
reader, writer = await connect
|
||||
writer.write(b'not http')
|
||||
writer.write(b"not http")
|
||||
while True:
|
||||
line = await reader.readline()
|
||||
if not line:
|
||||
break
|
||||
lines.append(line)
|
||||
app.stop()
|
||||
app.run(host='127.0.0.1', port=42101, debug=False)
|
||||
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
|
||||
assert lines[-1] == b'Error: Bad Request'
|
||||
|
||||
app.run(host="127.0.0.1", port=42101, debug=False)
|
||||
assert lines[0] == b"HTTP/1.1 400 Bad Request\r\n"
|
||||
assert lines[-1] == b"Error: Bad Request"
|
||||
|
||||
180
tests/test_blueprint_group.py
Normal file
180
tests/test_blueprint_group.py
Normal file
@@ -0,0 +1,180 @@
|
||||
from pytest import raises
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.request import Request
|
||||
from sanic.response import text, HTTPResponse
|
||||
|
||||
MIDDLEWARE_INVOKE_COUNTER = {"request": 0, "response": 0}
|
||||
|
||||
AUTH = "dGVzdDp0ZXN0Cg=="
|
||||
|
||||
|
||||
def test_bp_group_indexing(app: Sanic):
|
||||
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||
|
||||
group = Blueprint.group(blueprint_1, blueprint_2)
|
||||
assert group[0] == blueprint_1
|
||||
|
||||
with raises(expected_exception=IndexError) as e:
|
||||
_ = group[3]
|
||||
|
||||
|
||||
def test_bp_group_with_additional_route_params(app: Sanic):
|
||||
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||
|
||||
@blueprint_1.route(
|
||||
"/request_path", methods=frozenset({"PUT", "POST"}), version=2
|
||||
)
|
||||
def blueprint_1_v2_method_with_put_and_post(request: Request):
|
||||
if request.method == "PUT":
|
||||
return text("PUT_OK")
|
||||
elif request.method == "POST":
|
||||
return text("POST_OK")
|
||||
|
||||
@blueprint_2.route(
|
||||
"/route/<param>", methods=frozenset({"DELETE", "PATCH"}), name="test"
|
||||
)
|
||||
def blueprint_2_named_method(request: Request, param):
|
||||
if request.method == "DELETE":
|
||||
return text("DELETE_{}".format(param))
|
||||
elif request.method == "PATCH":
|
||||
return text("PATCH_{}".format(param))
|
||||
|
||||
blueprint_group = Blueprint.group(
|
||||
blueprint_1, blueprint_2, url_prefix="/api"
|
||||
)
|
||||
|
||||
@blueprint_group.middleware("request")
|
||||
def authenticate_request(request: Request):
|
||||
global AUTH
|
||||
auth = request.headers.get("authorization")
|
||||
if auth:
|
||||
# Dummy auth check. We can have anything here and it's fine.
|
||||
if AUTH not in auth:
|
||||
return text("Unauthorized", status=401)
|
||||
else:
|
||||
return text("Unauthorized", status=401)
|
||||
|
||||
@blueprint_group.middleware("response")
|
||||
def enhance_response_middleware(request: Request, response: HTTPResponse):
|
||||
response.headers.add("x-test-middleware", "value")
|
||||
|
||||
app.blueprint(blueprint_group)
|
||||
|
||||
header = {"authorization": " ".join(["Basic", AUTH])}
|
||||
_, response = app.test_client.put(
|
||||
"/v2/api/bp1/request_path", headers=header
|
||||
)
|
||||
assert response.text == "PUT_OK"
|
||||
assert response.headers.get("x-test-middleware") == "value"
|
||||
|
||||
_, response = app.test_client.post(
|
||||
"/v2/api/bp1/request_path", headers=header
|
||||
)
|
||||
assert response.text == "POST_OK"
|
||||
|
||||
_, response = app.test_client.delete("/api/bp2/route/bp2", headers=header)
|
||||
assert response.text == "DELETE_bp2"
|
||||
|
||||
_, response = app.test_client.patch("/api/bp2/route/bp2", headers=header)
|
||||
assert response.text == "PATCH_bp2"
|
||||
|
||||
_, response = app.test_client.get("/v2/api/bp1/request_path")
|
||||
assert response.status == 401
|
||||
|
||||
|
||||
def test_bp_group(app: Sanic):
|
||||
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||
|
||||
@blueprint_1.route("/")
|
||||
def blueprint_1_default_route(request):
|
||||
return text("BP1_OK")
|
||||
|
||||
@blueprint_2.route("/")
|
||||
def blueprint_2_default_route(request):
|
||||
return text("BP2_OK")
|
||||
|
||||
blueprint_group_1 = Blueprint.group(
|
||||
blueprint_1, blueprint_2, url_prefix="/bp"
|
||||
)
|
||||
|
||||
blueprint_3 = Blueprint("blueprint_3", url_prefix="/bp3")
|
||||
|
||||
@blueprint_group_1.middleware("request")
|
||||
def blueprint_group_1_middleware(request):
|
||||
global MIDDLEWARE_INVOKE_COUNTER
|
||||
MIDDLEWARE_INVOKE_COUNTER["request"] += 1
|
||||
|
||||
@blueprint_3.route("/")
|
||||
def blueprint_3_default_route(request):
|
||||
return text("BP3_OK")
|
||||
|
||||
blueprint_group_2 = Blueprint.group(
|
||||
blueprint_group_1, blueprint_3, url_prefix="/api"
|
||||
)
|
||||
|
||||
@blueprint_group_2.middleware("response")
|
||||
def blueprint_group_2_middleware(request, response):
|
||||
global MIDDLEWARE_INVOKE_COUNTER
|
||||
MIDDLEWARE_INVOKE_COUNTER["response"] += 1
|
||||
|
||||
app.blueprint(blueprint_group_2)
|
||||
|
||||
@app.route("/")
|
||||
def app_default_route(request):
|
||||
return text("APP_OK")
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
assert response.text == "APP_OK"
|
||||
|
||||
_, response = app.test_client.get("/api/bp/bp1")
|
||||
assert response.text == "BP1_OK"
|
||||
|
||||
_, response = app.test_client.get("/api/bp/bp2")
|
||||
assert response.text == "BP2_OK"
|
||||
|
||||
_, response = app.test_client.get("/api/bp3")
|
||||
assert response.text == "BP3_OK"
|
||||
|
||||
assert MIDDLEWARE_INVOKE_COUNTER["response"] == 4
|
||||
assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4
|
||||
|
||||
|
||||
def test_bp_group_list_operations(app: Sanic):
|
||||
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||
|
||||
@blueprint_1.route("/")
|
||||
def blueprint_1_default_route(request):
|
||||
return text("BP1_OK")
|
||||
|
||||
@blueprint_2.route("/")
|
||||
def blueprint_2_default_route(request):
|
||||
return text("BP2_OK")
|
||||
|
||||
blueprint_group_1 = Blueprint.group(
|
||||
blueprint_1, blueprint_2, url_prefix="/bp"
|
||||
)
|
||||
|
||||
blueprint_3 = Blueprint("blueprint_2", url_prefix="/bp3")
|
||||
|
||||
@blueprint_3.route("/second")
|
||||
def blueprint_3_second_route(request):
|
||||
return text("BP3_OK")
|
||||
|
||||
assert len(blueprint_group_1) == 2
|
||||
|
||||
blueprint_group_1.append(blueprint_3)
|
||||
assert len(blueprint_group_1) == 3
|
||||
|
||||
del blueprint_group_1[2]
|
||||
assert len(blueprint_group_1) == 2
|
||||
|
||||
blueprint_group_1[1] = blueprint_3
|
||||
assert len(blueprint_group_1) == 2
|
||||
|
||||
assert blueprint_group_1.url_prefix == "/bp"
|
||||
@@ -1,290 +1,288 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import json, text
|
||||
from sanic.exceptions import NotFound, ServerError, InvalidUsage
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import NotFound, ServerError, InvalidUsage
|
||||
from sanic.request import Request
|
||||
from sanic.response import text, json
|
||||
from sanic.views import CompositionView
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def static_file_directory():
|
||||
"""The static directory to serve"""
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
current_directory = os.path.dirname(os.path.abspath(current_file))
|
||||
static_directory = os.path.join(current_directory, "static")
|
||||
return static_directory
|
||||
|
||||
|
||||
def get_file_path(static_file_directory, file_name):
|
||||
return os.path.join(static_file_directory, file_name)
|
||||
|
||||
|
||||
def get_file_content(static_file_directory, file_name):
|
||||
"""The content of the static file to check"""
|
||||
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
|
||||
with open(get_file_path(static_file_directory, file_name), "rb") as file:
|
||||
return file.read()
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_versioned_routes_get(method):
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
@pytest.mark.parametrize("method", HTTP_METHODS)
|
||||
def test_versioned_routes_get(app, method):
|
||||
bp = Blueprint("test_text")
|
||||
|
||||
method = method.lower()
|
||||
|
||||
func = getattr(bp, method)
|
||||
if callable(func):
|
||||
@func('/{}'.format(method), version=1)
|
||||
|
||||
@func("/{}".format(method), version=1)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
else:
|
||||
print(func)
|
||||
raise
|
||||
raise Exception("{} is not callable".format(func))
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
client_method = getattr(app.test_client, method)
|
||||
|
||||
request, response = client_method('/v1/{}'.format(method))
|
||||
request, response = client_method("/v1/{}".format(method))
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_bp():
|
||||
app = Sanic('test_text')
|
||||
bp = Blueprint('test_text')
|
||||
def test_bp(app):
|
||||
bp = Blueprint("test_text")
|
||||
|
||||
@bp.route('/')
|
||||
@bp.route("/")
|
||||
def handler(request):
|
||||
return text('Hello')
|
||||
return text("Hello")
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = app.test_client.get('/')
|
||||
request, response = app.test_client.get("/")
|
||||
assert app.is_request_stream is False
|
||||
|
||||
assert response.text == 'Hello'
|
||||
assert response.text == "Hello"
|
||||
|
||||
def test_bp_strict_slash():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
@bp.get('/get', strict_slashes=True)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
def test_bp_strict_slash(app):
|
||||
bp = Blueprint("test_text")
|
||||
|
||||
@bp.post('/post/', strict_slashes=True)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
@bp.get("/get", strict_slashes=True)
|
||||
def get_handler(request):
|
||||
return text("OK")
|
||||
|
||||
@bp.post("/post/", strict_slashes=True)
|
||||
def post_handler(request):
|
||||
return text("OK")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
assert response.json == None
|
||||
request, response = app.test_client.get("/get")
|
||||
assert response.text == "OK"
|
||||
assert response.json is None
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
request, response = app.test_client.get("/get/")
|
||||
assert response.status == 404
|
||||
|
||||
request, response = app.test_client.post('/post/')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.post("/post/")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
request, response = app.test_client.post("/post")
|
||||
assert response.status == 404
|
||||
|
||||
def test_bp_strict_slash_default_value():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
bp = Blueprint('test_text', strict_slashes=True)
|
||||
|
||||
@bp.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
def test_bp_strict_slash_default_value(app):
|
||||
bp = Blueprint("test_text", strict_slashes=True)
|
||||
|
||||
@bp.post('/post/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
@bp.get("/get")
|
||||
def get_handler(request):
|
||||
return text("OK")
|
||||
|
||||
@bp.post("/post/")
|
||||
def post_handler(request):
|
||||
return text("OK")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
request, response = app.test_client.get("/get/")
|
||||
assert response.status == 404
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
request, response = app.test_client.post("/post")
|
||||
assert response.status == 404
|
||||
|
||||
def test_bp_strict_slash_without_passing_default_value():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
@bp.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
def test_bp_strict_slash_without_passing_default_value(app):
|
||||
bp = Blueprint("test_text")
|
||||
|
||||
@bp.post('/post/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
@bp.get("/get")
|
||||
def get_handler(request):
|
||||
return text("OK")
|
||||
|
||||
@bp.post("/post/")
|
||||
def post_handler(request):
|
||||
return text("OK")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.get("/get/")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.post("/post")
|
||||
assert response.text == "OK"
|
||||
|
||||
def test_bp_strict_slash_default_value_can_be_overwritten():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
bp = Blueprint('test_text', strict_slashes=True)
|
||||
|
||||
@bp.get('/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
def test_bp_strict_slash_default_value_can_be_overwritten(app):
|
||||
bp = Blueprint("test_text", strict_slashes=True)
|
||||
|
||||
@bp.post('/post/', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
@bp.get("/get", strict_slashes=False)
|
||||
def get_handler(request):
|
||||
return text("OK")
|
||||
|
||||
@bp.post("/post/", strict_slashes=False)
|
||||
def post_handler(request):
|
||||
return text("OK")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.get("/get/")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.post("/post")
|
||||
assert response.text == "OK"
|
||||
|
||||
def test_bp_with_url_prefix():
|
||||
app = Sanic('test_text')
|
||||
bp = Blueprint('test_text', url_prefix='/test1')
|
||||
|
||||
@bp.route('/')
|
||||
def test_bp_with_url_prefix(app):
|
||||
bp = Blueprint("test_text", url_prefix="/test1")
|
||||
|
||||
@bp.route("/")
|
||||
def handler(request):
|
||||
return text('Hello')
|
||||
return text("Hello")
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = app.test_client.get('/test1/')
|
||||
request, response = app.test_client.get("/test1/")
|
||||
|
||||
assert response.text == 'Hello'
|
||||
assert response.text == "Hello"
|
||||
|
||||
|
||||
def test_several_bp_with_url_prefix():
|
||||
app = Sanic('test_text')
|
||||
bp = Blueprint('test_text', url_prefix='/test1')
|
||||
bp2 = Blueprint('test_text2', url_prefix='/test2')
|
||||
def test_several_bp_with_url_prefix(app):
|
||||
bp = Blueprint("test_text", url_prefix="/test1")
|
||||
bp2 = Blueprint("test_text2", url_prefix="/test2")
|
||||
|
||||
@bp.route('/')
|
||||
@bp.route("/")
|
||||
def handler(request):
|
||||
return text('Hello')
|
||||
return text("Hello")
|
||||
|
||||
@bp2.route('/')
|
||||
@bp2.route("/")
|
||||
def handler2(request):
|
||||
return text('Hello2')
|
||||
return text("Hello2")
|
||||
|
||||
app.blueprint(bp)
|
||||
app.blueprint(bp2)
|
||||
request, response = app.test_client.get('/test1/')
|
||||
assert response.text == 'Hello'
|
||||
request, response = app.test_client.get("/test1/")
|
||||
assert response.text == "Hello"
|
||||
|
||||
request, response = app.test_client.get('/test2/')
|
||||
assert response.text == 'Hello2'
|
||||
request, response = app.test_client.get("/test2/")
|
||||
assert response.text == "Hello2"
|
||||
|
||||
def test_bp_with_host():
|
||||
app = Sanic('test_bp_host')
|
||||
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
|
||||
|
||||
@bp.route('/')
|
||||
def handler(request):
|
||||
return text('Hello')
|
||||
def test_bp_with_host(app):
|
||||
bp = Blueprint("test_bp_host", url_prefix="/test1", host="example.com")
|
||||
|
||||
@bp.route('/', host="sub.example.com")
|
||||
def handler(request):
|
||||
return text('Hello subdomain!')
|
||||
@bp.route("/")
|
||||
def handler1(request):
|
||||
return text("Hello")
|
||||
|
||||
@bp.route("/", host="sub.example.com")
|
||||
def handler2(request):
|
||||
return text("Hello subdomain!")
|
||||
|
||||
app.blueprint(bp)
|
||||
headers = {"Host": "example.com"}
|
||||
request, response = app.test_client.get(
|
||||
'/test1/',
|
||||
headers=headers)
|
||||
assert response.text == 'Hello'
|
||||
request, response = app.test_client.get("/test1/", headers=headers)
|
||||
assert response.text == "Hello"
|
||||
|
||||
headers = {"Host": "sub.example.com"}
|
||||
request, response = app.test_client.get(
|
||||
'/test1/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get("/test1/", headers=headers)
|
||||
|
||||
assert response.text == 'Hello subdomain!'
|
||||
assert response.text == "Hello subdomain!"
|
||||
|
||||
|
||||
def test_several_bp_with_host():
|
||||
app = Sanic('test_text')
|
||||
bp = Blueprint('test_text',
|
||||
url_prefix='/test',
|
||||
host="example.com")
|
||||
bp2 = Blueprint('test_text2',
|
||||
url_prefix='/test',
|
||||
host="sub.example.com")
|
||||
def test_several_bp_with_host(app):
|
||||
bp = Blueprint("test_text", url_prefix="/test", host="example.com")
|
||||
bp2 = Blueprint("test_text2", url_prefix="/test", host="sub.example.com")
|
||||
|
||||
@bp.route('/')
|
||||
@bp.route("/")
|
||||
def handler(request):
|
||||
return text('Hello')
|
||||
return text("Hello")
|
||||
|
||||
@bp2.route('/')
|
||||
@bp2.route("/")
|
||||
def handler1(request):
|
||||
return text("Hello2")
|
||||
|
||||
@bp2.route("/other/")
|
||||
def handler2(request):
|
||||
return text('Hello2')
|
||||
|
||||
@bp2.route('/other/')
|
||||
def handler2(request):
|
||||
return text('Hello3')
|
||||
|
||||
return text("Hello3")
|
||||
|
||||
app.blueprint(bp)
|
||||
app.blueprint(bp2)
|
||||
|
||||
assert bp.host == "example.com"
|
||||
headers = {"Host": "example.com"}
|
||||
request, response = app.test_client.get(
|
||||
'/test/',
|
||||
headers=headers)
|
||||
assert response.text == 'Hello'
|
||||
request, response = app.test_client.get("/test/", headers=headers)
|
||||
assert response.text == "Hello"
|
||||
|
||||
assert bp2.host == "sub.example.com"
|
||||
headers = {"Host": "sub.example.com"}
|
||||
request, response = app.test_client.get(
|
||||
'/test/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get("/test/", headers=headers)
|
||||
|
||||
assert response.text == 'Hello2'
|
||||
request, response = app.test_client.get(
|
||||
'/test/other/',
|
||||
headers=headers)
|
||||
assert response.text == 'Hello3'
|
||||
assert response.text == "Hello2"
|
||||
request, response = app.test_client.get("/test/other/", headers=headers)
|
||||
assert response.text == "Hello3"
|
||||
|
||||
def test_bp_middleware():
|
||||
app = Sanic('test_middleware')
|
||||
blueprint = Blueprint('test_middleware')
|
||||
|
||||
@blueprint.middleware('response')
|
||||
def test_bp_middleware(app):
|
||||
blueprint = Blueprint("test_middleware")
|
||||
|
||||
@blueprint.middleware("response")
|
||||
async def process_response(request, response):
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
async def handler(request):
|
||||
return text('FAIL')
|
||||
return text("FAIL")
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
request, response = app.test_client.get("/")
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
|
||||
def test_bp_exception_handler():
|
||||
app = Sanic('test_middleware')
|
||||
blueprint = Blueprint('test_middleware')
|
||||
|
||||
@blueprint.route('/1')
|
||||
def test_bp_exception_handler(app):
|
||||
blueprint = Blueprint("test_middleware")
|
||||
|
||||
@blueprint.route("/1")
|
||||
def handler_1(request):
|
||||
raise InvalidUsage("OK")
|
||||
|
||||
@blueprint.route('/2')
|
||||
@blueprint.route("/2")
|
||||
def handler_2(request):
|
||||
raise ServerError("OK")
|
||||
|
||||
@blueprint.route('/3')
|
||||
@blueprint.route("/3")
|
||||
def handler_3(request):
|
||||
raise NotFound("OK")
|
||||
|
||||
@@ -294,133 +292,132 @@ def test_bp_exception_handler():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = app.test_client.get('/1')
|
||||
request, response = app.test_client.get("/1")
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
request, response = app.test_client.get('/2')
|
||||
request, response = app.test_client.get("/2")
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.get('/3')
|
||||
request, response = app.test_client.get("/3")
|
||||
assert response.status == 200
|
||||
|
||||
def test_bp_listeners():
|
||||
app = Sanic('test_middleware')
|
||||
blueprint = Blueprint('test_middleware')
|
||||
|
||||
def test_bp_listeners(app):
|
||||
blueprint = Blueprint("test_middleware")
|
||||
|
||||
order = []
|
||||
|
||||
@blueprint.listener('before_server_start')
|
||||
@blueprint.listener("before_server_start")
|
||||
def handler_1(sanic, loop):
|
||||
order.append(1)
|
||||
|
||||
@blueprint.listener('after_server_start')
|
||||
@blueprint.listener("after_server_start")
|
||||
def handler_2(sanic, loop):
|
||||
order.append(2)
|
||||
|
||||
@blueprint.listener('after_server_start')
|
||||
@blueprint.listener("after_server_start")
|
||||
def handler_3(sanic, loop):
|
||||
order.append(3)
|
||||
|
||||
@blueprint.listener('before_server_stop')
|
||||
@blueprint.listener("before_server_stop")
|
||||
def handler_4(sanic, loop):
|
||||
order.append(5)
|
||||
|
||||
@blueprint.listener('before_server_stop')
|
||||
@blueprint.listener("before_server_stop")
|
||||
def handler_5(sanic, loop):
|
||||
order.append(4)
|
||||
|
||||
@blueprint.listener('after_server_stop')
|
||||
@blueprint.listener("after_server_stop")
|
||||
def handler_6(sanic, loop):
|
||||
order.append(6)
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
request, response = app.test_client.get("/")
|
||||
|
||||
assert order == [1,2,3,4,5,6]
|
||||
assert order == [1, 2, 3, 4, 5, 6]
|
||||
|
||||
def test_bp_static():
|
||||
|
||||
def test_bp_static(app):
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
with open(current_file, 'rb') as file:
|
||||
with open(current_file, "rb") as file:
|
||||
current_file_contents = file.read()
|
||||
|
||||
app = Sanic('test_static')
|
||||
blueprint = Blueprint('test_static')
|
||||
blueprint = Blueprint("test_static")
|
||||
|
||||
blueprint.static('/testing.file', current_file)
|
||||
blueprint.static("/testing.file", current_file)
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
request, response = app.test_client.get("/testing.file")
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.html'])
|
||||
def test_bp_static_content_type(file_name):
|
||||
|
||||
@pytest.mark.parametrize("file_name", ["test.html"])
|
||||
def test_bp_static_content_type(app, file_name):
|
||||
# This is done here, since no other test loads a file here
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
current_directory = os.path.dirname(os.path.abspath(current_file))
|
||||
static_directory = os.path.join(current_directory, 'static')
|
||||
static_directory = os.path.join(current_directory, "static")
|
||||
|
||||
app = Sanic('test_static')
|
||||
blueprint = Blueprint('test_static')
|
||||
blueprint = Blueprint("test_static")
|
||||
blueprint.static(
|
||||
'/testing.file',
|
||||
"/testing.file",
|
||||
get_file_path(static_directory, file_name),
|
||||
content_type='text/html; charset=utf-8'
|
||||
content_type="text/html; charset=utf-8",
|
||||
)
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
request, response = app.test_client.get("/testing.file")
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_directory, file_name)
|
||||
assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
|
||||
assert response.headers["Content-Type"] == "text/html; charset=utf-8"
|
||||
|
||||
def test_bp_shorthand():
|
||||
app = Sanic('test_shorhand_routes')
|
||||
blueprint = Blueprint('test_shorhand_routes')
|
||||
|
||||
def test_bp_shorthand(app):
|
||||
blueprint = Blueprint("test_shorhand_routes")
|
||||
ev = asyncio.Event()
|
||||
|
||||
@blueprint.get('/get')
|
||||
@blueprint.get("/get")
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
@blueprint.put('/put')
|
||||
def handler(request):
|
||||
@blueprint.put("/put")
|
||||
def put_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
@blueprint.post('/post')
|
||||
def handler(request):
|
||||
@blueprint.post("/post")
|
||||
def post_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
@blueprint.head('/head')
|
||||
def handler(request):
|
||||
@blueprint.head("/head")
|
||||
def head_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
@blueprint.options('/options')
|
||||
def handler(request):
|
||||
@blueprint.options("/options")
|
||||
def options_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
@blueprint.patch('/patch')
|
||||
def handler(request):
|
||||
@blueprint.patch("/patch")
|
||||
def patch_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
@blueprint.delete('/delete')
|
||||
def handler(request):
|
||||
@blueprint.delete("/delete")
|
||||
def delete_handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
@blueprint.websocket('/ws')
|
||||
async def handler(request, ws):
|
||||
@blueprint.websocket("/ws/", strict_slashes=True)
|
||||
async def websocket_handler(request, ws):
|
||||
assert request.stream is None
|
||||
ev.set()
|
||||
|
||||
@@ -428,93 +425,282 @@ def test_bp_shorthand():
|
||||
|
||||
assert app.is_request_stream is False
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.get("/get")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.post('/get')
|
||||
request, response = app.test_client.post("/get")
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.put('/put')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.put("/put")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.get('/post')
|
||||
request, response = app.test_client.get("/post")
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.post("/post")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.get('/post')
|
||||
request, response = app.test_client.get("/post")
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.head('/head')
|
||||
request, response = app.test_client.head("/head")
|
||||
assert response.status == 200
|
||||
|
||||
request, response = app.test_client.get('/head')
|
||||
request, response = app.test_client.get("/head")
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.options('/options')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.options("/options")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.get('/options')
|
||||
request, response = app.test_client.get("/options")
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.patch('/patch')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.patch("/patch")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.get('/patch')
|
||||
request, response = app.test_client.get("/patch")
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.delete('/delete')
|
||||
assert response.text == 'OK'
|
||||
request, response = app.test_client.delete("/delete")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = app.test_client.get('/delete')
|
||||
request, response = app.test_client.get("/delete")
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13'})
|
||||
request, response = app.test_client.get(
|
||||
"/ws/",
|
||||
headers={
|
||||
"Upgrade": "websocket",
|
||||
"Connection": "upgrade",
|
||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
||||
"Sec-WebSocket-Version": "13",
|
||||
},
|
||||
)
|
||||
assert response.status == 101
|
||||
assert ev.is_set()
|
||||
|
||||
def test_bp_group():
|
||||
app = Sanic('test_nested_bp_groups')
|
||||
|
||||
deep_0 = Blueprint('deep_0', url_prefix='/deep')
|
||||
deep_1 = Blueprint('deep_1', url_prefix = '/deep1')
|
||||
def test_bp_group(app):
|
||||
deep_0 = Blueprint("deep_0", url_prefix="/deep")
|
||||
deep_1 = Blueprint("deep_1", url_prefix="/deep1")
|
||||
|
||||
@deep_0.route('/')
|
||||
@deep_0.route("/")
|
||||
def handler(request):
|
||||
return text('D0_OK')
|
||||
return text("D0_OK")
|
||||
|
||||
@deep_1.route('/bottom')
|
||||
def handler(request):
|
||||
return text('D1B_OK')
|
||||
@deep_1.route("/bottom")
|
||||
def bottom_handler(request):
|
||||
return text("D1B_OK")
|
||||
|
||||
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
|
||||
mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
|
||||
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix="/mid")
|
||||
mid_1 = Blueprint("mid_tier", url_prefix="/mid1")
|
||||
|
||||
@mid_1.route('/')
|
||||
def handler(request):
|
||||
return text('M1_OK')
|
||||
@mid_1.route("/")
|
||||
def handler1(request):
|
||||
return text("M1_OK")
|
||||
|
||||
top = Blueprint.group(mid_0, mid_1)
|
||||
|
||||
app.blueprint(top)
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
return text('TOP_OK')
|
||||
@app.route("/")
|
||||
def handler2(request):
|
||||
return text("TOP_OK")
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'TOP_OK'
|
||||
request, response = app.test_client.get("/")
|
||||
assert response.text == "TOP_OK"
|
||||
|
||||
request, response = app.test_client.get('/mid1')
|
||||
assert response.text == 'M1_OK'
|
||||
request, response = app.test_client.get("/mid1")
|
||||
assert response.text == "M1_OK"
|
||||
|
||||
request, response = app.test_client.get('/mid/deep')
|
||||
assert response.text == 'D0_OK'
|
||||
request, response = app.test_client.get("/mid/deep")
|
||||
assert response.text == "D0_OK"
|
||||
|
||||
request, response = app.test_client.get('/mid/deep1/bottom')
|
||||
assert response.text == 'D1B_OK'
|
||||
request, response = app.test_client.get("/mid/deep1/bottom")
|
||||
assert response.text == "D1B_OK"
|
||||
|
||||
|
||||
def test_bp_group_with_default_url_prefix(app):
|
||||
from sanic.response import json
|
||||
|
||||
bp_resources = Blueprint("bp_resources")
|
||||
|
||||
@bp_resources.get("/")
|
||||
def list_resources_handler(request):
|
||||
resource = {}
|
||||
return json([resource])
|
||||
|
||||
bp_resource = Blueprint("bp_resource", url_prefix="/<resource_id>")
|
||||
|
||||
@bp_resource.get("/")
|
||||
def get_resource_hander(request, resource_id):
|
||||
resource = {"resource_id": resource_id}
|
||||
return json(resource)
|
||||
|
||||
bp_resources_group = Blueprint.group(
|
||||
bp_resources, bp_resource, url_prefix="/resources"
|
||||
)
|
||||
bp_api_v1 = Blueprint("bp_api_v1")
|
||||
|
||||
@bp_api_v1.get("/info")
|
||||
def api_v1_info(request):
|
||||
return text("api_version: v1")
|
||||
|
||||
bp_api_v1_group = Blueprint.group(
|
||||
bp_api_v1, bp_resources_group, url_prefix="/v1"
|
||||
)
|
||||
bp_api_group = Blueprint.group(bp_api_v1_group, url_prefix="/api")
|
||||
app.blueprint(bp_api_group)
|
||||
|
||||
request, response = app.test_client.get("/api/v1/info")
|
||||
assert response.text == "api_version: v1"
|
||||
|
||||
request, response = app.test_client.get("/api/v1/resources")
|
||||
assert response.json == [{}]
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
resource_id = str(uuid4())
|
||||
request, response = app.test_client.get(
|
||||
"/api/v1/resources/{0}".format(resource_id)
|
||||
)
|
||||
assert response.json == {"resource_id": resource_id}
|
||||
|
||||
|
||||
def test_blueprint_middleware_with_args(app: Sanic):
|
||||
bp = Blueprint(name="with_args_bp", url_prefix="/wa")
|
||||
|
||||
@bp.middleware
|
||||
def middleware_with_no_tag(request: Request):
|
||||
if request.headers.get("content-type") == "application/json":
|
||||
request.headers["accepts"] = "plain/text"
|
||||
else:
|
||||
request.headers["accepts"] = "application/json"
|
||||
|
||||
@bp.route("/")
|
||||
def default_route(request):
|
||||
if request.headers.get("accepts") == "application/json":
|
||||
return json({"test": "value"})
|
||||
else:
|
||||
return text("value")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
_, response = app.test_client.get(
|
||||
"/wa", headers={"content-type": "application/json"}
|
||||
)
|
||||
assert response.text == "value"
|
||||
|
||||
_, response = app.test_client.get(
|
||||
"/wa", headers={"content-type": "plain/text"}
|
||||
)
|
||||
assert response.json.get("test") == "value"
|
||||
d = {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("file_name", ["test.file"])
|
||||
def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
with open(current_file, "rb") as file:
|
||||
current_file_contents = file.read()
|
||||
|
||||
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
|
||||
|
||||
bp.static(
|
||||
"/test.file/",
|
||||
get_file_path(static_file_directory, file_name),
|
||||
name="static.testing",
|
||||
strict_slashes=True,
|
||||
)
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
uri = app.url_for("static", name="static.testing")
|
||||
assert uri == "/static/test.file"
|
||||
|
||||
_, response = app.test_client.get("/static/test.file")
|
||||
assert response.status == 404
|
||||
|
||||
_, response = app.test_client.get("/static/test.file/")
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_route_handler_add(app: Sanic):
|
||||
view = CompositionView()
|
||||
|
||||
async def get_handler(request):
|
||||
return json({"response": "OK"})
|
||||
|
||||
view.add(["GET"], get_handler, stream=False)
|
||||
|
||||
async def default_handler(request):
|
||||
return text("OK")
|
||||
|
||||
bp = Blueprint(name="handler", url_prefix="/handler")
|
||||
bp.add_route(default_handler, uri="/default/", strict_slashes=True)
|
||||
|
||||
bp.add_route(view, uri="/view", name="test")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
_, response = app.test_client.get("/handler/default/")
|
||||
assert response.text == "OK"
|
||||
|
||||
_, response = app.test_client.get("/handler/view")
|
||||
assert response.json["response"] == "OK"
|
||||
|
||||
|
||||
def test_websocket_route(app: Sanic):
|
||||
event = asyncio.Event()
|
||||
|
||||
async def websocket_handler(request, ws):
|
||||
assert ws.subprotocol is None
|
||||
event.set()
|
||||
|
||||
bp = Blueprint(name="handler", url_prefix="/ws")
|
||||
bp.add_websocket_route(websocket_handler, "/test", name="test")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
_, response = app.test_client.get(
|
||||
"/ws/test",
|
||||
headers={
|
||||
"Upgrade": "websocket",
|
||||
"Connection": "upgrade",
|
||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
||||
"Sec-WebSocket-Version": "13",
|
||||
},
|
||||
)
|
||||
assert response.status == 101
|
||||
assert event.is_set()
|
||||
|
||||
|
||||
def test_duplicate_blueprint(app):
|
||||
bp_name = "bp"
|
||||
bp = Blueprint(bp_name)
|
||||
bp1 = Blueprint(bp_name)
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
with pytest.raises(AssertionError) as excinfo:
|
||||
app.blueprint(bp1)
|
||||
|
||||
assert str(excinfo.value) == (
|
||||
'A blueprint with the name "{}" is already registered. '
|
||||
"Blueprint names must be unique."
|
||||
).format(bp_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("debug", [True, False, None])
|
||||
def test_register_blueprint(app, debug):
|
||||
bp = Blueprint("bp")
|
||||
|
||||
app.debug = debug
|
||||
with pytest.warns(DeprecationWarning) as record:
|
||||
app.register_blueprint(bp)
|
||||
|
||||
assert record[0].message.args[0] == (
|
||||
"Use of register_blueprint will be deprecated in "
|
||||
"version 1.0. Please use the blueprint method"
|
||||
" instead"
|
||||
)
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
from tempfile import TemporaryDirectory
|
||||
from textwrap import dedent
|
||||
import pytest
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.config import Config, DEFAULT_CONFIG
|
||||
from sanic.exceptions import PyFileError
|
||||
|
||||
|
||||
def test_load_from_object():
|
||||
app = Sanic('test_load_from_object')
|
||||
@contextmanager
|
||||
def temp_path():
|
||||
""" a simple cross platform replacement for NamedTemporaryFile """
|
||||
with TemporaryDirectory() as td:
|
||||
yield Path(td, "file")
|
||||
|
||||
|
||||
def test_load_from_object(app):
|
||||
class Config:
|
||||
not_for_config = 'should not be used'
|
||||
CONFIG_VALUE = 'should be used'
|
||||
not_for_config = "should not be used"
|
||||
CONFIG_VALUE = "should be used"
|
||||
|
||||
app.config.from_object(Config)
|
||||
assert 'CONFIG_VALUE' in app.config
|
||||
assert app.config.CONFIG_VALUE == 'should be used'
|
||||
assert 'not_for_config' not in app.config
|
||||
assert "CONFIG_VALUE" in app.config
|
||||
assert app.config.CONFIG_VALUE == "should be used"
|
||||
assert "not_for_config" not in app.config
|
||||
|
||||
|
||||
def test_auto_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
@@ -22,64 +34,98 @@ def test_auto_load_env():
|
||||
assert app.config.TEST_ANSWER == 42
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
|
||||
def test_auto_load_bool_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "True"
|
||||
app = Sanic()
|
||||
assert app.config.TEST_ANSWER == True
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
|
||||
def test_dont_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic(load_env=False)
|
||||
assert getattr(app.config, 'TEST_ANSWER', None) == None
|
||||
assert getattr(app.config, "TEST_ANSWER", None) is None
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
|
||||
def test_load_env_prefix():
|
||||
environ["MYAPP_TEST_ANSWER"] = "42"
|
||||
app = Sanic(load_env='MYAPP_')
|
||||
app = Sanic(load_env="MYAPP_")
|
||||
assert app.config.TEST_ANSWER == 42
|
||||
del environ["MYAPP_TEST_ANSWER"]
|
||||
|
||||
def test_load_from_file():
|
||||
app = Sanic('test_load_from_file')
|
||||
config = b"""
|
||||
VALUE = 'some value'
|
||||
condition = 1 == 1
|
||||
if condition:
|
||||
CONDITIONAL = 'should be set'
|
||||
|
||||
def test_load_env_prefix_float_values():
|
||||
environ["MYAPP_TEST_ROI"] = "2.3"
|
||||
app = Sanic(load_env="MYAPP_")
|
||||
assert app.config.TEST_ROI == 2.3
|
||||
del environ["MYAPP_TEST_ROI"]
|
||||
|
||||
|
||||
def test_load_env_prefix_string_value():
|
||||
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
|
||||
app = Sanic(load_env="MYAPP_")
|
||||
assert app.config.TEST_TOKEN == "somerandomtesttoken"
|
||||
del environ["MYAPP_TEST_TOKEN"]
|
||||
|
||||
|
||||
def test_load_from_file(app):
|
||||
config = dedent(
|
||||
"""
|
||||
VALUE = 'some value'
|
||||
condition = 1 == 1
|
||||
if condition:
|
||||
CONDITIONAL = 'should be set'
|
||||
"""
|
||||
with NamedTemporaryFile() as config_file:
|
||||
config_file.write(config)
|
||||
config_file.seek(0)
|
||||
app.config.from_pyfile(config_file.name)
|
||||
assert 'VALUE' in app.config
|
||||
assert app.config.VALUE == 'some value'
|
||||
assert 'CONDITIONAL' in app.config
|
||||
assert app.config.CONDITIONAL == 'should be set'
|
||||
assert 'condition' not in app.config
|
||||
)
|
||||
with temp_path() as config_path:
|
||||
config_path.write_text(config)
|
||||
app.config.from_pyfile(str(config_path))
|
||||
assert "VALUE" in app.config
|
||||
assert app.config.VALUE == "some value"
|
||||
assert "CONDITIONAL" in app.config
|
||||
assert app.config.CONDITIONAL == "should be set"
|
||||
assert "condition" not in app.config
|
||||
|
||||
|
||||
def test_load_from_missing_file():
|
||||
app = Sanic('test_load_from_missing_file')
|
||||
def test_load_from_missing_file(app):
|
||||
with pytest.raises(IOError):
|
||||
app.config.from_pyfile('non-existent file')
|
||||
app.config.from_pyfile("non-existent file")
|
||||
|
||||
|
||||
def test_load_from_envvar():
|
||||
app = Sanic('test_load_from_envvar')
|
||||
config = b"VALUE = 'some value'"
|
||||
with NamedTemporaryFile() as config_file:
|
||||
config_file.write(config)
|
||||
config_file.seek(0)
|
||||
environ['APP_CONFIG'] = config_file.name
|
||||
app.config.from_envvar('APP_CONFIG')
|
||||
assert 'VALUE' in app.config
|
||||
assert app.config.VALUE == 'some value'
|
||||
def test_load_from_envvar(app):
|
||||
config = "VALUE = 'some value'"
|
||||
with temp_path() as config_path:
|
||||
config_path.write_text(config)
|
||||
environ["APP_CONFIG"] = str(config_path)
|
||||
app.config.from_envvar("APP_CONFIG")
|
||||
assert "VALUE" in app.config
|
||||
assert app.config.VALUE == "some value"
|
||||
|
||||
|
||||
def test_load_from_missing_envvar():
|
||||
app = Sanic('test_load_from_missing_envvar')
|
||||
with pytest.raises(RuntimeError):
|
||||
app.config.from_envvar('non-existent variable')
|
||||
def test_load_from_missing_envvar(app):
|
||||
with pytest.raises(RuntimeError) as e:
|
||||
app.config.from_envvar("non-existent variable")
|
||||
assert str(e.value) == (
|
||||
"The environment variable 'non-existent "
|
||||
"variable' is not set and thus configuration "
|
||||
"could not be loaded."
|
||||
)
|
||||
|
||||
|
||||
def test_overwrite_exisiting_config():
|
||||
app = Sanic('test_overwrite_exisiting_config')
|
||||
def test_load_config_from_file_invalid_syntax(app):
|
||||
config = "VALUE = some value"
|
||||
with temp_path() as config_path:
|
||||
config_path.write_text(config)
|
||||
|
||||
with pytest.raises(PyFileError):
|
||||
app.config.from_pyfile(config_path)
|
||||
|
||||
|
||||
def test_overwrite_exisiting_config(app):
|
||||
app.config.DEFAULT = 1
|
||||
|
||||
class Config:
|
||||
DEFAULT = 2
|
||||
|
||||
@@ -87,7 +133,126 @@ def test_overwrite_exisiting_config():
|
||||
assert app.config.DEFAULT == 2
|
||||
|
||||
|
||||
def test_missing_config():
|
||||
app = Sanic('test_missing_config')
|
||||
with pytest.raises(AttributeError):
|
||||
app.config.NON_EXISTENT
|
||||
def test_overwrite_exisiting_config_ignore_lowercase(app):
|
||||
app.config.default = 1
|
||||
|
||||
class Config:
|
||||
default = 2
|
||||
|
||||
app.config.from_object(Config)
|
||||
assert app.config.default == 1
|
||||
|
||||
|
||||
def test_missing_config(app):
|
||||
with pytest.raises(
|
||||
AttributeError, match="Config has no 'NON_EXISTENT'"
|
||||
) as e:
|
||||
_ = app.config.NON_EXISTENT
|
||||
|
||||
|
||||
def test_config_defaults():
|
||||
"""
|
||||
load DEFAULT_CONFIG
|
||||
"""
|
||||
conf = Config()
|
||||
for key, value in DEFAULT_CONFIG.items():
|
||||
assert getattr(conf, key) == value
|
||||
|
||||
|
||||
def test_config_custom_defaults():
|
||||
"""
|
||||
we should have all the variables from defaults rewriting them with custom defaults passed in
|
||||
Config
|
||||
"""
|
||||
custom_defaults = {
|
||||
"REQUEST_MAX_SIZE": 1,
|
||||
"KEEP_ALIVE": False,
|
||||
"ACCESS_LOG": False,
|
||||
}
|
||||
conf = Config(defaults=custom_defaults)
|
||||
for key, value in DEFAULT_CONFIG.items():
|
||||
if key in custom_defaults.keys():
|
||||
value = custom_defaults[key]
|
||||
assert getattr(conf, key) == value
|
||||
|
||||
|
||||
def test_config_custom_defaults_with_env():
|
||||
"""
|
||||
test that environment variables has higher priority than DEFAULT_CONFIG and passed defaults dict
|
||||
"""
|
||||
custom_defaults = {
|
||||
"REQUEST_MAX_SIZE123": 1,
|
||||
"KEEP_ALIVE123": False,
|
||||
"ACCESS_LOG123": False,
|
||||
}
|
||||
|
||||
environ_defaults = {
|
||||
"SANIC_REQUEST_MAX_SIZE123": "2",
|
||||
"SANIC_KEEP_ALIVE123": "True",
|
||||
"SANIC_ACCESS_LOG123": "False",
|
||||
}
|
||||
|
||||
for key, value in environ_defaults.items():
|
||||
environ[key] = value
|
||||
|
||||
conf = Config(defaults=custom_defaults)
|
||||
for key, value in DEFAULT_CONFIG.items():
|
||||
if "SANIC_" + key in environ_defaults.keys():
|
||||
value = environ_defaults["SANIC_" + key]
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
if value in ["True", "False"]:
|
||||
value = value == "True"
|
||||
|
||||
assert getattr(conf, key) == value
|
||||
|
||||
for key, value in environ_defaults.items():
|
||||
del environ[key]
|
||||
|
||||
|
||||
def test_config_access_log_passing_in_run(app):
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
@app.listener("after_server_start")
|
||||
async def _request(sanic, loop):
|
||||
app.stop()
|
||||
|
||||
app.run(port=1340, access_log=False)
|
||||
assert app.config.ACCESS_LOG == False
|
||||
|
||||
app.run(port=1340, access_log=True)
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
|
||||
async def test_config_access_log_passing_in_create_server(app):
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
@app.listener("after_server_start")
|
||||
async def _request(sanic, loop):
|
||||
app.stop()
|
||||
|
||||
await app.create_server(
|
||||
port=1341, access_log=False, return_asyncio_server=True
|
||||
)
|
||||
assert app.config.ACCESS_LOG == False
|
||||
|
||||
await app.create_server(
|
||||
port=1342, access_log=True, return_asyncio_server=True
|
||||
)
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
|
||||
def test_config_rewrite_keep_alive():
|
||||
config = Config()
|
||||
assert config.KEEP_ALIVE == DEFAULT_CONFIG["KEEP_ALIVE"]
|
||||
config = Config(keep_alive=True)
|
||||
assert config.KEEP_ALIVE == True
|
||||
config = Config(keep_alive=False)
|
||||
assert config.KEEP_ALIVE == False
|
||||
|
||||
# use defaults
|
||||
config = Config(defaults={"KEEP_ALIVE": False})
|
||||
assert config.KEEP_ALIVE == False
|
||||
config = Config(defaults={"KEEP_ALIVE": True})
|
||||
assert config.KEEP_ALIVE == True
|
||||
|
||||
@@ -1,116 +1,190 @@
|
||||
from datetime import datetime, timedelta
|
||||
from http.cookies import SimpleCookie
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.response import text
|
||||
import pytest
|
||||
|
||||
from sanic.cookies import Cookie, DEFAULT_MAX_AGE
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
def test_cookies():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
def test_cookies(app):
|
||||
@app.route("/")
|
||||
def handler(request):
|
||||
response = text('Cookies are: {}'.format(request.cookies['test']))
|
||||
response.cookies['right_back'] = 'at you'
|
||||
response = text("Cookies are: {}".format(request.cookies["test"]))
|
||||
response.cookies["right_back"] = "at you"
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get('/', cookies={"test": "working!"})
|
||||
request, response = app.test_client.get("/", cookies={"test": "working!"})
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
response_cookies.load(response.headers.get("Set-Cookie", {}))
|
||||
|
||||
assert response.text == 'Cookies are: working!'
|
||||
assert response_cookies['right_back'].value == 'at you'
|
||||
assert response.text == "Cookies are: working!"
|
||||
assert response_cookies["right_back"].value == "at you"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("httponly,expected", [
|
||||
(False, False),
|
||||
(True, True),
|
||||
])
|
||||
def test_false_cookies_encoded(httponly, expected):
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
|
||||
def test_false_cookies_encoded(app, httponly, expected):
|
||||
@app.route("/")
|
||||
def handler(request):
|
||||
response = text('hello cookies')
|
||||
response.cookies['hello'] = 'world'
|
||||
response.cookies['hello']['httponly'] = httponly
|
||||
return text(response.cookies['hello'].encode('utf8'))
|
||||
response = text("hello cookies")
|
||||
response.cookies["hello"] = "world"
|
||||
response.cookies["hello"]["httponly"] = httponly
|
||||
return text(response.cookies["hello"].encode("utf8"))
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
request, response = app.test_client.get("/")
|
||||
|
||||
assert ('HttpOnly' in response.text) == expected
|
||||
assert ("HttpOnly" in response.text) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("httponly,expected", [
|
||||
(False, False),
|
||||
(True, True),
|
||||
])
|
||||
def test_false_cookies(httponly, expected):
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
|
||||
def test_false_cookies(app, httponly, expected):
|
||||
@app.route("/")
|
||||
def handler(request):
|
||||
response = text('hello cookies')
|
||||
response.cookies['right_back'] = 'at you'
|
||||
response.cookies['right_back']['httponly'] = httponly
|
||||
response = text("hello cookies")
|
||||
response.cookies["right_back"] = "at you"
|
||||
response.cookies["right_back"]["httponly"] = httponly
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
request, response = app.test_client.get("/")
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
response_cookies.load(response.headers.get("Set-Cookie", {}))
|
||||
|
||||
assert ('HttpOnly' in response_cookies['right_back'].output()) == expected
|
||||
assert ("HttpOnly" in response_cookies["right_back"].output()) == expected
|
||||
|
||||
def test_http2_cookies():
|
||||
app = Sanic('test_http2_cookies')
|
||||
|
||||
@app.route('/')
|
||||
def test_http2_cookies(app):
|
||||
@app.route("/")
|
||||
async def handler(request):
|
||||
response = text('Cookies are: {}'.format(request.cookies['test']))
|
||||
response = text("Cookies are: {}".format(request.cookies["test"]))
|
||||
return response
|
||||
|
||||
headers = {'cookie': 'test=working!'}
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
headers = {"cookie": "test=working!"}
|
||||
request, response = app.test_client.get("/", headers=headers)
|
||||
|
||||
assert response.text == 'Cookies are: working!'
|
||||
assert response.text == "Cookies are: working!"
|
||||
|
||||
def test_cookie_options():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
def test_cookie_options(app):
|
||||
@app.route("/")
|
||||
def handler(request):
|
||||
response = text("OK")
|
||||
response.cookies['test'] = 'at you'
|
||||
response.cookies['test']['httponly'] = True
|
||||
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
|
||||
response.cookies["test"] = "at you"
|
||||
response.cookies["test"]["httponly"] = True
|
||||
response.cookies["test"]["expires"] = datetime.now() + timedelta(
|
||||
seconds=10
|
||||
)
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
request, response = app.test_client.get("/")
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
response_cookies.load(response.headers.get("Set-Cookie", {}))
|
||||
|
||||
assert response_cookies['test'].value == 'at you'
|
||||
assert response_cookies['test']['httponly'] == True
|
||||
assert response_cookies["test"].value == "at you"
|
||||
assert response_cookies["test"]["httponly"] is True
|
||||
|
||||
def test_cookie_deletion():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
def test_cookie_deletion(app):
|
||||
@app.route("/")
|
||||
def handler(request):
|
||||
response = text("OK")
|
||||
del response.cookies['i_want_to_die']
|
||||
response.cookies['i_never_existed'] = 'testing'
|
||||
del response.cookies['i_never_existed']
|
||||
del response.cookies["i_want_to_die"]
|
||||
response.cookies["i_never_existed"] = "testing"
|
||||
del response.cookies["i_never_existed"]
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
request, response = app.test_client.get("/")
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
response_cookies.load(response.headers.get("Set-Cookie", {}))
|
||||
|
||||
assert int(response_cookies['i_want_to_die']['max-age']) == 0
|
||||
assert int(response_cookies["i_want_to_die"]["max-age"]) == 0
|
||||
with pytest.raises(KeyError):
|
||||
hold_my_beer = response.cookies['i_never_existed']
|
||||
_ = response.cookies["i_never_existed"]
|
||||
|
||||
|
||||
def test_cookie_reserved_cookie():
|
||||
with pytest.raises(expected_exception=KeyError) as e:
|
||||
Cookie("domain", "testdomain.com")
|
||||
assert e.message == "Cookie name is a reserved word"
|
||||
|
||||
|
||||
def test_cookie_illegal_key_format():
|
||||
with pytest.raises(expected_exception=KeyError) as e:
|
||||
Cookie("testå", "test")
|
||||
assert e.message == "Cookie key contains illegal characters"
|
||||
|
||||
|
||||
def test_cookie_set_unknown_property():
|
||||
c = Cookie("test_cookie", "value")
|
||||
with pytest.raises(expected_exception=KeyError) as e:
|
||||
c["invalid"] = "value"
|
||||
assert e.message == "Unknown cookie property"
|
||||
|
||||
|
||||
def test_cookie_set_same_key(app):
|
||||
|
||||
cookies = {"test": "wait"}
|
||||
|
||||
@app.get("/")
|
||||
def handler(request):
|
||||
response = text("pass")
|
||||
response.cookies["test"] = "modified"
|
||||
response.cookies["test"] = "pass"
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get("/", cookies=cookies)
|
||||
assert response.status == 200
|
||||
assert response.cookies["test"].value == "pass"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("max_age", ["0", 30, 30.0, 30.1, "30", "test"])
|
||||
def test_cookie_max_age(app, max_age):
|
||||
cookies = {"test": "wait"}
|
||||
|
||||
@app.get("/")
|
||||
def handler(request):
|
||||
response = text("pass")
|
||||
response.cookies["test"] = "pass"
|
||||
response.cookies["test"]["max-age"] = max_age
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get("/", cookies=cookies)
|
||||
assert response.status == 200
|
||||
|
||||
assert response.cookies["test"].value == "pass"
|
||||
|
||||
if str(max_age).isdigit() and int(max_age) == float(max_age):
|
||||
assert response.cookies["test"]["max-age"] == str(max_age)
|
||||
else:
|
||||
assert response.cookies["test"]["max-age"] == str(DEFAULT_MAX_AGE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expires", [datetime.now() + timedelta(seconds=60)])
|
||||
def test_cookie_expires(app, expires):
|
||||
cookies = {"test": "wait"}
|
||||
|
||||
@app.get("/")
|
||||
def handler(request):
|
||||
response = text("pass")
|
||||
response.cookies["test"] = "pass"
|
||||
response.cookies["test"]["expires"] = expires
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get("/", cookies=cookies)
|
||||
assert response.status == 200
|
||||
|
||||
assert response.cookies["test"].value == "pass"
|
||||
|
||||
if isinstance(expires, datetime):
|
||||
expires = expires.strftime("%a, %d-%b-%Y %T GMT")
|
||||
|
||||
assert response.cookies["test"]["expires"] == expires
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"])
|
||||
def test_cookie_expires_illegal_instance_type(expires):
|
||||
c = Cookie("test_cookie", "value")
|
||||
with pytest.raises(expected_exception=TypeError) as e:
|
||||
c["expires"] = expires
|
||||
assert e.message == "Cookie 'expires' property must be a datetime"
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from threading import Event
|
||||
import asyncio
|
||||
from queue import Queue
|
||||
|
||||
|
||||
def test_create_task():
|
||||
def test_create_task(app):
|
||||
e = Event()
|
||||
|
||||
async def coro():
|
||||
await asyncio.sleep(0.05)
|
||||
e.set()
|
||||
|
||||
app = Sanic('test_create_task')
|
||||
app.add_task(coro)
|
||||
|
||||
@app.route('/early')
|
||||
@app.route("/early")
|
||||
def not_set(request):
|
||||
return text(e.is_set())
|
||||
|
||||
@app.route('/late')
|
||||
@app.route("/late")
|
||||
async def set(request):
|
||||
await asyncio.sleep(0.1)
|
||||
return text(e.is_set())
|
||||
|
||||
request, response = app.test_client.get('/early')
|
||||
assert response.body == b'False'
|
||||
request, response = app.test_client.get("/early")
|
||||
assert response.body == b"False"
|
||||
|
||||
request, response = app.test_client.get('/late')
|
||||
assert response.body == b'True'
|
||||
request, response = app.test_client.get("/late")
|
||||
assert response.body == b"True"
|
||||
|
||||
def test_create_task_with_app_arg():
|
||||
app = Sanic('test_add_task')
|
||||
|
||||
def test_create_task_with_app_arg(app):
|
||||
q = Queue()
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
def not_set(request):
|
||||
return "hello"
|
||||
|
||||
@@ -43,5 +41,5 @@ def test_create_task_with_app_arg():
|
||||
|
||||
app.add_task(coro)
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert q.get() == 'test_add_task'
|
||||
request, response = app.test_client.get("/")
|
||||
assert q.get() == "test_create_task_with_app_arg"
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
from sanic import Sanic
|
||||
from sanic.server import HttpProtocol
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('test_custom_porotocol')
|
||||
|
||||
|
||||
class CustomHttpProtocol(HttpProtocol):
|
||||
|
||||
def write_response(self, response):
|
||||
if isinstance(response, str):
|
||||
response = text(response)
|
||||
self.transport.write(
|
||||
response.output(self.request.version)
|
||||
)
|
||||
self.transport.write(response.output(self.request.version))
|
||||
self.transport.close()
|
||||
|
||||
|
||||
@app.route('/1')
|
||||
async def handler_1(request):
|
||||
return 'OK'
|
||||
def test_use_custom_protocol(app):
|
||||
@app.route("/1")
|
||||
async def handler_1(request):
|
||||
return "OK"
|
||||
|
||||
|
||||
def test_use_custom_protocol():
|
||||
server_kwargs = {
|
||||
'protocol': CustomHttpProtocol
|
||||
}
|
||||
request, response = app.test_client.get(
|
||||
'/1', server_kwargs=server_kwargs)
|
||||
server_kwargs = {"protocol": CustomHttpProtocol}
|
||||
request, response = app.test_client.get("/1", server_kwargs=server_kwargs)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
|
||||
53
tests/test_custom_request.py
Normal file
53
tests/test_custom_request.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from io import BytesIO
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.request import Request
|
||||
from sanic.response import json_dumps, text
|
||||
|
||||
|
||||
class CustomRequest(Request):
|
||||
__slots__ = ("body_buffer",)
|
||||
|
||||
def body_init(self):
|
||||
self.body_buffer = BytesIO()
|
||||
|
||||
def body_push(self, data):
|
||||
self.body_buffer.write(data)
|
||||
|
||||
def body_finish(self):
|
||||
self.body = self.body_buffer.getvalue()
|
||||
self.body_buffer.close()
|
||||
|
||||
|
||||
def test_custom_request():
|
||||
app = Sanic(request_class=CustomRequest)
|
||||
|
||||
@app.route("/post", methods=["POST"])
|
||||
async def post_handler(request):
|
||||
return text("OK")
|
||||
|
||||
@app.route("/get")
|
||||
async def get_handler(request):
|
||||
return text("OK")
|
||||
|
||||
payload = {"test": "OK"}
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
request, response = app.test_client.post(
|
||||
"/post", data=json_dumps(payload), headers=headers
|
||||
)
|
||||
|
||||
assert isinstance(request.body_buffer, BytesIO)
|
||||
assert request.body_buffer.closed
|
||||
assert request.body == b'{"test":"OK"}'
|
||||
assert request.json.get("test") == "OK"
|
||||
assert response.text == "OK"
|
||||
assert response.status == 200
|
||||
|
||||
request, response = app.test_client.get("/get")
|
||||
|
||||
assert isinstance(request.body_buffer, BytesIO)
|
||||
assert request.body_buffer.closed
|
||||
assert request.body == b""
|
||||
assert response.text == "OK"
|
||||
assert response.status == 200
|
||||
@@ -1,44 +1,43 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.router import RouteExists
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method,attr, expected", [
|
||||
("get", "text", "OK1 test"),
|
||||
("post", "text", "OK2 test"),
|
||||
("put", "text", "OK2 test"),
|
||||
("delete", "status", 405),
|
||||
])
|
||||
def test_overload_dynamic_routes(method, attr, expected):
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@app.route('/overload/<param>', methods=['GET'])
|
||||
@pytest.mark.parametrize(
|
||||
"method,attr, expected",
|
||||
[
|
||||
("get", "text", "OK1 test"),
|
||||
("post", "text", "OK2 test"),
|
||||
("put", "text", "OK2 test"),
|
||||
("delete", "status", 405),
|
||||
],
|
||||
)
|
||||
def test_overload_dynamic_routes(app, method, attr, expected):
|
||||
@app.route("/overload/<param>", methods=["GET"])
|
||||
async def handler1(request, param):
|
||||
return text('OK1 ' + param)
|
||||
return text("OK1 " + param)
|
||||
|
||||
@app.route('/overload/<param>', methods=['POST', 'PUT'])
|
||||
@app.route("/overload/<param>", methods=["POST", "PUT"])
|
||||
async def handler2(request, param):
|
||||
return text('OK2 ' + param)
|
||||
return text("OK2 " + param)
|
||||
|
||||
request, response = getattr(app.test_client, method)('/overload/test')
|
||||
request, response = getattr(app.test_client, method)("/overload/test")
|
||||
assert getattr(response, attr) == expected
|
||||
|
||||
|
||||
def test_overload_dynamic_routes_exist():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@app.route('/overload/<param>', methods=['GET'])
|
||||
def test_overload_dynamic_routes_exist(app):
|
||||
@app.route("/overload/<param>", methods=["GET"])
|
||||
async def handler1(request, param):
|
||||
return text('OK1 ' + param)
|
||||
return text("OK1 " + param)
|
||||
|
||||
@app.route('/overload/<param>', methods=['POST', 'PUT'])
|
||||
@app.route("/overload/<param>", methods=["POST", "PUT"])
|
||||
async def handler2(request, param):
|
||||
return text('OK2 ' + param)
|
||||
return text("OK2 " + param)
|
||||
|
||||
# if this doesn't raise an error, than at least the below should happen:
|
||||
# assert response.text == 'Duplicated'
|
||||
with pytest.raises(RouteExists):
|
||||
@app.route('/overload/<param>', methods=['PUT', 'DELETE'])
|
||||
async def handler3(request):
|
||||
return text('Duplicated')
|
||||
|
||||
@app.route("/overload/<param>", methods=["PUT", "DELETE"])
|
||||
async def handler3(request, param):
|
||||
return text("Duplicated")
|
||||
|
||||
@@ -11,194 +11,204 @@ class SanicExceptionTestException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
@pytest.fixture(scope="module")
|
||||
def exception_app():
|
||||
app = Sanic('test_exceptions')
|
||||
app = Sanic("test_exceptions")
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
@app.route('/error')
|
||||
@app.route("/error")
|
||||
def handler_error(request):
|
||||
raise ServerError("OK")
|
||||
|
||||
@app.route('/404')
|
||||
@app.route("/404")
|
||||
def handler_404(request):
|
||||
raise NotFound("OK")
|
||||
|
||||
@app.route('/403')
|
||||
@app.route("/403")
|
||||
def handler_403(request):
|
||||
raise Forbidden("Forbidden")
|
||||
|
||||
@app.route('/401')
|
||||
@app.route("/401")
|
||||
def handler_401(request):
|
||||
raise Unauthorized("Unauthorized")
|
||||
|
||||
@app.route('/401/basic')
|
||||
@app.route("/401/basic")
|
||||
def handler_401_basic(request):
|
||||
raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic")
|
||||
|
||||
@app.route('/401/digest')
|
||||
@app.route("/401/digest")
|
||||
def handler_401_digest(request):
|
||||
raise Unauthorized("Unauthorized",
|
||||
scheme="Digest",
|
||||
realm="Sanic",
|
||||
qop="auth, auth-int",
|
||||
algorithm="MD5",
|
||||
nonce="abcdef",
|
||||
opaque="zyxwvu")
|
||||
raise Unauthorized(
|
||||
"Unauthorized",
|
||||
scheme="Digest",
|
||||
realm="Sanic",
|
||||
qop="auth, auth-int",
|
||||
algorithm="MD5",
|
||||
nonce="abcdef",
|
||||
opaque="zyxwvu",
|
||||
)
|
||||
|
||||
@app.route('/401/bearer')
|
||||
@app.route("/401/bearer")
|
||||
def handler_401_bearer(request):
|
||||
raise Unauthorized("Unauthorized", scheme="Bearer")
|
||||
|
||||
@app.route('/invalid')
|
||||
@app.route("/invalid")
|
||||
def handler_invalid(request):
|
||||
raise InvalidUsage("OK")
|
||||
|
||||
@app.route('/abort/401')
|
||||
@app.route("/abort/401")
|
||||
def handler_401_error(request):
|
||||
abort(401)
|
||||
|
||||
@app.route('/abort')
|
||||
@app.route("/abort")
|
||||
def handler_500_error(request):
|
||||
abort(500)
|
||||
return text("OK")
|
||||
|
||||
@app.route('/divide_by_zero')
|
||||
def handle_unhandled_exception(request):
|
||||
1 / 0
|
||||
@app.route("/abort/message")
|
||||
def handler_abort_message(request):
|
||||
abort(500, message="Abort")
|
||||
|
||||
@app.route('/error_in_error_handler_handler')
|
||||
@app.route("/divide_by_zero")
|
||||
def handle_unhandled_exception(request):
|
||||
_ = 1 / 0
|
||||
|
||||
@app.route("/error_in_error_handler_handler")
|
||||
def custom_error_handler(request):
|
||||
raise SanicExceptionTestException('Dummy message!')
|
||||
raise SanicExceptionTestException("Dummy message!")
|
||||
|
||||
@app.exception(SanicExceptionTestException)
|
||||
def error_in_error_handler_handler(request, exception):
|
||||
1 / 0
|
||||
_ = 1 / 0
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def test_catch_exception_list():
|
||||
app = Sanic('exception_list')
|
||||
|
||||
def test_catch_exception_list(app):
|
||||
@app.exception([SanicExceptionTestException, NotFound])
|
||||
def exception_list(request, exception):
|
||||
return text("ok")
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
def exception(request):
|
||||
raise SanicExceptionTestException("You won't see me")
|
||||
|
||||
request, response = app.test_client.get('/random')
|
||||
assert response.text == 'ok'
|
||||
request, response = app.test_client.get("/random")
|
||||
assert response.text == "ok"
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'ok'
|
||||
request, response = app.test_client.get("/")
|
||||
assert response.text == "ok"
|
||||
|
||||
|
||||
def test_no_exception(exception_app):
|
||||
"""Test that a route works without an exception"""
|
||||
request, response = exception_app.test_client.get('/')
|
||||
request, response = exception_app.test_client.get("/")
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
|
||||
|
||||
def test_server_error_exception(exception_app):
|
||||
"""Test the built-in ServerError exception works"""
|
||||
request, response = exception_app.test_client.get('/error')
|
||||
request, response = exception_app.test_client.get("/error")
|
||||
assert response.status == 500
|
||||
|
||||
|
||||
def test_invalid_usage_exception(exception_app):
|
||||
"""Test the built-in InvalidUsage exception works"""
|
||||
request, response = exception_app.test_client.get('/invalid')
|
||||
request, response = exception_app.test_client.get("/invalid")
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
def test_not_found_exception(exception_app):
|
||||
"""Test the built-in NotFound exception works"""
|
||||
request, response = exception_app.test_client.get('/404')
|
||||
request, response = exception_app.test_client.get("/404")
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_forbidden_exception(exception_app):
|
||||
"""Test the built-in Forbidden exception"""
|
||||
request, response = exception_app.test_client.get('/403')
|
||||
request, response = exception_app.test_client.get("/403")
|
||||
assert response.status == 403
|
||||
|
||||
|
||||
def test_unauthorized_exception(exception_app):
|
||||
"""Test the built-in Unauthorized exception"""
|
||||
request, response = exception_app.test_client.get('/401')
|
||||
request, response = exception_app.test_client.get("/401")
|
||||
assert response.status == 401
|
||||
|
||||
request, response = exception_app.test_client.get('/401/basic')
|
||||
request, response = exception_app.test_client.get("/401/basic")
|
||||
assert response.status == 401
|
||||
assert response.headers.get('WWW-Authenticate') is not None
|
||||
assert response.headers.get('WWW-Authenticate') == 'Basic realm="Sanic"'
|
||||
assert response.headers.get("WWW-Authenticate") is not None
|
||||
assert response.headers.get("WWW-Authenticate") == 'Basic realm="Sanic"'
|
||||
|
||||
request, response = exception_app.test_client.get('/401/digest')
|
||||
request, response = exception_app.test_client.get("/401/digest")
|
||||
assert response.status == 401
|
||||
|
||||
auth_header = response.headers.get('WWW-Authenticate')
|
||||
auth_header = response.headers.get("WWW-Authenticate")
|
||||
assert auth_header is not None
|
||||
assert auth_header.startswith('Digest')
|
||||
assert auth_header.startswith("Digest")
|
||||
assert 'qop="auth, auth-int"' in auth_header
|
||||
assert 'algorithm="MD5"' in auth_header
|
||||
assert 'nonce="abcdef"' in auth_header
|
||||
assert 'opaque="zyxwvu"' in auth_header
|
||||
|
||||
request, response = exception_app.test_client.get('/401/bearer')
|
||||
request, response = exception_app.test_client.get("/401/bearer")
|
||||
assert response.status == 401
|
||||
assert response.headers.get('WWW-Authenticate') == "Bearer"
|
||||
assert response.headers.get("WWW-Authenticate") == "Bearer"
|
||||
|
||||
|
||||
def test_handled_unhandled_exception(exception_app):
|
||||
"""Test that an exception not built into sanic is handled"""
|
||||
request, response = exception_app.test_client.get('/divide_by_zero')
|
||||
request, response = exception_app.test_client.get("/divide_by_zero")
|
||||
assert response.status == 500
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
assert soup.h1.text == 'Internal Server Error'
|
||||
soup = BeautifulSoup(response.body, "html.parser")
|
||||
assert soup.h1.text == "Internal Server Error"
|
||||
|
||||
message = " ".join(soup.p.text.split())
|
||||
assert message == (
|
||||
"The server encountered an internal error and "
|
||||
"cannot complete your request.")
|
||||
"cannot complete your request."
|
||||
)
|
||||
|
||||
|
||||
def test_exception_in_exception_handler(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler')
|
||||
"/error_in_error_handler_handler"
|
||||
)
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
assert response.body == b"An error occurred while handling an error"
|
||||
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler',
|
||||
debug=False)
|
||||
"/error_in_error_handler_handler", debug=False
|
||||
)
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
assert response.body == b"An error occurred while handling an error"
|
||||
|
||||
|
||||
def test_exception_in_exception_handler_debug_on(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler',
|
||||
debug=True)
|
||||
"/error_in_error_handler_handler", debug=True
|
||||
)
|
||||
assert response.status == 500
|
||||
assert response.body.startswith(b'Exception raised in exception ')
|
||||
assert response.body.startswith(b"Exception raised in exception ")
|
||||
|
||||
|
||||
def test_abort(exception_app):
|
||||
"""Test the abort function"""
|
||||
request, response = exception_app.test_client.get('/abort/401')
|
||||
request, response = exception_app.test_client.get("/abort/401")
|
||||
assert response.status == 401
|
||||
|
||||
request, response = exception_app.test_client.get('/abort')
|
||||
request, response = exception_app.test_client.get("/abort")
|
||||
assert response.status == 500
|
||||
|
||||
request, response = exception_app.test_client.get("/abort/message")
|
||||
assert response.status == 500
|
||||
assert response.text == "Error: Abort"
|
||||
|
||||
@@ -4,38 +4,39 @@ from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.handlers import ErrorHandler
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
exception_handler_app = Sanic('test_exception_handler')
|
||||
exception_handler_app = Sanic("test_exception_handler")
|
||||
|
||||
|
||||
@exception_handler_app.route('/1')
|
||||
@exception_handler_app.route("/1")
|
||||
def handler_1(request):
|
||||
raise InvalidUsage("OK")
|
||||
|
||||
|
||||
@exception_handler_app.route('/2')
|
||||
@exception_handler_app.route("/2")
|
||||
def handler_2(request):
|
||||
raise ServerError("OK")
|
||||
|
||||
|
||||
@exception_handler_app.route('/3')
|
||||
@exception_handler_app.route("/3")
|
||||
def handler_3(request):
|
||||
raise NotFound("OK")
|
||||
|
||||
|
||||
@exception_handler_app.route('/4')
|
||||
@exception_handler_app.route("/4")
|
||||
def handler_4(request):
|
||||
foo = bar # noqa -- F821 undefined name 'bar' is done to throw exception
|
||||
foo = bar # noqa -- F821 undefined name 'bar' is done to throw exception
|
||||
return text(foo)
|
||||
|
||||
|
||||
@exception_handler_app.route('/5')
|
||||
@exception_handler_app.route("/5")
|
||||
def handler_5(request):
|
||||
class CustomServerError(ServerError):
|
||||
pass
|
||||
raise CustomServerError('Custom server error')
|
||||
|
||||
raise CustomServerError("Custom server error")
|
||||
|
||||
|
||||
@exception_handler_app.route('/6/<arg:int>')
|
||||
@exception_handler_app.route("/6/<arg:int>")
|
||||
def handler_6(request, arg):
|
||||
try:
|
||||
foo = 1 / arg
|
||||
@@ -50,67 +51,67 @@ def handler_exception(request, exception):
|
||||
|
||||
|
||||
def test_invalid_usage_exception_handler():
|
||||
request, response = exception_handler_app.test_client.get('/1')
|
||||
request, response = exception_handler_app.test_client.get("/1")
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
def test_server_error_exception_handler():
|
||||
request, response = exception_handler_app.test_client.get('/2')
|
||||
request, response = exception_handler_app.test_client.get("/2")
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
|
||||
|
||||
def test_not_found_exception_handler():
|
||||
request, response = exception_handler_app.test_client.get('/3')
|
||||
request, response = exception_handler_app.test_client.get("/3")
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_text_exception__handler():
|
||||
request, response = exception_handler_app.test_client.get('/random')
|
||||
request, response = exception_handler_app.test_client.get("/random")
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
|
||||
|
||||
def test_html_traceback_output_in_debug_mode():
|
||||
request, response = exception_handler_app.test_client.get(
|
||||
'/4', debug=True)
|
||||
request, response = exception_handler_app.test_client.get("/4", debug=True)
|
||||
assert response.status == 500
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
soup = BeautifulSoup(response.body, "html.parser")
|
||||
html = str(soup)
|
||||
|
||||
assert 'response = handler(request, *args, **kwargs)' in html
|
||||
assert 'handler_4' in html
|
||||
assert 'foo = bar' in html
|
||||
assert "response = handler(request, *args, **kwargs)" in html
|
||||
assert "handler_4" in html
|
||||
assert "foo = bar" in html
|
||||
|
||||
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||
summary_text = " ".join(soup.select(".summary")[0].text.split())
|
||||
assert (
|
||||
"NameError: name 'bar' "
|
||||
"is not defined while handling path /4") == summary_text
|
||||
"NameError: name 'bar' " "is not defined while handling path /4"
|
||||
) == summary_text
|
||||
|
||||
|
||||
def test_inherited_exception_handler():
|
||||
request, response = exception_handler_app.test_client.get('/5')
|
||||
request, response = exception_handler_app.test_client.get("/5")
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_chained_exception_handler():
|
||||
request, response = exception_handler_app.test_client.get(
|
||||
'/6/0', debug=True)
|
||||
"/6/0", debug=True
|
||||
)
|
||||
assert response.status == 500
|
||||
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
soup = BeautifulSoup(response.body, "html.parser")
|
||||
html = str(soup)
|
||||
|
||||
assert 'response = handler(request, *args, **kwargs)' in html
|
||||
assert 'handler_6' in html
|
||||
assert 'foo = 1 / arg' in html
|
||||
assert 'ValueError' in html
|
||||
assert 'The above exception was the direct cause' in html
|
||||
assert "response = handler(request, *args, **kwargs)" in html
|
||||
assert "handler_6" in html
|
||||
assert "foo = 1 / arg" in html
|
||||
assert "ValueError" in html
|
||||
assert "The above exception was the direct cause" in html
|
||||
|
||||
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||
summary_text = " ".join(soup.select(".summary")[0].text.split())
|
||||
assert (
|
||||
"ZeroDivisionError: division by zero "
|
||||
"while handling path /6/0") == summary_text
|
||||
"ZeroDivisionError: division by zero " "while handling path /6/0"
|
||||
) == summary_text
|
||||
|
||||
|
||||
def test_exception_handler_lookup():
|
||||
@@ -131,7 +132,8 @@ def test_exception_handler_lookup():
|
||||
|
||||
try:
|
||||
ModuleNotFoundError
|
||||
except:
|
||||
except Exception:
|
||||
|
||||
class ModuleNotFoundError(ImportError):
|
||||
pass
|
||||
|
||||
@@ -143,12 +145,12 @@ def test_exception_handler_lookup():
|
||||
assert handler.lookup(ImportError()) == import_error_handler
|
||||
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
|
||||
assert handler.lookup(CustomError()) == custom_error_handler
|
||||
assert handler.lookup(ServerError('Error')) == server_error_handler
|
||||
assert handler.lookup(CustomServerError('Error')) == server_error_handler
|
||||
assert handler.lookup(ServerError("Error")) == server_error_handler
|
||||
assert handler.lookup(CustomServerError("Error")) == server_error_handler
|
||||
|
||||
# once again to ensure there is no caching bug
|
||||
assert handler.lookup(ImportError()) == import_error_handler
|
||||
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
|
||||
assert handler.lookup(CustomError()) == custom_error_handler
|
||||
assert handler.lookup(ServerError('Error')) == server_error_handler
|
||||
assert handler.lookup(CustomServerError('Error')) == server_error_handler
|
||||
assert handler.lookup(ServerError("Error")) == server_error_handler
|
||||
assert handler.lookup(CustomServerError("Error")) == server_error_handler
|
||||
|
||||
58
tests/test_helpers.py
Normal file
58
tests/test_helpers.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from sanic import helpers
|
||||
|
||||
|
||||
def test_has_message_body():
|
||||
tests = (
|
||||
(100, False),
|
||||
(102, False),
|
||||
(204, False),
|
||||
(200, True),
|
||||
(304, False),
|
||||
(400, True),
|
||||
)
|
||||
for status_code, expected in tests:
|
||||
assert helpers.has_message_body(status_code) is expected
|
||||
|
||||
|
||||
def test_is_entity_header():
|
||||
tests = (
|
||||
("allow", True),
|
||||
("extension-header", True),
|
||||
("", False),
|
||||
("test", False),
|
||||
)
|
||||
for header, expected in tests:
|
||||
assert helpers.is_entity_header(header) is expected
|
||||
|
||||
|
||||
def test_is_hop_by_hop_header():
|
||||
tests = (
|
||||
("connection", True),
|
||||
("upgrade", True),
|
||||
("", False),
|
||||
("test", False),
|
||||
)
|
||||
for header, expected in tests:
|
||||
assert helpers.is_hop_by_hop_header(header) is expected
|
||||
|
||||
|
||||
def test_remove_entity_headers():
|
||||
tests = (
|
||||
({}, {}),
|
||||
({"Allow": "GET, POST, HEAD"}, {}),
|
||||
(
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"Expires": "Wed, 21 Oct 2015 07:28:00 GMT",
|
||||
"Foo": "Bar",
|
||||
},
|
||||
{"Expires": "Wed, 21 Oct 2015 07:28:00 GMT", "Foo": "Bar"},
|
||||
),
|
||||
(
|
||||
{"Allow": "GET, POST, HEAD", "Content-Location": "/test"},
|
||||
{"Content-Location": "/test"},
|
||||
),
|
||||
)
|
||||
|
||||
for header, expected in tests:
|
||||
assert helpers.remove_entity_headers(header) == expected
|
||||
@@ -3,66 +3,32 @@ from sanic import Sanic
|
||||
import asyncio
|
||||
from asyncio import sleep as aio_sleep
|
||||
from sanic.response import text
|
||||
from sanic.config import Config
|
||||
from sanic import server
|
||||
import aiohttp
|
||||
from aiohttp import TCPConnector
|
||||
from sanic.testing import SanicTestClient, HOST, PORT
|
||||
|
||||
try:
|
||||
try:
|
||||
import packaging # direct use
|
||||
except ImportError:
|
||||
# setuptools v39.0 and above.
|
||||
try:
|
||||
from setuptools.extern import packaging
|
||||
except ImportError:
|
||||
# Before setuptools v39.0
|
||||
from pkg_resources.extern import packaging
|
||||
version = packaging.version
|
||||
except ImportError:
|
||||
raise RuntimeError("The 'packaging' library is missing.")
|
||||
|
||||
aiohttp_version = version.parse(aiohttp.__version__)
|
||||
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
|
||||
|
||||
|
||||
class ReuseableTCPConnector(TCPConnector):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
|
||||
self.old_proto = None
|
||||
|
||||
if aiohttp_version >= version.parse('3.3.0'):
|
||||
async def connect(self, req, traces, timeout):
|
||||
new_conn = await super(ReuseableTCPConnector, self)\
|
||||
.connect(req, traces, timeout)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!")
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
elif aiohttp_version >= version.parse('3.0.0'):
|
||||
async def connect(self, req, traces=None):
|
||||
new_conn = await super(ReuseableTCPConnector, self)\
|
||||
.connect(req, traces=traces)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!")
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
else:
|
||||
async def connect(self, req):
|
||||
new_conn = await super(ReuseableTCPConnector, self)\
|
||||
.connect(req)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!")
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
async def connect(self, req, *args, **kwargs):
|
||||
new_conn = await super(ReuseableTCPConnector, self).connect(
|
||||
req, *args, **kwargs
|
||||
)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!"
|
||||
)
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
|
||||
|
||||
class ReuseableSanicTestClient(SanicTestClient):
|
||||
@@ -78,31 +44,39 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
# Copied from SanicTestClient, but with some changes to reuse the
|
||||
# same loop for the same app.
|
||||
def _sanic_endpoint_test(
|
||||
self, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
self,
|
||||
method="get",
|
||||
uri="/",
|
||||
gather_request=True,
|
||||
debug=False,
|
||||
server_kwargs={"return_asyncio_server": True},
|
||||
*request_args,
|
||||
**request_kwargs
|
||||
):
|
||||
loop = self._loop
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
do_kill_server = request_kwargs.pop('end_server', False)
|
||||
do_kill_server = request_kwargs.pop("end_server", False)
|
||||
if gather_request:
|
||||
|
||||
def _collect_request(request):
|
||||
if results[0] is None:
|
||||
results[0] = request
|
||||
|
||||
self.app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
@self.app.listener('after_server_start')
|
||||
@self.app.listener("after_server_start")
|
||||
async def _collect_response(loop):
|
||||
try:
|
||||
if do_kill_server:
|
||||
request_kwargs['end_session'] = True
|
||||
request_kwargs["end_session"] = True
|
||||
response = await self._local_request(
|
||||
method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
method, uri, *request_args, **request_kwargs
|
||||
)
|
||||
results[-1] = response
|
||||
except Exception as e2:
|
||||
import traceback
|
||||
|
||||
traceback.print_tb(e2.__traceback__)
|
||||
exceptions.append(e2)
|
||||
# Don't stop here! self.app.stop()
|
||||
@@ -110,23 +84,25 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
if self._server is not None:
|
||||
_server = self._server
|
||||
else:
|
||||
_server_co = self.app.create_server(host=HOST, debug=debug,
|
||||
port=PORT, **server_kwargs)
|
||||
_server_co = self.app.create_server(
|
||||
host=HOST, debug=debug, port=PORT, **server_kwargs
|
||||
)
|
||||
|
||||
server.trigger_events(
|
||||
self.app.listeners['before_server_start'], loop)
|
||||
self.app.listeners["before_server_start"], loop
|
||||
)
|
||||
|
||||
try:
|
||||
loop._stopping = False
|
||||
http_server = loop.run_until_complete(_server_co)
|
||||
except Exception as e1:
|
||||
import traceback
|
||||
|
||||
traceback.print_tb(e1.__traceback__)
|
||||
raise e1
|
||||
self._server = _server = http_server
|
||||
server.trigger_events(
|
||||
self.app.listeners['after_server_start'], loop)
|
||||
self.app.listeners['after_server_start'].pop()
|
||||
server.trigger_events(self.app.listeners["after_server_start"], loop)
|
||||
self.app.listeners["after_server_start"].pop()
|
||||
|
||||
if do_kill_server:
|
||||
try:
|
||||
@@ -136,60 +112,66 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
self.app.stop()
|
||||
except Exception as e3:
|
||||
import traceback
|
||||
|
||||
traceback.print_tb(e3.__traceback__)
|
||||
exceptions.append(e3)
|
||||
if exceptions:
|
||||
raise ValueError(
|
||||
"Exception during request: {}".format(exceptions))
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
|
||||
if gather_request:
|
||||
self.app.request_middleware.pop()
|
||||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
results
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
"Request object expected, got ({})".format(results)
|
||||
)
|
||||
|
||||
# Copied from SanicTestClient, but with some changes to reuse the
|
||||
# same TCPConnection and the sane ClientSession more than once.
|
||||
# Note, you cannot use the same session if you are in a _different_
|
||||
# loop, so the changes above are required too.
|
||||
async def _local_request(self, method, uri, cookies=None, *args,
|
||||
**kwargs):
|
||||
request_keepalive = kwargs.pop('request_keepalive',
|
||||
Config.KEEP_ALIVE_TIMEOUT)
|
||||
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
|
||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
||||
request_keepalive = kwargs.pop(
|
||||
"request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"]
|
||||
)
|
||||
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
|
||||
url = uri
|
||||
else:
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=self.port, uri=uri)
|
||||
do_kill_session = kwargs.pop('end_session', False)
|
||||
url = "http://{host}:{port}{uri}".format(
|
||||
host=HOST, port=self.port, uri=uri
|
||||
)
|
||||
do_kill_session = kwargs.pop("end_session", False)
|
||||
if self._session:
|
||||
session = self._session
|
||||
else:
|
||||
if self._tcp_connector:
|
||||
conn = self._tcp_connector
|
||||
else:
|
||||
conn = ReuseableTCPConnector(verify_ssl=False,
|
||||
loop=self._loop,
|
||||
keepalive_timeout=
|
||||
request_keepalive)
|
||||
conn = ReuseableTCPConnector(
|
||||
ssl=False,
|
||||
loop=self._loop,
|
||||
keepalive_timeout=request_keepalive,
|
||||
)
|
||||
self._tcp_connector = conn
|
||||
session = aiohttp.ClientSession(cookies=cookies,
|
||||
connector=conn,
|
||||
loop=self._loop)
|
||||
session = aiohttp.ClientSession(
|
||||
cookies=cookies, connector=conn, loop=self._loop
|
||||
)
|
||||
self._session = session
|
||||
|
||||
async with getattr(session, method.lower())(
|
||||
url, *args, **kwargs) as response:
|
||||
url, *args, **kwargs
|
||||
) as response:
|
||||
try:
|
||||
response.text = await response.text()
|
||||
except UnicodeDecodeError:
|
||||
@@ -197,9 +179,11 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
|
||||
try:
|
||||
response.json = await response.json()
|
||||
except (JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError):
|
||||
except (
|
||||
JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError,
|
||||
):
|
||||
response.json = None
|
||||
|
||||
response.body = await response.read()
|
||||
@@ -209,26 +193,28 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
return response
|
||||
|
||||
|
||||
Config.KEEP_ALIVE_TIMEOUT = 2
|
||||
Config.KEEP_ALIVE = True
|
||||
keep_alive_timeout_app_reuse = Sanic('test_ka_timeout_reuse')
|
||||
keep_alive_app_client_timeout = Sanic('test_ka_client_timeout')
|
||||
keep_alive_app_server_timeout = Sanic('test_ka_server_timeout')
|
||||
keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse")
|
||||
keep_alive_app_client_timeout = Sanic("test_ka_client_timeout")
|
||||
keep_alive_app_server_timeout = Sanic("test_ka_server_timeout")
|
||||
|
||||
keep_alive_timeout_app_reuse.config.update(CONFIG_FOR_TESTS)
|
||||
keep_alive_app_client_timeout.config.update(CONFIG_FOR_TESTS)
|
||||
keep_alive_app_server_timeout.config.update(CONFIG_FOR_TESTS)
|
||||
|
||||
|
||||
@keep_alive_timeout_app_reuse.route('/1')
|
||||
@keep_alive_timeout_app_reuse.route("/1")
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
|
||||
@keep_alive_app_client_timeout.route('/1')
|
||||
@keep_alive_app_client_timeout.route("/1")
|
||||
async def handler2(request):
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
|
||||
@keep_alive_app_server_timeout.route('/1')
|
||||
@keep_alive_app_server_timeout.route("/1")
|
||||
async def handler3(request):
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
|
||||
def test_keep_alive_timeout_reuse():
|
||||
@@ -238,16 +224,14 @@ def test_keep_alive_timeout_reuse():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop)
|
||||
headers = {
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
request, response = client.get('/1', headers=headers)
|
||||
headers = {"Connection": "keep-alive"}
|
||||
request, response = client.get("/1", headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
loop.run_until_complete(aio_sleep(1))
|
||||
request, response = client.get('/1', end_server=True)
|
||||
request, response = client.get("/1", end_server=True)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
|
||||
|
||||
def test_keep_alive_client_timeout():
|
||||
@@ -255,20 +239,17 @@ def test_keep_alive_client_timeout():
|
||||
keep-alive timeout, client will try to create a new connection here."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_client_timeout,
|
||||
loop)
|
||||
headers = {
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
request, response = client.get('/1', headers=headers,
|
||||
request_keepalive=1)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
|
||||
headers = {"Connection": "keep-alive"}
|
||||
request, response = client.get("/1", headers=headers, request_keepalive=1)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
loop.run_until_complete(aio_sleep(2))
|
||||
exception = None
|
||||
try:
|
||||
request, response = client.get('/1', end_server=True,
|
||||
request_keepalive=1)
|
||||
request, response = client.get(
|
||||
"/1", end_server=True, request_keepalive=1
|
||||
)
|
||||
except ValueError as e:
|
||||
exception = e
|
||||
assert exception is not None
|
||||
@@ -283,23 +264,22 @@ def test_keep_alive_server_timeout():
|
||||
broken server connection."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_server_timeout,
|
||||
loop)
|
||||
headers = {
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
request, response = client.get('/1', headers=headers,
|
||||
request_keepalive=60)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
|
||||
headers = {"Connection": "keep-alive"}
|
||||
request, response = client.get("/1", headers=headers, request_keepalive=60)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
assert response.text == "OK"
|
||||
loop.run_until_complete(aio_sleep(3))
|
||||
exception = None
|
||||
try:
|
||||
request, response = client.get('/1', request_keepalive=60,
|
||||
end_server=True)
|
||||
request, response = client.get(
|
||||
"/1", request_keepalive=60, end_server=True
|
||||
)
|
||||
except ValueError as e:
|
||||
exception = e
|
||||
assert exception is not None
|
||||
assert isinstance(exception, ValueError)
|
||||
assert "Connection reset" in exception.args[0] or \
|
||||
"got a new connection" in exception.args[0]
|
||||
assert (
|
||||
"Connection reset" in exception.args[0]
|
||||
or "got a new connection" in exception.args[0]
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user