Compare commits

...

136 Commits

Author SHA1 Message Date
Adam Hopkins
68d5039c5f Merge pull request #1624 from huge-success/release-19-6-2
19.6.2 release
2019-07-07 05:04:23 +03:00
Yun Xu
9d07988d75 19.6.2 release 2019-07-06 18:05:44 -07:00
7
1eaa2e3a5f Merge pull request #1614 from huge-success/asgi-custom-request
Add custom request support to ASGI mode; fix a couple tests
2019-07-06 11:47:58 -07:00
Yun Xu
c7f2399ded remove commented code 2019-07-06 11:34:22 -07:00
7
650ab61c2e Merge pull request #1619 from huge-success/abc-fix
Resolve deprecation notice for import of an ABC from collections module
2019-07-04 15:07:53 -07:00
Lagicrus
b7df86e7dd Updated routing docs (#1620)
* Updated routing docs

Updated routing docs to show all supported types as defined within 3685b4de85/sanic/router.py (L18)
Added example code for all examples besides regex
Added examples of queries that work with that type and ones that would not

* Tweak to call out string not str

Related to https://github.com/huge-success/sanic/pull/1620#discussion_r300120962

* Changed to using code comments to achieve a mono space like display

To address https://github.com/huge-success/sanic/pull/1620#discussion_r300120726

* Adjusted to list

Following https://github.com/huge-success/sanic/pull/1620#discussion_r300120726
2019-07-04 07:14:10 -05:00
BananaWanted
72b445621b Respect X-Forward-* headers and generate correct URLs in url_for (#1465)
* handle forwarded address in request

* added test cases

* Fix lint errors

* Fix uncovered code branch

* Update docstrings

* Update documents

* fix import order
2019-07-04 07:13:43 -05:00
Adam Hopkins
ba0e9baffa Resolve deprecation notice for import of an ABC from collections module 2019-07-03 09:39:38 +03:00
Adam Hopkins
503622438a Merge pull request #1617 from newAM/patch-2
Fix a minor typo in websocket.rst.
2019-07-01 09:37:40 +03:00
Alex
d5e9aae425 Fix a minor typo in websocket.rst. 2019-06-30 22:11:02 -07:00
Adam Hopkins
a2666a2b8a Add custom request support to ASGI mode; fix a couple tests
Undo change to request stream test
2019-06-24 22:59:23 +03:00
7
966b05b47e Merge pull request #1612 from c-goosen/bandit_security_static_analysis
Add bandit code static analyzer for security.
2019-06-24 10:05:20 -07:00
Christo Goosen
78fe97b9cb Add bandit code static analyzer for security, some false positives removed with #nosec.
Bandit is a python package for staticly scanning code for security issues.
* Added to tox.ini
* Added to setup.py
* Added to .travis.yml

As part of CI/CD pipeline
2019-06-24 09:53:29 +02:00
7
d2094fed38 Merge pull request #1607 from huge-success/doc-changelog
Changelog for 19.6.0 release
2019-06-21 09:42:12 -07:00
Yun Xu
e2d65ba57c fix readthedoc changelog page 2019-06-20 22:35:47 -07:00
Yun Xu
c9d8ab4b27 release: add 19.6.0 standard release changelog 2019-06-20 22:35:26 -07:00
7
891f99d71d Merge pull request #1475 from tomchristie/asgi-refactor-attempt
ASGI refactoring attempt
2019-06-20 16:33:44 -07:00
Adam Hopkins
3f47fa9f99 Specify websockets version 2019-06-19 00:40:44 +03:00
Adam Hopkins
b1c23fdbaa Increase testing coverage for ASGI
Beautify

Specify websockets version
2019-06-19 00:38:58 +03:00
Adam Hopkins
62e0e5b9ec Increase testing coverage for ASGI
Beautify
2019-06-19 00:19:40 +03:00
Adam Hopkins
fb61834a2e Add ASGI documentation 2019-06-18 09:57:42 +03:00
7
8fbbe94fe1 Merge pull request #1436 from jotagesales/config_from_object_string
Config from object string
2019-06-16 16:58:43 -07:00
Adam Hopkins
ab706dda7d Resolve linting issues with imports 2019-06-11 11:21:37 +03:00
Adam Hopkins
b2d4132a14 Merge branch 'master' into asgi-refactor-attempt 2019-06-11 11:11:32 +03:00
7
322cf89c92 Merge pull request #1605 from GTedHa/fix_typo/request_data_md
Fix typo in request_data.md, docs.
2019-06-10 20:20:12 -07:00
G.Ted
09acd64ba1 Fix typo in request_data.md, docs. 2019-06-11 11:09:29 +09:00
Eli Uriegas
072fcfe03e Fix #1587: add support for handling Expect Header (#1600)
Fix #1587: add support for handling Expect Header
2019-06-10 14:45:37 -07:00
Harsha Narayana
13079c6e30 GIT-1591 Strict Slashes behavior fix (#1594)
* fix: GIT-1591: fix strict_slashes option inheriting behavior

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>

* doc: GIT-1591: add documentation exlaining the strict_slashes behavior

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>

* fix: GIT-1591: fix deprecated for test_client

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-06-06 07:21:58 -05:00
Yun Xu
1b1a51c1bb minor: fix typo in error msg 2019-06-04 10:37:03 -07:00
Yun Xu
39d134994d minor: address pr feedbacks, small refactoring and fix 2019-06-04 10:25:32 -07:00
Adam Hopkins
5f9e98554f Run black and manually break up some text lines to correct linting 2019-06-04 13:26:05 +03:00
Adam Hopkins
0d9a21718f Run black and manually break up some text lines to correct linting 2019-06-04 13:18:05 +03:00
Adam Hopkins
daf42c5f43 Add placement of before_server_start and after_server_stop 2019-06-04 12:59:15 +03:00
Adam Hopkins
3685b4de85 Lifespan and code cleanup 2019-06-04 10:58:00 +03:00
Yun Xu
2631f10c5e lint: fix isort and flake8 complains 2019-06-03 22:12:10 -07:00
Yun Xu
f21db60859 fix: handle expect header 2019-06-03 22:08:24 -07:00
Adam Hopkins
c15158224b Set testing.PORT on all app.create_server() in tests (#1593) 2019-05-30 09:10:00 -05:00
Adam Hopkins
a57c14c70b Add requests-async as a hard requirement. See #1592 (#1595) 2019-05-28 08:30:07 -05:00
Adam Hopkins
bb2bd2fe53 Point extensions page to awesome-sanic repo (#1596) 2019-05-28 08:13:12 -05:00
Adam Hopkins
aebe2b5809 Merge branch 'master' into asgi-refactor-attempt 2019-05-27 21:03:23 +03:00
Adam Hopkins
9172399b8c Implement ASGI lifespan events to match Sanic listeners 2019-05-27 12:33:25 +03:00
Adam Hopkins
22c0d97783 Streaming responses 2019-05-27 02:11:52 +03:00
Adam Hopkins
3ead529693 Setup streaming on ASGI 2019-05-27 00:57:50 +03:00
7
e36f398aa6 Merge pull request #1590 from huge-success/security-md
Create SECURITY.md
2019-05-23 17:40:19 -07:00
Adam Hopkins
18cd4caf70 Create SECURITY.md 2019-05-23 23:58:15 +03:00
7
80df45ba6d Merge pull request #1588 from huge-success/prepare-19.6.0
Prepare 19.6.0
2019-05-22 16:32:36 -07:00
Yun Xu
16d262e3e5 release: v19.6.0 2019-05-22 15:51:56 -07:00
Harsha Narayana
83e3d4ca1f doc: GIT-1582: add fedora package dependency
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-05-22 15:51:17 -07:00
zach valenta
1c9141bd5d fix typo 2019-05-22 15:51:17 -07:00
Yun Xu
1b984422db add help wanted in stale.yml 2019-05-22 15:51:17 -07:00
Adam Hopkins
b6453e9fac Update stale.yml 2019-05-22 15:51:17 -07:00
Adam Hopkins
7b8e3624b8 Prepare initial websocket support 2019-05-22 01:42:19 +03:00
Adam Hopkins
8a56da84e6 Create SanicASGITestClient and refactor ASGI methods 2019-05-21 19:30:55 +03:00
7
14a00490e2 Merge pull request #1585 from harshanarayana/fix/GIT-1582-Fix_Install_Documentation
doc: GIT-1582: add fedora package dependency
2019-05-19 18:56:20 -07:00
Harsha Narayana
29bf967a7e doc: GIT-1582: add fedora package dependency
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-05-20 06:46:18 +05:30
7
eeb79f2587 Merge pull request #1583 from zachvalenta/patch-1
fix typo
2019-05-18 15:56:09 -07:00
zach valenta
6d1741694d fix typo 2019-05-18 15:05:03 -04:00
7
746dccf8f9 Merge pull request #1575 from huge-success/add-necessary-stale
Update stale.yml
2019-05-18 12:00:35 -07:00
Yun Xu
28a897e599 add help wanted in stale.yml 2019-05-18 11:02:46 -07:00
7
21ebf6d777 Merge pull request #1581 from huge-success/fix-build-time
Fix build time
2019-05-17 13:45:36 -07:00
Yun Xu
a2dbbb25a1 add try/finally block for always clean up resources 2019-05-17 00:25:46 -07:00
Yun Xu
2a64dabe82 fix request_timeout and request_streaming tests 2019-05-17 00:22:34 -07:00
Yun Xu
046ca6eaf1 fix unit tests due to dependency upgrade 2019-05-16 22:44:46 -07:00
Yun Xu
3661afa461 bump request-async version for fixing build time issue 2019-05-16 08:56:25 -07:00
7
12f1985375 Merge pull request #1576 from huge-success/conda-docs
Add conda install and download stats
2019-05-15 22:42:25 -07:00
Adam Hopkins
bb800c9db8 Add conda install and download stats 2019-05-15 09:54:02 +03:00
Adam Hopkins
262048df95 Update stale.yml 2019-05-15 07:46:58 +03:00
7
9255eb6902 Merge pull request #1573 from huge-success/doc-fix-for-35
Remove Python 3.5 references in docs
2019-05-14 09:15:52 -07:00
7
56d386f152 Merge pull request #1571 from huge-success/stale
Add stale to repo
2019-05-14 09:15:16 -07:00
Adam Hopkins
193dbe89cd Remove Python 3.5 references in docs 2019-05-14 11:21:24 +03:00
Adam Hopkins
601e158ffe Add stale to repo 2019-05-14 10:50:34 +03:00
7
42b9fa3779 Merge pull request #1570 from 5onic/FIX-add-missed-documentation
Added documentation for missed arguments
2019-05-12 21:53:30 -07:00
Adam Hopkins
4767a67acd Merge branch 'master' into asgi-refactor-attempt 2019-05-12 22:57:02 +03:00
Mike Yusko
4c8cc84b64 Delete unnecessary whitespace 2019-05-12 15:36:13 +03:00
Mike Yusko
c5efc4b64b Added documentation for missed arguments 2019-05-12 15:32:34 +03:00
7
25e2151fdf Merge pull request #1568 from huge-success/deprecate-route-removal
deprecation: deprecate the use of remove_route
2019-05-11 02:24:11 +08:00
7
cb10e261a2 Merge pull request #1567 from huge-success/fix-readthedoc-build
minor: fix readthedoc build
2019-05-10 12:16:09 +08:00
Yun Xu
bd89c7f269 fix lint issue 2019-05-09 21:14:27 -07:00
Yun Xu
d4ef151cc4 deprecation: deprecate the use of remove_route 2019-05-09 20:52:24 -07:00
7
669cfa33df Merge pull request #1566 from ketan86/developer-guide-improvements
developer guide enhancements.
2019-05-09 04:14:21 +08:00
7
f70ab2f68a Merge pull request #1565 from ketan86/1564-processes-initialization-fix
1564 - Moving `processes` variable intialization before `sig_handler`.
2019-05-09 04:13:29 +08:00
Ketan Patel
900020ddc9 developer guide enhancements. 2019-05-08 00:40:40 -07:00
Ketan Patel
ec428135ce 1564 - Moving processes variable intialization before sig_handler. 2019-05-07 22:38:29 -07:00
Yun Xu
8e2a1a61a5 minor: fix readthedoc build 2019-05-07 16:51:24 -07:00
Adam Hopkins
5fb8f5d3e7 Add Awesome Sanic list button (#1563) 2019-05-06 07:47:16 -05:00
Adam Hopkins
c68523150f Merge branch 'master' into asgi-refactor-attempt 2019-05-06 12:59:56 +03:00
7
ae2b8f0056 Merge pull request #1562 from huge-success/testing-client
Testing client
2019-05-03 06:32:26 +08:00
Eli Uriegas
ef6d78c580 Allow to disable Transfer-Encoding: chunked (#1560)
Allow to disable Transfer-Encoding: chunked
2019-04-30 14:56:27 -07:00
Adam Hopkins
ccd4c9615c Create requests-async based TestClient, remove aiohttp dependency, drop Python 3.5
Update all tests to be compatible with requests-async
Cleanup testing client changes with black and isort
Remove Python 3.5 and other meta doc cleanup
rename pyproject and fix pep517 error
Add black config to tox.ini
Cleanup tests and remove aiohttp
tox.ini change for easier development commands
Remove aiohttp from changelog and requirements
Cleanup imports and Makefile
2019-04-30 15:26:06 +03:00
andreymal
7d6e60ab7d Never use chunked transfer encoding for HTTP/1.0 2019-04-22 10:53:13 +03:00
andreymal
9615e37ef9 Add file streaming section to the streaming documentation page 2019-04-20 23:50:19 +03:00
andreymal
6be12ba773 Upadte documentation for streaming response 2019-04-20 23:38:16 +03:00
andreymal
03855d316b Update tests for StreamingHTTPResponse 2019-04-20 22:27:10 +03:00
andreymal
9f07109616 Allow to disable Transfer-Encoding: chunked 2019-04-20 22:26:30 +03:00
7
6a4a3f617f Merge pull request #1558 from andreymal/fix/graceful_shutdown
Fix graceful shutdown
2019-04-20 20:02:16 +08:00
Eli Uriegas
f32c9be41f Merge pull request #1559 from andreymal/fix/pytest_behchmark_require
Add pytest-benchmark to tests_require
2019-04-19 15:52:39 -07:00
andreymal
d83d829e0a Add pytest-benchmark to tests_require 2019-04-19 17:31:23 +03:00
andreymal
99e56ef74b Fix broken bail_out when HttpProtocol is closed 2019-04-19 16:14:27 +03:00
andreymal
df23692802 Fix graceful shutdown (the connections set was always empty in serve function) 2019-04-19 15:58:17 +03:00
7
b68a7fe7ae doc: fix README.rst for pip installing sanic without uvloop and ujson (#1554) 2019-04-17 08:48:21 -05:00
andreymal
5c9ba189bc Add options to control the behavior of Request.remote_addr (#1539)
* Add options to control the behavior of Request.remote_addr

* Update tests for Request.remote_addr

* Update documentation for Request.remote_addr
2019-04-16 08:30:28 -05:00
7
5631a31099 Merge pull request #1553 from jrmi/master
Fix #1551 add missing parameter in create_server
2019-04-15 17:36:51 -07:00
Jeremie Pardou-Piquemal
f4bc0efc31 Fix #1551 missing parameter in create_server 2019-04-15 22:18:35 +02:00
7
53f45810ff Fix #1528 (#1549)
* assign app before handle_request so that request.app could be used in case of connection timeout

* gitignore pip-wheel-metadata/

* remove default app for request class and fix lint issue
2019-04-12 07:48:32 -05:00
7
d58151a0eb Merge pull request #1546 from krigar1184/master
Fixed a docstring typo and simplified code a little
2019-04-11 10:37:32 -07:00
Nikita Antonenkov
de582d2fc7 Refactor the app.route decorator 2019-04-06 22:26:56 +03:00
Nikita Antonenkov
653ac7ee14 Fix app.patch decorator docstring typo 2019-04-06 22:23:50 +03:00
Zaar Hai
0b4769289a Drop dependency on disutils (#1544)
* Drop dependency on distutils

While distutils is part of stdlib, it feels odd to use distutils in main application code.

I personally use a (lean)[https://hub.docker.com/r/haizaar/python-minimal/tags] Python distribution for running my applications that does not include distutils.

* Flake8 fixes

* "black" fixes

* strtobool should actually return bool
2019-04-02 08:22:26 -05:00
Daniel Golding
3bedb223fc Add 19.03 release to changelog (#1537) 2019-03-29 10:34:13 -05:00
7
94a1720e04 Merge pull request #1541 from cakemanny/fix-number-route-accepting-invalid-float
stop number route accepting excess '.'s
2019-03-28 10:42:24 -07:00
Eli Uriegas
d0c8808340 Merge pull request #1542 from cakemanny/some-typo-fixes
Fix some typos in docs
2019-03-27 21:24:33 -07:00
cakemanny
dd32d81726 fix typos in docs 2019-03-28 01:05:39 +00:00
cakemanny
378a732968 fix expected float error message 2019-03-27 22:46:30 +00:00
cakemanny
b2e82685b4 stop number route accepting excess '.'s
We stop getting:

    ValueError: could not convert string to float: '12.34.56'

when passing 12.34.56 as a number route parameter argument.
By accepting ".12" and "12.", this is a non-breaking change. All valid
floats described by [0-9\.]+ are still accepted, just invalid ones are
now rejected.
2019-03-27 02:49:05 +00:00
andreymal
566940e052 Fix typo in CONTRIBUTING.md: [.dev] -> .[dev] (#1538) 2019-03-26 11:08:08 -05:00
Eli Uriegas
dab802fbf4 Merge pull request #1530 from seemethere/bump_19031
Bump version to 19.03.1
2019-03-22 19:48:50 -07:00
Eli Uriegas
7bca95205d Bump version to 19.03.1
Couldn't delete the release on github so we go with the next best thing
which is to just bump the patch version

Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
2019-03-22 16:44:28 -07:00
Eli Uriegas
669e2ed5b0 Merge pull request #1529 from huge-success/pypi-credentials
Update PyPI credentials
2019-03-22 16:41:12 -07:00
Eli Uriegas
783eb1a6e8 Merge pull request #1527 from seemethere/bump_1903
Bump version to 19.03.0
2019-03-22 16:40:54 -07:00
Adam Hopkins
9b9599b12f Update PyPI credentials 2019-03-22 13:44:13 +02:00
Eli Uriegas
6ed0d3def7 Bump version to 19.03.0
Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
2019-03-21 16:24:57 -07:00
Amit Garu
c42731a55c await keyword missing fix in response doc (#1520) 2019-03-19 07:15:28 -05:00
Jotagê Sales
b534df242b rename config in class in test_config 2019-03-05 14:36:54 -03:00
Jotagê Sales
734730640a added param package to relative imports 2019-03-05 01:40:17 -03:00
Jotagê Sales
bee7cfa6aa Merge branch 'master' of github.com:huge-success/sanic into config_from_object_string 2019-03-05 01:10:09 -03:00
Jotagê Sales
eacf78b83c Merge branch 'master' of github.com:huge-success/sanic into config_from_object_string 2019-03-04 00:37:59 -03:00
Tom Christie
95526a82de ASGI refactoring attempt 2019-01-18 14:50:42 +00:00
Jotagê Sales
62420e0f40 resolve conflict 2019-01-02 21:19:40 -02:00
Jotagê Sales
b36dc22b45 resolve conflict in setup.py 2018-12-28 12:08:10 -02:00
Jotagê Sales
f2a55d01ea fix error in import_string 2018-12-27 15:20:58 -02:00
Jotagê Sales
bf029c1b9d added docstring to helper function import_string 2018-12-27 14:35:04 -02:00
Jotagê Sales
375ebd39f0 fix pep8 errors 2018-12-26 21:28:42 -02:00
Jotagê Sales
a33ebbaf11 remove dependence and implmented import_string 2018-12-26 21:19:54 -02:00
Jotagê Sales
19b304b0fc fix doc 2018-12-26 18:31:43 -02:00
Jotagê Sales
0b64fe6746 create a documentation for config path 2018-12-26 18:27:02 -02:00
Jotagê Sales
e978121d58 configure app from object by path string 2018-12-26 16:23:16 -02:00
93 changed files with 4101 additions and 950 deletions

View File

@@ -2,11 +2,6 @@ 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"

View File

@@ -5,3 +5,11 @@ omit = site-packages, sanic/utils.py, sanic/__main__.py
[html]
directory = coverage
[report]
exclude_lines =
no cov
no qa
noqa
NOQA
pragma: no cover

19
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 90
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- bug
- urgent
- necessary
- help wanted
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. If this
is incorrect, please respond with an update. Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

3
.gitignore vendored
View File

@@ -15,4 +15,5 @@ docs/_build/
docs/_api/
build/*
.DS_Store
dist/*
dist/*
pip-wheel-metadata/

View File

@@ -5,10 +5,6 @@ cache:
- $HOME/.cache/pip
matrix:
include:
- env: TOX_ENV=py35
python: 3.5
- env: TOX_ENV=py35-no-ext
python: 3.5
- env: TOX_ENV=py36
python: 3.6
- env: TOX_ENV=py36-no-ext
@@ -25,6 +21,10 @@ matrix:
python: 3.6
- env: TOX_ENV=check
python: 3.6
- env: TOX_ENV=security
python: 3.7
dist: xenial
sudo: true
install:
- pip install -U tox
- pip install codecov
@@ -33,9 +33,9 @@ after_success:
- codecov
deploy:
provider: pypi
user: channelcat
user: brewmaster
password:
secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
secure: "GoawLwmbtJOgKB6AJ0ZSYUUnNwIoonseHBxaAUH3zu79TS/Afrq+yB3lsVaMSG0CbyDgN4FrfD1phT1NzbvZ1VcLIOTDtCrmpQ1kLDw+zwgF40ab8sp8fPkKVHHHfCCs1mjltHIpxQa5lZTJcAs6Bpi/lbUWWwYxFzSV8pHw4W4hY09EHUd2o+evLTSVxaploetSt725DJUYKICUr2eAtCC11IDnIW4CzBJEx6krVV3uhzfTJW0Ls17x0c6sdZ9icMnV/G9xO/eQH6RIHe4xcrWJ6cmLDNKoGAkJp+BKr1CeVVg7Jw/MzPjvZKL2/ki6Beue1y6GUIy7lOS7jPVaOEhJ23b0zQwFcLMZw+Tt+E3v6QfHk+B/WBBBnM3zUZed9UI+QyW8+lqLLt39sQX0FO0P3eaDh8qTXtUuon2jTyFMMAMTFRTNpJmpAzuBH9yeMmDeALPTh0HphI+BkoUl5q1QbWFYjjnZMH2CatApxpLybt9A7rwm//PbOG0TSI93GEKNQ4w5DYryKTfwHzRBptNSephJSuxZYEfJsmUtas5es1D7Fe0PkyjxNNSU+eO+8wsTlitLUsJO4k0jAgy+cEKdU7YJ3J0GZVXocSkrNnUfd2hQPcJ3UtEJx3hLqqr8EM7EZBAasc1yGHh36NFetclzFY24YPih0G1+XurhTys="
on:
tags: true
distributions: "sdist bdist_wheel"

View File

@@ -1,3 +1,156 @@
Version 19.6
------------
19.6.0
- Changes:
- [#1562](https://github.com/huge-success/sanic/pull/1562)
Remove `aiohttp` dependencey and create new `SanicTestClient` based upon
[`requests-async`](https://github.com/encode/requests-async).
- [#1475](https://github.com/huge-success/sanic/pull/1475)
Added ASGI support (Beta)
- [#1436](https://github.com/huge-success/sanic/pull/1436)
Add Configure support from object string
- [#1544](https://github.com/huge-success/sanic/pull/1544)
Drop dependency on distutil
- Fixes:
- [#1587](https://github.com/huge-success/sanic/pull/1587)
Add missing handle for Expect header.
- [#1560](https://github.com/huge-success/sanic/pull/1560)
Allow to disable Transfer-Encoding: chunked.
- [#1558](https://github.com/huge-success/sanic/pull/1558)
Fix graceful shutdown.
- [#1594](https://github.com/huge-success/sanic/pull/1594)
Strict Slashes behavior fix
- Deprecation:
- [#1562](https://github.com/huge-success/sanic/pull/1562)
Drop support for Python 3.5
- [#1568](https://github.com/huge-success/sanic/pull/1568)
Deprecate route removal.
Note: Sanic will not support Python 3.5 from version 19.6 and forward. However,
version 18.12LTS will have its support period extended thru December 2020, and
therefore passing Python's official support version 3.5, which is set to expire
in September 2020.
Version 19.3
-------------
19.3.1
- Changes:
- [#1497](https://github.com/huge-success/sanic/pull/1497)
Add support for zero-length and RFC 5987 encoded filename for
multipart/form-data requests.
- [#1484](https://github.com/huge-success/sanic/pull/1484)
The type of `expires` attribute of `sanic.cookies.Cookie` is now
enforced to be of type `datetime`.
- [#1482](https://github.com/huge-success/sanic/pull/1482)
Add support for the `stream` parameter of `sanic.Sanic.add_route()`
available to `sanic.Blueprint.add_route()`.
- [#1481](https://github.com/huge-success/sanic/pull/1481)
Accept negative values for route parameters with type `int` or `number`.
- [#1476](https://github.com/huge-success/sanic/pull/1476)
Deprecated the use of `sanic.request.Request.raw_args` - it has a
fundamental flaw in which is drops repeated query string parameters.
Added `sanic.request.Request.query_args` as a replacement for the
original use-case.
- [#1472](https://github.com/huge-success/sanic/pull/1472)
Remove an unwanted `None` check in Request class `repr` implementation.
This changes the default `repr` of a Request from `<Request>` to
`<Request: None />`
- [#1470](https://github.com/huge-success/sanic/pull/1470)
Added 2 new parameters to `sanic.app.Sanic.create_server`:
- `return_asyncio_server` - whether to return an asyncio.Server.
- `asyncio_server_kwargs` - kwargs to pass to `loop.create_server` for
the event loop that sanic is using.
This is a breaking change.
- [#1499](https://github.com/huge-success/sanic/pull/1499)
Added a set of test cases that test and benchmark route resolution.
- [#1457](https://github.com/huge-success/sanic/pull/1457)
The type of the `"max-age"` value in a `sanic.cookies.Cookie` is now
enforced to be an integer. Non-integer values are replaced with `0`.
- [#1445](https://github.com/huge-success/sanic/pull/1445)
Added the `endpoint` attribute to an incoming `request`, containing the
name of the handler function.
- [#1423](https://github.com/huge-success/sanic/pull/1423)
Improved request streaming. `request.stream` is now a bounded-size buffer
instead of an unbounded queue. Callers must now call
`await request.stream.read()` instead of `await request.stream.get()`
to read each portion of the body.
This is a breaking change.
- Fixes:
- [#1502](https://github.com/huge-success/sanic/pull/1502)
Sanic was prefetching `time.time()` and updating it once per second to
avoid excessive `time.time()` calls. The implementation was observed to
cause memory leaks in some cases. The benefit of the prefetch appeared
to negligible, so this has been removed. Fixes
[#1500](https://github.com/huge-success/sanic/pull/1500)
- [#1501](https://github.com/huge-success/sanic/pull/1501)
Fix a bug in the auto-reloader when the process was launched as a module
i.e. `python -m init0.mod1` where the sanic server is started
in `init0/mod1.py` with `debug` enabled and imports another module in
`init0`.
- [#1376](https://github.com/huge-success/sanic/pull/1376)
Allow sanic test client to bind to a random port by specifying
`port=None` when constructing a `SanicTestClient`
- [#1399](https://github.com/huge-success/sanic/pull/1399)
Added the ability to specify middleware on a blueprint group, so that all
routes produced from the blueprints in the group have the middleware
applied.
- [#1442](https://github.com/huge-success/sanic/pull/1442)
Allow the the use the `SANIC_ACCESS_LOG` environment variable to
enable/disable the access log when not explicitly passed to `app.run()`.
This allows the access log to be disabled for example when running via
gunicorn.
- Developer infrastructure:
- [#1529](https://github.com/huge-success/sanic/pull/1529) Update project PyPI credentials
- [#1515](https://github.com/huge-success/sanic/pull/1515) fix linter issue causing travis build failures (fix #1514)
- [#1490](https://github.com/huge-success/sanic/pull/1490) Fix python version in doc build
- [#1478](https://github.com/huge-success/sanic/pull/1478) Upgrade setuptools version and use native docutils in doc build
- [#1464](https://github.com/huge-success/sanic/pull/1464) Upgrade pytest, and fix caplog unit tests
- Typos and Documentation:
- [#1516](https://github.com/huge-success/sanic/pull/1516) Fix typo at the exception documentation
- [#1510](https://github.com/huge-success/sanic/pull/1510) fix typo in Asyncio example
- [#1486](https://github.com/huge-success/sanic/pull/1486) Documentation typo
- [#1477](https://github.com/huge-success/sanic/pull/1477) Fix grammar in README.md
- [#1489](https://github.com/huge-success/sanic/pull/1489) Added "databases" to the extensions list
- [#1483](https://github.com/huge-success/sanic/pull/1483) Add sanic-zipkin to extensions list
- [#1487](https://github.com/huge-success/sanic/pull/1487) Removed link to deleted repo, Sanic-OAuth, from the extensions list
- [#1460](https://github.com/huge-success/sanic/pull/1460) 18.12 changelog
- [#1449](https://github.com/huge-success/sanic/pull/1449) Add example of amending request object
- [#1446](https://github.com/huge-success/sanic/pull/1446) Update README
- [#1444](https://github.com/huge-success/sanic/pull/1444) Update README
- [#1443](https://github.com/huge-success/sanic/pull/1443) Update README, including new logo
- [#1440](https://github.com/huge-success/sanic/pull/1440) fix minor type and pip install instruction mismatch
- [#1424](https://github.com/huge-success/sanic/pull/1424) Documentation Enhancements
Note: 19.3.0 was skipped for packagement purposes and not released on PyPI
Version 18.12
-------------
18.12.0

View File

@@ -18,7 +18,7 @@ So assume you have already cloned the repo and are in the working directory with
a virtual environment already set up, then run:
```bash
pip3 install -e . "[.dev]"
pip3 install -e . ".[dev]"
```
# Dependency Changes
@@ -30,32 +30,88 @@ the document that explains the way `sanic` manages dependencies inside the `setu
| 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]' |
| 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:
## Running all tests
To run the tests for Sanic it is recommended to use tox like so:
```bash
tox
```
See it's that simple!
`tox.ini` contains different environments. Running `tox` without any arguments will
run all unittests, perform lint and other checks.
## Run unittests :
`tox` environment -> `[testenv]`
To execute only unittests, run `tox` with environment like so:
```bash
tox -e py36 -v -- tests/test_config.py
# or
tox -e py37 -v -- tests/test_config.py
```
## Run lint checks :
`tox` environment -> `[testenv:lint]`
Permform `flake8`, `black` and `isort` checks.
```bash
tox -e lint
```
## Run other checks :
`tox` environment -> `[testenv:check]`
Perform other checks.
```bash
tox -e check
```
# Code Style
To maintain the code consistency, Sanic uses following tools.
1. [isort](https://github.com/timothycrosley/isort)
2. [black](https://github.com/python/black)
2. [flake8](https://github.com/PyCQA/flake8)
## isort
`isort` sorts Python imports. It divides imports into three
categories sorted each in alphabetical order.
1. built-in
2. third-party
3. project-specific
## black
`black` is a Python code formatter.
## flake8
`flake8` is a Python style guide that wraps following tools into one.
1. PyFlakes
2. pycodestyle
3. Ned Batchelder's McCabe script
`isort`, `black` and `flake8` checks are performed during `tox` lint checks.
Refer [tox](https://tox.readthedocs.io/en/latest/index.html) documentation for more details.
## Pull requests!
So the pull request approval rules are pretty simple:
1. All pull requests must pass unit tests.
2. All pull requests must be reviewed and approved by at least
one current collaborator on the project.
one current collaborator on the project.
3. All pull requests must pass flake8 checks.
4. All pull requests must be consistent with the existing code.
5. If you decide to remove/change anything from any common interface
a deprecation message should accompany it.
a deprecation message should accompany it.
6. If you implement a new feature you should have at least one unit
test to accompany it.
test to accompany it.
7. An example must be one of the following:
* Example of how to use Sanic
* Example of how to use Sanic extensions

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2016-present Channel Cat
Copyright (c) 2016-present Sanic Community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -47,12 +47,12 @@ ifdef include_tests
isort -rc sanic tests
else
$(info Sorting Imports)
isort -rc sanic
isort -rc sanic tests
endif
endif
black:
black --config ./pyproject.toml sanic tests
black --config ./.black.toml sanic tests
fix-import: black
isort -rc sanic
isort -rc sanic tests

View File

@@ -16,7 +16,9 @@ Sanic | Build fast. Run fast.
* - Package
- | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black|
* - Support
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby|
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby| |Awesome|
* - Stats
- | |Downloads| |Conda downloads|
.. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg
:target: https://community.sanicframework.org/
@@ -42,14 +44,23 @@ Sanic | Build fast. Run fast.
.. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg
:alt: Supported implementations
:target: https://pypi.python.org/pypi/sanic
.. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg
:alt: Awesome Sanic List
:target: https://github.com/mekicha/awesome-sanic
.. |Downloads| image:: https://pepy.tech/badge/sanic/month
:alt: Downloads
:target: https://pepy.tech/project/sanic
.. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg
:alt: Downloads
:target: https://anaconda.org/conda-forge/sanic
.. end-badges
Sanic is a Python 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.
Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
The project is maintained by the community, for the community **Contributions are welcome!**
The 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.
@@ -64,7 +75,13 @@ Installation
$ export SANIC_NO_UVLOOP=true
$ export SANIC_NO_UJSON=true
$ pip3 install sanic
$ pip3 install --no-binary :all: sanic
.. note::
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
use ``sanic`` with ``ujson`` dependency.
Hello World Example

25
SECURITY.md Normal file
View File

@@ -0,0 +1,25 @@
# Security Policy
## Supported Versions
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
| Version | LTS | Supported |
| ------- | ------------------ | ------------------ |
| 19.6.0 | | :white_check_mark: |
| 19.3.1 | | :heavy_check_mark: |
| 18.12.0 | :heavy_check_mark: | :heavy_check_mark: |
| 0.8.3 | | :x: |
| 0.7.0 | | :x: |
| 0.6.0 | | :x: |
| 0.5.4 | | :x: |
| 0.4.1 | | :x: |
| 0.3.1 | | :x: |
| 0.2.0 | | :x: |
| 0.1.9 | | :x: |
## Reporting a Vulnerability
If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button.
This will help to not publicize the issue until the team can address it and resolve it.

View File

@@ -4,7 +4,7 @@ Python 3.7 AsyncIO examples
With Python 3.7 AsyncIO got major update for the following types:
- asyncio.AbstractEventLoop
- asyncio.AnstractServer
- asyncio.AbstractServer
This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance:

View File

@@ -1,6 +1,159 @@
Version 18.12
-------------
18.12.0
# Changelog
## Version 19.6
- Changes:
- [#1562](https://github.com/huge-success/sanic/pull/1562)
Remove `aiohttp` dependencey and create new `SanicTestClient` based upon
[`requests-async`](https://github.com/encode/requests-async).
- [#1475](https://github.com/huge-success/sanic/pull/1475)
Added ASGI support (Beta)
- [#1436](https://github.com/huge-success/sanic/pull/1436)
Add Configure support from object string
- [#1544](https://github.com/huge-success/sanic/pull/1544)
Drop dependency on distutil
- Fixes:
- [#1587](https://github.com/huge-success/sanic/pull/1587)
Add missing handle for Expect header.
- [#1560](https://github.com/huge-success/sanic/pull/1560)
Allow to disable Transfer-Encoding: chunked.
- [#1558](https://github.com/huge-success/sanic/pull/1558)
Fix graceful shutdown.
- [#1594](https://github.com/huge-success/sanic/pull/1594)
Strict Slashes behavior fix
- Deprecation:
- [#1562](https://github.com/huge-success/sanic/pull/1562)
Drop support for Python 3.5
- [#1568](https://github.com/huge-success/sanic/pull/1568)
Deprecate route removal.
Note: Sanic will not support Python 3.5 from version 19.6 and forward. However,
version 18.12LTS will have its support period extended thru December 2020, and
therefore passing Python's official support version 3.5, which is set to expire
in September 2020.
## Version 19.3
- Changes:
- [#1497](https://github.com/huge-success/sanic/pull/1497)
Add support for zero-length and RFC 5987 encoded filename for
multipart/form-data requests.
- [#1484](https://github.com/huge-success/sanic/pull/1484)
The type of `expires` attribute of `sanic.cookies.Cookie` is now
enforced to be of type `datetime`.
- [#1482](https://github.com/huge-success/sanic/pull/1482)
Add support for the `stream` parameter of `sanic.Sanic.add_route()`
available to `sanic.Blueprint.add_route()`.
- [#1481](https://github.com/huge-success/sanic/pull/1481)
Accept negative values for route parameters with type `int` or `number`.
- [#1476](https://github.com/huge-success/sanic/pull/1476)
Deprecated the use of `sanic.request.Request.raw_args` - it has a
fundamental flaw in which is drops repeated query string parameters.
Added `sanic.request.Request.query_args` as a replacement for the
original use-case.
- [#1472](https://github.com/huge-success/sanic/pull/1472)
Remove an unwanted `None` check in Request class `repr` implementation.
This changes the default `repr` of a Request from `<Request>` to
`<Request: None />`
- [#1470](https://github.com/huge-success/sanic/pull/1470)
Added 2 new parameters to `sanic.app.Sanic.create_server`:
- `return_asyncio_server` - whether to return an asyncio.Server.
- `asyncio_server_kwargs` - kwargs to pass to `loop.create_server` for
the event loop that sanic is using.
This is a breaking change.
- [#1499](https://github.com/huge-success/sanic/pull/1499)
Added a set of test cases that test and benchmark route resolution.
- [#1457](https://github.com/huge-success/sanic/pull/1457)
The type of the `"max-age"` value in a `sanic.cookies.Cookie` is now
enforced to be an integer. Non-integer values are replaced with `0`.
- [#1445](https://github.com/huge-success/sanic/pull/1445)
Added the `endpoint` attribute to an incoming `request`, containing the
name of the handler function.
- [#1423](https://github.com/huge-success/sanic/pull/1423)
Improved request streaming. `request.stream` is now a bounded-size buffer
instead of an unbounded queue. Callers must now call
`await request.stream.read()` instead of `await request.stream.get()`
to read each portion of the body.
This is a breaking change.
- Fixes:
- [#1502](https://github.com/huge-success/sanic/pull/1502)
Sanic was prefetching `time.time()` and updating it once per second to
avoid excessive `time.time()` calls. The implementation was observed to
cause memory leaks in some cases. The benefit of the prefetch appeared
to negligible, so this has been removed. Fixes
[#1500](https://github.com/huge-success/sanic/pull/1500)
- [#1501](https://github.com/huge-success/sanic/pull/1501)
Fix a bug in the auto-reloader when the process was launched as a module
i.e. `python -m init0.mod1` where the sanic server is started
in `init0/mod1.py` with `debug` enabled and imports another module in
`init0`.
- [#1376](https://github.com/huge-success/sanic/pull/1376)
Allow sanic test client to bind to a random port by specifying
`port=None` when constructing a `SanicTestClient`
- [#1399](https://github.com/huge-success/sanic/pull/1399)
Added the ability to specify middleware on a blueprint group, so that all
routes produced from the blueprints in the group have the middleware
applied.
- [#1442](https://github.com/huge-success/sanic/pull/1442)
Allow the the use the `SANIC_ACCESS_LOG` environment variable to
enable/disable the access log when not explicitly passed to `app.run()`.
This allows the access log to be disabled for example when running via
gunicorn.
- Developer infrastructure:
- [#1529](https://github.com/huge-success/sanic/pull/1529) Update project PyPI credentials
- [#1515](https://github.com/huge-success/sanic/pull/1515) fix linter issue causing travis build failures (fix #1514)
- [#1490](https://github.com/huge-success/sanic/pull/1490) Fix python version in doc build
- [#1478](https://github.com/huge-success/sanic/pull/1478) Upgrade setuptools version and use native docutils in doc build
- [#1464](https://github.com/huge-success/sanic/pull/1464) Upgrade pytest, and fix caplog unit tests
- Typos and Documentation:
- [#1516](https://github.com/huge-success/sanic/pull/1516) Fix typo at the exception documentation
- [#1510](https://github.com/huge-success/sanic/pull/1510) fix typo in Asyncio example
- [#1486](https://github.com/huge-success/sanic/pull/1486) Documentation typo
- [#1477](https://github.com/huge-success/sanic/pull/1477) Fix grammar in README.md
- [#1489](https://github.com/huge-success/sanic/pull/1489) Added "databases" to the extensions list
- [#1483](https://github.com/huge-success/sanic/pull/1483) Add sanic-zipkin to extensions list
- [#1487](https://github.com/huge-success/sanic/pull/1487) Removed link to deleted repo, Sanic-OAuth, from the extensions list
- [#1460](https://github.com/huge-success/sanic/pull/1460) 18.12 changelog
- [#1449](https://github.com/huge-success/sanic/pull/1449) Add example of amending request object
- [#1446](https://github.com/huge-success/sanic/pull/1446) Update README
- [#1444](https://github.com/huge-success/sanic/pull/1444) Update README
- [#1443](https://github.com/huge-success/sanic/pull/1443) Update README, including new logo
- [#1440](https://github.com/huge-success/sanic/pull/1440) fix minor type and pip install instruction mismatch
- [#1424](https://github.com/huge-success/sanic/pull/1424) Documentation Enhancements
Note: 19.3.0 was skipped for packagement purposes and not released on PyPI
## Version 18.12
- Changes:
- Improved codebase test coverage from 81% to 91%.
- Added stream_large_files and host examples in static_file document
@@ -34,8 +187,8 @@ Version 18.12
- 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
-----------
## Version 0.8
0.8.3
- Changes:
- Ownership changed to org 'huge-success'
@@ -111,8 +264,8 @@ Version 0.8
Note: Changelog was unmaintained between 0.1 and 0.7
Version 0.1
-----------
## Version 0.1
- 0.1.7
- Reversed static url and directory arguments to meet spec
- 0.1.6

View File

@@ -53,6 +53,13 @@ import myapp.default_settings
app = Sanic('myapp')
app.config.from_object(myapp.default_settings)
```
or also by path to config:
```
app = Sanic('myapp')
app.config.from_object('config.path.config.Class')
```
You could use a class or any other object as well.
@@ -85,16 +92,19 @@ 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_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 |
| Variable | Default | Description |
| ------------------------- | ----------------- | --------------------------------------------------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
| ACCESS_LOG | True | Disable or enable access log |
| PROXIES_COUNT | -1 | The number of proxy servers in front of the app (e.g. nginx; see below) |
| FORWARDED_FOR_HEADER | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip |
| REAL_IP_HEADER | "X-Real-IP" | The name of "X-Real-IP" HTTP header that contains real client ip |
### The different Timeout variables:
@@ -143,3 +153,17 @@ Firefox client hard keepalive limit = 115 seconds
Opera 11 client hard keepalive limit = 120 seconds
Chrome 13+ client keepalive limit > 300+ seconds
```
### About proxy servers and client ip
When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain ip of a proxy, typically `127.0.0.1`. To determine the real client ip, `X-Forwarded-For` and `X-Real-IP` HTTP headers are used. But client can fake these headers if they have not been overridden by a proxy. Sanic has a set of options to determine the level of confidence in these headers.
* If you have a single proxy, set `PROXIES_COUNT` to `1`. Then Sanic will use `X-Real-IP` if available or the last ip from `X-Forwarded-For`.
* If you have multiple proxies, set `PROXIES_COUNT` equal to their number to allow Sanic to select the correct ip from `X-Forwarded-For`.
* If you don't use a proxy, set `PROXIES_COUNT` to `0` to ignore these headers and prevent ip falsification.
* If you don't use `X-Real-IP` (e.g. your proxy sends only `X-Forwarded-For`), set `REAL_IP_HEADER` to an empty string.
The real ip will be available in `request.remote_addr`. If HTTP headers are unavailable or untrusted, `request.remote_addr` will be an empty string; in this case use `request.ip` instead.

View File

@@ -1,7 +1,12 @@
# Deploying
Deploying Sanic is made simple by the inbuilt webserver. After defining an
instance of `sanic.Sanic`, we can call the `run` method with the following
Deploying Sanic is very simple using one of three options: the inbuilt webserver,
an [ASGI webserver](https://asgi.readthedocs.io/en/latest/implementations.html), or `gunicorn`.
It is also very common to place Sanic behind a reverse proxy, like `nginx`.
## Running via Sanic webserver
After defining an instance of `sanic.Sanic`, we can call the `run` method with the following
keyword arguments:
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
@@ -17,7 +22,13 @@ keyword arguments:
[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
```python
app.run(host='0.0.0.0', port=1337, access_log=False)
```
In the above example, we decided to turn off the access log in order to increase performance.
### Workers
By default, Sanic listens in the main process using only one CPU core. To crank
up the juice, just specify the number of workers in the `run` arguments.
@@ -29,9 +40,9 @@ app.run(host='0.0.0.0', port=1337, workers=4)
Sanic will automatically spin up multiple processes and route traffic between
them. We recommend as many workers as you have available cores.
## Running via command
### Running via command
If you like using command line arguments, you can launch a Sanic server by
If you like using command line arguments, you can launch a Sanic webserver by
executing the module. For example, if you initialized Sanic as `app` in a file
named `server.py`, you could run the server like so:
@@ -46,6 +57,33 @@ if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
```
## Running via ASGI
Sanic is also ASGI-compliant. This means you can use your preferred ASGI webserver
to run Sanic. The three main implementations of ASGI are
[Daphne](http://github.com/django/daphne), [Uvicorn](https://www.uvicorn.org/),
and [Hypercorn](https://pgjones.gitlab.io/hypercorn/index.html).
Follow their documentation for the proper way to run them, but it should look
something like:
```
daphne myapp:app
uvicorn myapp:app
hypercorn myapp:app
```
A couple things to note when using ASGI:
1. When using the Sanic webserver, websockets will run using the [`websockets`](https://websockets.readthedocs.io/) package. In ASGI mode, there is no need for this package since websockets are managed in the ASGI server.
1. The ASGI [lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) supports
only two server events: startup and shutdown. Sanic has four: before startup, after startup,
before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and shutdown events will
run consecutively and not actually around the server process beginning and ending (since that
is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and
`before_server_stop`.
1. ASGI mode is still in "beta" as of Sanic v19.6.
## Running via Gunicorn
[Gunicorn](http://gunicorn.org/) Green Unicorn is a WSGI HTTP Server for UNIX.
@@ -64,7 +102,29 @@ of the memory leak.
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
## Disable debug logging
## Other deployment considerations
### Running behind a reverse proxy
Sanic can be used with a reverse proxy (e.g. nginx). There's a simple example of nginx configuration:
```
server {
listen 80;
server_name example.org;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
```
If you want to get real client ip, you should configure `X-Real-IP` and `X-Forwarded-For` HTTP headers and set `app.config.PROXIES_COUNT` to `1`; see the configuration page for more information.
### Disable debug logging for performance
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
@@ -84,16 +144,17 @@ Or you can rewrite app config directly
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
### Asynchronous support and sharing the loop
This is suitable if you *need* to share the Sanic process with other applications, in particular the `loop`.
However, be advised that this method does not support using multiple processes, and is not the preferred way
to run the app in general.
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
```python
server = app.create_server(host="0.0.0.0", port=8000)
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
loop.run_forever()
```
```

View File

@@ -160,7 +160,7 @@ execution support provided by the ``pytest-xdist`` plugin.
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.
The ``request`` object in ``Sanic`` is a kind of ``dict`` object, this means that ``request`` object can be manipulated as a regular ``dict`` object.
.. literalinclude:: ../../examples/amending_request_object.py

View File

@@ -1,75 +1 @@
# 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.
- [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.
- [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-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
- [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.
Moved to the [`awesome-sanic`](https://github.com/mekicha/awesome-sanic) list.

View File

@@ -1,14 +1,16 @@
# Getting Started
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
least version 3.6 of Python before starting. Sanic uses the new `async`/`await`
syntax, so earlier versions of python won't work.
## 1. Install Sanic
```
pip3 install sanic
```
> If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency.
```bash
pip3 install sanic
```
To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`)
@@ -18,6 +20,13 @@ to true will stop that features installation.
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic
```
You can also install Sanic from [`conda-forge`](https://anaconda.org/conda-forge/sanic)
```bash
conda config --add channels conda-forge
conda install sanic
```
## 2. Create a file called `main.py`
```python

View File

@@ -1,9 +1,9 @@
Sanic
=================================
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
Sanic is a Python 3.6+ web server and web framework that's written to go fast. It allows the usage of the async/await syntax added in Python 3.5, which makes your code non-blocking and speedy.
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
@@ -22,4 +22,8 @@ Sanic aspires to be simple
return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
app.run(host="0.0.0.0", port=8000)
.. note::
Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru December 2020. Official Python support for version 3.5 is set to expire in September 2020.

View File

@@ -145,13 +145,17 @@ The following variables are accessible as properties on `Request` objects:
```
- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar`
- `scheme`: The URL scheme associated with the request: `http` or `https`
- `host`: The host associated with the request: `localhost:8080`
- `scheme`: The URL scheme associated with the request: 'http|https|ws|wss' or arbitrary value given by the headers.
- `host`: The host associated with the request(which in the `Host` header): `localhost:8080`
- `server_name`: The hostname of the server, without port number. the value is seeked in this order: `config.SERVER_NAME`, `x-forwarded-host` header, :func:`Request.host`
- `server_port`: Like `server_name`. Seeked in this order: `x-forwarded-port` header, :func:`Request.host`, actual port used by the transport layer socket.
- `path`: The path of the request: `/posts/1/`
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
- `uri_template`: Template for matching route handler: `/posts/<id>/`
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
- `url_for`: Just like `sanic.Sanic.url_for`, but automatically determine `scheme` and `netloc` base on the request. Since this method is aiming to generate correct schema & netloc, `_external` is implied.
## Changing the default parsing rules of the queryset
@@ -193,7 +197,7 @@ 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"]},
"args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]},
"query_string": "test1=value1&test2=&test3=value3"
}
```

View File

@@ -55,11 +55,13 @@ from sanic import response
@app.route("/streaming")
async def index(request):
async def streaming_fn(response):
response.write('foo')
response.write('bar')
await response.write('foo')
await response.write('bar')
return response.stream(streaming_fn, content_type='text/plain')
```
See [Streaming](streaming.md) for more information.
## File Streaming
For large files, a combination of File and Streaming above
```python

View File

@@ -41,27 +41,75 @@ inside the quotes. If the parameter does not match the specified type, Sanic
will throw a `NotFound` exception, resulting in a `404: Page not found` error
on the URL.
### Supported types
* `string`
* "Bob"
* "Python 3"
* `int`
* 10
* 20
* 30
* -10
* (No floats work here)
* `number`
* 1
* 1.5
* 10
* -10
* `alpha`
* "Bob"
* "Python"
* (If it contains a symbol or a non alphanumeric character it will fail)
* `path`
* "hello"
* "hello.text"
* "hello world"
* `uuid`
* 123a123a-a12a-1a1a-a1a1-1a12a1a12345 (UUIDv4 Support)
* `regex expression`
If no type is set then a string is expected. The argument given to the function will always be a string, independent of the type.
```python
from sanic.response import text
@app.route('/number/<integer_arg:int>')
@app.route('/string/<string_arg:string>')
async def string_handler(request, string_arg):
return text('String - {}'.format(string_arg))
@app.route('/int/<integer_arg:int>')
async def integer_handler(request, integer_arg):
return text('Integer - {}'.format(integer_arg))
return text('Integer - {}'.format(integer_arg))
@app.route('/number/<number_arg:number>')
async def number_handler(request, number_arg):
return text('Number - {}'.format(number_arg))
return text('Number - {}'.format(number_arg))
@app.route('/alpha/<alpha_arg:alpha>')
async def number_handler(request, alpha_arg):
return text('Alpha - {}'.format(alpha_arg))
@app.route('/path/<path_arg:path>')
async def number_handler(request, path_arg):
return text('Path - {}'.format(path_arg))
@app.route('/uuid/<uuid_arg:uuid>')
async def number_handler(request, uuid_arg):
return text('Uuid - {}'.format(uuid_arg))
@app.route('/person/<name:[A-z]+>')
async def person_handler(request, name):
return text('Person - {}'.format(name))
return text('Person - {}'.format(name))
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id))
return text('Folder - {}'.format(folder_id))
```
**Warning** `str` is not a valid type tag. If you want `str` recognition then you must use `string`
## HTTP request types
By default, a route defined on a URL will be available for only GET requests to that URL.
@@ -241,6 +289,45 @@ def handler(request):
app.blueprint(bp)
```
The behavior of how the `strict_slashes` flag follows a defined hierarchy which decides if a specific route
falls under the `strict_slashes` behavior.
```bash
|___ Route
|___ Blueprint
|___ Application
```
Above hierarchy defines how the `strict_slashes` flag will behave. The first non `None` value of the `strict_slashes`
found in the above order will be applied to the route in question.
```python
from sanic import Sanic, Blueprint
from sanic.response import text
app = Sanic("sample_strict_slashes", strict_slashes=True)
@app.get("/r1")
def r1(request):
return text("strict_slashes is applicable from App level")
@app.get("/r2", strict_slashes=False)
def r2(request):
return text("strict_slashes is not applicable due to False value set in route level")
bp = Blueprint("bp", strict_slashes=False)
@bp.get("/r3", strict_slashes=True)
def r3(request):
return text("strict_slashes applicable from blueprint route level")
bp1 = Blueprint("bp1", strict_slashes=True)
@bp.get("/r4")
def r3(request):
return text("strict_slashes applicable from blueprint level")
```
## User defined route name
A custom route name can be used by passing a `name` argument while registering the route which will

View File

@@ -117,3 +117,27 @@ async def index(request):
return stream(stream_from_db)
```
If a client supports HTTP/1.1, Sanic will use [chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding); you can explicitly enable or disable it using `chunked` option of the `stream` function.
## File Streaming
Sanic provides `sanic.response.file_stream` function that is useful when you want to send a large file. It returns a `StreamingHTTPResponse` object and will use chunked transfer encoding by default; for this reason Sanic doesn't add `Content-Length` HTTP header in the response. If you want to use this header, you can disable chunked transfer encoding and add it manually:
```python
from aiofiles import os as async_os
from sanic.response import file_stream
@app.route("/")
async def index(request):
file_path = "/srv/www/whatever.png"
file_stat = await async_os.stat(file_path)
headers = {"Content-Length": str(file_stat.st_size)}
return await file_stream(
file_path,
headers=headers,
chunked=False,
)
```

View File

@@ -1,8 +1,8 @@
# Testing
Sanic endpoints can be tested locally using the `test_client` object, which
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
library.
depends on the additional [`requests-async`](https://github.com/encode/requests-async)
library, which implements an API that mirrors the `requests` library.
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
for you to run against your application. A simple example (using pytest) is like follows:
@@ -21,7 +21,7 @@ def test_index_put_not_allowed():
```
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
your test request is executed against your application, using `aiohttp`.
your test request is executed against your application, using `requests-async`.
The `test_client` methods accept the following arguments and keyword arguments:
@@ -33,7 +33,7 @@ The `test_client` methods accept the following arguments and keyword arguments:
- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the aiohttp ClientSession request.
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the request.
For example, to supply data to a GET request, you would do the following:
@@ -55,8 +55,8 @@ def test_post_json_request_includes_data():
More information about
the available arguments to aiohttp can be found
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
the available arguments to `requests-async` can be found
[in the documentation for `requests`](https://2.python-requests.org/en/master/).
## Using a random port

View File

@@ -1,7 +1,7 @@
WebSocket
=========
Sanic provides an easy to user abstraction on top of `websockets`. To setup a WebSocket:
Sanic provides an easy to use abstraction on top of `websockets`. To setup a WebSocket:
.. code:: python

View File

@@ -13,6 +13,7 @@ dependencies:
- sphinx==1.8.3
- sphinx_rtd_theme==0.4.2
- recommonmark==0.5.0
- requests-async==0.5.0
- sphinxcontrib-asyncio>=0.2.0
- docutils==0.14
- pygments==2.3.1

View File

@@ -76,7 +76,7 @@ async def test(request):
if __name__ == '__main__':
asyncio.set_event_loop(uvloop.new_event_loop())
server = app.create_server(host="0.0.0.0", port=8000)
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
loop.set_task_factory(context.task_factory)
task = asyncio.ensure_future(server)

88
examples/run_asgi.py Normal file
View File

@@ -0,0 +1,88 @@
"""
1. Create a simple Sanic app
0. Run with an ASGI server:
$ uvicorn run_asgi:app
or
$ hypercorn run_asgi:app
"""
from pathlib import Path
from sanic import Sanic, response
app = Sanic(__name__)
@app.route("/text")
def handler_text(request):
return response.text("Hello")
@app.route("/json")
def handler_json(request):
return response.json({"foo": "bar"})
@app.websocket("/ws")
async def handler_ws(request, ws):
name = "<someone>"
while True:
data = f"Hello {name}"
await ws.send(data)
name = await ws.recv()
if not name:
break
@app.route("/file")
async def handler_file(request):
return await response.file(Path("../") / "setup.py")
@app.route("/file_stream")
async def handler_file_stream(request):
return await response.file_stream(
Path("../") / "setup.py", chunk_size=1024
)
@app.route("/stream", stream=True)
async def handler_stream(request):
while True:
body = await request.stream.read()
if body is None:
break
body = body.decode("utf-8").replace("1", "A")
# await response.write(body)
return response.stream(body)
@app.listener("before_server_start")
async def listener_before_server_start(*args, **kwargs):
print("before_server_start")
@app.listener("after_server_start")
async def listener_after_server_start(*args, **kwargs):
print("after_server_start")
@app.listener("before_server_stop")
async def listener_before_server_stop(*args, **kwargs):
print("before_server_stop")
@app.listener("after_server_stop")
async def listener_after_server_stop(*args, **kwargs):
print("after_server_stop")
@app.middleware("request")
async def print_on_request(request):
print("print_on_request")
@app.middleware("response")
async def print_on_response(request, response):
print("print_on_response")

View File

@@ -12,7 +12,7 @@ async def test(request):
return response.json({"answer": "42"})
asyncio.set_event_loop(uvloop.new_event_loop())
server = app.create_server(host="0.0.0.0", port=8000)
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
signal(SIGINT, lambda s, f: loop.stop())

View File

@@ -2,6 +2,6 @@ from sanic.app import Sanic
from sanic.blueprints import Blueprint
__version__ = "18.12.0"
__version__ = "19.6.2"
__all__ = ["Sanic", "Blueprint"]

View File

@@ -15,6 +15,7 @@ from typing import Any, Optional, Type, Union
from urllib.parse import urlencode, urlunparse
from sanic import reloader_helpers
from sanic.asgi import ASGIApp
from sanic.blueprint_group import BlueprintGroup
from sanic.config import BASE_LOGO, Config
from sanic.constants import HTTP_METHODS
@@ -25,7 +26,7 @@ from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.router import Router
from sanic.server import HttpProtocol, Signal, serve, serve_multiple
from sanic.static import register as static_register
from sanic.testing import SanicTestClient
from sanic.testing import SanicASGITestClient, SanicTestClient
from sanic.views import CompositionView
from sanic.websocket import ConnectionClosed, WebSocketProtocol
@@ -53,6 +54,7 @@ class Sanic:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
self.name = name
self.asgi = False
self.router = router or Router()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
@@ -80,7 +82,7 @@ class Sanic:
Only supported when using the `app.run` method.
"""
if not self.is_running:
if not self.is_running and self.asgi is False:
raise SanicException(
"Loop can only be retrieved after the app has started "
"running. Not supported with `create_server` function"
@@ -180,27 +182,28 @@ class Sanic:
strict_slashes = self.strict_slashes
def response(handler):
args = [key for key in signature(handler).parameters.keys()]
if args:
if stream:
handler.is_stream = stream
args = list(signature(handler).parameters.keys())
self.router.add(
uri=uri,
methods=methods,
handler=handler,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
return handler
else:
if not args:
raise ValueError(
"Required parameter `request` missing "
"in the {0}() route?".format(handler.__name__)
)
if stream:
handler.is_stream = stream
self.router.add(
uri=uri,
methods=methods,
handler=handler,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
return handler
return response
# Shorthand method decorators
@@ -332,7 +335,7 @@ class Sanic:
name=None,
):
"""
Add an API URL under the **DELETE** *HTTP* method
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
@@ -440,8 +443,12 @@ class Sanic:
):
"""Decorate a function to be registered as a websocket route
:param uri: path of the URL
:param host: Host IP or FQDN details
:param strict_slashes: If the API endpoint needs to terminate
with a "/" or not
:param subprotocols: optional list of str with supported subprotocols
:param host:
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:return: decorated function
"""
self.enable_websocket()
@@ -464,13 +471,23 @@ class Sanic:
getattr(handler, "__blueprintname__", "")
+ handler.__name__
)
try:
protocol = request.transport.get_protocol()
except AttributeError:
# On Python3.5 the Transport classes in asyncio do not
# have a get_protocol() method as in uvloop
protocol = request.transport._protocol
ws = await protocol.websocket_handshake(request, subprotocols)
pass
if self.asgi:
ws = request.transport.get_websocket_connection()
else:
try:
protocol = request.transport.get_protocol()
except AttributeError:
# On Python3.5 the Transport classes in asyncio do not
# have a get_protocol() method as in uvloop
protocol = request.transport._protocol
protocol.app = self
ws = await protocol.websocket_handshake(
request, subprotocols
)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
@@ -554,12 +571,22 @@ class Sanic:
This method provides the app user a mechanism by which an already
existing route can be removed from the :class:`Sanic` object
.. warning::
remove_route is deprecated in v19.06 and will be removed
from future versions.
:param uri: URL Path to be removed from the app
:param clean_cache: Instruct sanic if it needs to clean up the LRU
route cache
:param host: IP address or FQDN specific to the host
:return: None
"""
warnings.warn(
"remove_route is deprecated and will be removed "
"from future versions.",
DeprecationWarning,
stacklevel=2,
)
self.router.remove(uri, clean_cache, host)
# Decorator
@@ -880,8 +907,6 @@ class Sanic:
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
request.app = self
response = await self._run_request_middleware(request)
# No middleware results
if not response:
@@ -970,8 +995,16 @@ class Sanic:
raise CancelledError()
# pass the response to the correct callback
if isinstance(response, StreamingHTTPResponse):
await stream_callback(response)
if write_callback is None or isinstance(
response, StreamingHTTPResponse
):
if stream_callback:
await stream_callback(response)
else:
# Should only end here IF it is an ASGI websocket.
# TODO:
# - Add exception handling
pass
else:
write_callback(response)
@@ -983,6 +1016,10 @@ class Sanic:
def test_client(self):
return SanicTestClient(self)
@property
def asgi_client(self):
return SanicASGITestClient(self)
# -------------------------------------------------------------------- #
# Execution
# -------------------------------------------------------------------- #
@@ -1109,10 +1146,6 @@ class Sanic:
"""This kills the Sanic"""
get_event_loop().stop()
def __call__(self):
"""gunicorn compatibility"""
return self
async def create_server(
self,
host: Optional[str] = None,
@@ -1287,6 +1320,7 @@ class Sanic:
"port": port,
"sock": sock,
"ssl": ssl,
"app": self,
"signal": Signal(),
"debug": debug,
"request_handler": self.handle_request,
@@ -1353,3 +1387,15 @@ class Sanic:
def _build_endpoint_name(self, *parts):
parts = [self.name, *parts]
return ".".join(parts)
# -------------------------------------------------------------------- #
# ASGI
# -------------------------------------------------------------------- #
async def __call__(self, scope, receive, send):
"""To be ASGI compliant, our instance must be a callable that accepts
three arguments: scope, receive, send. See the ASGI reference for more
details: https://asgi.readthedocs.io/en/latest/"""
self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send)
await asgi_app()

349
sanic/asgi.py Normal file
View File

@@ -0,0 +1,349 @@
import asyncio
import warnings
from http.cookies import SimpleCookie
from inspect import isawaitable
from typing import Any, Awaitable, Callable, MutableMapping, Union
from urllib.parse import quote
from multidict import CIMultiDict
from sanic.exceptions import InvalidUsage, ServerError
from sanic.log import logger
from sanic.request import Request
from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.server import StreamBuffer
from sanic.websocket import WebSocketConnection
ASGIScope = MutableMapping[str, Any]
ASGIMessage = MutableMapping[str, Any]
ASGISend = Callable[[ASGIMessage], Awaitable[None]]
ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol:
def __init__(self, transport: "MockTransport", loop):
self.transport = transport
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
def pause_writing(self) -> None:
self._not_paused.clear()
def resume_writing(self) -> None:
self._not_paused.set()
async def complete(self) -> None:
self._not_paused.set()
await self.transport.send(
{"type": "http.response.body", "body": b"", "more_body": False}
)
@property
def is_complete(self) -> bool:
return self._complete.is_set()
async def push_data(self, data: bytes) -> None:
if not self.is_complete:
await self.transport.send(
{"type": "http.response.body", "body": data, "more_body": True}
)
async def drain(self) -> None:
await self._not_paused.wait()
class MockTransport:
def __init__(
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
) -> None:
self.scope = scope
self._receive = receive
self._send = send
self._protocol = None
self.loop = None
def get_protocol(self) -> MockProtocol:
if not self._protocol:
self._protocol = MockProtocol(self, self.loop)
return self._protocol
def get_extra_info(self, info: str) -> Union[str, bool]:
if info == "peername":
return self.scope.get("server")
elif info == "sslcontext":
return self.scope.get("scheme") in ["https", "wss"]
def get_websocket_connection(self) -> WebSocketConnection:
try:
return self._websocket_connection
except AttributeError:
raise InvalidUsage("Improper websocket connection.")
def create_websocket_connection(
self, send: ASGISend, receive: ASGIReceive
) -> WebSocketConnection:
self._websocket_connection = WebSocketConnection(send, receive)
return self._websocket_connection
def add_task(self) -> None:
raise NotImplementedError
async def send(self, data) -> None:
# TODO:
# - Validation on data and that it is formatted properly and is valid
await self._send(data)
async def receive(self) -> ASGIMessage:
return await self._receive()
class Lifespan:
def __init__(self, asgi_app: "ASGIApp") -> None:
self.asgi_app = asgi_app
if "before_server_start" in self.asgi_app.sanic_app.listeners:
warnings.warn(
'You have set a listener for "before_server_start" '
"in ASGI mode. "
"It will be executed as early as possible, but not before "
"the ASGI server is started."
)
if "after_server_stop" in self.asgi_app.sanic_app.listeners:
warnings.warn(
'You have set a listener for "after_server_stop" '
"in ASGI mode. "
"It will be executed as late as possible, but not after "
"the ASGI server is stopped."
)
async def startup(self) -> None:
"""
Gather the listeners to fire on server start.
Because we are using a third-party server and not Sanic server, we do
not have access to fire anything BEFORE the server starts.
Therefore, we fire before_server_start and after_server_start
in sequence since the ASGI lifespan protocol only supports a single
startup event.
"""
listeners = self.asgi_app.sanic_app.listeners.get(
"before_server_start", []
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
for handler in listeners:
response = handler(
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
)
if isawaitable(response):
await response
async def shutdown(self) -> None:
"""
Gather the listeners to fire on server stop.
Because we are using a third-party server and not Sanic server, we do
not have access to fire anything AFTER the server stops.
Therefore, we fire before_server_stop and after_server_stop
in sequence since the ASGI lifespan protocol only supports a single
shutdown event.
"""
listeners = self.asgi_app.sanic_app.listeners.get(
"before_server_stop", []
) + self.asgi_app.sanic_app.listeners.get("after_server_stop", [])
for handler in listeners:
response = handler(
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
)
if isawaitable(response):
await response
async def __call__(
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
) -> None:
message = await receive()
if message["type"] == "lifespan.startup":
await self.startup()
await send({"type": "lifespan.startup.complete"})
message = await receive()
if message["type"] == "lifespan.shutdown":
await self.shutdown()
await send({"type": "lifespan.shutdown.complete"})
class ASGIApp:
def __init__(self) -> None:
self.ws = None
@classmethod
async def create(
cls, sanic_app, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
) -> "ASGIApp":
instance = cls()
instance.sanic_app = sanic_app
instance.transport = MockTransport(scope, receive, send)
instance.transport.add_task = sanic_app.loop.create_task
instance.transport.loop = sanic_app.loop
headers = CIMultiDict(
[
(key.decode("latin-1"), value.decode("latin-1"))
for key, value in scope.get("headers", [])
]
)
instance.do_stream = (
True if headers.get("expect") == "100-continue" else False
)
instance.lifespan = Lifespan(instance)
if scope["type"] == "lifespan":
await instance.lifespan(scope, receive, send)
else:
url_bytes = scope.get("root_path", "") + quote(scope["path"])
url_bytes = url_bytes.encode("latin-1")
url_bytes += b"?" + scope["query_string"]
if scope["type"] == "http":
version = scope["http_version"]
method = scope["method"]
elif scope["type"] == "websocket":
version = "1.1"
method = "GET"
instance.ws = instance.transport.create_websocket_connection(
send, receive
)
await instance.ws.accept()
else:
pass
# TODO:
# - close connection
request_class = sanic_app.request_class or Request
instance.request = request_class(
url_bytes,
headers,
version,
method,
instance.transport,
sanic_app,
)
if sanic_app.is_request_stream:
is_stream_handler = sanic_app.router.is_stream_handler(
instance.request
)
if is_stream_handler:
instance.request.stream = StreamBuffer(
sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE
)
instance.do_stream = True
return instance
async def read_body(self) -> bytes:
"""
Read and return the entire body from an incoming ASGI message.
"""
body = b""
more_body = True
while more_body:
message = await self.transport.receive()
body += message.get("body", b"")
more_body = message.get("more_body", False)
return body
async def stream_body(self) -> None:
"""
Read and stream the body in chunks from an incoming ASGI message.
"""
more_body = True
while more_body:
message = await self.transport.receive()
chunk = message.get("body", b"")
await self.request.stream.put(chunk)
more_body = message.get("more_body", False)
await self.request.stream.put(None)
async def __call__(self) -> None:
"""
Handle the incoming request.
"""
if not self.do_stream:
self.request.body = await self.read_body()
else:
self.sanic_app.loop.create_task(self.stream_body())
handler = self.sanic_app.handle_request
callback = None if self.ws else self.stream_callback
await handler(self.request, None, callback)
async def stream_callback(self, response: HTTPResponse) -> None:
"""
Write the response.
"""
try:
headers = [
(str(name).encode("latin-1"), str(value).encode("latin-1"))
for name, value in response.headers.items()
]
except AttributeError:
logger.error(
"Invalid response object for url %s, "
"Expected Type: HTTPResponse, Actual Type: %s",
self.request.url,
type(response),
)
exception = ServerError("Invalid response type")
response = self.sanic_app.error_handler.response(
self.request, exception
)
headers = [
(str(name).encode("latin-1"), str(value).encode("latin-1"))
for name, value in response.headers.items()
if name not in (b"Set-Cookie",)
]
if "content-length" not in response.headers and not isinstance(
response, StreamingHTTPResponse
):
headers += [
(b"content-length", str(len(response.body)).encode("latin-1"))
]
if response.cookies:
cookies = SimpleCookie()
cookies.load(response.cookies)
headers += [
(b"set-cookie", cookie.encode("utf-8"))
for name, cookie in response.cookies.items()
]
await self.transport.send(
{
"type": "http.response.start",
"status": response.status,
"headers": headers,
}
)
if isinstance(response, StreamingHTTPResponse):
response.protocol = self.transport.get_protocol()
await response.stream()
await response.protocol.complete()
else:
await self.transport.send(
{
"type": "http.response.body",
"body": response.body,
"more_body": False,
}
)

View File

@@ -1,4 +1,4 @@
from collections import MutableSequence
from collections.abc import MutableSequence
class BlueprintGroup(MutableSequence):

View File

@@ -37,7 +37,7 @@ class Blueprint:
url_prefix=None,
host=None,
version=None,
strict_slashes=False,
strict_slashes=None,
):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of

View File

@@ -1,9 +1,8 @@
import os
import types
from distutils.util import strtobool
from sanic.exceptions import PyFileError
from sanic.helpers import import_string
SANIC_PREFIX = "SANIC_"
@@ -27,6 +26,9 @@ DEFAULT_CONFIG = {
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True,
"PROXIES_COUNT": -1,
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"REAL_IP_HEADER": "X-Real-IP",
}
@@ -78,7 +80,7 @@ class Config(dict):
module.__file__ = filename
try:
with open(filename) as config_file:
exec(
exec( # nosec
compile(config_file.read(), filename, "exec"),
module.__dict__,
)
@@ -101,6 +103,9 @@ class Config(dict):
from yourapplication import default_config
app.config.from_object(default_config)
or also:
app.config.from_object('myproject.config.MyConfigClass')
You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
@@ -108,6 +113,8 @@ class Config(dict):
:param obj: an object holding the configuration
"""
if isinstance(obj, str):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
@@ -127,6 +134,23 @@ class Config(dict):
self[config_key] = float(v)
except ValueError:
try:
self[config_key] = bool(strtobool(v))
self[config_key] = strtobool(v)
except ValueError:
self[config_key] = v
def strtobool(val):
"""
This function was borrowed from distutils.utils. While distutils
is part of stdlib, it feels odd to use distutils in main application code.
The function was modified to walk its talk and actually return bool
and not int.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return True
elif val in ("n", "no", "f", "false", "off", "0"):
return False
else:
raise ValueError("invalid truth value %r" % (val,))

View File

@@ -218,6 +218,11 @@ class ContentRangeError(SanicException):
}
@add_status_code(417)
class HeaderExpectationFailed(SanicException):
pass
@add_status_code(403)
class Forbidden(SanicException):
pass

View File

@@ -1,5 +1,9 @@
"""Defines basics of HTTP standard."""
from importlib import import_module
from inspect import ismodule
STATUS_CODES = {
100: b"Continue",
101: b"Switching Protocols",
@@ -131,3 +135,21 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")):
if not is_entity_header(header) or header.lower() in allowed
}
return headers
def import_string(module_name, package=None):
"""
import a module or class by string path.
:module_name: str with path of module or path to import and
instanciate a class
:returns: a module object or one instance from class if
module_name is a valid path to class
"""
module, klass = module_name.rsplit(".", 1)
module = import_module(module, package=package)
obj = getattr(module, klass)
if ismodule(obj):
return obj
return obj()

View File

@@ -27,9 +27,8 @@ except ImportError:
else:
json_loads = json.loads
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
EXPECT_HEADER = "EXPECT"
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
# > If the media type remains unknown, the recipient SHOULD treat it
@@ -95,11 +94,11 @@ class Request(dict):
"version",
)
def __init__(self, url_bytes, headers, version, method, transport):
def __init__(self, url_bytes, headers, version, method, transport, app):
self.raw_url = url_bytes
# TODO: Content-Encoding detection
self._parsed_url = parse_url(url_bytes)
self.app = None
self.app = app
self.headers = headers
self.version = version
@@ -329,12 +328,18 @@ class Request(dict):
@property
def ip(self):
"""
:return: peer ip of the socket
"""
if not hasattr(self, "_socket"):
self._get_address()
return self._ip
@property
def port(self):
"""
:return: peer port of the socket
"""
if not hasattr(self, "_socket"):
self._get_address()
return self._port
@@ -353,27 +358,93 @@ class Request(dict):
self._ip = self._socket[0]
self._port = self._socket[1]
@property
def server_name(self):
"""
Attempt to get the server's hostname in this order:
`config.SERVER_NAME`, `x-forwarded-host` header, :func:`Request.host`
:return: the server name without port number
:rtype: str
"""
return (
self.app.config.get("SERVER_NAME")
or self.headers.get("x-forwarded-host")
or self.host.split(":")[0]
)
@property
def server_port(self):
"""
Attempt to get the server's port in this order:
`x-forwarded-port` header, :func:`Request.host`, actual port used by
the transport layer socket.
:return: server port
:rtype: int
"""
forwarded_port = self.headers.get("x-forwarded-port") or (
self.host.split(":")[1] if ":" in self.host else None
)
if forwarded_port:
return int(forwarded_port)
else:
_, port = self.transport.get_extra_info("sockname")
return port
@property
def remote_addr(self):
"""Attempt to return the original client ip based on X-Forwarded-For.
"""Attempt to return the original client ip based on X-Forwarded-For
or X-Real-IP. If HTTP headers are unavailable or untrusted, returns
an empty string.
:return: original client ip.
"""
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
]
if len(remote_addrs) > 0:
self._remote_addr = remote_addrs[0]
if self.app.config.PROXIES_COUNT == 0:
self._remote_addr = ""
elif self.app.config.REAL_IP_HEADER and self.headers.get(
self.app.config.REAL_IP_HEADER
):
self._remote_addr = self.headers[
self.app.config.REAL_IP_HEADER
]
elif self.app.config.FORWARDED_FOR_HEADER:
forwarded_for = self.headers.get(
self.app.config.FORWARDED_FOR_HEADER, ""
).split(",")
remote_addrs = [
addr
for addr in [addr.strip() for addr in forwarded_for]
if addr
]
if self.app.config.PROXIES_COUNT == -1:
self._remote_addr = remote_addrs[0]
elif len(remote_addrs) >= self.app.config.PROXIES_COUNT:
self._remote_addr = remote_addrs[
-self.app.config.PROXIES_COUNT
]
else:
self._remote_addr = ""
else:
self._remote_addr = ""
return self._remote_addr
@property
def scheme(self):
"""
Attempt to get the request scheme.
Seeking the value in this order:
`x-forwarded-proto` header, `x-scheme` header, the sanic app itself.
:return: http|https|ws|wss or arbitrary value given by the headers.
:rtype: str
"""
forwarded_proto = self.headers.get(
"x-forwarded-proto"
) or self.headers.get("x-scheme")
if forwarded_proto:
return forwarded_proto
if (
self.app.websocket_enabled
and self.headers.get("upgrade") == "websocket"
@@ -389,8 +460,12 @@ class Request(dict):
@property
def host(self):
"""
:return: the Host specified in the header, may contains port number.
"""
# it appears that httptools doesn't return the host
# so pull it from the headers
return self.headers.get("Host", "")
@property
@@ -419,6 +494,31 @@ class Request(dict):
(self.scheme, self.host, self.path, None, self.query_string, None)
)
def url_for(self, view_name, **kwargs):
"""
Same as :func:`sanic.Sanic.url_for`, but automatically determine
`scheme` and `netloc` base on the request. Since this method is aiming
to generate correct schema & netloc, `_external` is implied.
:param kwargs: takes same parameters as in :func:`sanic.Sanic.url_for`
:return: an absolute url to the given view
:rtype: str
"""
scheme = self.scheme
host = self.server_name
port = self.server_port
if (scheme.lower() in ("http", "ws") and port == 80) or (
scheme.lower() in ("https", "wss") and port == 443
):
netloc = host
else:
netloc = "{}:{}".format(host, port)
return self.app.url_for(
view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs
)
File = namedtuple("File", ["type", "body", "name"])

View File

@@ -59,16 +59,23 @@ class StreamingHTTPResponse(BaseHTTPResponse):
"status",
"content_type",
"headers",
"chunked",
"_cookies",
)
def __init__(
self, streaming_fn, status=200, headers=None, content_type="text/plain"
self,
streaming_fn,
status=200,
headers=None,
content_type="text/plain",
chunked=True,
):
self.content_type = content_type
self.streaming_fn = streaming_fn
self.status = status
self.headers = CIMultiDict(headers or {})
self.chunked = chunked
self._cookies = None
async def write(self, data):
@@ -79,7 +86,10 @@ 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))
if self.chunked:
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
else:
await self.protocol.push_data(data)
await self.protocol.drain()
async def stream(
@@ -88,15 +98,18 @@ class StreamingHTTPResponse(BaseHTTPResponse):
"""Streams headers, runs the `streaming_fn` callback that writes
content to the response body, then finalizes the response body.
"""
if version != "1.1":
self.chunked = False
headers = self.get_headers(
version,
keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout,
)
self.protocol.push_data(headers)
await self.protocol.push_data(headers)
await self.protocol.drain()
await self.streaming_fn(self)
self.protocol.push_data(b"0\r\n\r\n")
if self.chunked:
await self.protocol.push_data(b"0\r\n\r\n")
# no need to await drain here after this write, because it is the
# very last thing we write and nothing needs to wait for it.
@@ -109,8 +122,9 @@ class StreamingHTTPResponse(BaseHTTPResponse):
if keep_alive and keep_alive_timeout is not None:
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
self.headers["Transfer-Encoding"] = "chunked"
self.headers.pop("Content-Length", None)
if self.chunked and version == "1.1":
self.headers["Transfer-Encoding"] = "chunked"
self.headers.pop("Content-Length", None)
self.headers["Content-Type"] = self.headers.get(
"Content-Type", self.content_type
)
@@ -327,6 +341,7 @@ async def file_stream(
mime_type=None,
headers=None,
filename=None,
chunked=True,
_range=None,
):
"""Return a streaming response object with file data.
@@ -336,6 +351,7 @@ async def file_stream(
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param filename: Override filename.
:param chunked: Enable or disable chunked transfer-encoding
:param _range:
"""
headers = headers or {}
@@ -383,6 +399,7 @@ async def file_stream(
status=status,
headers=headers,
content_type=mime_type,
chunked=chunked,
)
@@ -391,6 +408,7 @@ def stream(
status=200,
headers=None,
content_type="text/plain; charset=utf-8",
chunked=True,
):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
@@ -409,9 +427,14 @@ def stream(
writes content to that response.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param chunked: Enable or disable chunked transfer-encoding
"""
return StreamingHTTPResponse(
streaming_fn, headers=headers, content_type=content_type, status=status
streaming_fn,
headers=headers,
content_type=content_type,
status=status,
chunked=chunked,
)

View File

@@ -18,7 +18,7 @@ Parameter = namedtuple("Parameter", ["name", "cast"])
REGEX_TYPES = {
"string": (str, r"[^/]+"),
"int": (int, r"-?\d+"),
"number": (float, r"-?[0-9\\.]+"),
"number": (float, r"-?(?:\d+(?:\.\d*)?|\.\d+)"),
"alpha": (str, r"[A-Za-z]+"),
"path": (str, r"[^/].*?"),
"uuid": (
@@ -406,6 +406,7 @@ class Router:
if not self.hosts:
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", "")

View File

@@ -15,6 +15,7 @@ from httptools.parser.errors import HttpParserError
from multidict import CIMultiDict
from sanic.exceptions import (
HeaderExpectationFailed,
InvalidUsage,
PayloadTooLarge,
RequestTimeout,
@@ -22,7 +23,7 @@ from sanic.exceptions import (
ServiceUnavailable,
)
from sanic.log import access_logger, logger
from sanic.request import Request, StreamBuffer
from sanic.request import EXPECT_HEADER, Request, StreamBuffer
from sanic.response import HTTPResponse
@@ -44,6 +45,8 @@ class HttpProtocol(asyncio.Protocol):
"""
__slots__ = (
# app
"app",
# event loop, connection
"loop",
"transport",
@@ -88,6 +91,7 @@ class HttpProtocol(asyncio.Protocol):
self,
*,
loop,
app,
request_handler,
error_handler,
signal=Signal(),
@@ -107,6 +111,7 @@ class HttpProtocol(asyncio.Protocol):
**kwargs
):
self.loop = loop
self.app = app
self.transport = None
self.request = None
self.parser = None
@@ -115,7 +120,7 @@ class HttpProtocol(asyncio.Protocol):
self.router = router
self.signal = signal
self.access_log = access_log
self.connections = connections or set()
self.connections = connections if connections is not None else set()
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
@@ -303,12 +308,17 @@ class HttpProtocol(asyncio.Protocol):
version=self.parser.get_http_version(),
method=self.parser.get_method().decode(),
transport=self.transport,
app=self.app,
)
# Remove any existing KeepAlive handler here,
# It will be recreated if required on the new request.
if self._keep_alive_timeout_handler:
self._keep_alive_timeout_handler.cancel()
self._keep_alive_timeout_handler = None
if self.request.headers.get(EXPECT_HEADER):
self.expect_handler()
if self.is_request_stream:
self._is_stream_handler = self.router.is_stream_handler(
self.request
@@ -319,6 +329,21 @@ class HttpProtocol(asyncio.Protocol):
)
self.execute_request_handler()
def expect_handler(self):
"""
Handler for Expect Header.
"""
expect = self.request.headers.get(EXPECT_HEADER)
if self.request.version == "1.1":
if expect.lower() == "100-continue":
self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
else:
self.write_error(
HeaderExpectationFailed(
"Unknown Expect: {expect}".format(expect=expect)
)
)
def on_body(self, body):
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
@@ -452,7 +477,7 @@ class HttpProtocol(asyncio.Protocol):
async def drain(self):
await self._not_paused.wait()
def push_data(self, data):
async def push_data(self, data):
self.transport.write(data)
async def stream_response(self, response):
@@ -549,11 +574,15 @@ class HttpProtocol(asyncio.Protocol):
:return: None
"""
if from_error or self.transport.is_closing():
if from_error or self.transport is None or self.transport.is_closing():
logger.error(
"Transport closed @ %s and exception "
"experienced during error handling",
self.transport.get_extra_info("peername"),
(
self.transport.get_extra_info("peername")
if self.transport is not None
else "N/A"
),
)
logger.debug("Exception:", exc_info=True)
else:
@@ -607,6 +636,7 @@ def trigger_events(events, loop):
def serve(
host,
port,
app,
request_handler,
error_handler,
before_start=None,
@@ -698,12 +728,15 @@ def serve(
if debug:
loop.set_debug(debug)
app.asgi = False
connections = connections if connections is not None else set()
server = partial(
protocol,
loop=loop,
connections=connections,
signal=signal,
app=app,
request_handler=request_handler,
error_handler=error_handler,
request_timeout=request_timeout,
@@ -831,6 +864,8 @@ def serve_multiple(server_settings, workers):
server_settings["host"] = None
server_settings["port"] = None
processes = []
def sig_handler(signal, frame):
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
for process in processes:
@@ -839,8 +874,6 @@ def serve_multiple(server_settings, workers):
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
processes = []
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.daemon = True

View File

@@ -1,11 +1,22 @@
import asyncio
import types
import typing
from json import JSONDecodeError
from socket import socket
from urllib.parse import unquote, urlsplit
import httpcore
import requests_async as requests
import websockets
from sanic.asgi import ASGIApp
from sanic.exceptions import MethodNotSupported
from sanic.log import logger
from sanic.response import text
ASGI_HOST = "mockserver"
HOST = "127.0.0.1"
PORT = 42101
@@ -16,32 +27,41 @@ class SanicTestClient:
self.app = app
self.port = port
async def _local_request(self, method, url, cookies=None, *args, **kwargs):
import aiohttp
def get_new_session(self):
return requests.Session()
async def _local_request(self, method, url, *args, **kwargs):
logger.info(url)
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:
try:
response.text = await response.text()
except UnicodeDecodeError:
response.text = None
raw_cookies = kwargs.pop("raw_cookies", None)
if method == "websocket":
async with websockets.connect(url, *args, **kwargs) as websocket:
websocket.opened = websocket.open
return websocket
else:
async with self.get_new_session() as session:
try:
response.json = await response.json()
except (
JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError,
):
response = await getattr(session, method.lower())(
url, verify=False, *args, **kwargs
)
except NameError:
raise Exception(response.status_code)
try:
response.json = response.json()
except (JSONDecodeError, UnicodeDecodeError):
response.json = None
response.body = await response.read()
response.status = response.status_code
response.content_type = response.headers.get("content-type")
if raw_cookies:
response.raw_cookies = {}
for cookie in response.cookies:
response.raw_cookies[cookie.name] = cookie
return response
def _sanic_endpoint_test(
@@ -52,7 +72,7 @@ class SanicTestClient:
debug=False,
server_kwargs={"auto_reload": False},
*request_args,
**request_kwargs
**request_kwargs,
):
results = [None, None]
exceptions = []
@@ -83,11 +103,15 @@ class SanicTestClient:
server_kwargs = dict(sock=sock, **server_kwargs)
host, port = sock.getsockname()
if uri.startswith(("http:", "https:", "ftp:", "ftps://", "//")):
if uri.startswith(
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
):
url = uri
else:
url = "http://{host}:{port}{uri}".format(
host=host, port=port, uri=uri
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
scheme = "ws" if method == "websocket" else "http"
url = "{scheme}://{host}:{port}{uri}".format(
scheme=scheme, host=host, port=port, uri=uri
)
@self.app.listener("after_server_start")
@@ -112,7 +136,7 @@ class SanicTestClient:
try:
request, response = results
return request, response
except BaseException:
except BaseException: # noqa
raise ValueError(
"Request and response object expected, got ({})".format(
results
@@ -121,7 +145,7 @@ class SanicTestClient:
else:
try:
return results[-1]
except BaseException:
except BaseException: # noqa
raise ValueError(
"Request object expected, got ({})".format(results)
)
@@ -146,3 +170,254 @@ class SanicTestClient:
def head(self, *args, **kwargs):
return self._sanic_endpoint_test("head", *args, **kwargs)
def websocket(self, *args, **kwargs):
return self._sanic_endpoint_test("websocket", *args, **kwargs)
class SanicASGIAdapter(requests.asgi.ASGIAdapter): # noqa
async def send( # type: ignore
self,
request: requests.PreparedRequest,
gather_return: bool = False,
*args: typing.Any,
**kwargs: typing.Any,
) -> requests.Response:
"""This method is taken MOSTLY verbatim from requests-asyn. The
difference is the capturing of a response on the ASGI call and then
returning it on the response object. This is implemented to achieve:
request, response = await app.asgi_client.get("/")
You can see the original code here:
https://github.com/encode/requests-async/blob/614f40f77f19e6c6da8a212ae799107b0384dbf9/requests_async/asgi.py#L51""" # noqa
scheme, netloc, path, query, fragment = urlsplit(
request.url
) # type: ignore
default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme]
if ":" in netloc:
host, port_string = netloc.split(":", 1)
port = int(port_string)
else:
host = netloc
port = default_port
# Include the 'host' header.
if "host" in request.headers:
headers = [] # type: typing.List[typing.Tuple[bytes, bytes]]
elif port == default_port:
headers = [(b"host", host.encode())]
else:
headers = [(b"host", (f"{host}:{port}").encode())]
# Include other request headers.
headers += [
(key.lower().encode(), value.encode())
for key, value in request.headers.items()
]
no_response = False
if scheme in {"ws", "wss"}:
subprotocol = request.headers.get("sec-websocket-protocol", None)
if subprotocol is None:
subprotocols = [] # type: typing.Sequence[str]
else:
subprotocols = [
value.strip() for value in subprotocol.split(",")
]
scope = {
"type": "websocket",
"path": unquote(path),
"root_path": "",
"scheme": scheme,
"query_string": query.encode(),
"headers": headers,
"client": ["testclient", 50000],
"server": [host, port],
"subprotocols": subprotocols,
}
no_response = True
else:
scope = {
"type": "http",
"http_version": "1.1",
"method": request.method,
"path": unquote(path),
"root_path": "",
"scheme": scheme,
"query_string": query.encode(),
"headers": headers,
"client": ["testclient", 50000],
"server": [host, port],
"extensions": {"http.response.template": {}},
}
async def receive():
nonlocal request_complete, response_complete
if request_complete:
while not response_complete:
await asyncio.sleep(0.0001)
return {"type": "http.disconnect"}
body = request.body
if isinstance(body, str):
body_bytes = body.encode("utf-8") # type: bytes
elif body is None:
body_bytes = b""
elif isinstance(body, types.GeneratorType):
try:
chunk = body.send(None)
if isinstance(chunk, str):
chunk = chunk.encode("utf-8")
return {
"type": "http.request",
"body": chunk,
"more_body": True,
}
except StopIteration:
request_complete = True
return {"type": "http.request", "body": b""}
else:
body_bytes = body
request_complete = True
return {"type": "http.request", "body": body_bytes}
async def send(message) -> None:
nonlocal raw_kwargs, response_started, response_complete, template, context # noqa
if message["type"] == "http.response.start":
assert (
not response_started
), 'Received multiple "http.response.start" messages.'
raw_kwargs["status_code"] = message["status"]
raw_kwargs["headers"] = message["headers"]
response_started = True
elif message["type"] == "http.response.body":
assert response_started, (
'Received "http.response.body" '
'without "http.response.start".'
)
assert (
not response_complete
), 'Received "http.response.body" after response completed.'
body = message.get("body", b"")
more_body = message.get("more_body", False)
if request.method != "HEAD":
raw_kwargs["content"] += body
if not more_body:
response_complete = True
elif message["type"] == "http.response.template":
template = message["template"]
context = message["context"]
request_complete = False
response_started = False
response_complete = False
raw_kwargs = {"content": b""} # type: typing.Dict[str, typing.Any]
template = None
context = None
return_value = None
try:
return_value = await self.app(scope, receive, send)
except BaseException as exc:
if not self.suppress_exceptions:
raise exc from None
if no_response:
response_started = True
raw_kwargs = {"status_code": 204, "headers": []}
if not self.suppress_exceptions:
assert response_started, "TestClient did not receive any response."
elif not response_started:
raw_kwargs = {"status_code": 500, "headers": []}
raw = httpcore.Response(**raw_kwargs)
response = self.build_response(request, raw)
if template is not None:
response.template = template
response.context = context
if gather_return:
response.return_value = return_value
return response
class TestASGIApp(ASGIApp):
async def __call__(self):
await super().__call__()
return self.request
async def app_call_with_return(self, scope, receive, send):
asgi_app = await TestASGIApp.create(self, scope, receive, send)
return await asgi_app()
class SanicASGITestClient(requests.ASGISession):
def __init__(
self,
app,
base_url: str = "http://{}".format(ASGI_HOST),
suppress_exceptions: bool = False,
) -> None:
app.__class__.__call__ = app_call_with_return
app.asgi = True
super().__init__(app)
adapter = SanicASGIAdapter(
app, suppress_exceptions=suppress_exceptions
)
self.mount("http://", adapter)
self.mount("https://", adapter)
self.mount("ws://", adapter)
self.mount("wss://", adapter)
self.headers.update({"user-agent": "testclient"})
self.app = app
self.base_url = base_url
async def request(self, method, url, gather_request=True, *args, **kwargs):
self.gather_request = gather_request
response = await super().request(method, url, *args, **kwargs)
response.status = response.status_code
response.body = response.content
response.content_type = response.headers.get("content-type")
if hasattr(response, "return_value"):
request = response.return_value
del response.return_value
return request, response
return response
def merge_environment_settings(self, *args, **kwargs):
settings = super().merge_environment_settings(*args, **kwargs)
settings.update({"gather_return": self.gather_request})
return settings
async def websocket(self, uri, subprotocols=None, *args, **kwargs):
if uri.startswith(("ws:", "wss:")):
url = uri
else:
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
url = "ws://testserver{uri}".format(uri=uri)
headers = kwargs.get("headers", {})
headers.setdefault("connection", "upgrade")
headers.setdefault("sec-websocket-key", "testserver==")
headers.setdefault("sec-websocket-version", "13")
if subprotocols is not None:
headers.setdefault(
"sec-websocket-protocol", ", ".join(subprotocols)
)
kwargs["headers"] = headers
return await self.request("websocket", url, **kwargs)

View File

@@ -1,3 +1,5 @@
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
from httptools import HttpParserUpgrade
from websockets import ConnectionClosed # noqa
from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake
@@ -6,6 +8,9 @@ from sanic.exceptions import InvalidUsage
from sanic.server import HttpProtocol
ASIMessage = MutableMapping[str, Any]
class WebSocketProtocol(HttpProtocol):
def __init__(
self,
@@ -19,6 +24,7 @@ class WebSocketProtocol(HttpProtocol):
):
super().__init__(*args, **kwargs)
self.websocket = None
# self.app = None
self.websocket_timeout = websocket_timeout
self.websocket_max_size = websocket_max_size
self.websocket_max_queue = websocket_max_queue
@@ -103,3 +109,45 @@ class WebSocketProtocol(HttpProtocol):
self.websocket.connection_made(request.transport)
self.websocket.connection_open()
return self.websocket
class WebSocketConnection:
# TODO
# - Implement ping/pong
def __init__(
self,
send: Callable[[ASIMessage], Awaitable[None]],
receive: Callable[[], Awaitable[ASIMessage]],
) -> None:
self._send = send
self._receive = receive
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
message = {"type": "websocket.send"}
try:
data.decode()
except AttributeError:
message.update({"text": str(data)})
else:
message.update({"bytes": data})
await self._send(message)
async def recv(self, *args, **kwargs) -> Optional[str]:
message = await self._receive()
if message["type"] == "websocket.receive":
return message["text"]
elif message["type"] == "websocket.disconnect":
pass
receive = recv
async def accept(self) -> None:
await self._send({"type": "websocket.accept", "subprotocol": ""})
async def close(self) -> None:
pass

View File

@@ -14,7 +14,7 @@ multi_line_output = 3
not_skip = __init__.py
[version]
current_version = 0.8.3
current_version = 19.3.1
file = sanic/__init__.py
current_version_pattern = __version__ = "{current_version}"
new_version_pattern = __version__ = "{new_version}"

View File

@@ -50,12 +50,12 @@ with open_local(["README.rst"]) as rm:
setup_kwargs = {
"name": "sanic",
"version": version,
"url": "http://github.com/channelcat/sanic/",
"url": "http://github.com/huge-success/sanic/",
"license": "MIT",
"author": "Channel Cat",
"author_email": "channelcat@gmail.com",
"author": "Sanic Community",
"author_email": "admhpkns@gmail.com",
"description": (
"A microframework based on uvloop, httptools, and learnings of flask"
"A web server and web framework that's written to go fast. Build fast. Run fast."
),
"long_description": long_description,
"packages": ["sanic"],
@@ -64,7 +64,6 @@ setup_kwargs = {
"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",
],
@@ -83,6 +82,7 @@ requirements = [
"aiofiles>=0.3.0",
"websockets>=6.0,<7.0",
"multidict>=4.0,<5.0",
"requests-async==0.5.0",
]
tests_require = [
@@ -90,12 +90,13 @@ tests_require = [
"multidict>=4.0,<5.0",
"gunicorn",
"pytest-cov",
"aiohttp>=2.3.0,<=3.2.1",
"httpcore==0.3.0",
"beautifulsoup4",
uvloop,
ujson,
"pytest-sanic",
"pytest-sugar",
"pytest-benchmark",
]
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
@@ -111,14 +112,14 @@ if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
extras_require = {
"test": tests_require,
"dev": tests_require + ["aiofiles", "tox", "black", "flake8"],
"dev": tests_require + ["aiofiles", "tox", "black", "flake8", "bandit"],
"docs": [
"sphinx",
"sphinx_rtd_theme",
"recommonmark",
"sphinxcontrib-asyncio",
"docutils",
"pygments"
"pygments",
],
}

View File

@@ -1,8 +1,10 @@
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

View File

@@ -9,6 +9,7 @@ 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"]:

View File

@@ -1,10 +1,13 @@
# Run with python3 simple_server.py PORT
from aiohttp import web
import asyncio
import sys
import uvloop
import ujson as json
import uvloop
from aiohttp import web
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)

View File

@@ -1,8 +1,9 @@
# Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker -b :8000 simple_server:app
import bottle
from bottle import route, run
import ujson
from bottle import route, run
@route("/")
def index():

View File

@@ -1,10 +1,13 @@
# Run with: python3 -O simple_server.py
import asyncio
from kyoukai import Kyoukai, HTTPRequestContext
import logging
import ujson
import uvloop
from kyoukai import HTTPRequestContext, Kyoukai
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)

View File

@@ -1,16 +1,18 @@
import asyncpg
import sys
import os
import inspect
import os
import sys
import timeit
import asyncpg
from sanic.response import json
currentdir = os.path.dirname(
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
sys.path.insert(0, currentdir + "/../../../")
import timeit
from sanic.response import json
print(json({"test": True}).output())

View File

@@ -1,14 +1,16 @@
import sys
import os
import inspect
import os
import sys
from sanic import Sanic
from sanic.response import json
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
app = Sanic("test")

View File

@@ -1,15 +1,17 @@
import sys
import os
import inspect
import os
import sys
from sanic import Sanic
from sanic.exceptions import ServerError
from sanic.response import json, text
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
from sanic.exceptions import ServerError
app = Sanic("test")
@@ -56,8 +58,6 @@ def query_string(request):
)
import sys
app.run(host="0.0.0.0", port=sys.argv[1])

View File

@@ -1,5 +1,6 @@
# Run with: python simple_server.py
import ujson
from tornado import ioloop, web

View File

@@ -2,15 +2,16 @@
""" Minimal helloworld application.
"""
from wheezy.http import HTTPResponse
from wheezy.http import WSGIApplication
import ujson
from wheezy.http import HTTPResponse, WSGIApplication
from wheezy.http.response import json_response
from wheezy.routing import url
from wheezy.web.handlers import BaseHandler
from wheezy.web.middleware import bootstrap_defaults
from wheezy.web.middleware import path_routing_middleware_factory
import ujson
from wheezy.web.middleware import (
bootstrap_defaults,
path_routing_middleware_factory,
)
class WelcomeHandler(BaseHandler):

View File

@@ -3,6 +3,7 @@ import logging
import sys
from inspect import isawaitable
import pytest
from sanic.exceptions import SanicException
@@ -11,7 +12,7 @@ from sanic.response import text
def uvloop_installed():
try:
import uvloop
import uvloop # noqa
return True
except ImportError:
@@ -56,7 +57,7 @@ def test_asyncio_server_start_serving(app):
def test_app_loop_not_running(app):
with pytest.raises(SanicException) as excinfo:
_ = app.loop
app.loop
assert str(excinfo.value) == (
"Loop can only be retrieved after the app has started "

231
tests/test_asgi.py Normal file
View File

@@ -0,0 +1,231 @@
import asyncio
from collections import deque
import pytest
import uvicorn
from sanic import Sanic
from sanic.asgi import MockTransport
from sanic.exceptions import InvalidUsage
from sanic.request import Request
from sanic.response import text
from sanic.websocket import WebSocketConnection
@pytest.fixture
def message_stack():
return deque()
@pytest.fixture
def receive(message_stack):
async def _receive():
return message_stack.popleft()
return _receive
@pytest.fixture
def send(message_stack):
async def _send(message):
message_stack.append(message)
return _send
@pytest.fixture
def transport(message_stack, receive, send):
return MockTransport({}, receive, send)
@pytest.fixture
# @pytest.mark.asyncio
def protocol(transport, loop):
return transport.get_protocol()
def test_listeners_triggered(app):
before_server_start = False
after_server_start = False
before_server_stop = False
after_server_stop = False
@app.listener("before_server_start")
def do_before_server_start(*args, **kwargs):
nonlocal before_server_start
before_server_start = True
@app.listener("after_server_start")
def do_after_server_start(*args, **kwargs):
nonlocal after_server_start
after_server_start = True
@app.listener("before_server_stop")
def do_before_server_stop(*args, **kwargs):
nonlocal before_server_stop
before_server_stop = True
@app.listener("after_server_stop")
def do_after_server_stop(*args, **kwargs):
nonlocal after_server_stop
after_server_stop = True
class CustomServer(uvicorn.Server):
def install_signal_handlers(self):
pass
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
server = CustomServer(config=config)
with pytest.warns(UserWarning):
server.run()
for task in asyncio.Task.all_tasks():
task.cancel()
assert before_server_start
assert after_server_start
assert before_server_stop
assert after_server_stop
def test_listeners_triggered_async(app):
before_server_start = False
after_server_start = False
before_server_stop = False
after_server_stop = False
@app.listener("before_server_start")
async def do_before_server_start(*args, **kwargs):
nonlocal before_server_start
before_server_start = True
@app.listener("after_server_start")
async def do_after_server_start(*args, **kwargs):
nonlocal after_server_start
after_server_start = True
@app.listener("before_server_stop")
async def do_before_server_stop(*args, **kwargs):
nonlocal before_server_stop
before_server_stop = True
@app.listener("after_server_stop")
async def do_after_server_stop(*args, **kwargs):
nonlocal after_server_stop
after_server_stop = True
class CustomServer(uvicorn.Server):
def install_signal_handlers(self):
pass
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
server = CustomServer(config=config)
with pytest.warns(UserWarning):
server.run()
for task in asyncio.Task.all_tasks():
task.cancel()
assert before_server_start
assert after_server_start
assert before_server_stop
assert after_server_stop
@pytest.mark.asyncio
async def test_mockprotocol_events(protocol):
assert protocol._not_paused.is_set()
protocol.pause_writing()
assert not protocol._not_paused.is_set()
protocol.resume_writing()
assert protocol._not_paused.is_set()
@pytest.mark.asyncio
async def test_protocol_push_data(protocol, message_stack):
text = b"hello"
await protocol.push_data(text)
await protocol.complete()
assert len(message_stack) == 2
message = message_stack.popleft()
assert message["type"] == "http.response.body"
assert message["more_body"]
assert message["body"] == text
message = message_stack.popleft()
assert message["type"] == "http.response.body"
assert not message["more_body"]
assert message["body"] == b""
@pytest.mark.asyncio
async def test_websocket_send(send, receive, message_stack):
text_string = "hello"
text_bytes = b"hello"
ws = WebSocketConnection(send, receive)
await ws.send(text_string)
await ws.send(text_bytes)
assert len(message_stack) == 2
message = message_stack.popleft()
assert message["type"] == "websocket.send"
assert message["text"] == text_string
assert "bytes" not in message
message = message_stack.popleft()
assert message["type"] == "websocket.send"
assert message["bytes"] == text_bytes
assert "text" not in message
@pytest.mark.asyncio
async def test_websocket_receive(send, receive, message_stack):
msg = {"text": "hello", "type": "websocket.receive"}
message_stack.append(msg)
ws = WebSocketConnection(send, receive)
text = await ws.receive()
assert text == msg["text"]
def test_improper_websocket_connection(transport, send, receive):
with pytest.raises(InvalidUsage):
transport.get_websocket_connection()
transport.create_websocket_connection(send, receive)
connection = transport.get_websocket_connection()
assert isinstance(connection, WebSocketConnection)
@pytest.mark.asyncio
async def test_request_class_regular(app):
@app.get("/regular")
def regular_request(request):
return text(request.__class__.__name__)
_, response = await app.asgi_client.get("/regular")
assert response.body == b"Request"
@pytest.mark.asyncio
async def test_request_class_custom():
class MyCustomRequest(Request):
pass
app = Sanic(request_class=MyCustomRequest)
@app.get("/custom")
def custom_request(request):
return text(request.__class__.__name__)
_, response = await app.asgi_client.get("/custom")
assert response.body == b"MyCustomRequest"

View File

@@ -0,0 +1,5 @@
from sanic.testing import SanicASGITestClient
def test_asgi_client_instantiation(app):
assert isinstance(app.asgi_client, SanicASGITestClient)

View File

@@ -3,7 +3,8 @@ 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
from sanic.response import HTTPResponse, text
MIDDLEWARE_INVOKE_COUNTER = {"request": 0, "response": 0}

View File

@@ -7,9 +7,9 @@ import pytest
from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.constants import HTTP_METHODS
from sanic.exceptions import NotFound, ServerError, InvalidUsage
from sanic.exceptions import InvalidUsage, NotFound, ServerError
from sanic.request import Request
from sanic.response import text, json
from sanic.response import json, text
from sanic.views import CompositionView
@@ -467,16 +467,8 @@ def test_bp_shorthand(app):
request, response = app.test_client.get("/delete")
assert response.status == 405
request, response = app.test_client.get(
"/ws/",
headers={
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
},
)
assert response.status == 101
request, response = app.test_client.websocket("/ws/")
assert response.opened is True
assert ev.is_set()
@@ -595,14 +587,13 @@ def test_blueprint_middleware_with_args(app: Sanic):
"/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()
file.read()
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
@@ -662,16 +653,8 @@ def test_websocket_route(app: Sanic):
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
_, response = app.test_client.websocket("/ws/test")
assert response.opened is True
assert event.is_set()
@@ -704,3 +687,49 @@ def test_register_blueprint(app, debug):
"version 1.0. Please use the blueprint method"
" instead"
)
def test_strict_slashes_behavior_adoption(app):
app.strict_slashes = True
@app.get("/test")
def handler_test(request):
return text("Test")
assert app.test_client.get("/test")[1].status == 200
assert app.test_client.get("/test/")[1].status == 404
bp = Blueprint("bp")
@bp.get("/one", strict_slashes=False)
def one(request):
return text("one")
@bp.get("/second")
def second(request):
return text("second")
app.blueprint(bp)
assert app.test_client.get("/one")[1].status == 200
assert app.test_client.get("/one/")[1].status == 200
assert app.test_client.get("/second")[1].status == 200
assert app.test_client.get("/second/")[1].status == 404
bp2 = Blueprint("bp2", strict_slashes=False)
@bp2.get("/third")
def third(request):
return text("third")
app.blueprint(bp2)
assert app.test_client.get("/third")[1].status == 200
assert app.test_client.get("/third/")[1].status == 200
@app.get("/f1", strict_slashes=False)
def f1(request):
return text("f1")
assert app.test_client.get("/f1")[1].status == 200
assert app.test_client.get("/f1/")[1].status == 200

View File

@@ -1,12 +1,13 @@
from contextlib import contextmanager
from os import environ
from pathlib import Path
from contextlib import contextmanager
from tempfile import TemporaryDirectory
from textwrap import dedent
import pytest
from sanic import Sanic
from sanic.config import Config, DEFAULT_CONFIG
from sanic.config import DEFAULT_CONFIG, Config
from sanic.exceptions import PyFileError
@@ -17,17 +18,30 @@ def temp_path():
yield Path(td, "file")
def test_load_from_object(app):
class Config:
not_for_config = "should not be used"
CONFIG_VALUE = "should be used"
class ConfigTest:
not_for_config = "should not be used"
CONFIG_VALUE = "should be used"
app.config.from_object(Config)
def test_load_from_object(app):
app.config.from_object(ConfigTest)
assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config
def test_load_from_object_string(app):
app.config.from_object("test_config.ConfigTest")
assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config
def test_load_from_object_string_exception(app):
with pytest.raises(ImportError):
app.config.from_object("test_config.Config.test")
def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic()
@@ -225,6 +239,7 @@ def test_config_access_log_passing_in_run(app):
assert app.config.ACCESS_LOG == True
@pytest.mark.asyncio
async def test_config_access_log_passing_in_create_server(app):
assert app.config.ACCESS_LOG == True

View File

@@ -1,8 +1,11 @@
from datetime import datetime, timedelta
from http.cookies import SimpleCookie
from sanic.response import text
import pytest
from sanic.cookies import Cookie, DEFAULT_MAX_AGE
from sanic.cookies import Cookie
from sanic.response import text
# ------------------------------------------------------------ #
# GET
@@ -24,6 +27,24 @@ def test_cookies(app):
assert response_cookies["right_back"].value == "at you"
@pytest.mark.asyncio
async def test_cookies_asgi(app):
@app.route("/")
def handler(request):
response = text("Cookies are: {}".format(request.cookies["test"]))
response.cookies["right_back"] = "at you"
return response
request, response = await app.asgi_client.get(
"/", cookies={"test": "working!"}
)
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get("set-cookie", {}))
assert response.text == "Cookies are: working!"
assert response_cookies["right_back"].value == "at you"
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
def test_false_cookies_encoded(app, httponly, expected):
@app.route("/")
@@ -100,7 +121,7 @@ def test_cookie_deletion(app):
assert int(response_cookies["i_want_to_die"]["max-age"]) == 0
with pytest.raises(KeyError):
_ = response.cookies["i_never_existed"]
response.cookies["i_never_existed"]
def test_cookie_reserved_cookie():
@@ -135,7 +156,7 @@ def test_cookie_set_same_key(app):
request, response = app.test_client.get("/", cookies=cookies)
assert response.status == 200
assert response.cookies["test"].value == "pass"
assert response.cookies["test"] == "pass"
@pytest.mark.parametrize("max_age", ["0", 30, 30.0, 30.1, "30", "test"])
@@ -149,19 +170,42 @@ def test_cookie_max_age(app, max_age):
response.cookies["test"]["max-age"] = max_age
return response
request, response = app.test_client.get("/", cookies=cookies)
request, response = app.test_client.get(
"/", cookies=cookies, raw_cookies=True
)
assert response.status == 200
assert response.cookies["test"].value == "pass"
cookie = response.cookies.get("test")
if (
str(max_age).isdigit()
and int(max_age) == float(max_age)
and int(max_age) != 0
):
cookie_expires = datetime.utcfromtimestamp(
response.raw_cookies["test"].expires
).replace(microsecond=0)
if str(max_age).isdigit() and int(max_age) == float(max_age):
assert response.cookies["test"]["max-age"] == str(max_age)
# Grabbing utcnow after the response may lead to it being off slightly.
# Therefore, we 0 out the microseconds, and accept the test if there
# is a 1 second difference.
expires = datetime.utcnow().replace(microsecond=0) + timedelta(
seconds=int(max_age)
)
assert cookie == "pass"
assert (
cookie_expires == expires
or cookie_expires == expires + timedelta(seconds=-1)
)
else:
assert response.cookies["test"]["max-age"] == str(DEFAULT_MAX_AGE)
assert cookie is None
@pytest.mark.parametrize("expires", [datetime.now() + timedelta(seconds=60)])
@pytest.mark.parametrize(
"expires", [datetime.utcnow() + timedelta(seconds=60)]
)
def test_cookie_expires(app, expires):
expires = expires.replace(microsecond=0)
cookies = {"test": "wait"}
@app.get("/")
@@ -171,15 +215,16 @@ def test_cookie_expires(app, expires):
response.cookies["test"]["expires"] = expires
return response
request, response = app.test_client.get("/", cookies=cookies)
request, response = app.test_client.get(
"/", cookies=cookies, raw_cookies=True
)
cookie_expires = datetime.utcfromtimestamp(
response.raw_cookies["test"].expires
).replace(microsecond=0)
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
assert response.cookies["test"] == "pass"
assert cookie_expires == expires
@pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"])

View File

@@ -1,7 +1,9 @@
from sanic.response import text
from threading import Event
import asyncio
from queue import Queue
from threading import Event
from sanic.response import text
def test_create_task(app):

View File

@@ -1,5 +1,5 @@
from sanic.server import HttpProtocol
from sanic.response import text
from sanic.server import HttpProtocol
class CustomHttpProtocol(HttpProtocol):

View File

@@ -1,6 +1,7 @@
import pytest
from sanic.response import text
from sanic.router import RouteExists
import pytest
@pytest.mark.parametrize(

View File

@@ -1,10 +1,17 @@
import pytest
from bs4 import BeautifulSoup
from sanic import Sanic
from sanic.exceptions import (
Forbidden,
InvalidUsage,
NotFound,
ServerError,
Unauthorized,
abort,
)
from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized
from sanic.exceptions import Forbidden, abort
class SanicExceptionTestException(Exception):

View File

@@ -1,9 +1,11 @@
from sanic import Sanic
from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.handlers import ErrorHandler
from bs4 import BeautifulSoup
from sanic import Sanic
from sanic.exceptions import InvalidUsage, NotFound, ServerError
from sanic.handlers import ErrorHandler
from sanic.response import text
exception_handler_app = Sanic("test_exception_handler")

View File

@@ -1,4 +1,9 @@
import inspect
import pytest
from sanic import helpers
from sanic.config import Config
def test_has_message_body():
@@ -56,3 +61,18 @@ def test_remove_entity_headers():
for header, expected in tests:
assert helpers.remove_entity_headers(header) == expected
def test_import_string_class():
obj = helpers.import_string("sanic.config.Config")
assert isinstance(obj, Config)
def test_import_string_module():
module = helpers.import_string("sanic.config")
assert inspect.ismodule(module)
def test_import_string_exception():
with pytest.raises(ImportError):
helpers.import_string("test.test.test")

View File

@@ -1,39 +1,72 @@
from json import JSONDecodeError
from sanic import Sanic
import asyncio
import functools
import socket
from asyncio import sleep as aio_sleep
from http.client import _encode
from json import JSONDecodeError
import httpcore
import requests_async as requests
from httpcore import PoolTimeout
from sanic import Sanic, server
from sanic.response import text
from sanic import server
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
from sanic.testing import HOST, PORT, SanicTestClient
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
old_conn = None
class ReuseableTCPConnector(TCPConnector):
def __init__(self, *args, **kwargs):
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
self.old_proto = None
async def connect(self, req, *args, **kwargs):
new_conn = await super(ReuseableTCPConnector, self).connect(
req, *args, **kwargs
class ReusableSanicConnectionPool(httpcore.ConnectionPool):
async def acquire_connection(self, origin):
global old_conn
connection = self.active_connections.pop_by_origin(
origin, http2_only=True
)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
if connection is None:
connection = self.keepalive_connections.pop_by_origin(origin)
if connection is None:
await self.max_connections.acquire()
connection = httpcore.HTTPConnection(
origin,
ssl=self.ssl,
timeout=self.timeout,
backend=self.backend,
release_func=self.release_connection,
)
self.active_connections.add(connection)
if old_conn is not None:
if old_conn != connection:
raise RuntimeError(
"We got a new connection, wanted the same one!"
)
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
old_conn = connection
return connection
class ReusableSanicAdapter(requests.adapters.HTTPAdapter):
def __init__(self):
self.pool = ReusableSanicConnectionPool()
class ResusableSanicSession(requests.Session):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
adapter = ReusableSanicAdapter()
self.mount("http://", adapter)
self.mount("https://", adapter)
class ReuseableSanicTestClient(SanicTestClient):
def __init__(self, app, loop=None):
super(ReuseableSanicTestClient, self).__init__(app)
super().__init__(app)
if loop is None:
loop = asyncio.get_event_loop()
self._loop = loop
@@ -49,14 +82,14 @@ class ReuseableSanicTestClient(SanicTestClient):
uri="/",
gather_request=True,
debug=False,
server_kwargs={"return_asyncio_server": True},
server_kwargs=None,
*request_args,
**request_kwargs
**request_kwargs,
):
loop = self._loop
results = [None, None]
exceptions = []
do_kill_server = request_kwargs.pop("end_server", False)
server_kwargs = server_kwargs or {"return_asyncio_server": True}
if gather_request:
def _collect_request(request):
@@ -65,21 +98,26 @@ class ReuseableSanicTestClient(SanicTestClient):
self.app.request_middleware.appendleft(_collect_request)
if uri.startswith(
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
):
url = uri
else:
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
scheme = "http"
url = "{scheme}://{host}:{port}{uri}".format(
scheme=scheme, host=HOST, port=PORT, uri=uri
)
@self.app.listener("after_server_start")
async def _collect_response(loop):
try:
if do_kill_server:
request_kwargs["end_session"] = True
response = await self._local_request(
method, uri, *request_args, **request_kwargs
method, url, *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()
if self._server is not None:
_server = self._server
@@ -94,27 +132,13 @@ class ReuseableSanicTestClient(SanicTestClient):
try:
loop._stopping = False
http_server = loop.run_until_complete(_server_co)
_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
self._server = _server
server.trigger_events(self.app.listeners["after_server_start"], loop)
self.app.listeners["after_server_start"].pop()
if do_kill_server:
try:
_server.close()
self._server = None
loop.run_until_complete(_server.wait_closed())
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))
@@ -137,59 +161,53 @@ class ReuseableSanicTestClient(SanicTestClient):
"Request object expected, got ({})".format(results)
)
def kill_server(self):
try:
if self._server:
self._server.close()
self._loop.run_until_complete(self._server.wait_closed())
self._server = None
self.app.stop()
if self._session:
self._loop.run_until_complete(self._session.close())
self._session = None
except Exception as e3:
raise e3
# 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):
async def _local_request(self, method, url, *args, **kwargs):
raw_cookies = kwargs.pop("raw_cookies", None)
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
if not self._session:
self._session = ResusableSanicSession()
try:
response = await getattr(self._session, method.lower())(
url, verify=False, timeout=request_keepalive, *args, **kwargs
)
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(
ssl=False,
loop=self._loop,
keepalive_timeout=request_keepalive,
)
self._tcp_connector = conn
session = aiohttp.ClientSession(
cookies=cookies, connector=conn, loop=self._loop
)
self._session = session
except NameError:
raise Exception(response.status_code)
async with getattr(session, method.lower())(
url, *args, **kwargs
) as response:
try:
response.text = await response.text()
except UnicodeDecodeError:
response.text = None
try:
response.json = response.json()
except (JSONDecodeError, UnicodeDecodeError):
response.json = None
try:
response.json = await response.json()
except (
JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError,
):
response.json = None
response.body = await response.read()
response.status = response.status_code
response.content_type = response.headers.get("content-type")
if raw_cookies:
response.raw_cookies = {}
for cookie in response.cookies:
response.raw_cookies[cookie.name] = cookie
response.body = await response.read()
if do_kill_session:
await session.close()
self._session = None
return response
@@ -221,40 +239,46 @@ def test_keep_alive_timeout_reuse():
"""If the server keep-alive timeout and client keep-alive timeout are
both longer than the delay, the client _and_ server will successfully
reuse the existing connection."""
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)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(1))
request, response = client.get("/1", end_server=True)
assert response.status == 200
assert response.text == "OK"
try:
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)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(1))
request, response = client.get("/1")
assert response.status == 200
assert response.text == "OK"
finally:
client.kill_server()
def test_keep_alive_client_timeout():
"""If the server keep-alive timeout is longer than the client
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)
assert response.status == 200
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
)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert "got a new connection" in exception.args[0]
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
headers = {"Connection": "keep-alive"}
try:
request, response = client.get(
"/1", headers=headers, request_keepalive=1
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(2))
exception = None
request, response = client.get("/1", request_keepalive=1)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert "got a new connection" in exception.args[0]
finally:
client.kill_server()
def test_keep_alive_server_timeout():
@@ -262,24 +286,27 @@ def test_keep_alive_server_timeout():
keep-alive timeout, the client will either a 'Connection reset' error
_or_ a new connection. Depending on how the event-loop handles the
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)
assert response.status == 200
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
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
headers = {"Connection": "keep-alive"}
try:
request, response = client.get(
"/1", headers=headers, request_keepalive=60
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(3))
exception = None
request, response = client.get("/1", request_keepalive=60)
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]
)
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]
)
finally:
client.kill_server()

View File

@@ -1,17 +1,17 @@
import uuid
import logging
import uuid
from io import StringIO
from importlib import reload
import pytest
from io import StringIO
from unittest.mock import Mock
import pytest
import sanic
from sanic.response import text
from sanic.log import LOGGING_CONFIG_DEFAULTS
from sanic import Sanic
from sanic.log import logger
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
from sanic.response import text
logging_format = """module: %(module)s; \

View File

@@ -1,7 +1,9 @@
import logging
import asyncio
import logging
from sanic.config import BASE_LOGO
from sanic.testing import PORT
try:
import uvloop # noqa
@@ -12,7 +14,9 @@ except BaseException:
def test_logo_base(app, caplog):
server = app.create_server(debug=True, return_asyncio_server=True)
server = app.create_server(
debug=True, return_asyncio_server=True, port=PORT
)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop._stopping = False
@@ -31,7 +35,9 @@ def test_logo_base(app, caplog):
def test_logo_false(app, caplog):
app.config.LOGO = False
server = app.create_server(debug=True, return_asyncio_server=True)
server = app.create_server(
debug=True, return_asyncio_server=True, port=PORT
)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop._stopping = False
@@ -44,13 +50,17 @@ def test_logo_false(app, caplog):
app.stop()
assert caplog.record_tuples[ROW][1] == logging.INFO
assert caplog.record_tuples[ROW][2] == "Goin' Fast @ http://127.0.0.1:8000"
assert caplog.record_tuples[ROW][
2
] == "Goin' Fast @ http://127.0.0.1:{}".format(PORT)
def test_logo_true(app, caplog):
app.config.LOGO = True
server = app.create_server(debug=True, return_asyncio_server=True)
server = app.create_server(
debug=True, return_asyncio_server=True, port=PORT
)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop._stopping = False
@@ -69,7 +79,9 @@ def test_logo_true(app, caplog):
def test_logo_custom(app, caplog):
app.config.LOGO = "My Custom Logo"
server = app.create_server(debug=True, return_asyncio_server=True)
server = app.create_server(
debug=True, return_asyncio_server=True, port=PORT
)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop._stopping = False

View File

@@ -1,10 +1,12 @@
import logging
from asyncio import CancelledError
from sanic.exceptions import NotFound
from sanic.request import Request
from sanic.response import HTTPResponse, text
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #

View File

@@ -1,11 +1,12 @@
import multiprocessing
import pickle
import random
import signal
import pickle
import pytest
from sanic.testing import HOST, PORT
from sanic.response import text
from sanic.testing import HOST, PORT
@pytest.mark.skipif(

View File

@@ -2,12 +2,13 @@
# -*- coding: utf-8 -*-
import asyncio
import pytest
from sanic.blueprints import Blueprint
from sanic.response import text
from sanic.exceptions import URLBuildError
from sanic.constants import HTTP_METHODS
from sanic.exceptions import URLBuildError
from sanic.response import text
# ------------------------------------------------------------ #

View File

@@ -1,7 +1,8 @@
import pytest
from urllib.parse import quote
from sanic.response import text, redirect
import pytest
from sanic.response import redirect, text
@pytest.fixture
@@ -109,21 +110,19 @@ def test_redirect_with_header_injection(redirect_app):
@pytest.mark.parametrize("test_str", ["sanic-test", "sanictest", "sanic test"])
async def test_redirect_with_params(app, test_client, test_str):
def test_redirect_with_params(app, test_str):
use_in_uri = quote(test_str)
@app.route("/api/v1/test/<test>/")
async def init_handler(request, test):
assert test == test_str
return redirect("/api/v2/test/{}/".format(quote(test)))
return redirect("/api/v2/test/{}/".format(use_in_uri))
@app.route("/api/v2/test/<test>/")
async def target_handler(request, test):
assert test == test_str
return text("OK")
test_cli = await test_client(app)
response = await test_cli.get("/api/v1/test/{}/".format(quote(test_str)))
_, response = app.test_client.get("/api/v1/test/{}/".format(use_in_uri))
assert response.status == 200
txt = await response.text()
assert txt == "OK"
assert response.content == b"OK"

View File

@@ -1,10 +1,13 @@
import asyncio
import contextlib
from sanic.response import text, stream
import pytest
from sanic.response import stream, text
async def test_request_cancel_when_connection_lost(loop, app, test_client):
@pytest.mark.asyncio
async def test_request_cancel_when_connection_lost(app):
app.still_serving_cancelled_request = False
@app.get("/")
@@ -14,10 +17,9 @@ async def test_request_cancel_when_connection_lost(loop, app, test_client):
app.still_serving_cancelled_request = True
return text("OK")
test_cli = await test_client(app)
# schedule client call
task = loop.create_task(test_cli.get("/"))
loop = asyncio.get_event_loop()
task = loop.create_task(app.asgi_client.get("/"))
loop.call_later(0.01, task)
await asyncio.sleep(0.5)
@@ -33,7 +35,8 @@ async def test_request_cancel_when_connection_lost(loop, app, test_client):
assert app.still_serving_cancelled_request is False
async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
@pytest.mark.asyncio
async def test_stream_request_cancel_when_conn_lost(app):
app.still_serving_cancelled_request = False
@app.post("/post/<id>", stream=True)
@@ -53,10 +56,9 @@ async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
return stream(streaming)
test_cli = await test_client(app)
# schedule client call
task = loop.create_task(test_cli.post("/post/1"))
loop = asyncio.get_event_loop()
task = loop.create_task(app.asgi_client.post("/post/1"))
loop.call_later(0.01, task)
await asyncio.sleep(0.5)

View File

@@ -2,6 +2,7 @@ import random
from sanic.response import json
try:
from ujson import loads
except ImportError:

View File

@@ -1,10 +1,11 @@
import asyncio
import pytest
from sanic.blueprints import Blueprint
from sanic.views import CompositionView
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
from sanic.response import stream, text
from sanic.exceptions import HeaderExpectationFailed
from sanic.request import StreamBuffer
from sanic.response import stream, text
from sanic.views import CompositionView, HTTPMethodView
from sanic.views import stream as stream_decorator
data = "abc" * 10000000
@@ -42,6 +43,46 @@ def test_request_stream_method_view(app):
assert response.text == data
@pytest.mark.parametrize(
"headers, expect_raise_exception",
[
({"EXPECT": "100-continue"}, False),
({"EXPECT": "100-continue-extra"}, True),
],
)
def test_request_stream_100_continue(app, headers, expect_raise_exception):
class SimpleView(HTTPMethodView):
@stream_decorator
async def post(self, request):
assert isinstance(request.stream, StreamBuffer)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
app.add_route(SimpleView.as_view(), "/method_view")
assert app.is_request_stream is True
if not expect_raise_exception:
request, response = app.test_client.post(
"/method_view", data=data, headers={"EXPECT": "100-continue"}
)
assert response.status == 200
assert response.text == data
else:
with pytest.raises(ValueError) as e:
app.test_client.post(
"/method_view",
data=data,
headers={"EXPECT": "100-continue-extra"},
)
assert "Unknown Expect: 100-continue-extra" in str(e)
def test_request_stream_app(app):
"""for self.is_request_stream = True and decorators"""
@@ -73,15 +114,13 @@ def test_request_stream_app(app):
@app.post("/post/<id>", stream=True)
async def post(request, id):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
@app.put("/_put")
async def _put(request):
@@ -91,15 +130,13 @@ def test_request_stream_app(app):
@app.put("/put", stream=True)
async def put(request):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
@app.patch("/_patch")
async def _patch(request):
@@ -109,15 +146,13 @@ def test_request_stream_app(app):
@app.patch("/patch", stream=True)
async def patch(request):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
assert app.is_request_stream is True
@@ -162,21 +197,134 @@ def test_request_stream_app(app):
assert response.text == data
@pytest.mark.asyncio
async def test_request_stream_app_asgi(app):
"""for self.is_request_stream = True and decorators"""
@app.get("/get")
async def get(request):
assert request.stream is None
return text("GET")
@app.head("/head")
async def head(request):
assert request.stream is None
return text("HEAD")
@app.delete("/delete")
async def delete(request):
assert request.stream is None
return text("DELETE")
@app.options("/options")
async def options(request):
assert request.stream is None
return text("OPTIONS")
@app.post("/_post/<id>")
async def _post(request, id):
assert request.stream is None
return text("_POST")
@app.post("/post/<id>", stream=True)
async def post(request, id):
assert isinstance(request.stream, StreamBuffer)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
@app.put("/_put")
async def _put(request):
assert request.stream is None
return text("_PUT")
@app.put("/put", stream=True)
async def put(request):
assert isinstance(request.stream, StreamBuffer)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
@app.patch("/_patch")
async def _patch(request):
assert request.stream is None
return text("_PATCH")
@app.patch("/patch", stream=True)
async def patch(request):
assert isinstance(request.stream, StreamBuffer)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
assert app.is_request_stream is True
request, response = await app.asgi_client.get("/get")
assert response.status == 200
assert response.text == "GET"
request, response = await app.asgi_client.head("/head")
assert response.status == 200
assert response.text == ""
request, response = await app.asgi_client.delete("/delete")
assert response.status == 200
assert response.text == "DELETE"
request, response = await app.asgi_client.options("/options")
assert response.status == 200
assert response.text == "OPTIONS"
request, response = await app.asgi_client.post("/_post/1", data=data)
assert response.status == 200
assert response.text == "_POST"
request, response = await app.asgi_client.post("/post/1", data=data)
assert response.status == 200
assert response.text == data
request, response = await app.asgi_client.put("/_put", data=data)
assert response.status == 200
assert response.text == "_PUT"
request, response = await app.asgi_client.put("/put", data=data)
assert response.status == 200
assert response.text == data
request, response = await app.asgi_client.patch("/_patch", data=data)
assert response.status == 200
assert response.text == "_PATCH"
request, response = await app.asgi_client.patch("/patch", data=data)
assert response.status == 200
assert response.text == data
def test_request_stream_handle_exception(app):
"""for handling exceptions properly"""
@app.post("/post/<id>", stream=True)
async def post(request, id):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
# 404
request, response = app.test_client.post("/in_valid_post", data=data)
@@ -224,15 +372,13 @@ def test_request_stream_blueprint(app):
@bp.post("/post/<id>", stream=True)
async def post(request, id):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
@bp.put("/_put")
async def _put(request):
@@ -242,15 +388,13 @@ def test_request_stream_blueprint(app):
@bp.put("/put", stream=True)
async def put(request):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
@bp.patch("/_patch")
async def _patch(request):
@@ -260,27 +404,23 @@ def test_request_stream_blueprint(app):
@bp.patch("/patch", stream=True)
async def patch(request):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
async def post_add_route(request):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
bp.add_route(
post_add_route, "/post/add_route", methods=["POST"], stream=True
@@ -390,15 +530,13 @@ def test_request_stream(app):
@app.post("/stream", stream=True)
async def handler(request):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
result = ""
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode("utf-8")
return text(result)
@app.get("/get")
async def get(request):

View File

@@ -1,183 +1,59 @@
from json import JSONDecodeError
import asyncio
import httpcore
import requests_async as requests
from sanic import Sanic
import asyncio
from sanic.response import text
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST
from sanic.testing import SanicTestClient
try:
try:
# direct use
import packaging
version = packaging.version
except (ImportError, AttributeError):
# setuptools v39.0 and above.
class DelayableSanicConnectionPool(httpcore.ConnectionPool):
def __init__(self, request_delay=None, *args, **kwargs):
self._request_delay = request_delay
super().__init__(*args, **kwargs)
async def send(self, request, stream=False, ssl=None, timeout=None):
connection = await self.acquire_connection(request.url.origin)
if (
connection.h11_connection is None
and connection.h2_connection is None
):
await connection.connect(ssl=ssl, timeout=timeout)
if self._request_delay:
await asyncio.sleep(self._request_delay)
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.")
response = await connection.send(
request, stream=stream, ssl=ssl, timeout=timeout
)
except BaseException as exc:
self.active_connections.remove(connection)
self.max_connections.release()
raise exc
return response
aiohttp_version = version.parse(aiohttp.__version__)
class DelayableSanicAdapter(requests.adapters.HTTPAdapter):
def __init__(self, request_delay=None):
self.pool = DelayableSanicConnectionPool(request_delay=request_delay)
class DelayableTCPConnector(TCPConnector):
class RequestContextManager(object):
def __new__(cls, req, delay):
cls = super(
DelayableTCPConnector.RequestContextManager, cls
).__new__(cls)
cls.req = req
cls.send_task = None
cls.resp = None
cls.orig_send = getattr(req, "send")
cls.orig_start = None
cls.delay = delay
cls._acting_as = req
return cls
def __getattr__(self, item):
acting_as = self._acting_as
return getattr(acting_as, item)
async def start(self, connection, read_until_eof=False):
if self.send_task is None:
raise RuntimeError("do a send() before you do a start()")
resp = await self.send_task
self.send_task = None
self.resp = resp
self._acting_as = self.resp
self.orig_start = getattr(resp, "start")
try:
if aiohttp_version >= version.parse("3.3.0"):
ret = await self.orig_start(connection)
else:
ret = await self.orig_start(connection, read_until_eof)
except Exception as e:
raise e
return ret
def close(self):
if self.resp is not None:
self.resp.close()
if self.send_task is not None:
self.send_task.cancel()
async def delayed_send(self, *args, **kwargs):
req = self.req
if self.delay and self.delay > 0:
# sync_sleep(self.delay)
await asyncio.sleep(self.delay)
t = req.loop.time()
print("sending at {}".format(t), flush=True)
next(iter(args)) # first arg is connection
try:
return await self.orig_send(*args, **kwargs)
except Exception as e:
if aiohttp_version < version.parse("3.1.0"):
return aiohttp.ClientResponse(req.method, req.url)
kw = dict(
writer=None,
continue100=None,
timer=None,
request_info=None,
traces=[],
loop=req.loop,
session=None,
)
if aiohttp_version < version.parse("3.3.0"):
kw["auto_decompress"] = None
return aiohttp.ClientResponse(req.method, req.url, **kw)
def _send(self, *args, **kwargs):
gen = self.delayed_send(*args, **kwargs)
task = self.req.loop.create_task(gen)
self.send_task = task
self._acting_as = task
return self
if aiohttp_version >= version.parse("3.1.0"):
# aiohttp changed the request.send method to async
async def send(self, *args, **kwargs):
return self._send(*args, **kwargs)
else:
send = _send
def __init__(self, *args, **kwargs):
_post_connect_delay = kwargs.pop("post_connect_delay", 0)
_pre_request_delay = kwargs.pop("pre_request_delay", 0)
super(DelayableTCPConnector, self).__init__(*args, **kwargs)
self._post_connect_delay = _post_connect_delay
self._pre_request_delay = _pre_request_delay
async def connect(self, req, *args, **kwargs):
d_req = DelayableTCPConnector.RequestContextManager(
req, self._pre_request_delay
)
conn = await super(DelayableTCPConnector, self).connect(
req, *args, **kwargs
)
if self._post_connect_delay and self._post_connect_delay > 0:
await asyncio.sleep(self._post_connect_delay, loop=self._loop)
req.send = d_req.send
t = req.loop.time()
print("Connected at {}".format(t), flush=True)
return conn
class DelayableSanicSession(requests.Session):
def __init__(self, request_delay=None, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
adapter = DelayableSanicAdapter(request_delay=request_delay)
self.mount("http://", adapter)
self.mount("https://", adapter)
class DelayableSanicTestClient(SanicTestClient):
def __init__(self, app, loop, request_delay=1):
super(DelayableSanicTestClient, self).__init__(app)
def __init__(self, app, request_delay=None):
super().__init__(app)
self._request_delay = request_delay
self._loop = None
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
if self._loop is None:
self._loop = asyncio.get_event_loop()
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
url = uri
else:
url = "http://{host}:{port}{uri}".format(
host=HOST, port=self.port, uri=uri
)
conn = DelayableTCPConnector(
pre_request_delay=self._request_delay, ssl=False, loop=self._loop
)
async with aiohttp.ClientSession(
cookies=cookies, connector=conn, loop=self._loop
) as session:
# Insert a delay after creating the connection
# But before sending the request.
async with getattr(session, method.lower())(
url, *args, **kwargs
) as response:
try:
response.text = await response.text()
except UnicodeDecodeError:
response.text = None
try:
response.json = await response.json()
except (
JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError,
):
response.json = None
response.body = await response.read()
return response
def get_new_session(self):
return DelayableSanicSession(request_delay=self._request_delay)
request_timeout_default_app = Sanic("test_request_timeout_default")
@@ -202,14 +78,14 @@ async def ws_handler1(request, ws):
def test_default_server_error_request_timeout():
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
client = DelayableSanicTestClient(request_timeout_default_app, 2)
request, response = client.get("/1")
assert response.status == 408
assert response.text == "Error: Request Timeout"
def test_default_server_error_request_dont_timeout():
client = DelayableSanicTestClient(request_no_timeout_app, None, 0.2)
client = DelayableSanicTestClient(request_no_timeout_app, 0.2)
request, response = client.get("/1")
assert response.status == 200
assert response.text == "OK"
@@ -224,7 +100,7 @@ def test_default_server_error_websocket_request_timeout():
"Sec-WebSocket-Version": "13",
}
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
client = DelayableSanicTestClient(request_timeout_default_app, 2)
request, response = client.get("/ws1", headers=headers)
assert response.status == 408

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import asyncio
import inspect
import os
from collections import namedtuple
from mimetypes import guess_type
from random import choice
@@ -8,6 +9,7 @@ from unittest.mock import MagicMock
from urllib.parse import unquote
import pytest
from aiofiles import os as async_os
from sanic.response import (
@@ -18,11 +20,11 @@ from sanic.response import (
json,
raw,
stream,
text,
)
from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT
JSON_DATA = {"ok": True}
@@ -77,10 +79,10 @@ def test_response_header(app):
request, response = app.test_client.get("/")
assert dict(response.headers) == {
"Connection": "keep-alive",
"Keep-Alive": str(app.config.KEEP_ALIVE_TIMEOUT),
"Content-Length": "11",
"Content-Type": "application/json",
"connection": "keep-alive",
"keep-alive": str(app.config.KEEP_ALIVE_TIMEOUT),
"content-length": "11",
"content-type": "application/json",
}
@@ -192,22 +194,57 @@ def test_no_content(json_app):
def streaming_app(app):
@app.route("/")
async def test(request):
return stream(sample_streaming_fn, content_type="text/csv")
return stream(
sample_streaming_fn,
headers={"Content-Length": "7"},
content_type="text/csv",
)
return app
def test_streaming_adds_correct_headers(streaming_app):
@pytest.fixture
def non_chunked_streaming_app(app):
@app.route("/")
async def test(request):
return stream(
sample_streaming_fn,
headers={"Content-Length": "7"},
content_type="text/csv",
chunked=False,
)
return app
def test_chunked_streaming_adds_correct_headers(streaming_app):
request, response = streaming_app.test_client.get("/")
assert response.headers["Transfer-Encoding"] == "chunked"
assert response.headers["Content-Type"] == "text/csv"
# Content-Length is not allowed by HTTP/1.1 specification
# when "Transfer-Encoding: chunked" is used
assert "Content-Length" not in response.headers
def test_streaming_returns_correct_content(streaming_app):
def test_chunked_streaming_returns_correct_content(streaming_app):
request, response = streaming_app.test_client.get("/")
assert response.text == "foo,bar"
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
request, response = non_chunked_streaming_app.test_client.get("/")
assert "Transfer-Encoding" not in response.headers
assert response.headers["Content-Type"] == "text/csv"
assert response.headers["Content-Length"] == "7"
def test_non_chunked_streaming_returns_correct_content(
non_chunked_streaming_app
):
request, response = non_chunked_streaming_app.test_client.get("/")
assert response.text == "foo,bar"
@pytest.mark.parametrize("status", [200, 201, 400, 401])
def test_stream_response_status_returns_correct_headers(status):
response = StreamingHTTPResponse(sample_streaming_fn, status=status)
@@ -227,13 +264,27 @@ def test_stream_response_keep_alive_returns_correct_headers(
assert b"Keep-Alive: %s\r\n" % str(keep_alive_timeout).encode() in headers
def test_stream_response_includes_chunked_header():
def test_stream_response_includes_chunked_header_http11():
response = StreamingHTTPResponse(sample_streaming_fn)
headers = response.get_headers()
headers = response.get_headers(version="1.1")
assert b"Transfer-Encoding: chunked\r\n" in headers
def test_stream_response_writes_correct_content_to_transport(streaming_app):
def test_stream_response_does_not_include_chunked_header_http10():
response = StreamingHTTPResponse(sample_streaming_fn)
headers = response.get_headers(version="1.0")
assert b"Transfer-Encoding: chunked\r\n" not in headers
def test_stream_response_does_not_include_chunked_header_if_disabled():
response = StreamingHTTPResponse(sample_streaming_fn, chunked=False)
headers = response.get_headers(version="1.1")
assert b"Transfer-Encoding: chunked\r\n" not in headers
def test_stream_response_writes_correct_content_to_transport_when_chunked(
streaming_app
):
response = StreamingHTTPResponse(sample_streaming_fn)
response.protocol = MagicMock(HttpProtocol)
response.protocol.transport = MagicMock(asyncio.Transport)
@@ -241,7 +292,7 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
async def mock_drain():
pass
def mock_push_data(data):
async def mock_push_data(data):
response.protocol.transport.write(data)
response.protocol.push_data = mock_push_data
@@ -262,6 +313,42 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
b"0\r\n\r\n"
)
assert len(response.protocol.transport.write.call_args_list) == 4
app.stop()
streaming_app.run(host=HOST, port=PORT)
def test_stream_response_writes_correct_content_to_transport_when_not_chunked(
streaming_app,
):
response = StreamingHTTPResponse(sample_streaming_fn)
response.protocol = MagicMock(HttpProtocol)
response.protocol.transport = MagicMock(asyncio.Transport)
async def mock_drain():
pass
async def mock_push_data(data):
response.protocol.transport.write(data)
response.protocol.push_data = mock_push_data
response.protocol.drain = mock_drain
@streaming_app.listener("after_server_start")
async def run_stream(app, loop):
await response.stream(version="1.0")
assert response.protocol.transport.write.call_args_list[1][0][0] == (
b"foo,"
)
assert response.protocol.transport.write.call_args_list[2][0][0] == (
b"bar"
)
assert len(response.protocol.transport.write.call_args_list) == 3
app.stop()
streaming_app.run(host=HOST, port=PORT)
@@ -276,7 +363,7 @@ def test_stream_response_with_cookies(app):
return response
request, response = app.test_client.get("/")
assert response.cookies["test"].value == "pass"
assert response.cookies["test"] == "pass"
def test_stream_response_without_cookies(app):

View File

@@ -1,7 +1,9 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic import Sanic
from sanic.exceptions import ServiceUnavailable
from sanic.response import text
response_timeout_app = Sanic("test_response_timeout")
response_timeout_default_app = Sanic("test_response_timeout_default")

View File

@@ -7,6 +7,7 @@ from sanic.constants import HTTP_METHODS
from sanic.response import json, text
from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists
# ------------------------------------------------------------ #
# UTF-8
# ------------------------------------------------------------ #
@@ -365,9 +366,18 @@ def test_dynamic_route_number(app):
request, response = app.test_client.get("/weight/1234.56")
assert response.status == 200
request, response = app.test_client.get("/weight/.12")
assert response.status == 200
request, response = app.test_client.get("/weight/12.")
assert response.status == 200
request, response = app.test_client.get("/weight/1234-56")
assert response.status == 404
request, response = app.test_client.get("/weight/12.34.56")
assert response.status == 404
def test_dynamic_route_regex(app):
@app.route("/folder/<folder_id:[A-Za-z0-9]{0,4}>")
@@ -459,16 +469,21 @@ def test_websocket_route(app, url):
assert ws.subprotocol is None
ev.set()
request, response = app.test_client.get(
"/ws",
headers={
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
},
)
assert response.status == 101
request, response = app.test_client.websocket(url)
assert response.opened is True
assert ev.is_set()
@pytest.mark.asyncio
@pytest.mark.parametrize("url", ["/ws", "ws"])
async def test_websocket_route_asgi(app, url):
ev = asyncio.Event()
@app.websocket(url)
async def handler(request, ws):
ev.set()
request, response = await app.asgi_client.websocket(url)
assert ev.is_set()
@@ -478,54 +493,24 @@ def test_websocket_route_with_subprotocols(app):
@app.websocket("/ws", subprotocols=["foo", "bar"])
async def handler(request, ws):
results.append(ws.subprotocol)
assert ws.subprotocol is not None
request, response = app.test_client.get(
"/ws",
headers={
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Protocol": "bar",
},
request, response = app.test_client.websocket("/ws", subprotocols=["bar"])
assert response.opened is True
assert results == ["bar"]
request, response = app.test_client.websocket(
"/ws", subprotocols=["bar", "foo"]
)
assert response.status == 101
assert response.opened is True
assert results == ["bar", "bar"]
request, response = app.test_client.get(
"/ws",
headers={
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Protocol": "bar, foo",
},
)
assert response.status == 101
request, response = app.test_client.get(
"/ws",
headers={
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Protocol": "baz",
},
)
assert response.status == 101
request, response = app.test_client.get(
"/ws",
headers={
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
},
)
assert response.status == 101
request, response = app.test_client.websocket("/ws", subprotocols=["baz"])
assert response.opened is True
assert results == ["bar", "bar", None]
request, response = app.test_client.websocket("/ws")
assert response.opened is True
assert results == ["bar", "bar", None, None]
@@ -538,16 +523,8 @@ def test_add_webscoket_route(app, strict_slashes):
ev.set()
app.add_websocket_route(handler, "/ws", strict_slashes=strict_slashes)
request, response = app.test_client.get(
"/ws",
headers={
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
},
)
assert response.status == 101
request, response = app.test_client.websocket("/ws")
assert response.opened is True
assert ev.is_set()
@@ -672,9 +649,18 @@ def test_dynamic_add_route_number(app):
request, response = app.test_client.get("/weight/1234.56")
assert response.status == 200
request, response = app.test_client.get("/weight/.12")
assert response.status == 200
request, response = app.test_client.get("/weight/12.")
assert response.status == 200
request, response = app.test_client.get("/weight/1234-56")
assert response.status == 404
request, response = app.test_client.get("/weight/12.34.56")
assert response.status == 404
def test_dynamic_add_route_regex(app):
async def handler(request, folder_id):

View File

@@ -4,6 +4,7 @@ import pytest
from sanic.testing import HOST, PORT
AVAILABLE_LISTENERS = [
"before_server_start",
"after_server_start",
@@ -75,6 +76,7 @@ def test_all_listeners(app):
assert app.name + listener_name == output.pop()
@pytest.mark.asyncio
async def test_trigger_before_events_create_server(app):
class MySanicDb:
pass
@@ -83,7 +85,7 @@ async def test_trigger_before_events_create_server(app):
async def init_db(app, loop):
app.db = MySanicDb()
await app.create_server(debug=True, return_asyncio_server=True)
await app.create_server(debug=True, return_asyncio_server=True, port=PORT)
assert hasattr(app, "db")
assert isinstance(app.db, MySanicDb)

View File

@@ -1,8 +1,10 @@
import asyncio
from queue import Queue
from unittest.mock import MagicMock
from sanic.response import HTTPResponse
from sanic.testing import HOST, PORT
from unittest.mock import MagicMock
import asyncio
from queue import Queue
async def stop(app, loop):

View File

@@ -1,5 +1,6 @@
import inspect
import os
from time import gmtime, strftime
import pytest

View File

@@ -1,7 +1,8 @@
import socket
from sanic.testing import PORT, SanicTestClient
from sanic.response import json, text
from sanic.testing import PORT, SanicTestClient
# ------------------------------------------------------------ #
# UTF-8
@@ -9,26 +10,26 @@ from sanic.response import json, text
def test_test_client_port_none(app):
@app.get('/get')
@app.get("/get")
def handler(request):
return text('OK')
return text("OK")
test_client = SanicTestClient(app, port=None)
request, response = test_client.get('/get')
assert response.text == 'OK'
request, response = test_client.get("/get")
assert response.text == "OK"
request, response = test_client.post('/get')
request, response = test_client.post("/get")
assert response.status == 405
def test_test_client_port_default(app):
@app.get('/get')
@app.get("/get")
def handler(request):
return json(request.transport.get_extra_info('sockname')[1])
return json(request.transport.get_extra_info("sockname")[1])
test_client = SanicTestClient(app)
assert test_client.port == PORT
request, response = test_client.get('/get')
request, response = test_client.get("/get")
assert response.json == PORT

View File

@@ -1,14 +1,17 @@
import pytest as pytest
from urllib.parse import urlsplit, parse_qsl
from sanic.response import text
from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint
from sanic.testing import PORT as test_port, HOST as test_host
from sanic.exceptions import URLBuildError
import string
from urllib.parse import parse_qsl, urlsplit
import pytest as pytest
from sanic.blueprints import Blueprint
from sanic.exceptions import URLBuildError
from sanic.response import text
from sanic.testing import HOST as test_host
from sanic.testing import PORT as test_port
from sanic.views import HTTPMethodView
URL_FOR_ARGS1 = dict(arg1=["v1", "v2"])
URL_FOR_VALUE1 = "/myurl?arg1=v1&arg1=v2"
URL_FOR_ARGS2 = dict(arg1=["v1", "v2"], _anchor="anchor")
@@ -170,7 +173,7 @@ def test_fails_with_int_message(app):
expected_error = (
r'Value "not_int" for parameter `foo` '
r'does not match pattern for type `int`: -?\d+'
r"does not match pattern for type `int`: -?\d+"
)
assert str(e.value) == expected_error
@@ -223,7 +226,7 @@ def test_fails_with_number_message(app):
expected_error = (
'Value "foo" for parameter `some_number` '
"does not match pattern for type `float`: -?[0-9\\\\.]+"
r"does not match pattern for type `float`: -?(?:\d+(?:\.\d*)?|\.\d+)"
)
assert str(e.value) == expected_error

View File

@@ -1,4 +1,5 @@
from json import dumps as json_dumps
from sanic.response import text

View File

@@ -1,11 +1,11 @@
import pytest as pytest
from sanic.exceptions import InvalidUsage
from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView, CompositionView
from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage
from sanic.request import Request
from sanic.response import HTTPResponse, text
from sanic.views import CompositionView, HTTPMethodView
@pytest.mark.parametrize("method", HTTP_METHODS)

View File

@@ -1,14 +1,17 @@
import time
import asyncio
import json
import shlex
import subprocess
import time
import urllib.request
from unittest import mock
from sanic.worker import GunicornWorker
from sanic.app import Sanic
import asyncio
import pytest
from sanic.app import Sanic
from sanic.worker import GunicornWorker
@pytest.fixture(scope="module")
def gunicorn_worker():

23
tox.ini
View File

@@ -1,24 +1,27 @@
[tox]
envlist = py35, py36, py37, {py35,py36,py37}-no-ext, lint, check
envlist = py36, py37, {py36,py37}-no-ext, lint, check, security
[testenv]
usedevelop = True
setenv =
{py35,py36,py37}-no-ext: SANIC_NO_UJSON=1
{py35,py36,py37}-no-ext: SANIC_NO_UVLOOP=1
{py36,py37}-no-ext: SANIC_NO_UJSON=1
{py36,py37}-no-ext: SANIC_NO_UVLOOP=1
deps =
coverage
pytest==4.1.0
pytest-cov
pytest-sanic
pytest-sugar
aiohttp>=2.3,<=3.2.1
httpcore==0.3.0
requests-async==0.5.0
chardet<=2.3.0
beautifulsoup4
gunicorn
pytest-benchmark
uvicorn
websockets>=6.0,<7.0
commands =
pytest tests --cov sanic --cov-report= {posargs}
pytest {posargs:tests --cov sanic}
- coverage combine --append
coverage report -m
coverage html -i
@@ -28,10 +31,11 @@ deps =
flake8
black
isort
bandit
commands =
flake8 sanic
black --check --verbose sanic
black --config ./.black.toml --check --verbose sanic/
isort --check-only --recursive sanic
[testenv:check]
@@ -44,3 +48,10 @@ commands =
[pytest]
filterwarnings =
ignore:.*async with lock.* instead:DeprecationWarning
[testenv:security]
deps =
bandit
commands =
bandit --recursive sanic --skip B404,B101 --exclude sanic/reloader_helpers.py