Compare commits

..

270 Commits

Author SHA1 Message Date
7
228a31ee0a Merge pull request #1657 from huge-success/release-19.6.3
release: 19.6.3
2019-08-21 23:00:51 -07:00
Yun Xu
8bf2bdff74 Bumping up version from 19.6.2 to 19.6.3 2019-08-20 18:51:17 -07:00
7
41862eca61 Merge pull request #1654 from huge-success/asgi-content-type
Add content-type headers in response in ASGI mode
2019-08-13 12:30:40 -07:00
Yun Xu
21307b397b release: 19.6.3 2019-08-13 10:03:08 -07:00
7
3f9c94ba4a Merge pull request #1635 from huge-success/upgrade-websockets
Upgrade websockets, resolve incompatible issue between multidict and websockets
2019-08-12 10:48:56 -07:00
Adam Hopkins
aa270d3ac2 Add content-type headers in response in ASGI mode 2019-08-11 11:29:08 +03:00
7
a15d9552c4 Merge pull request #1632 from harshanarayana/feature/GIT-1631-Enable_Towncrier
feature: GIT-1631 enable towncrier
2019-08-06 08:33:10 -07:00
7
2363c0653e Merge pull request #1640 from Tronic/sockaddrfix
Fix server crash on request.server_port when bound to IPv6.
2019-07-25 00:10:56 -07:00
Harsha Narayana
651c98d19a fix: #1631: add ignore file to ensure empty changelog dir is retained
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-24 05:39:20 +05:30
Harsha Narayana
c1a7e0e3cd feat: #1631: enable change log as part of release script
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-24 05:32:00 +05:30
Harsha Narayana
80b32d0c71 feat: #1631: enable make command to support settings up release
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-24 05:03:04 +05:30
Harsha Narayana
3842eb36fd fix: #1631: fix pyproject toml indentation
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-24 04:28:11 +05:30
L. Kärkkäinen
7c7bedfa5d Fix server crash on request.server_port when bound to IPv6.
If no X-Forwarded-Port nor Host headers are present, Sanic uses "sockname"
to determine the port. This expected (host, port) tuple to be returned but
for IPv6 a 4-tuple is returned instead. Changed code so that port is picked
up in either case. Handling of "peername" was already correct in this regard.

_get_address and server_port both still return incorrect data or crash for
other socket types (e.g unix). Socket type should checked before any queries.
2019-07-22 15:32:57 +03:00
Yun Xu
5dafa9a170 bugfix: replace CIMultiDict with compat.Header in all places 2019-07-18 20:11:25 -07:00
Yun Xu
b397637bb9 bugfix: fix incompatible api between multidict and websockets, and bump up websockets version to match uvicorn 2019-07-18 19:57:17 -07:00
Harsha Narayana
95a0b2db2c fix: #1631: move pyproject.toml to avoid PEP 517 conflict 2019-07-14 14:26:22 +05:30
Harsha Narayana
83864f890a fix: #1631: add common contribution guidelines and towncrier detail to contribution guides
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-13 21:48:34 +05:30
Harsha Narayana
a019ff61e3 fix: #1631: linter fix and tox platform selector
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-13 21:48:26 +05:30
Harsha Narayana
b3ada6308b fix: #1631: add doc test for travis CI
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-13 21:48:16 +05:30
Harsha Narayana
4e50295bf0 fix: #1631: add tox test support for documentation
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-13 21:48:06 +05:30
Harsha Narayana
32eb8abb63 fix: #1631: add towncrier support and fix documentation warnings
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-13 21:47:48 +05:30
7
84b41123f2 Merge pull request #1625 from harshanarayana/fix/GIT-1623-Cookie_Handling
fix: GIT-1623: handle cookie duplication and serialization issue
2019-07-10 21:35:35 -07:00
Harsha Narayana
23f2d33394 fix: GIT-1623: fix dict initalization for empty case
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-11 06:38:55 +05:30
Harsha Narayana
97f288a534 fix: GIT-1623: handle cookie duplication and serialization issue
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-07-08 13:03:33 +05:30
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
Sam Havens
abf8534ea9 fix typo in Asyncio example (#1510)
* fix typo

* args to kwargs
2019-03-15 12:28:15 -05:00
Moshe Zada
773a66bc5b Fix typo (#1516) 2019-03-15 11:49:18 -05:00
Harsha Narayana
269100eac1 format: fix linter issue causing travis build failures (fix #1514) (#1515)
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-03-14 12:18:47 -05:00
Serge Fedoruk
2a15583b87 add Request.not_grouped_args, deprecation warning Request.raw_args (#1476)
* add Request.not_grouped_args, deprecation warning Request.raw_args

* add 1 more test for coverage

* custom parser for Request.args and Request.query_args, some additional tests

* add docs for custom queryset parsing

* fix import sorting

* docstrings for get_query_args and get_args methods

* lost import
2019-03-14 09:04:05 -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
Daniel Thorn
d5813152ab Allow sanic test client to bind to a random port (#1376) 2019-03-04 15:23:03 -06: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
Harsha Narayana
348964fe12 Enable Middleware Support for Blueprint Groups (#1399)
* enable blueprint group middleware support

This commit will enable the users to implement a middleware at the
blueprint group level whereby enforcing the middleware automatically to
each of the available Blueprints that are part of the group.

This will eanble a simple way in which a certain set of common features
and criteria can be enforced on a Blueprint group. i.e. authentication
and authorization

This commit will address the feature request raised as part of Issue #1386

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

* enable indexing of BlueprintGroup object

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

* rename blueprint group file to fix spelling error

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

* add documentation and additional unit tests

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

* cleanup and optimize headers in unit test file

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

* fix Bluprint Group iteratable method

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

* add additional unit test to check StopIteration condition

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

* cleanup iter protocol implemenation for blueprint group and add slots

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

* fix blueprint group middleware invocation identification

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

* feat: enable list behavior on blueprint group object and use append instead of properly to add blueprint to group

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-03-03 16:26:05 -06:00
Markus Lång
e5c7589fc0 Remove update_current_time refresh (#1502) 2019-03-03 11:22:26 -06:00
Ashley Sommer
4260528645 Fix the auto_reloader to work when the executable was launched with a module, rather than a script. (#1501) 2019-03-03 11:03:26 -06:00
Harsha Narayana
34fe26e51b Add Route Resolution Benchmarking to Unit Test (#1499)
* feat: add benchmark tester for route resolution and cleanup test warnings

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

* feat: refactor sanic benchmark test util into fixtures

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-02-28 08:56:41 -06:00
PWZER
8a59907319 Recognizes non-ASCII filenames in RFC 2231, and suport filename length is zero for multipart/form-data. (#1497)
* suport filename length is 0

* 1. suport filename length is zero for multipart/form-data.
2. Now recognizes non-ASCII filenames in RFC 2231, "filename*" format
3. Add some test cases in tests/test_requests.py::test_request_multipart_files.

* reformat sanic/request.py
2019-02-28 08:55:32 -06:00
7
52deebaf65 Merge pull request #1490 from chenjr0719/fix_doc_build
Fix python version in doc build
2019-02-19 16:26:56 -08:00
jacob
1e05b22fbc Fix python version in environment.yml 2019-02-18 14:02:45 +08:00
7
ab56af5d15 Merge pull request #1489 from tomchristie/patch-1
Added "databases"
2019-02-15 16:43:38 -08:00
Tom Christie
123f00eee6 Added "databases"
Adds https://github.com/encode/databases to the "Database Integration" section.
2019-02-14 13:44:18 +00:00
Mykhailo Kushchenko
42bf103269 Remove deleted repo (#1487)
https://github.com/Sniedes722/Sanic-OAuth  (Sanic-OAuth: OAuth Library for connecting to & creating your own token providers.) returns  404
2019-02-08 08:43:43 -06:00
0xflotus
c8d2a462e3 did you mean specific? (#1486) 2019-02-06 16:28:32 -06:00
Leonardo Teixeira Menezes
08794ae1cf Enforce Datetime Type for Expires on Set-Cookie (#1484)
* Enforce Datetime Type for Expires on Set-Cookie

* Fix lint issues

* Format code and improve error type

* Fix import order
2019-02-06 12:29:33 -06:00
Kevin ZHANG Qing
4f70dba935 sanic-zipkin (#1483) 2019-02-05 07:59:33 -06:00
Enda Farrell
b926a2c9b0 sanic#1480 Allow negative int/number in path (#1481)
* sanic#1480 Allow negative int/number

* Rerun ``make beautify`` on this change.
2019-02-05 07:54:48 -06:00
Jacob
52bdd1d5a2 Add stream support for bp.add_route() (#1482)
* Fix #1454

* Update doc

* Fix F632 in response.py
2019-02-05 07:47:46 -06:00
7
bc7d0f0da5 Merge pull request #1478 from chenjr0719/fix_doc_build
Upgrade setuptools version and use native docutils in doc build
2019-01-21 22:42:51 -08:00
jacob
6a8e9c9e95 Add deps based on docs extras require, Remove unnecessary deps 2019-01-22 14:05:29 +08:00
jacob
211a922f3c Upgrade setuptools version and use native docutils 2019-01-21 10:16:57 +08:00
7
2758a3ade6 Merge pull request #1472 from xxNB/dev
Remove unwanted None check for __repr__ in `Request` class
2019-01-20 14:21:57 -08:00
7
ef3c9eae73 Merge pull request #1477 from kyb3r/patch-2
Fix grammar in README.md
2019-01-20 14:21:27 -08:00
7
9cf2e1b519 Merge pull request #1470 from denismakogon/create-server
make Sanic.create_server return an asyncio.Server
2019-01-20 14:21:11 -08:00
Kyber
51c2f7a599 Use backticks 2019-01-19 20:10:44 +11:00
Kyber
5bdd046b11 Fix grammar in README.md
>  It allows usage the async and await syntax 

Doesn't make sense.
2019-01-19 20:08:47 +11:00
Tom Christie
95526a82de ASGI refactoring attempt 2019-01-18 14:50:42 +00:00
章昕
af7ad0a621 Remove unwanted None check for __repr__ in class 2019-01-17 00:24:11 +08:00
Denis Makogon
1473753d43 linter fix 2019-01-15 17:48:26 +02:00
Denis Makogon
b36bd21813 fix uvloop check 2019-01-15 17:45:47 +02:00
Denis Makogon
f8f0241c27 refactor uvloop detection in its own method 2019-01-15 17:33:53 +02:00
Denis Makogon
1af16836d4 make tests dependent on uvloop 2019-01-15 17:30:32 +02:00
Denis Makogon
757974714e skip tests if python version is not 3.7 at least 2019-01-15 17:27:41 +02:00
Denis Makogon
eed22a7a24 Fix app.create_server calls 2019-01-15 15:47:35 +02:00
Denis Makogon
0242bc999f Fix type asserting 2019-01-15 15:11:38 +02:00
Denis Makogon
b89c533818 Adding doc 2019-01-15 15:04:30 +02:00
Denis Makogon
2cb05ab865 More tests, attempting to fix CI 2019-01-15 14:52:53 +02:00
Denis Makogon
391639210e make Sanic.create_server return an asyncio.Server
- adding 2 new parameters to Sanic.create_server:
   * return_asyncio_server=False - defines whether there's
     a need to return an asyncio.Server or run it right away
   * asyncio_server_kwargs=None - for python 3.7 uvloop doesn't
     support all necessary features like "start_serving",
     so, in order to make sanic work well with asyncio from 3.7
     there's a need to introduce generic way for passing
     kwargs for "loop.create_server"

Closes: #1469
2019-01-15 13:38:47 +02:00
7
99f34c9f50 Merge pull request #1457 from huge-success/max-age-integer
enforce integer for max-age cookie
2019-01-13 13:15:10 -08:00
Raphael Deem
d418cc9950 formatting 2019-01-12 20:41:35 -08:00
Raphael Deem
6dfafb0787 test float handling 2019-01-12 20:41:35 -08:00
Raphael Deem
7067295e67 enforce integer for max-age cookie 2019-01-12 20:41:35 -08:00
Eli Uriegas
2af229eb1a Merge pull request #1445 from huge-success/r0fls-977
add handler name to request as endpoint
2019-01-08 16:12:25 -08:00
7
8dd8e9916e upgrade pytest version that compatible with pytest-cov, fixes some caplog unit tests (#1464) 2019-01-08 09:15:23 -06:00
7
96af1fe7cf Merge pull request #1460 from huge-success/18.12-changelog
18.12 Changelog
2019-01-06 22:33:37 -08:00
Yun Xu
cb3a03356b added changelogs to README and readthedocs 2019-01-06 13:50:40 -08:00
Yun Xu
68aa2ae3ce added changelog for 18.12 release 2019-01-06 13:44:18 -08:00
7
52de354e24 Merge pull request #1442 from Amanit/feature/gunicorn-logging
add an option to change access_log using gunicorn
2019-01-05 11:40:55 -08:00
7
f4f90cada4 Merge pull request #1449 from chenjr0719/add_amending_request_object_example
Add example of amending request object
2019-01-02 18:32:24 -08:00
Jotagê Sales
62420e0f40 resolve conflict 2019-01-02 21:19:40 -02:00
Sergey Fedoruk
102e651741 refactor typing imports 2019-01-02 23:28:06 +01:00
Sergey Fedoruk
65daaaf64b linteger fix and delete old tests 2019-01-02 23:28:05 +01:00
Sergey Fedoruk
b7a6f36e95 add type annotations in run and create_server 2019-01-02 23:28:05 +01:00
Sergey Fedoruk
a86a10b128 add control of access_log argument type 2019-01-02 23:28:05 +01:00
Sergey Fedoruk
0b728ade3a change Config.__init__ 2019-01-02 23:28:05 +01:00
Sergey Fedoruk
74f05108d7 async test for access_log in create_server 2019-01-02 23:28:05 +01:00
Sergey Fedoruk
9d4d15ddc7 add config tests 2019-01-02 23:28:05 +01:00
Sergey Fedoruk
0c5c6dff8f fix linting 2019-01-02 23:28:05 +01:00
Sergey Fedoruk
391fcdc83d fix access_log in run server and fix bool in env variables 2019-01-02 23:28:05 +01:00
Sergey Fedoruk
d76d5e2c5f add an option to change access_log using gunicorn 2019-01-02 23:28:05 +01:00
jacob
f0ada573bb Fix a grammar error 2019-01-02 20:37:26 +08:00
jacob
ec5b790b51 Extend example of modifying the request in middleware document 2019-01-02 17:29:01 +08:00
jacob
613b23748d Add example of amending request object 2019-01-02 14:52:25 +08:00
Adam Hopkins
cea1547e08 Merge pull request #1446 from huge-success/ahopkins-patch-1
Update README.rst
2019-01-01 14:51:05 +02:00
7
fd5ae01e1d Merge pull request #1444 from ja8zyjits/master
Updated README.md
2018-12-31 11:49:03 -08:00
Adam Hopkins
9b6b93d467 Update README.rst 2018-12-31 21:41:35 +02:00
Adam Hopkins
ca179c12a1 Update README.rst 2018-12-31 18:47:27 +02:00
Adam Hopkins
4d527035ae Add dotted endpoint notation and additional tests 2018-12-31 13:40:07 +02:00
Jitesh Nair
19b42830ea Merge pull request #1 from ja8zyjits/ja8zyjits-patch-1-readme
Update README.rst
2018-12-31 16:01:01 +05:30
Jitesh Nair
f5162f8ab1 Update README.rst
Made the optional Environment variable declaration for installation more clear.
2018-12-31 16:00:34 +05:30
7
ff38a3c6b6 Merge pull request #1443 from huge-success/new-readme
Update README with new logo, change Congfig.LOGO, run linter
2018-12-30 13:23:12 -08:00
Adam Hopkins
94e85686b5 Ignore first row of logs when no uvloop 2018-12-30 14:07:21 +02:00
Adam Hopkins
aea4a8ed33 Modify test_logo runner 2018-12-30 13:46:08 +02:00
Adam Hopkins
05dd3b2e9d Run linter 2018-12-30 13:18:06 +02:00
Adam Hopkins
040468755c Change ASCII Logo
Update logo text

Reformat app.py
2018-12-30 12:49:23 +02:00
Adam Hopkins
50b359fdb2 Update README.rst
Add new logo and update contents of README.

Update README.rst

Fix image syntax.

Update README.rst

Fix README links.
2018-12-30 11:48:59 +02:00
7
72f2e18a84 Merge pull request #1440 from harshanarayana/fix/Contribution_Guide_Pip_Install
fix minor type and pip install instruction mismatch
2018-12-28 18:43:41 -08:00
Jotagê Sales
b36dc22b45 resolve conflict in setup.py 2018-12-28 12:08:10 -02:00
Harsha Narayana
15b1c875f5 fix minor type and pip install instruction mismatch 2018-12-28 11:32:30 +05:30
7
13804dc380 Merge pull request #1424 from harshanarayana/enh/Documentation_Update
Documentation Enhancements
2018-12-27 21:30:02 -08:00
Harsha Narayana
9bea23da29 fix makefile phony targets
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:24:03 +05:30
Harsha Narayana
7005fabd4d add code beautification task to makefile
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:24:03 +05:30
Harsha Narayana
de8c37ad00 fix pip install typo in contribution page
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:24:03 +05:30
Harsha Narayana
a80499c4b7 update installation steps to be consistent across documentation and readme
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:24:03 +05:30
Harsha Narayana
82f7f847ba cleanup requirements and move dependency inside setup.py
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:24:03 +05:30
Harsha Narayana
4880761fe0 add setuputil based test running and makefile support
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:24:02 +05:30
Harsha Narayana
87ab0b386d fix current version in setup.cfg for relase script
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:24:02 +05:30
Harsha Narayana
c42c274002 update manifest configuration
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:24:02 +05:30
Harsha Narayana
2d82b8951f make release script black compliant and tweak documentation with indexing and format
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:24:02 +05:30
Harsha Narayana
b7702bc3e8 add monitoring examples and documents 2018-12-28 10:22:28 +05:30
Harsha Narayana
9c6b83501f add release note chnage log generation
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:28 +05:30
Harsha Narayana
5189d8b14c fix string formatting error in git commands for release script
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:28 +05:30
Harsha Narayana
e13053ed89 add automated calendar version manager
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:28 +05:30
Harsha Narayana
efa77cf5ec add api documentation for router and server
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:28 +05:30
Harsha Narayana
f6355bd075 add additional examples to documentation
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:28 +05:30
Harsha Narayana
e3dfce88ff fix linter issues
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:28 +05:30
Harsha Narayana
939b5ea095 update copyright date and add example section with category
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:28 +05:30
Harsha Narayana
e6fba01682 add documentation for cookies, exception, blueprint and handlers
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:28 +05:30
Harsha Narayana
1623d397be categorize the sanic extensions list
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:27 +05:30
Harsha Narayana
09678d601d add sanic app module documentations
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-12-28 10:22:27 +05:30
7
67d51f7e1b Merge pull request #1423 from yunstanford/request-streaming-support
basic request streaming support with flow control
2018-12-27 18:06:02 -08: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
Yun Xu
956793e066 address review feedback, small code refactoring 2018-12-09 15:18:33 -08:00
Yun Xu
1bfbc67c62 expose request_buffer_queue_size to be configurable and update documentation
fix StreamBuffer buffer_size
2018-12-04 20:21:00 -08:00
Yun Xu
b5287184e9 fix lint
fix isort
2018-12-03 23:25:41 -08:00
Yun Xu
7c9957e058 update README.rst (clean up badges) 2018-12-03 23:03:14 -08:00
Yun Xu
fca7cb9fb0 update request streaming doc 2018-12-03 22:51:09 -08:00
Yun Xu
268d254d85 fix unit tests 2018-12-03 22:28:22 -08:00
Yun Xu
181adebf82 add StreamBuffer for request flow control 2018-12-03 22:19:26 -08:00
Raphael Deem
63bbcb5152 Merge branch 'master' into 977 2017-10-25 22:18:25 -07:00
Raphael Deem
9150767574 add blueprint name to request.endpoint 2017-10-16 23:25:37 -07:00
Raphael Deem
75f2180cb1 add handler name to request as endpoint 2017-10-16 22:43:40 -07:00
132 changed files with 10822 additions and 4295 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

2
.gitignore vendored
View File

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

View File

@@ -5,26 +5,38 @@ 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
name: "Python 3.6 with Extensions"
- env: TOX_ENV=py36-no-ext
python: 3.6
name: "Python 3.6 without Extensions"
- env: TOX_ENV=py37
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 with Extensions"
- env: TOX_ENV=py37-no-ext
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 without Extensions"
- env: TOX_ENV=lint
python: 3.6
name: "Python 3.6 Linter checks"
- env: TOX_ENV=check
python: 3.6
name: "Python 3.6 Package checks"
- env: TOX_ENV=security
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Bandit security scan"
- env: TOX_ENV=docs
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Documentation tests"
install:
- pip install -U tox
- pip install codecov
@@ -33,9 +45,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,99 +0,0 @@
Version 0.8
-----------
0.8.3
- Changes:
- Ownership changed to org 'huge-success'
0.8.0
- Changes:
- Add Server-Sent Events extension (Innokenty Lebedev)
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
- Sanitize URL before redirection (aveao)
- Add url_bytes to request (johndoe46)
- py37 support for travisci (yunstanford)
- Auto reloader support for OSX (garyo)
- Add UUID route support (Volodymyr Maksymiv)
- Add pausable response streams (Ashley Sommer)
- Add weakref to request slots (vopankov)
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
- Allow streaming handlers in add_route (kinware)
- use travis_retry for tox (Raphael Deem)
- update aiohttp version for test client (yunstanford)
- add redirect import for clarity (yingshaoxo)
- Update HTTP Entity headers (Arnulfo Solís)
- Add register_listener method (Stephan Fitzpatrick)
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
- Content-length header on 204/304 responses (Arnulfo Solís)
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
- Update development status from pre-alpha to beta (Maksim Anisenkov)
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
- Add support for blueprint groups and nesting (Elias Tarhini)
- Remove uvloop for windows setup (Aleksandr Kurlov)
- Auto Reload (Yaser Amari)
- Documentation updates/fixups (multiple contributors)
- Fixes:
- Fix: auto_reload in Linux (Ashley Sommer)
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
- Fix: disable auto_reload by default on windows (abuckenheimer)
- Fix (1143): Turn off access log with gunicorn (hqy)
- Fix (1268): Support status code for file response (Cosmo Borsky)
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
- Fix (1242): Responses for CI header (yunstanford)
- Fix (1237): add version constraint for websockets (yunstanford)
- Fix (1231): memory leak - always release resource (Phillip Xu)
- Fix (1221): make request truthy if transport exists (Raphael Deem)
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
- Fix try_everything examples (PyManiacGR, kot83)
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
- Fix: raw requires bytes-like object (cloudship)
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
- Fix: Bug in multipart/form-data parser (DirkGuijt)
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
- Fix: Parameter check (Howie Hu)
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
- Fix: single quote in readme.rst (Cosven)
- Fix: method typos (Dmitry Dygalo)
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
- Fix (1042): Exception Handling (Raphael Deem)
- Fix: Chinese URIs (Howie Hu)
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
- Fix (1065): allow add_task after server starts (Raphael Deem)
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
- Fix (1062): inject the app in add_task method (Raphael Deem)
- Fix: update environment.yml for readthedocs (Eli Uriegas)
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
Note: Changelog was unmaintained between 0.1 and 0.7
Version 0.1
-----------
- 0.1.7
- Reversed static url and directory arguments to meet spec
- 0.1.6
- Static files
- Lazy Cookie Loading
- 0.1.5
- Cookies
- Blueprint listeners and ordering
- Faster Router
- Fix: Incomplete file reads on medium+ sized post requests
- Breaking: after_start and before_stop now pass sanic as their first argument
- 0.1.4
- Multiprocessing
- 0.1.3
- Blueprint support
- Faster Response processing
- 0.1.1 - 0.1.2
- Struggling to update pypi via CI
- 0.1.0
- Released to public

398
CHANGELOG.rst Normal file
View File

@@ -0,0 +1,398 @@
Version 19.6.3
==============
Features
********
- Enable Towncrier Support
As part of this feature, `towncrier` is being introduced as a mechanism to partially automate the process
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/huge-success/sanic/issues/1631>`__)
Improved Documentation
**********************
- Documentation infrastructure changes
- Enable having a single common `CHANGELOG` file for both GitHub page and documentation
- Fix Sphinix deprecation warnings
- Fix documentation warnings due to invalid `rst` indentation
- Enable common contribution guidelines file across GitHub and documentation via `CONTRIBUTING.rst` (`#1631 <https://github.com/huge-success/sanic/issues/1631>`__)
Version 19.6.2
==============
Features
********
*
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_
Remove ``aiohttp`` dependencey and create new ``SanicTestClient`` based upon
`requests-async <https://github.com/encode/requests-async>`_
*
`#1475 <https://github.com/huge-success/sanic/pull/1475>`_
Added ASGI support (Beta)
*
`#1436 <https://github.com/huge-success/sanic/pull/1436>`_
Add Configure support from object string
Bugfixes
********
*
`#1587 <https://github.com/huge-success/sanic/pull/1587>`_
Add missing handle for Expect header.
*
`#1560 <https://github.com/huge-success/sanic/pull/1560>`_
Allow to disable Transfer-Encoding: chunked.
*
`#1558 <https://github.com/huge-success/sanic/pull/1558>`_
Fix graceful shutdown.
*
`#1594 <https://github.com/huge-success/sanic/pull/1594>`_
Strict Slashes behavior fix
Deprecations and Removals
*************************
*
`#1544 <https://github.com/huge-success/sanic/pull/1544>`_
Drop dependency on distutil
*
`#1562 <https://github.com/huge-success/sanic/pull/1562>`_
Drop support for Python 3.5
*
`#1568 <https://github.com/huge-success/sanic/pull/1568>`_
Deprecate route removal.
.. warning::
Sanic will not support Python 3.5 from version 19.6 and forward. However,
version 18.12LTS will have its support period extended thru December 2020, and
therefore passing Python's official support version 3.5, which is set to expire
in September 2020.
Version 19.3
============
Features
********
*
`#1497 <https://github.com/huge-success/sanic/pull/1497>`_
Add support for zero-length and RFC 5987 encoded filename for
multipart/form-data requests.
*
`#1484 <https://github.com/huge-success/sanic/pull/1484>`_
The type of ``expires`` attribute of ``sanic.cookies.Cookie`` is now
enforced to be of type ``datetime``.
*
`#1482 <https://github.com/huge-success/sanic/pull/1482>`_
Add support for the ``stream`` parameter of ``sanic.Sanic.add_route()``
available to ``sanic.Blueprint.add_route()``.
*
`#1481 <https://github.com/huge-success/sanic/pull/1481>`_
Accept negative values for route parameters with type ``int`` or ``number``.
*
`#1476 <https://github.com/huge-success/sanic/pull/1476>`_
Deprecated the use of ``sanic.request.Request.raw_args`` - it has a
fundamental flaw in which is drops repeated query string parameters.
Added ``sanic.request.Request.query_args`` as a replacement for the
original use-case.
*
`#1472 <https://github.com/huge-success/sanic/pull/1472>`_
Remove an unwanted ``None`` check in Request class ``repr`` implementation.
This changes the default ``repr`` of a Request from ``<Request>`` to
``<Request: None />``
*
`#1470 <https://github.com/huge-success/sanic/pull/1470>`_
Added 2 new parameters to ``sanic.app.Sanic.create_server``\ :
* ``return_asyncio_server`` - whether to return an asyncio.Server.
* ``asyncio_server_kwargs`` - kwargs to pass to ``loop.create_server`` for
the event loop that sanic is using.
This is a breaking change.
*
`#1499 <https://github.com/huge-success/sanic/pull/1499>`_
Added a set of test cases that test and benchmark route resolution.
*
`#1457 <https://github.com/huge-success/sanic/pull/1457>`_
The type of the ``"max-age"`` value in a ``sanic.cookies.Cookie`` is now
enforced to be an integer. Non-integer values are replaced with ``0``.
*
`#1445 <https://github.com/huge-success/sanic/pull/1445>`_
Added the ``endpoint`` attribute to an incoming ``request``\ , containing the
name of the handler function.
*
`#1423 <https://github.com/huge-success/sanic/pull/1423>`_
Improved request streaming. ``request.stream`` is now a bounded-size buffer
instead of an unbounded queue. Callers must now call
``await request.stream.read()`` instead of ``await request.stream.get()``
to read each portion of the body.
This is a breaking change.
Bugfixes
********
*
`#1502 <https://github.com/huge-success/sanic/pull/1502>`_
Sanic was prefetching ``time.time()`` and updating it once per second to
avoid excessive ``time.time()`` calls. The implementation was observed to
cause memory leaks in some cases. The benefit of the prefetch appeared
to negligible, so this has been removed. Fixes
`#1500 <https://github.com/huge-success/sanic/pull/1500>`_
*
`#1501 <https://github.com/huge-success/sanic/pull/1501>`_
Fix a bug in the auto-reloader when the process was launched as a module
i.e. ``python -m init0.mod1`` where the sanic server is started
in ``init0/mod1.py`` with ``debug`` enabled and imports another module in
``init0``.
*
`#1376 <https://github.com/huge-success/sanic/pull/1376>`_
Allow sanic test client to bind to a random port by specifying
``port=None`` when constructing a ``SanicTestClient``
*
`#1399 <https://github.com/huge-success/sanic/pull/1399>`_
Added the ability to specify middleware on a blueprint group, so that all
routes produced from the blueprints in the group have the middleware
applied.
*
`#1442 <https://github.com/huge-success/sanic/pull/1442>`_
Allow the the use the ``SANIC_ACCESS_LOG`` environment variable to
enable/disable the access log when not explicitly passed to ``app.run()``.
This allows the access log to be disabled for example when running via
gunicorn.
Developer infrastructure
************************
* `#1529 <https://github.com/huge-success/sanic/pull/1529>`_ Update project PyPI credentials
* `#1515 <https://github.com/huge-success/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
* `#1490 <https://github.com/huge-success/sanic/pull/1490>`_ Fix python version in doc build
* `#1478 <https://github.com/huge-success/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
* `#1464 <https://github.com/huge-success/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
Improved Documentation
**********************
* `#1516 <https://github.com/huge-success/sanic/pull/1516>`_ Fix typo at the exception documentation
* `#1510 <https://github.com/huge-success/sanic/pull/1510>`_ fix typo in Asyncio example
* `#1486 <https://github.com/huge-success/sanic/pull/1486>`_ Documentation typo
* `#1477 <https://github.com/huge-success/sanic/pull/1477>`_ Fix grammar in README.md
* `#1489 <https://github.com/huge-success/sanic/pull/1489>`_ Added "databases" to the extensions list
* `#1483 <https://github.com/huge-success/sanic/pull/1483>`_ Add sanic-zipkin to extensions list
* `#1487 <https://github.com/huge-success/sanic/pull/1487>`_ Removed link to deleted repo, Sanic-OAuth, from the extensions list
* `#1460 <https://github.com/huge-success/sanic/pull/1460>`_ 18.12 changelog
* `#1449 <https://github.com/huge-success/sanic/pull/1449>`_ Add example of amending request object
* `#1446 <https://github.com/huge-success/sanic/pull/1446>`_ Update README
* `#1444 <https://github.com/huge-success/sanic/pull/1444>`_ Update README
* `#1443 <https://github.com/huge-success/sanic/pull/1443>`_ Update README, including new logo
* `#1440 <https://github.com/huge-success/sanic/pull/1440>`_ fix minor type and pip install instruction mismatch
* `#1424 <https://github.com/huge-success/sanic/pull/1424>`_ Documentation Enhancements
Note: 19.3.0 was skipped for packagement purposes and not released on PyPI
Version 18.12
=============
18.12.0
*******
*
Changes:
* Improved codebase test coverage from 81% to 91%.
* Added stream_large_files and host examples in static_file document
* Added methods to append and finish body content on Request (#1379)
* Integrated with .appveyor.yml for windows ci support
* Added documentation for AF_INET6 and AF_UNIX socket usage
* Adopt black/isort for codestyle
* Cancel task when connection_lost
* Simplify request ip and port retrieval logic
* Handle config error in load config file.
* Integrate with codecov for CI
* Add missed documentation for config section.
* Deprecate Handler.log
* Pinned httptools requirement to version 0.0.10+
*
Fixes:
* Fix ``remove_entity_headers`` helper function (#1415)
* Fix TypeError when use Blueprint.group() to group blueprint with default url_prefix, Use os.path.normpath to avoid invalid url_prefix like api//v1
f8a6af1 Rename the ``http`` module to ``helpers`` to prevent conflicts with the built-in Python http library (fixes #1323)
* Fix unittests on windows
* Fix Namespacing of sanic logger
* Fix missing quotes in decorator example
* Fix redirect with quoted param
* Fix doc for latest blueprint code
* Fix build of latex documentation relating to markdown lists
* Fix loop exception handling in app.py
* Fix content length mismatch in windows and other platform
* Fix Range header handling for static files (#1402)
* Fix the logger and make it work (#1397)
* Fix type pikcle->pickle in multiprocessing test
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
* Fix document for logging
Version 0.8
===========
0.8.3
*****
* Changes:
* Ownership changed to org 'huge-success'
0.8.0
*****
* Changes:
* Add Server-Sent Events extension (Innokenty Lebedev)
* Graceful handling of request_handler_task cancellation (Ashley Sommer)
* Sanitize URL before redirection (aveao)
* Add url_bytes to request (johndoe46)
* py37 support for travisci (yunstanford)
* Auto reloader support for OSX (garyo)
* Add UUID route support (Volodymyr Maksymiv)
* Add pausable response streams (Ashley Sommer)
* Add weakref to request slots (vopankov)
* remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
* Allow streaming handlers in add_route (kinware)
* use travis_retry for tox (Raphael Deem)
* update aiohttp version for test client (yunstanford)
* add redirect import for clarity (yingshaoxo)
* Update HTTP Entity headers (Arnulfo Solís)
* Add register_listener method (Stephan Fitzpatrick)
* Remove uvloop/ujson dependencies for Windows (abuckenheimer)
* Content-length header on 204/304 responses (Arnulfo Solís)
* Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
* Update development status from pre-alpha to beta (Maksim Anisenkov)
* KeepAlive Timout log level changed to debug (Arnulfo Solís)
* Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
* Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
* Add support for blueprint groups and nesting (Elias Tarhini)
* Remove uvloop for windows setup (Aleksandr Kurlov)
* Auto Reload (Yaser Amari)
* Documentation updates/fixups (multiple contributors)
* Fixes:
* Fix: auto_reload in Linux (Ashley Sommer)
* Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
* Fix: disable auto_reload by default on windows (abuckenheimer)
* Fix (1143): Turn off access log with gunicorn (hqy)
* Fix (1268): Support status code for file response (Cosmo Borsky)
* Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
* Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
* Fix (1242): Responses for CI header (yunstanford)
* Fix (1237): add version constraint for websockets (yunstanford)
* Fix (1231): memory leak - always release resource (Phillip Xu)
* Fix (1221): make request truthy if transport exists (Raphael Deem)
* Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
* Fix try_everything examples (PyManiacGR, kot83)
* Fix (1158): default to auto_reload in debug mode (Raphael Deem)
* Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
* Fix: raw requires bytes-like object (cloudship)
* Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
* Fix: Bug in multipart/form-data parser (DirkGuijt)
* Fix: Exception for missing parameter when value is null (NyanKiyoshi)
* Fix: Parameter check (Howie Hu)
* Fix (1089): Routing issue with named parameters and different methods (yunstanford)
* Fix (1085): Signal handling in multi-worker mode (yunstanford)
* Fix: single quote in readme.rst (Cosven)
* Fix: method typos (Dmitry Dygalo)
* Fix: log_response correct output for ip and port (Wibowo Arindrarto)
* Fix (1042): Exception Handling (Raphael Deem)
* Fix: Chinese URIs (Howie Hu)
* Fix (1079): timeout bug when self.transport is None (Raphael Deem)
* Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
* Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
* Fix (1065): allow add_task after server starts (Raphael Deem)
* Fix (1061): double quotes in unauthorized exception (Raphael Deem)
* Fix (1062): inject the app in add_task method (Raphael Deem)
* Fix: update environment.yml for readthedocs (Eli Uriegas)
* Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
* Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
* Fix: IPv6 Address and Socket Data Format (Dan Palmer)
Note: Changelog was unmaintained between 0.1 and 0.7
Version 0.1
===========
0.1.7
*****
* Reversed static url and directory arguments to meet spec
0.1.6
*****
* Static files
* Lazy Cookie Loading
0.1.5
*****
* Cookies
* Blueprint listeners and ordering
* Faster Router
* Fix: Incomplete file reads on medium+ sized post requests
* Breaking: after_start and before_stop now pass sanic as their first argument
0.1.4
*****
* Multiprocessing
0.1.3
*****
* Blueprint support
* Faster Response processing
0.1.1 - 0.1.2
*************
* Struggling to update pypi via CI
0.1.0
*****
* Released to public

View File

@@ -1,72 +0,0 @@
# Contributing
Thank you for your interest! Sanic is always looking for contributors. If you
don't feel comfortable contributing code, adding docstrings to the source files
is very appreciated.
We are committed to providing a friendly, safe and welcoming environment for all,
regardless of gender, sexual orientation, disability, ethnicity, religion,
or similar personal characteristic.
Our [code of conduct](./CONDUCT.md) sets the standards for behavior.
## Installation
To develop on sanic (and mainly to just run the tests) it is highly recommend to
install from sources.
So assume you have already cloned the repo and are in the working directory with
a virtual environment already set up, then run:
```bash
python setup.py develop && pip install -r requirements-dev.txt
```
## Running tests
To run the tests for sanic it is recommended to use tox like so:
```bash
tox
```
See it's that simple!
## Pull requests!
So the pull request approval rules are pretty simple:
1. All pull requests must pass unit tests.
2. All pull requests must be reviewed and approved by at least
one current collaborator on the project.
3. All pull requests must pass flake8 checks.
4. All pull requests must be consistent with the existing code.
5. If you decide to remove/change anything from any common interface
a deprecation message should accompany it.
6. If you implement a new feature you should have at least one unit
test to accompany it.
7. An example must be one of the following:
* Example of how to use Sanic
* Example of how to use Sanic extensions
* Example of how to use Sanic and asynchronous library
## Documentation
Sanic's documentation is built
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
Markdown and can be found in the `docs` folder, while the module reference is
automatically generated using `sphinx-apidoc`.
To generate the documentation from scratch:
```bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
```
The HTML documentation will be created in the `docs/_build` folder.
## Warning
One of the main goals of Sanic is speed. Code that lowers the performance of
Sanic without significant gains in usability, security, or features may not be
merged. Please don't let this intimidate you! If you have any concerns about an
idea, open an issue for discussion and help.

252
CONTRIBUTING.rst Normal file
View File

@@ -0,0 +1,252 @@
Contributing
============
Thank you for your interest! Sanic is always looking for contributors. If you
don't feel comfortable contributing code, adding docstrings to the source files
is very appreciated.
We are committed to providing a friendly, safe and welcoming environment for all,
regardless of gender, sexual orientation, disability, ethnicity, religion,
or similar personal characteristic.
Our `code of conduct <./CONDUCT.md>`_ sets the standards for behavior.
Installation
------------
To develop on sanic (and mainly to just run the tests) it is highly recommend to
install from sources.
So assume you have already cloned the repo and are in the working directory with
a virtual environment already set up, then run:
.. code-block:: bash
pip3 install -e . ".[dev]"
Dependency Changes
------------------
``Sanic`` doesn't use ``requirements*.txt`` files to manage any kind of dependencies related to it in order to simplify the
effort required in managing the dependencies. Please make sure you have read and understood the following section of
the document that explains the way ``sanic`` manages dependencies inside the ``setup.py`` file.
.. list-table::
:header-rows: 1
* - Dependency Type
- Usage
- Installation
* - requirements
- Bare minimum dependencies required for sanic to function
- ``pip3 install -e .``
* - tests_require / extras_require['test']
- Dependencies required to run the Unit Tests for ``sanic``
- ``pip3 install -e '.[test]'``
* - extras_require['dev']
- Additional Development requirements to add contributing
- ``pip3 install -e '.[dev]'``
* - extras_require['docs']
- Dependencies required to enable building and enhancing sanic documentation
- ``pip3 install -e '.[docs]'``
Running all tests
-----------------
To run the tests for Sanic it is recommended to use tox like so:
.. code-block:: bash
tox
See it's that simple!
``tox.ini`` contains different environments. Running ``tox`` without any arguments will
run all unittests, perform lint and other checks.
Run unittests
-------------
``tox`` environment -> ``[testenv]`
To execute only unittests, run ``tox`` with environment like so:
.. code-block:: bash
tox -e py36 -v -- tests/test_config.py
# or
tox -e py37 -v -- tests/test_config.py
Run lint checks
---------------
``tox`` environment -> ``[testenv:lint]``
Permform ``flake8``\ , ``black`` and ``isort`` checks.
.. code-block:: bash
tox -e lint
Run other checks
----------------
``tox`` environment -> ``[testenv:check]``
Perform other checks.
.. code-block:: bash
tox -e check
Run Static Analysis
-------------------
``tox`` environment -> ``[testenv:security]``
Perform static analysis security scan
.. code-block:: bash
tox -e security
Run Documentation sanity check
------------------------------
``tox`` environment -> ``[testenv:docs]``
Perform sanity check on documentation
.. code-block:: bash
tox -e docs
Code Style
----------
To maintain the code consistency, Sanic uses following tools.
#. `isort <https://github.com/timothycrosley/isort>`_
#. `black <https://github.com/python/black>`_
#. `flake8 <https://github.com/PyCQA/flake8>`_
isort
*****
``isort`` sorts Python imports. It divides imports into three
categories sorted each in alphabetical order.
#. built-in
#. third-party
#. project-specific
black
*****
``black`` is a Python code formatter.
flake8
******
``flake8`` is a Python style guide that wraps following tools into one.
#. PyFlakes
#. pycodestyle
#. Ned Batchelder's McCabe script
``isort``\ , ``black`` and ``flake8`` checks are performed during ``tox`` lint checks.
Refer `tox <https://tox.readthedocs.io/en/latest/index.html>`_ documentation for more details.
Pull requests
-------------
So the pull request approval rules are pretty simple:
#. All pull requests must have a changelog details associated with it.
#. All pull requests must pass unit tests.
#. All pull requests must be reviewed and approved by at least one current collaborator on the project.
#. All pull requests must pass flake8 checks.
#. All pull requests must be consistent with the existing code.
#. If you decide to remove/change anything from any common interface a deprecation message should accompany it.
#. If you implement a new feature you should have at least one unit test to accompany it.
#. An example must be one of the following:
* Example of how to use Sanic
* Example of how to use Sanic extensions
* Example of how to use Sanic and asynchronous library
Changelog
---------
It is mandatory to add documentation for Change log as part of your Pull request when you fix/contribute something
to the ``sanic`` community. This will enable us in generating better and well defined change logs during the
release which can aid community users in a great way.
.. note::
Single line explaining the details of the PR in brief
Detailed description of what the PR is about and what changes or enhancements are being done.
No need to include examples or any other details here. But it is important that you provide
enough context here to let user understand what this change is all about and why it is being
introduced into the ``sanic`` codebase.
Make sure you leave an line space after the first line to make sure the document rendering is clean
.. list-table::
:header-rows: 1
* - Contribution Type
- Changelog file name format
- Changelog file location
* - Features
- <git_issue>.feature.rst
- ``changelogs``
* - Bugfixes
- <git_issue>.bugfix.rst
- ``changelogs``
* - Improved Documentation
- <git_issue>.doc.rst
- ``changelogs``
* - Deprecations and Removals
- <git_issue>.removal.rst
- ``changelogs``
* - Miscellaneous internal changes
- <git_issue>.misc.rst
- ``changelogs``
Documentation
-------------
Sanic's documentation is built
using `sphinx <http://www.sphinx-doc.org/en/1.5.1/>`_. Guides are written in
Markdown and can be found in the ``docs`` folder, while the module reference is
automatically generated using ``sphinx-apidoc``.
To generate the documentation from scratch:
.. code-block:: bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
# There is a simple make command provided to ease the work required in generating
# the documentation
make docs
The HTML documentation will be created in the ``docs/_build`` folder.
.. warning::
One of the main goals of Sanic is speed. Code that lowers the performance of
Sanic without significant gains in usability, security, or features may not be
merged. Please don't let this intimidate you! If you have any concerns about an
idea, open an issue for discussion and help.

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

@@ -1,7 +1,15 @@
include README.rst
include MANIFEST.in
# Non Code related contents
include LICENSE
include setup.py
include README.rst
include pyproject.toml
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
# Setup
include setup.py
include Makefile
# Tests
include .coveragerc
graft tests
global-exclude __pycache__
global-exclude *.py[co]

View File

@@ -1,4 +1,95 @@
test:
find . -name "*.pyc" -delete
.PHONY: help test test-coverage install docker-test black fix-import beautify
.DEFAULT: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo "test"
@echo " Run Sanic Unit Tests"
@echo "test-coverage"
@echo " Run Sanic Unit Tests with Coverage"
@echo "install"
@echo " Install Sanic"
@echo "docker-test"
@echo " Run Sanic Unit Tests using Docker"
@echo "black"
@echo " Analyze and fix linting issues using Black"
@echo "fix-import"
@echo " Analyze and fix import order using isort"
@echo "beautify [sort_imports=1] [include_tests=1]"
@echo " Analyze and fix linting issue using black and optionally fix import sort using isort"
@echo ""
@echo "docs"
@echo " Generate Sanic documentation"
@echo ""
@echo "clean-docs"
@echo " Clean Sanic documentation"
@echo ""
@echo "docs-test"
@echo " Test Sanic Documentation for errors"
@echo ""
@echo "changelog"
@echo " Generate changelog for Sanic to prepare for new release"
@echo ""
@echo "release"
@echo " Prepare Sanic for a new changes by version bump and changelog"
@echo ""
clean:
find . ! -path "./.eggs/*" -name "*.pyc" -exec rm {} \;
find . ! -path "./.eggs/*" -name "*.pyo" -exec rm {} \;
find . ! -path "./.eggs/*" -name ".coverage" -exec rm {} \;
rm -rf build/* > /dev/null 2>&1
rm -rf dist/* > /dev/null 2>&1
test: clean
python setup.py test
test-coverage: clean
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
install:
python setup.py install
docker-test: clean
docker build -t sanic/test-image -f docker/Dockerfile .
docker run -t sanic/test-image tox
beautify: black
ifdef sort_imports
ifdef include_tests
$(warning It is suggested that you do not run sort import on tests)
isort -rc sanic tests
else
$(info Sorting Imports)
isort -rc sanic tests
endif
endif
black:
black --config ./.black.toml sanic tests
fix-import: black
isort -rc sanic tests
docs-clean:
cd docs && make clean
docs: docs-clean
cd docs && make html
docs-test: docs-clean
cd docs && make dummy
changelog:
python scripts/changelog.py
release:
ifdef version
python scripts/release.py --release-version ${version} --generate-changelog
else
python scripts/release.py --generate-changelog
endif

View File

@@ -1,15 +1,88 @@
Sanic
=====
.. image:: https://raw.githubusercontent.com/huge-success/sanic-assets/master/png/sanic-framework-logo-400x97.png
:alt: Sanic | Build fast. Run fast.
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |AppVeyor Build Status| |Documentation| |Codecov| |PyPI| |PyPI version| |Code style black|
Sanic | Build fast. Run fast.
=============================
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
.. start-badges
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
.. list-table::
:stub-columns: 1
Sanic is developed `on GitHub <https://github.com/huge-success/sanic/>`_. We also have `a community discussion board <https://community.sanicframework.org/>`_. Contributions are welcome!
* - Build
- | |Build Status| |AppVeyor Build Status| |Codecov|
* - Docs
- |Documentation|
* - Package
- | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black|
* - Support
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby| |Awesome|
* - Stats
- | |Downloads| |Conda downloads|
.. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg
:target: https://community.sanicframework.org/
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/huge-success/sanic
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
:target: https://travis-ci.org/huge-success/sanic
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
:target: https://ci.appveyor.com/project/huge-success/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
.. |Wheel| image:: https://img.shields.io/pypi/wheel/sanic.svg
:alt: PyPI Wheel
:target: https://pypi.python.org/pypi/sanic
.. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg
:alt: Supported implementations
:target: https://pypi.python.org/pypi/sanic
.. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg
:alt: Awesome Sanic List
:target: https://github.com/mekicha/awesome-sanic
.. |Downloads| image:: https://pepy.tech/badge/sanic/month
:alt: Downloads
:target: https://pepy.tech/project/sanic
.. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg
:alt: Downloads
:target: https://anaconda.org/conda-forge/sanic
.. end-badges
Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
The project is maintained by the community, for the community. **Contributions are welcome!**
The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.
Installation
------------
``pip3 install sanic``
Sanic makes use of ``uvloop`` and ``ujson`` to help with performance. If you do not want to use those packages, simply add an environmental variable ``SANIC_NO_UVLOOP=true`` or ``SANIC_NO_UJSON=true`` at install time.
.. code:: shell
$ export SANIC_NO_UVLOOP=true
$ export SANIC_NO_UJSON=true
$ pip3 install --no-binary :all: sanic
.. note::
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
use ``sanic`` with ``ujson`` dependency.
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/huge-success/sanic/issues/396>`_ that we use to track those projects!
Hello World Example
-------------------
@@ -27,17 +100,27 @@ Hello World Example
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
Sanic can now be easily run using ``python3 hello.py``.
Installation
------------
.. code::
- ``pip install sanic``
[2018-12-30 11:37:41 +0200] [13564] [INFO] Goin' Fast @ http://0.0.0.0:8000
[2018-12-30 11:37:41 +0200] [13564] [INFO] Starting worker [13564]
To install sanic without uvloop or ujson using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
installation.
And, we can verify it is working: ``curl localhost:8000 -i``
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip install sanic``
.. code::
HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: 5
Content-Length: 17
Content-Type: application/json
{"hello":"world"}
**Now, let's go build something fast!**
Documentation
@@ -45,59 +128,18 @@ Documentation
`Documentation on Readthedocs <http://sanic.readthedocs.io/>`_.
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/huge-success/sanic
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
:target: https://travis-ci.org/huge-success/sanic
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
:target: https://ci.appveyor.com/project/huge-success/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
Changelog
---------
`Release Changelogs <https://github.com/huge-success/sanic/blob/master/CHANGELOG.md>`_.
Questions and Discussion
------------------------
`Ask a question or join the conversation <https://community.sanicframework.org/>`_.
Contribution
------------
Examples
--------
`Non-Core examples <https://github.com/huge-success/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
`Extensions <https://github.com/huge-success/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
`Projects <https://github.com/huge-success/sanic/wiki/Projects/>`_. Sanic in production use.
Final Thoughts
--------------
::
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/huge-success/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://sanic.readthedocs.io/en/latest/sanic/contributing.html>`_.

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.

2
changelogs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Except this file
!.gitignore

View File

@@ -10,10 +10,8 @@
import os
import sys
# Add support for Markdown documentation using Recommonmark
from recommonmark.parser import CommonMarkParser
# Add support for auto-doc
import recommonmark
from recommonmark.transform import AutoStructify
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
@@ -25,12 +23,11 @@ import sanic
# -- General configuration ------------------------------------------------
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio']
extensions = ['sphinx.ext.autodoc', "recommonmark"]
templates_path = ['_templates']
# Enable support for both Restructured Text and Markdown
source_parsers = {'.md': CommonMarkParser}
source_suffix = ['.rst', '.md']
# The master toctree document.
@@ -38,7 +35,7 @@ master_doc = 'index'
# General information about the project.
project = 'Sanic'
copyright = '2016, Sanic contributors'
copyright = '2018, Sanic contributors'
author = 'Sanic contributors'
# The version info for the project you're documenting, acts as replacement for
@@ -149,6 +146,6 @@ suppress_warnings = ['image.nonlocal_uri']
def setup(app):
app.add_config_value('recommonmark_config', {
'enable_eval_rst': True,
'enable_auto_doc_ref': True,
'enable_auto_doc_ref': False,
}, True)
app.add_transform(AutoStructify)

View File

@@ -7,30 +7,33 @@ Guides
:maxdepth: 2
sanic/getting_started
sanic/routing
sanic/config
sanic/logging
sanic/request_data
sanic/response
sanic/cookies
sanic/routing
sanic/blueprints
sanic/static_files
sanic/versioning
sanic/exceptions
sanic/middleware
sanic/blueprints
sanic/websocket
sanic/config
sanic/cookies
sanic/decorators
sanic/streaming
sanic/class_based_views
sanic/custom_protocol
sanic/sockets
sanic/ssl
sanic/logging
sanic/versioning
sanic/debug_mode
sanic/testing
sanic/deploying
sanic/extensions
sanic/examples
sanic/changelog
sanic/contributing
sanic/api_reference
sanic/asyncio_python37
Module Documentation

View File

@@ -20,6 +20,15 @@ sanic.blueprints module
:undoc-members:
:show-inheritance:
sanic.blueprint_group module
----------------------------
.. automodule:: sanic.blueprint_group
:members:
:undoc-members:
:show-inheritance:
sanic.config module
-------------------

View File

@@ -0,0 +1,58 @@
Python 3.7 AsyncIO examples
###########################
With Python 3.7 AsyncIO got major update for the following types:
- asyncio.AbstractEventLoop
- asyncio.AbstractServer
This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance:
.. code:: python
import asyncio
import socket
import os
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/")
async def test(request):
return json({"hello": "world"})
server_socket = '/tmp/sanic.sock'
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
os.remove(server_socket)
finally:
sock.bind(server_socket)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
srv_coro = app.create_server(
sock=sock,
return_asyncio_server=True,
asyncio_server_kwargs=dict(
start_serving=False
)
)
srv = loop.run_until_complete(srv_coro)
try:
assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
loop.run_until_complete(srv.serve_forever())
except KeyboardInterrupt:
srv.close()
loop.close()
Please note that uvloop does not support these features yet.

View File

@@ -118,16 +118,16 @@ app = Sanic(__name__)
app.blueprint(api)
```
## Using blueprints
## Using Blueprints
Blueprints have much the same functionality as an application instance.
Blueprints have almost the same functionality as an application instance.
### WebSocket routes
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
decorator or `bp.add_websocket_route` method.
### Middleware
### Blueprint Middleware
Using blueprints allows you to also register middleware globally.
@@ -145,6 +145,36 @@ async def halt_response(request, response):
return text('I halted the response')
```
### Blueprint Group Middleware
Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the
current blueprint group under consideration.
```python
bp1 = Blueprint('bp1', url_prefix='/bp1')
bp2 = Blueprint('bp2', url_prefix='/bp2')
@bp1.middleware('request')
async def bp1_only_middleware(request):
print('applied on Blueprint : bp1 Only')
@bp1.route('/')
async def bp1_route(request):
return text('bp1')
@bp2.route('/<param>')
async def bp2_route(request, param):
return text(param)
group = Blueprint.group(bp1, bp2)
@group.middleware('request')
async def group_middleware(request):
print('common middleware applied for both bp1 and bp2')
# Register Blueprint group under the app
app.blueprint(group)
```
### Exceptions
Exceptions can be applied exclusively to blueprints globally.
@@ -201,7 +231,7 @@ async def close_connection(app, loop):
Blueprints can be very useful for API versioning, where one blueprint may point
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
When a blueprint is initialised, it can take an optional `url_prefix` argument,
When a blueprint is initialised, it can take an optional `version` argument,
which will be prepended to all routes defined on the blueprint. This feature
can be used to implement our API versioning scheme.
@@ -210,8 +240,8 @@ can be used to implement our API versioning scheme.
from sanic.response import text
from sanic import Blueprint
blueprint_v1 = Blueprint('v1', url_prefix='/v1')
blueprint_v2 = Blueprint('v2', url_prefix='/v2')
blueprint_v1 = Blueprint('v1', url_prefix='/api', version="v1")
blueprint_v2 = Blueprint('v2', url_prefix='/api', version="v2")
@blueprint_v1.route('/')
async def api_v1_root(request):
@@ -222,7 +252,7 @@ async def api_v2_root(request):
return text('Welcome to version 2 of our documentation')
```
When we register our blueprints on the app, the routes `/v1` and `/v2` will now
When we register our blueprints on the app, the routes `/v1/api` and `/v2/api` will now
point to the individual blueprints, which allows the creation of *sub-sites*
for each API version.
@@ -232,8 +262,8 @@ from sanic import Sanic
from blueprints import blueprint_v1, blueprint_v2
app = Sanic(__name__)
app.blueprint(blueprint_v1, url_prefix='/v1')
app.blueprint(blueprint_v2, url_prefix='/v2')
app.blueprint(blueprint_v1)
app.blueprint(blueprint_v2)
app.run(host='0.0.0.0', port=8000, debug=True)
```
@@ -246,7 +276,7 @@ takes the format `<blueprint_name>.<handler_name>`. For example:
```python
@blueprint_v1.route('/')
async def root(request):
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/api/post/5'
return redirect(url)

4
docs/sanic/changelog.rst Normal file
View File

@@ -0,0 +1,4 @@
Changelog
---------
.. include:: ../../CHANGELOG.rst

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,29 +92,56 @@ DB_USER = 'appuser'
Out of the box there are just a few predefined values which can be overwritten when creating the application.
| Variable | Default | Description |
| ------------------------- | --------- | ------------------------------------------------------ |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long take 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:
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly.
#### `REQUEST_TIMEOUT`
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response.
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the
Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the
`REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates an `HTTP 408` response
and sends that to the client. Set this parameter's value higher if your clients routinely pass very large request payloads
or upload requests very slowly.
### What is Keep Alive? And what does the Keep Alive Timeout value do?
#### `RESPONSE_TIMEOUT`
Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server.
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the
Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT`
value (in seconds), this is considered a Server Error so Sanic generates an `HTTP 503` response and sends that to the
client. Set this parameter's value higher if your application is likely to have long-running process that delay the
generation of a response.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request.
#### `KEEP_ALIVE_TIMEOUT`
The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long.
##### What is Keep Alive? And what does the Keep Alive Timeout value do?
`Keep-Alive` is a HTTP feature introduced in `HTTP 1.1`. When sending a HTTP request, the client (usually a web browser application)
can set a `Keep-Alive` header to indicate the http server (Sanic) to not close the TCP connection after it has send the response.
This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient
network traffic for both the client and the server.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application,
set it to `False` to cause all client connections to close immediately after a response is sent, regardless of
the `Keep-Alive` header on the request.
The amount of time the server holds the TCP connection open is decided by the server itself.
In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds.
This is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for
the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless
you know your clients are using a browser which supports TCP connections held open for that long.
For reference:
```
@@ -119,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,62 +0,0 @@
# Contributing
Thank you for your interest! Sanic is always looking for contributors. If you
don't feel comfortable contributing code, adding docstrings to the source files
is very appreciated.
## Installation
To develop on sanic (and mainly to just run the tests) it is highly recommend to
install from sources.
So assume you have already cloned the repo and are in the working directory with
a virtual environment already set up, then run:
```bash
python setup.py develop && pip install -r requirements-dev.txt
```
## Running tests
To run the tests for sanic it is recommended to use tox like so:
```bash
tox
```
See it's that simple!
## Pull requests!
So the pull request approval rules are pretty simple:
* All pull requests must pass unit tests
* All pull requests must be reviewed and approved by at least
one current collaborator on the project
* All pull requests must pass flake8 checks
* If you decide to remove/change anything from any common interface
a deprecation message should accompany it.
* If you implement a new feature you should have at least one unit
test to accompany it.
## Documentation
Sanic's documentation is built
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
Markdown and can be found in the `docs` folder, while the module reference is
automatically generated using `sphinx-apidoc`.
To generate the documentation from scratch:
```bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
```
The HTML documentation will be created in the `docs/_build` folder.
## Warning
One of the main goals of Sanic is speed. Code that lowers the performance of
Sanic without significant gains in usability, security, or features may not be
merged. Please don't let this intimidate you! If you have any concerns about an
idea, open an issue for discussion and help.

View File

@@ -0,0 +1 @@
.. include:: ../../CONTRIBUTING.rst

View File

@@ -1,72 +0,0 @@
# Custom Protocols
*Note: this is advanced usage, and most readers will not need such functionality.*
You can change the behavior of Sanic's protocol by specifying a custom
protocol, which should be a subclass
of
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method.
The constructor of the custom protocol class receives the following keyword
arguments from Sanic.
- `loop`: an `asyncio`-compatible event loop.
- `connections`: a `set` to store protocol objects. When Sanic receives
`SIGINT` or `SIGTERM`, it executes `protocol.close_if_idle` for all protocol
objects stored in this set.
- `signal`: a `sanic.server.Signal` object with the `stopped` attribute. When
Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` is assigned `True`.
- `request_handler`: a coroutine that takes a `sanic.request.Request` object
and a `response` callback as arguments.
- `error_handler`: a `sanic.exceptions.Handler` which is called when exceptions
are raised.
- `request_timeout`: the number of seconds before a request times out.
- `request_max_size`: an integer specifying the maximum size of a request, in bytes.
## Example
An error occurs in the default protocol if a handler function does not return
an `HTTPResponse` object.
By overriding the `write_response` protocol method, if a handler returns a
string it will be converted to an `HTTPResponse object`.
```python
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic(__name__)
class CustomHttpProtocol(HttpProtocol):
def __init__(self, *, loop, request_handler, error_handler,
signal, connections, request_timeout, request_max_size):
super().__init__(
loop=loop, request_handler=request_handler,
error_handler=error_handler, signal=signal,
connections=connections, request_timeout=request_timeout,
request_max_size=request_max_size)
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/')
async def string(request):
return 'string'
@app.route('/1')
async def response(request):
return text('response')
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
```

View File

@@ -0,0 +1,76 @@
Custom Protocols
================
.. note::
This is advanced usage, and most readers will not need such functionality.
You can change the behavior of Sanic's protocol by specifying a custom
protocol, which should be a subclass
of `asyncio.protocol <https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes>`_.
This protocol can then be passed as the keyword argument ``protocol`` to the ``sanic.run`` method.
The constructor of the custom protocol class receives the following keyword
arguments from Sanic.
- ``loop``: an ``asyncio``-compatible event loop.
- ``connections``: a ``set`` to store protocol objects. When Sanic receives
``SIGINT`` or ``SIGTERM``, it executes ``protocol.close_if_idle`` for all protocol
objects stored in this set.
- ``signal``: a ``sanic.server.Signal`` object with the ``stopped`` attribute. When
Sanic receives ``SIGINT`` or ``SIGTERM``, ``signal.stopped`` is assigned ``True``.
- ``request_handler``: a coroutine that takes a ``sanic.request.Request`` object
and a ``response`` callback as arguments.
- ``error_handler``: a ``sanic.exceptions.Handler`` which is called when exceptions
are raised.
- ``request_timeout``: the number of seconds before a request times out.
- ``request_max_size``: an integer specifying the maximum size of a request, in bytes.
Example
-------
An error occurs in the default protocol if a handler function does not return
an ``HTTPResponse`` object.
By overriding the ``write_response`` protocol method, if a handler returns a
string it will be converted to an ``HTTPResponse object``.
.. code:: python
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic(__name__)
class CustomHttpProtocol(HttpProtocol):
def __init__(self, *, loop, request_handler, error_handler,
signal, connections, request_timeout, request_max_size):
super().__init__(
loop=loop, request_handler=request_handler,
error_handler=error_handler, signal=signal,
connections=connections, request_timeout=request_timeout,
request_max_size=request_max_size)
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/')
async def string(request):
return 'string'
@app.route('/1')
async def response(request):
return text('response')
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)

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.
@@ -15,8 +20,15 @@ keyword arguments:
- `protocol` *(default `HttpProtocol`)*: Subclass
of
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
- `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server).
## Workers
```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.
@@ -28,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:
@@ -45,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.
@@ -63,16 +102,59 @@ of the memory leak.
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
## Asynchronous support
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
However be advised that this method does not support using multiple processes, and is not the preferred way
## 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.
```python
app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False)
```
Running via Gunicorn you can set Environment variable `SANIC_ACCESS_LOG="False"`
```
env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning
```
Or you can rewrite app config directly
```python
app.config.ACCESS_LOG = False
```
### Asynchronous support 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()
```
```

167
docs/sanic/examples.rst Normal file
View File

@@ -0,0 +1,167 @@
Examples
========
This section of the documentation is a simple collection of example code that can help you get a quick start
on your application development. Most of these examples are categorized and provide you with a link to the
working code example in the `Sanic Repository <https://github.com/huge-success/sanic/tree/master/examples>`_
Basic Examples
--------------
This section of the examples are a collection of code that provide a simple use case example of the sanic application.
Simple Apps
~~~~~~~~~~~~
A simple sanic application with a single ``async`` method with ``text`` and ``json`` type response.
.. literalinclude:: ../../examples/teapot.py
.. literalinclude:: ../../examples/simple_server.py
Simple App with ``Sanic Views``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Showcasing the simple mechanism of using :class:`sanic.viewes.HTTPMethodView` as well as a way to extend the same
into providing a custom ``async`` behavior for ``view``.
.. literalinclude:: ../../examples/simple_async_view.py
URL Redirect
~~~~~~~~~~~~
.. literalinclude:: ../../examples/redirect_example.py
Named URL redirection
~~~~~~~~~~~~~~~~~~~~~
``Sanic`` provides an easy to use way of redirecting the requests via a helper method called ``url_for`` that takes a
unique url name as argument and returns you the actual route assigned for it. This will help in simplifying the
efforts required in redirecting the user between different section of the application.
.. literalinclude:: ../../examples/url_for_example.py
Blueprints
~~~~~~~~~~
``Sanic`` provides an amazing feature to group your APIs and routes under a logical collection that can easily be
imported and plugged into any of your sanic application and it's called ``blueprints``
.. literalinclude:: ../../examples/blueprints.py
Logging Enhancements
~~~~~~~~~~~~~~~~~~~~
Even though ``Sanic`` comes with a battery of Logging support it allows the end users to customize the way logging
is handled in the application runtime.
.. literalinclude:: ../../examples/override_logging.py
The following sample provides an example code that demonstrates the usage of :func:`sanic.app.Sanic.middleware` in order
to provide a mechanism to assign a unique request ID for each of the incoming requests and log them via
`aiotask-context <https://github.com/Skyscanner/aiotask-context>`_.
.. literalinclude:: ../../examples/log_request_id.py
Sanic Streaming Support
~~~~~~~~~~~~~~~~~~~~~~~
``Sanic`` framework comes with in-built support for streaming large files and the following code explains the process
to setup a ``Sanic`` application with streaming support.
.. literalinclude:: ../../examples/request_stream/server.py
Sample Client app to show the usage of streaming application by a client code.
.. literalinclude:: ../../examples/request_stream/client.py
Sanic Concurrency Support
~~~~~~~~~~~~~~~~~~~~~~~~~
``Sanic`` supports the ability to start an app with multiple worker support. However, it's important to be able to limit
the concurrency per process/loop in order to ensure an efficient execution. The following section of the code provides a
brief example of how to limit the concurrency with the help of :class:`asyncio.Semaphore`
.. literalinclude:: ../../examples/limit_concurrency.py
Sanic Deployment via Docker
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Deploying a ``sanic`` app via ``docker`` and ``docker-compose`` is an easy task to achieve and the following example
provides a deployment of the sample ``simple_server.py``
.. literalinclude:: ../../examples/Dockerfile
.. literalinclude:: ../../examples/docker-compose.yml
Monitoring and Error Handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``Sanic`` provides an extendable bare minimum implementation of a global exception handler via
:class:`sanic.handlers.ErrorHandler`. This example shows how to extend it to enable some custom behaviors.
.. literalinclude:: ../../examples/exception_monitoring.py
Monitoring using external Service Providers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* `LogDNA <https://logdna.com/>`_
.. literalinclude:: ../../examples/logdna_example.py
* `RayGun <https://raygun.com/>`_
.. literalinclude:: ../../examples/raygun_example.py
* `Rollbar <https://rollbar.com>`_
.. literalinclude:: ../../examples/rollbar_example.py
* `Sentry <http://sentry.io>`_
.. literalinclude:: ../../examples/sentry_example.py
Security
~~~~~~~~
The following sample code shows a simple decorator based authentication and authorization mechanism that can be setup
to secure your ``sanic`` api endpoints.
.. literalinclude:: ../../examples/authorized_sanic.py
Sanic Websocket
~~~~~~~~~~~~~~~
``Sanic`` provides an ability to easily add a route and map it to a ``websocket`` handlers.
.. literalinclude:: ../../examples/websocket.html
.. literalinclude:: ../../examples/websocket.py
vhost Suppport
~~~~~~~~~~~~~~
.. literalinclude:: ../../examples/vhosts.py
Unit Testing With Parallel Test Run Support
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following example shows you how to get up and running with unit testing ``sanic`` application with parallel test
execution support provided by the ``pytest-xdist`` plugin.
.. literalinclude:: ../../examples/pytest_xdist.py
Amending Request Object
~~~~~~~~~~~~~~~~~~~~~~~
The ``request`` object in ``Sanic`` is a kind of ``dict`` object, this means that ``request`` object can be manipulated as a regular ``dict`` object.
.. literalinclude:: ../../examples/amending_request_object.py
For more examples and useful samples please visit the `Huge-Sanic's GitHub Page <https://github.com/huge-success/sanic/tree/master/examples>`_

View File

@@ -59,7 +59,7 @@ app = Sanic()
app.error_handler.add(Exception, server_error_handler)
```
In some cases, you might want want to add some more error handling
In some cases, you might want to add some more error handling
functionality to what is provided by default. In that case, you
can subclass Sanic's default error handler as such:

View File

@@ -1,36 +1,3 @@
# Extensions
A list of Sanic extensions created by the community.
- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins.
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
Allows using redis, memcache or an in memory store.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic.
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for Sanic
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
`Babel` library
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
- [sanic-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/))
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.
- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.
Moved to the [awesome-sanic](https://github.com/mekicha/awesome-sanic) list.

View File

@@ -1,14 +1,31 @@
# 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
```
python3 -m pip 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`)
to true will stop that features installation.
```bash
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic
```
You can also install Sanic from [`conda-forge`](https://anaconda.org/conda-forge/sanic)
```bash
conda config --add channels conda-forge
conda install sanic
```
## 2. Create a file called `main.py`

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

@@ -1,100 +0,0 @@
# Logging
Sanic allows you to do different types of logging (access log, error log) on the requests based on the [python3 logging API](https://docs.python.org/3/howto/logging.html). You should have some basic knowledge on python3 logging if you want to create a new configuration.
### Quick Start
A simple example using default settings would be like this:
```python
from sanic import Sanic
from sanic.log import logger
from sanic.response import text
app = Sanic('test')
@app.route('/')
async def test(request):
logger.info('Here is your log')
return text('Hello World!')
if __name__ == "__main__":
app.run(debug=True, access_log=True)
```
After the server is running, you can see some messages looks like:
```
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
```
You can send a request to server and it will print the log messages:
```
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
```
To use your own logging config, simply use `logging.config.dictConfig`, or
pass `log_config` when you initialize `Sanic` app:
```python
app = Sanic('test', log_config=LOGGING_CONFIG)
```
And to close logging, simply assign access_log=False:
```python
if __name__ == "__main__":
app.run(access_log=False)
```
This would skip calling logging functions when handling requests.
And you could even do further in production to gain extra speed:
```python
if __name__ == "__main__":
# disable debug messages
app.run(debug=False, access_log=False)
```
### Configuration
By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration.
There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
- sanic.root:<br>
Used to log internal messages.
- sanic.error:<br>
Used to log error logs.
- sanic.access:<br>
Used to log access logs.
#### Log format:
In addition to default parameters provided by python (asctime, levelname, message),
Sanic provides additional parameters for access logger with:
- host (str)<br>
request.ip
- request (str)<br>
request.method + " " + request.url
- status (int)<br>
response.status
- byte (int)<br>
len(response.body)
The default access log format is
```python
%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d
```

103
docs/sanic/logging.rst Normal file
View File

@@ -0,0 +1,103 @@
Logging
=======
Sanic allows you to do different types of logging (access log, error
log) on the requests based on the `python3 logging API`_. You should
have some basic knowledge on python3 logging if you want to create a new
configuration.
Quick Start
~~~~~~~~~~~
A simple example using default settings would be like this:
.. code:: python
from sanic import Sanic
from sanic.log import logger
from sanic.response import text
app = Sanic('test')
@app.route('/')
async def test(request):
logger.info('Here is your log')
return text('Hello World!')
if __name__ == "__main__":
app.run(debug=True, access_log=True)
After the server is running, you can see some messages looks like:
::
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
You can send a request to server and it will print the log messages:
::
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
To use your own logging config, simply use
``logging.config.dictConfig``, or pass ``log_config`` when you
initialize ``Sanic`` app:
.. code:: python
app = Sanic('test', log_config=LOGGING_CONFIG)
And to close logging, simply assign access_log=False:
.. code:: python
if __name__ == "__main__":
app.run(access_log=False)
This would skip calling logging functions when handling requests. And
you could even do further in production to gain extra speed:
.. code:: python
if __name__ == "__main__":
# disable debug messages
app.run(debug=False, access_log=False)
Configuration
~~~~~~~~~~~~~
By default, ``log_config`` parameter is set to use
``sanic.log.LOGGING_CONFIG_DEFAULTS`` dictionary for configuration.
There are three ``loggers`` used in sanic, and **must be defined if you
want to create your own logging configuration**:
================ ==============================
Logger Name Usecase
================ ==============================
``sanic.root`` Used to log internal messages.
``sanic.error`` Used to log error logs.
``sanic.access`` Used to log access logs.
================ ==============================
Log format:
^^^^^^^^^^^
In addition to default parameters provided by python (``asctime``,
``levelname``, ``message``), Sanic provides additional parameters for
access logger with:
===================== ========================================== ========
Log Context Parameter Parameter Value Datatype
===================== ========================================== ========
``host`` ``request.ip`` str
``request`` ``request.method`` + " " + ``request.url`` str
``status`` ``response.status`` int
``byte`` ``len(response.body)`` int
===================== ========================================== ========
The default access log format is ``%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d``
.. _python3 logging API: https://docs.python.org/3/howto/logging.html

View File

@@ -36,20 +36,31 @@ this.
```
app = Sanic(__name__)
@app.middleware('request')
async def add_key(request):
# Add a key to request object like dict object
request['foo'] = 'bar'
@app.middleware('response')
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
app.run(host="0.0.0.0", port=8000)
```
The above code will apply the two middleware in order. First, the middleware
The above code will apply the three middleware in order. The first middleware
**add_key** will add a new key `foo` into `request` object. This worked because
`request` object can be manipulated like `dict` object. Then, the second middleware
**custom_banner** will change the HTTP response header *Server* to
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
*Fake-Server*, and the last middleware **prevent_xss** will add the HTTP
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
are invoked *after* a user function returns a response.

View File

@@ -19,6 +19,8 @@ The following variables are accessible as properties on `Request` objects:
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
The request's `query_string` variable holds the unparsed string value.
Property is providing the default parsing strategy. If you would like to change it look to the section below
(`Changing the default parsing rules of the queryset`).
```python
from sanic.response import json
@@ -28,9 +30,54 @@ The following variables are accessible as properties on `Request` objects:
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
```
- `raw_args` (dict) - On many cases you would need to access the url arguments in
a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
`raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
- `query_args` (list) - On many cases you would need to access the url arguments in
a less packed form. `query_args` is the list of `(key, value)` tuples.
Property is providing the default parsing strategy. If you would like to change it look to the section below
(`Changing the default parsing rules of the queryset`).
For the same previous URL queryset `?key1=value1&key2=value2`, the
`query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`.
And in case of the multiple params with the same key like `?key1=value1&key2=value2&key1=value3`
the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`.
The difference between Request.args and Request.query_args
for the queryset `?key1=value1&key2=value2&key1=value3`
```python
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/test_request_args")
async def test_request_args(request):
return json({
"parsed": True,
"url": request.url,
"query_string": request.query_string,
"args": request.args,
"raw_args": request.raw_args,
"query_args": request.query_args,
})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
```
Output
```
{
"parsed":true,
"url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3",
"query_string":"key1=value1&key2=value2&key1=value3",
"args":{"key1":["value1","value3"],"key2":["value2"]},
"raw_args":{"key1":"value1","key2":"value2"},
"query_args":[["key1","value1"],["key2","value2"],["key1","value3"]]
}
```
`raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions.
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
@@ -98,13 +145,62 @@ 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
The default parameters that are using internally in `args` and `query_args` properties to parse queryset:
- `keep_blank_values` (bool): `False` - flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
- `strict_parsing` (bool): `False` - flag indicating what to do with parsing errors. If
false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
- `encoding` and `errors` (str): 'utf-8' and 'replace' - specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
If you would like to change that default parameters you could call `get_args` and `get_query_args` methods
with the new values.
For the queryset `/?test1=value1&test2=&test3=value3`:
```python
from sanic.response import json
@app.route("/query_string")
def query_string(request):
args_with_blank_values = request.get_args(keep_blank_values=True)
return json({
"parsed": True,
"url": request.url,
"args_with_blank_values": args_with_blank_values,
"query_string": request.query_string
})
```
The output will be:
```
{
"parsed": true,
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3",
"args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]},
"query_string": "test1=value1&test2=&test3=value3"
}
```
## Accessing values using `get` and `getlist`
@@ -126,3 +222,40 @@ args.get('titles') # => 'Post 1'
args.getlist('titles') # => ['Post 1', 'Post 2']
```
## Accessing the handler name with the request.endpoint attribute
The `request.endpoint` attribute holds the handler's name. For instance, the below
route will return "hello".
```python
from sanic.response import text
from sanic import Sanic
app = Sanic()
@app.get("/")
def hello(request):
return text(request.endpoint)
```
Or, with a blueprint it will be include both, separated by a period. For example,
the below route would return foo.bar:
```python
from sanic import Sanic
from sanic import Blueprint
from sanic.response import text
app = Sanic(__name__)
blueprint = Blueprint('foo')
@blueprint.get('/')
async def bar(request):
return text(request.endpoint)
app.blueprint(blueprint)
app.run(host="0.0.0.0", port=8000, debug=True)
```

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.
@@ -155,33 +203,39 @@ async def post_handler(request, post_id):
Other things to keep in mind when using `url_for`:
- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example:
```python
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
# /posts/5?arg_one=one&arg_two=two
```
- Multivalue argument can be passed to `url_for`. For example:
```python
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
# /posts/5?arg_one=one&arg_one=two
```
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not support now and will be ignored). For example:
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not supported now and will be ignored). For example:
```python
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
# /posts/5?arg_one=one#anchor
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
# //server/posts/5?arg_one=one
# _external requires passed argument _server or SERVER_NAME in app.config or url will be same as no _external
# _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
# http://server/posts/5?arg_one=one
# when specifying _scheme, _external must be True
# you can pass all special arguments one time
# you can pass all special arguments at once
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
```
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be raised.
## WebSocket routes
@@ -209,7 +263,7 @@ async def feed(request, ws):
app.add_websocket_route(my_websocket_handler, '/feed')
```
Handlers for a WebSocket route are passed the request as first argument, and a
Handlers to a WebSocket route are invoked with the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has `send`
and `recv` methods to send and receive data respectively.
@@ -241,9 +295,49 @@ 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
You can pass `name` to change the route name to avoid using the default name (`handler.__name__`).
A custom route name can be used by passing a `name` argument while registering the route which will
override the default route name generated using the `handler.__name__` attribute.
```python
@@ -305,8 +399,8 @@ def handler(request):
## Build URL for static files
You can use `url_for` for static file url building now.
If it's for file directly, `filename` can be ignored.
Sanic supports using `url_for` method to build static file urls. In case if the static url
is pointing to a directory, `filename` parameter to the `url_for` can be ignored. q
```python

View File

@@ -48,7 +48,7 @@ app.run(host="0.0.0.0", port=8000)
## Virtual Host
The `app.static()` method also support **virtual host**. You can serve your static files with spefic **virtual host** with `host` argument. For example:
The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example:
```python
from sanic import Sanic

View File

@@ -2,7 +2,7 @@
## Request Streaming
Sanic allows you to get request data by stream, as below. When the request ends, `request.stream.get()` returns `None`. Only post, put and patch decorator have stream argument.
Sanic allows you to get request data by stream, as below. When the request ends, `await request.stream.read()` returns `None`. Only post, put and patch decorator have stream argument.
```python
from sanic import Sanic
@@ -22,7 +22,7 @@ class SimpleView(HTTPMethodView):
async def post(self, request):
result = ''
while True:
body = await request.stream.get()
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
@@ -33,7 +33,7 @@ class SimpleView(HTTPMethodView):
async def handler(request):
async def streaming(response):
while True:
body = await request.stream.get()
body = await request.stream.read()
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
@@ -42,20 +42,33 @@ async def handler(request):
@bp.put('/bp_stream', stream=True)
async def bp_handler(request):
async def bp_put_handler(request):
result = ''
while True:
body = await request.stream.get()
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
# You can also use `bp.add_route()` with stream argument
async def bp_post_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
bp.add_route(bp_post_handler, '/bp_stream', methods=['POST'], stream=True)
async def post_handler(request):
result = ''
while True:
body = await request.stream.get()
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8')
@@ -104,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,25 @@ 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
If you need to test using a free unpriveleged port chosen by the kernel
instead of the default with `SanicTestClient`, you can do so by specifying
`port=None`. On most systems the port will be in the range 1024 to 65535.
```python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
from sanic.testing import SanicTestClient
def test_index_returns_200():
request, response = SanicTestClient(app, port=None).get('/')
assert response.status == 200
```
## pytest-sanic

View File

@@ -1,7 +1,7 @@
WebSocket
=========
Sanic supports websockets, to setup a WebSocket:
Sanic provides an easy to use abstraction on top of `websockets`. To setup a WebSocket:
.. code:: python
@@ -35,7 +35,7 @@ decorator:
app.add_websocket_route(feed, '/feed')
Handlers for a WebSocket route are passed the request as first argument, and a
Handlers for a WebSocket route is invoked with the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has ``send``
and ``recv`` methods to send and receive data respectively.

View File

@@ -1,21 +1,19 @@
name: py35
name: py36
dependencies:
- openssl=1.0.2g=0
- pip=8.1.1=py35_0
- python=3.5.1=0
- readline=6.2=2
- setuptools=20.3=py35_0
- sqlite=3.9.2=0
- tk=8.5.18=0
- wheel=0.29.0=py35_0
- xz=5.0.5=1
- zlib=1.2.8=0
- pip=18.1=py36_0
- python=3.6=0
- setuptools=40.4.3=py36_0
- pip:
- uvloop>=0.5.3
- httptools>=0.0.10
- uvloop>=0.5.3
- ujson>=1.35
- aiofiles>=0.3.0
- websockets>=6.0
- sphinxcontrib-asyncio>=0.2.0
- websockets>=6.0,<7.0
- multidict>=4.0,<5.0
- https://github.com/channelcat/docutils-fork/zipball/master
- sphinx==1.8.3
- sphinx_rtd_theme==0.4.2
- recommonmark==0.5.0
- requests-async==0.5.0
- sphinxcontrib-asyncio>=0.2.0
- docutils==0.14
- pygments==2.3.1

View File

@@ -0,0 +1,30 @@
from sanic import Sanic
from sanic.response import text
from random import randint
app = Sanic()
@app.middleware('request')
def append_request(request):
# Add new key with random value
request['num'] = randint(0, 100)
@app.get('/pop')
def pop_handler(request):
# Pop key from request object
num = request.pop('num')
return text(num)
@app.get('/key_exist')
def key_exist_handler(request):
# Check the key is exist or not
if 'num' in request:
return text('num exist in request')
return text('num does not exist in reqeust')
app.run(host="0.0.0.0", port=8000, debug=True)

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)

View File

@@ -0,0 +1,61 @@
import logging
import socket
from os import getenv
from platform import node
from uuid import getnode as get_mac
from logdna import LogDNAHandler
from sanic import Sanic
from sanic.response import json
from sanic.request import Request
log = logging.getLogger('logdna')
log.setLevel(logging.INFO)
def get_my_ip_address(remote_server="google.com"):
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect((remote_server, 80))
return s.getsockname()[0]
def get_mac_address():
h = iter(hex(get_mac())[2:].zfill(12))
return ":".join(i + next(h) for i in h)
logdna_options = {
"app": __name__,
"index_meta": True,
"hostname": node(),
"ip": get_my_ip_address(),
"mac": get_mac_address()
}
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
logdna = logging.getLogger(__name__)
logdna.setLevel(logging.INFO)
logdna.addHandler(logdna_handler)
app = Sanic(__name__)
@app.middleware
def log_request(request: Request):
logdna.info("I was Here with a new Request to URL: {}".format(request.url))
@app.route("/")
def default(request):
return json({
"response": "I was here"
})
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -0,0 +1,37 @@
from os import getenv
from raygun4py.raygunprovider import RaygunSender
from sanic import Sanic
from sanic.exceptions import SanicException
from sanic.handlers import ErrorHandler
class RaygunExceptionReporter(ErrorHandler):
def __init__(self, raygun_api_key=None):
super().__init__()
if raygun_api_key is None:
raygun_api_key = getenv("RAYGUN_API_KEY")
self.sender = RaygunSender(raygun_api_key)
def default(self, request, exception):
self.sender.send_exception(exception=exception)
return super().default(request, exception)
raygun_error_reporter = RaygunExceptionReporter()
app = Sanic(__name__, error_handler=raygun_error_reporter)
@app.route("/raise")
async def test(request):
raise SanicException('You Broke It!')
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -0,0 +1,30 @@
import rollbar
from sanic.handlers import ErrorHandler
from sanic import Sanic
from sanic.exceptions import SanicException
from os import getenv
rollbar.init(getenv("ROLLBAR_API_KEY"))
class RollbarExceptionHandler(ErrorHandler):
def default(self, request, exception):
rollbar.report_message(str(exception))
return super().default(request, exception)
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
@app.route("/raise")
def create_error(request):
raise SanicException("I was here and I don't like where I am")
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

88
examples/run_asgi.py Normal file
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

@@ -0,0 +1,35 @@
from os import getenv
from sentry_sdk import init as sentry_init
from sentry_sdk.integrations.sanic import SanicIntegration
from sanic import Sanic
from sanic.response import json
sentry_init(
dsn=getenv("SENTRY_DSN"),
integrations=[SanicIntegration()],
)
app = Sanic(__name__)
# noinspection PyUnusedLocal
@app.route("/working")
async def working_path(request):
return json({
"response": "Working API Response"
})
# noinspection PyUnusedLocal
@app.route("/raise-error")
async def raise_error(request):
raise Exception("Testing Sentry Integration")
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -1,13 +0,0 @@
aiofiles
aiohttp>=2.3.0,<=3.2.1
chardet<=2.3.0
beautifulsoup4
coverage
httptools>=0.0.10
flake8
pytest==3.3.2
tox
ujson; sys_platform != "win32" and implementation_name == "cpython"
uvloop; sys_platform != "win32" and implementation_name == "cpython"
gunicorn
multidict>=4.0,<5.0

View File

@@ -1,4 +0,0 @@
sphinx
sphinx_rtd_theme
recommonmark
sphinxcontrib-asyncio

View File

@@ -1,6 +0,0 @@
aiofiles
httptools>=0.0.10
ujson; sys_platform != "win32" and implementation_name == "cpython"
uvloop; sys_platform != "win32" and implementation_name == "cpython"
websockets>=6.0,<7.0
multidict>=4.0,<5.0

View File

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

1
sanic/__version__.py Normal file
View File

@@ -0,0 +1 @@
__version__ = "19.6.3"

View File

@@ -4,16 +4,20 @@ import os
import re
import warnings
from asyncio import CancelledError, ensure_future, get_event_loop
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from collections import defaultdict, deque
from functools import partial
from inspect import getmodulename, isawaitable, signature, stack
from ssl import Purpose, create_default_context
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc
from typing import Any, Optional, Type, Union
from urllib.parse import urlencode, urlunparse
from sanic import reloader_helpers
from sanic.config import Config
from sanic.asgi import ASGIApp
from sanic.blueprint_group import BlueprintGroup
from sanic.config import BASE_LOGO, Config
from sanic.constants import HTTP_METHODS
from sanic.exceptions import SanicException, ServerError, URLBuildError
from sanic.handlers import ErrorHandler
@@ -22,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
@@ -50,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()
@@ -77,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"
@@ -177,33 +182,45 @@ 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
def get(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **GET** *HTTP* method
:param uri: URL to be tagged to **GET** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"GET"}),
@@ -222,6 +239,17 @@ class Sanic:
version=None,
name=None,
):
"""
Add an API URL under the **POST** *HTTP* method
:param uri: URL to be tagged to **POST** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"POST"}),
@@ -241,6 +269,17 @@ class Sanic:
version=None,
name=None,
):
"""
Add an API URL under the **PUT** *HTTP* method
:param uri: URL to be tagged to **PUT** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"PUT"}),
@@ -266,6 +305,17 @@ class Sanic:
def options(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **OPTIONS** *HTTP* method
:param uri: URL to be tagged to **OPTIONS** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"OPTIONS"}),
@@ -284,6 +334,17 @@ class Sanic:
version=None,
name=None,
):
"""
Add an API URL under the **PATCH** *HTTP* method
:param uri: URL to be tagged to **PATCH** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"PATCH"}),
@@ -297,6 +358,17 @@ class Sanic:
def delete(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **DELETE** *HTTP* method
:param uri: URL to be tagged to **DELETE** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"DELETE"}),
@@ -369,10 +441,16 @@ class Sanic:
def websocket(
self, uri, host=None, strict_slashes=None, subprotocols=None, name=None
):
"""Decorate a function to be registered as a websocket route
"""
Decorate a function to be registered as a websocket route
:param uri: path of the URL
:param 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()
@@ -388,13 +466,30 @@ class Sanic:
def response(handler):
async def websocket_handler(request, *args, **kwargs):
request.app = self
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)
if not getattr(handler, "__blueprintname__", False):
request.endpoint = handler.__name__
else:
request.endpoint = (
getattr(handler, "__blueprintname__", "")
+ handler.__name__
)
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
@@ -430,7 +525,22 @@ class Sanic:
subprotocols=None,
name=None,
):
"""A helper method to register a function as a websocket route."""
"""
A helper method to register a function as a websocket route.
:param handler: a callable function or instance of a class
that can handle the websocket request
:param host: Host IP or FQDN details
:param uri: URL path that will be mapped to the websocket
handler
:param strict_slashes: If the API endpoint needs to terminate
with a "/" or not
:param subprotocols: Subprotocols to be used with websocket
handshake
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:return: Objected decorated by :func:`websocket`
"""
if strict_slashes is None:
strict_slashes = self.strict_slashes
@@ -459,6 +569,26 @@ class Sanic:
self.websocket_enabled = enable
def remove_route(self, uri, clean_cache=True, host=None):
"""
This method provides the app user a mechanism by which an already
existing route can be removed from the :class:`Sanic` object
.. warning::
remove_route is deprecated in v19.06 and will be removed
from future versions.
:param uri: URL Path to be removed from the app
:param clean_cache: Instruct sanic if it needs to clean up the LRU
route cache
:param host: IP address or FQDN specific to the host
:return: None
"""
warnings.warn(
"remove_route is deprecated and will be removed "
"from future versions.",
DeprecationWarning,
stacklevel=2,
)
self.router.remove(uri, clean_cache, host)
# Decorator
@@ -481,18 +611,39 @@ class Sanic:
return response
def register_middleware(self, middleware, attach_to="request"):
"""
Register an application level middleware that will be attached
to all the API URLs registered under this application.
This method is internally invoked by the :func:`middleware`
decorator provided at the app level.
:param middleware: Callback method to be attached to the
middleware
:param attach_to: The state at which the middleware needs to be
invoked in the lifecycle of an *HTTP Request*.
**request** - Invoke before the request is processed
**response** - Invoke before the response is returned back
:return: decorated method
"""
if attach_to == "request":
self.request_middleware.append(middleware)
if middleware not in self.request_middleware:
self.request_middleware.append(middleware)
if attach_to == "response":
self.response_middleware.appendleft(middleware)
if middleware not in self.response_middleware:
self.response_middleware.appendleft(middleware)
return middleware
# Decorator
def middleware(self, middleware_or_request):
"""Decorate and register middleware to be called before a request.
Can either be called as @app.middleware or @app.middleware('request')
"""
Decorate and register middleware to be called before a request.
Can either be called as *@app.middleware* or
*@app.middleware('request')*
:param: middleware_or_request: Optional parameter to use for
identifying which type of middleware is being registered.
"""
# Detect which way this was called, @middleware or @middleware('AT')
if callable(middleware_or_request):
return self.register_middleware(middleware_or_request)
@@ -516,8 +667,30 @@ class Sanic:
strict_slashes=None,
content_type=None,
):
"""Register a root to serve files from. The input can either be a
file or a directory. See
"""
Register a root to serve files from. The input can either be a
file or a directory. This method will enable an easy and simple way
to setup the :class:`Route` necessary to serve the static files.
:param uri: URL path to be used for serving static content
:param file_or_directory: Path for the Static file/directory with
static files
:param pattern: Regex Pattern identifying the valid static files
:param use_modified_since: If true, send file modified time, and return
not modified if the browser's matches the server's
:param use_content_range: If true, process header for range requests
and sends the file part that is requested
:param stream_large_files: If true, use the
:func:`StreamingHTTPResponse.file_stream` handler rather
than the :func:`HTTPResponse.file` handler to send the file.
If this is an integer, this represents the threshold size to
switch to :func:`StreamingHTTPResponse.file_stream`
:param name: user defined name used for url_for
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param content_type: user defined content type for header
:return: None
"""
static_register(
self,
@@ -540,7 +713,7 @@ class Sanic:
:param options: option dictionary with blueprint defaults
:return: Nothing
"""
if isinstance(blueprint, (list, tuple)):
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
for item in blueprint:
self.blueprint(item, **options)
return
@@ -555,7 +728,17 @@ class Sanic:
blueprint.register(self, options)
def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0
"""
Proxy method provided for invoking the :func:`blueprint` method
.. note::
To be deprecated in 1.0. Use :func:`blueprint` instead.
:param args: Blueprint object or (list, tuple) thereof
:param kwargs: option dictionary with blueprint defaults
:return: None
"""
if self.debug:
warnings.simplefilter("default")
warnings.warn(
@@ -700,6 +883,9 @@ class Sanic:
# -------------------------------------------------------------------- #
def converted_response_type(self, response):
"""
No implementation provided.
"""
pass
async def handle_request(self, request, write_callback, stream_callback):
@@ -723,8 +909,6 @@ class Sanic:
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
request.app = self
response = await self._run_request_middleware(request)
# No middleware results
if not response:
@@ -743,6 +927,16 @@ class Sanic:
"handler from the router"
)
)
else:
if not getattr(handler, "__blueprintname__", False):
request.endpoint = self._build_endpoint_name(
handler.__name__
)
else:
request.endpoint = self._build_endpoint_name(
getattr(handler, "__blueprintname__", ""),
handler.__name__,
)
# Run response handler
response = handler(request, *args, **kwargs)
@@ -803,8 +997,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)
@@ -816,25 +1018,59 @@ class Sanic:
def test_client(self):
return SanicTestClient(self)
@property
def asgi_client(self):
return SanicASGITestClient(self)
# -------------------------------------------------------------------- #
# Execution
# -------------------------------------------------------------------- #
def run(
self,
host=None,
port=None,
debug=False,
ssl=None,
sock=None,
workers=1,
protocol=None,
backlog=100,
stop_event=None,
register_sys_signals=True,
access_log=True,
**kwargs
):
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
ssl: Union[dict, SSLContext, None] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Type[Protocol] = None,
backlog: int = 100,
stop_event: Any = None,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
**kwargs: Any
) -> None:
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
:param sock: Socket for the server to accept connections from
:type sock: socket
:param workers: Number of processes received before it is respected
:type workers: int
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param stop_event: event to be triggered
before stopping the app - deprecated
:type stop_event: None
:param register_sys_signals: Register SIG* events
:type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:return: Nothing
"""
if "loop" in kwargs:
raise TypeError(
"loop is not a valid argument. To use an existing loop, "
@@ -843,23 +1079,6 @@ class Sanic:
"#asynchronous-support"
)
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param backlog:
:param stop_event:
:param register_sys_signals:
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
# Default auto_reload to false
auto_reload = False
# If debug is set, default it to true (unless on windows)
@@ -882,8 +1101,10 @@ class Sanic:
"stop_event will be removed from future versions.",
DeprecationWarning,
)
# compatibility old access_log params
self.config.ACCESS_LOG = access_log
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log
server_settings = self._helper(
host=host,
port=port,
@@ -927,26 +1148,60 @@ class Sanic:
"""This kills the Sanic"""
get_event_loop().stop()
def __call__(self):
"""gunicorn compatibility"""
return self
async def create_server(
self,
host=None,
port=None,
debug=False,
ssl=None,
sock=None,
protocol=None,
backlog=100,
stop_event=None,
access_log=True,
):
"""Asynchronous version of `run`.
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
ssl: Union[dict, SSLContext, None] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
stop_event: Any = None,
access_log: Optional[bool] = None,
return_asyncio_server=False,
asyncio_server_kwargs=None,
) -> None:
"""
Asynchronous version of :func:`run`.
NOTE: This does not support multiprocessing and is not the preferred
way to run a Sanic application.
This method will take care of the operations necessary to invoke
the *before_start* events via :func:`trigger_events` method invocation
before starting the *sanic* app in Async mode.
.. note::
This does not support multiprocessing and is not the preferred
way to run a :class:`Sanic` application.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
:param sock: Socket for the server to accept connections from
:type sock: socket
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param stop_event: event to be triggered
before stopping the app - deprecated
:type stop_event: None
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need
to return asyncio.Server or
start it serving right away
:type return_asyncio_server: bool
:param asyncio_server_kwargs: key-value arguments for
asyncio/uvloop create_server method
:type asyncio_server_kwargs: dict
:return: Nothing
"""
if sock is None:
@@ -963,8 +1218,10 @@ class Sanic:
"stop_event will be removed from future versions.",
DeprecationWarning,
)
# compatibility old access_log params
self.config.ACCESS_LOG = access_log
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log
server_settings = self._helper(
host=host,
port=port,
@@ -974,7 +1231,7 @@ class Sanic:
loop=get_event_loop(),
protocol=protocol,
backlog=backlog,
run_async=True,
run_async=return_asyncio_server,
)
# Trigger before_start events
@@ -983,7 +1240,9 @@ class Sanic:
server_settings.get("loop"),
)
return await serve(**server_settings)
return await serve(
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
)
async def trigger_events(self, events, loop):
"""Trigger events (functions or async)
@@ -1063,6 +1322,7 @@ class Sanic:
"port": port,
"sock": sock,
"ssl": ssl,
"app": self,
"signal": Signal(),
"debug": debug,
"request_handler": self.handle_request,
@@ -1071,6 +1331,7 @@ class Sanic:
"response_timeout": self.config.RESPONSE_TIMEOUT,
"keep_alive_timeout": self.config.KEEP_ALIVE_TIMEOUT,
"request_max_size": self.config.REQUEST_MAX_SIZE,
"request_buffer_queue_size": self.config.REQUEST_BUFFER_QUEUE_SIZE,
"keep_alive": self.config.KEEP_ALIVE,
"loop": loop,
"register_sys_signals": register_sys_signals,
@@ -1104,10 +1365,14 @@ class Sanic:
logger.setLevel(logging.DEBUG)
if (
self.config.LOGO is not None
self.config.LOGO
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
):
logger.debug(self.config.LOGO)
logger.debug(
self.config.LOGO
if isinstance(self.config.LOGO, str)
else BASE_LOGO
)
if run_async:
server_settings["run_async"] = True
@@ -1120,3 +1385,19 @@ class Sanic:
logger.info("Goin' Fast @ {}://{}:{}".format(proto, host, port))
return server_settings
def _build_endpoint_name(self, *parts):
parts = [self.name, *parts]
return ".".join(parts)
# -------------------------------------------------------------------- #
# ASGI
# -------------------------------------------------------------------- #
async def __call__(self, scope, receive, send):
"""To be ASGI compliant, our instance must be a callable that accepts
three arguments: scope, receive, send. See the ASGI reference for more
details: https://asgi.readthedocs.io/en/latest/"""
self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send)
await asgi_app()

369
sanic/asgi.py Normal file
View File

@@ -0,0 +1,369 @@
import asyncio
import warnings
from inspect import isawaitable
from typing import Any, Awaitable, Callable, MutableMapping, Union
from urllib.parse import quote
from sanic.compat import Header
from sanic.exceptions import InvalidUsage, ServerError
from sanic.log import logger
from sanic.request import Request
from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.server import StreamBuffer
from sanic.websocket import WebSocketConnection
ASGIScope = MutableMapping[str, Any]
ASGIMessage = MutableMapping[str, Any]
ASGISend = Callable[[ASGIMessage], Awaitable[None]]
ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol:
def __init__(self, transport: "MockTransport", loop):
self.transport = transport
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
def pause_writing(self) -> None:
self._not_paused.clear()
def resume_writing(self) -> None:
self._not_paused.set()
async def complete(self) -> None:
self._not_paused.set()
await self.transport.send(
{"type": "http.response.body", "body": b"", "more_body": False}
)
@property
def is_complete(self) -> bool:
return self._complete.is_set()
async def push_data(self, data: bytes) -> None:
if not self.is_complete:
await self.transport.send(
{"type": "http.response.body", "body": data, "more_body": True}
)
async def drain(self) -> None:
await self._not_paused.wait()
class MockTransport:
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 = Header(
[
(key.decode("latin-1"), value.decode("latin-1"))
for key, value in scope.get("headers", [])
]
)
instance.do_stream = (
True if headers.get("expect") == "100-continue" else False
)
instance.lifespan = Lifespan(instance)
if scope["type"] == "lifespan":
await instance.lifespan(scope, receive, send)
else:
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.
"""
headers = []
cookies = {}
try:
cookies = {
v.key: v
for _, v in list(
filter(
lambda item: item[0].lower() == "set-cookie",
response.headers.items(),
)
)
}
headers += [
(str(name).encode("latin-1"), str(value).encode("latin-1"))
for name, value in response.headers.items()
if name.lower() not in ["set-cookie"]
]
except AttributeError:
logger.error(
"Invalid response object for url %s, "
"Expected Type: HTTPResponse, Actual Type: %s",
self.request.url,
type(response),
)
exception = ServerError("Invalid response type")
response = self.sanic_app.error_handler.response(
self.request, exception
)
headers = [
(str(name).encode("latin-1"), str(value).encode("latin-1"))
for name, value in response.headers.items()
if name not in (b"Set-Cookie",)
]
if "content-length" not in response.headers and not isinstance(
response, StreamingHTTPResponse
):
headers += [
(b"content-length", str(len(response.body)).encode("latin-1"))
]
if "content-type" not in response.headers:
headers += [
(b"content-type", str(response.content_type).encode("latin-1"))
]
if response.cookies:
cookies.update(
{
v.key: v
for _, v in response.cookies.items()
if v.key not in cookies.keys()
}
)
headers += [
(b"set-cookie", cookie.encode("utf-8"))
for k, cookie in cookies.items()
]
await self.transport.send(
{
"type": "http.response.start",
"status": response.status,
"headers": headers,
}
)
if isinstance(response, StreamingHTTPResponse):
response.protocol = self.transport.get_protocol()
await response.stream()
await response.protocol.complete()
else:
await self.transport.send(
{
"type": "http.response.body",
"body": response.body,
"more_body": False,
}
)

121
sanic/blueprint_group.py Normal file
View File

@@ -0,0 +1,121 @@
from collections.abc import MutableSequence
class BlueprintGroup(MutableSequence):
"""
This class provides a mechanism to implement a Blueprint Group
using the :meth:`~sanic.blueprints.Blueprint.group` method in
:class:`~sanic.blueprints.Blueprint`. To avoid having to re-write
some of the existing implementation, this class provides a custom
iterator implementation that will let you use the object of this
class as a list/tuple inside the existing implementation.
"""
__slots__ = ("_blueprints", "_url_prefix")
def __init__(self, url_prefix=None):
"""
Create a new Blueprint Group
:param url_prefix: URL: to be prefixed before all the Blueprint Prefix
"""
self._blueprints = []
self._url_prefix = url_prefix
@property
def url_prefix(self):
"""
Retrieve the URL prefix being used for the Current Blueprint Group
:return: string with url prefix
"""
return self._url_prefix
@property
def blueprints(self):
"""
Retrieve a list of all the available blueprints under this group.
:return: List of Blueprint instance
"""
return self._blueprints
def __iter__(self):
"""Tun the class Blueprint Group into an Iterable item"""
return iter(self._blueprints)
def __getitem__(self, item):
"""
This method returns a blueprint inside the group specified by
an index value. This will enable indexing, splice and slicing
of the blueprint group like we can do with regular list/tuple.
This method is provided to ensure backward compatibility with
any of the pre-existing usage that might break.
:param item: Index of the Blueprint item in the group
:return: Blueprint object
"""
return self._blueprints[item]
def __setitem__(self, index: int, item: object) -> None:
"""
Abstract method implemented to turn the `BlueprintGroup` class
into a list like object to support all the existing behavior.
This method is used to perform the list's indexed setter operation.
:param index: Index to use for inserting a new Blueprint item
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints[index] = item
def __delitem__(self, index: int) -> None:
"""
Abstract method implemented to turn the `BlueprintGroup` class
into a list like object to support all the existing behavior.
This method is used to delete an item from the list of blueprint
groups like it can be done on a regular list with index.
:param index: Index to use for removing a new Blueprint item
:return: None
"""
del self._blueprints[index]
def __len__(self) -> int:
"""
Get the Length of the blueprint group object.
:return: Length of Blueprint group object
"""
return len(self._blueprints)
def insert(self, index: int, item: object) -> None:
"""
The Abstract class `MutableSequence` leverages this insert method to
perform the `BlueprintGroup.append` operation.
:param index: Index to use for removing a new Blueprint item
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints.insert(index, item)
def middleware(self, *args, **kwargs):
"""
A decorator that can be used to implement a Middleware plugin to
all of the Blueprints that belongs to this specific Blueprint Group.
In case of nested Blueprint Groups, the same middleware is applied
across each of the Blueprints recursively.
:param args: Optional positional Parameters to be use middleware
:param kwargs: Optional Keyword arg to use with Middleware
:return: Partial function to apply the middleware
"""
kwargs["bp_group"] = True
def register_middleware_for_blueprints(fn):
for blueprint in self.blueprints:
blueprint.middleware(fn, *args, **kwargs)
return register_middleware_for_blueprints

View File

@@ -1,5 +1,6 @@
from collections import defaultdict, namedtuple
from sanic.blueprint_group import BlueprintGroup
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
@@ -36,13 +37,19 @@ class Blueprint:
url_prefix=None,
host=None,
version=None,
strict_slashes=False,
strict_slashes=None,
):
"""Create a new blueprint
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
URLs that perform a specific set of tasks which can be identified by
a unique name.
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param strict_slashes: strict to trailing slash
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
training */*
"""
self.name = name
self.url_prefix = url_prefix
@@ -59,8 +66,9 @@ class Blueprint:
@staticmethod
def group(*blueprints, url_prefix=""):
"""Create a list of blueprints, optionally
grouping them under a general URL prefix.
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.
:param blueprints: blueprints to be registered as a group
:param url_prefix: URL route to be prepended to all sub-prefixes
@@ -71,10 +79,12 @@ class Blueprint:
for i in nested:
if isinstance(i, (list, tuple)):
yield from chain(i)
elif isinstance(i, BlueprintGroup):
yield from i.blueprints
else:
yield i
bps = []
bps = BlueprintGroup(url_prefix=url_prefix)
for bp in chain(blueprints):
if bp.url_prefix is None:
bp.url_prefix = ""
@@ -83,7 +93,14 @@ class Blueprint:
return bps
def register(self, app, options):
"""Register the blueprint to the sanic app."""
"""
Register the blueprint to the sanic app.
:param app: Instance of :class:`sanic.app.Sanic` class
:param options: Options to be used while registering the
blueprint into the app.
*url_prefix* - URL Prefix to override the blueprint prefix
"""
url_prefix = options.get("url_prefix", self.url_prefix)
@@ -160,6 +177,15 @@ class Blueprint:
:param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods.
:param host: IP Address of FQDN for the sanic server to use.
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param stream: If the route should provide a streaming support
:param version: Blueprint Version
:param name: Unique name to identify the Route
:return a decorated method that when invoked will return an object
of type :class:`FutureRoute`
"""
if strict_slashes is None:
strict_slashes = self.strict_slashes
@@ -189,6 +215,7 @@ class Blueprint:
strict_slashes=None,
version=None,
name=None,
stream=False,
):
"""Create a blueprint route from a function.
@@ -196,10 +223,12 @@ class Blueprint:
or class instance with a view_class method.
:param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods.
:param host:
:param strict_slashes:
:param version:
:param host: IP Address of FQDN for the sanic server to use.
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param version: Blueprint Version
:param name: user defined route name for url_for
:param stream: boolean specifying if the handler is a stream handler
:return: function or class instance
"""
# Handle HTTPMethodView differently
@@ -222,6 +251,7 @@ class Blueprint:
methods=methods,
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)(handler)
@@ -233,6 +263,11 @@ class Blueprint:
"""Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible.
:param host: IP Address of FQDN for the sanic server to use.
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param version: Blueprint Version
:param name: Unique name to identify the Websocket Route
"""
if strict_slashes is None:
strict_slashes = self.strict_slashes
@@ -254,6 +289,9 @@ class Blueprint:
:param handler: function for handling uri requests. Accepts function,
or class instance with a view_class method.
:param uri: endpoint at which the route will be accessible.
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param name: Unique name to identify the Websocket Route
:return: function or class instance
"""
self.websocket(uri=uri, host=host, version=version, name=name)(handler)
@@ -272,7 +310,14 @@ class Blueprint:
return decorator
def middleware(self, *args, **kwargs):
"""Create a blueprint middleware from a decorated function."""
"""
Create a blueprint middleware from a decorated function.
:param args: Positional arguments to be used while invoking the
middleware
:param kwargs: optional keyword args that can be used with the
middleware.
"""
def register_middleware(_middleware):
future_middleware = FutureMiddleware(_middleware, args, kwargs)
@@ -285,10 +330,26 @@ class Blueprint:
args = []
return register_middleware(middleware)
else:
return register_middleware
if kwargs.get("bp_group") and callable(args[0]):
middleware = args[0]
args = args[1:]
kwargs.pop("bp_group")
return register_middleware(middleware)
else:
return register_middleware
def exception(self, *args, **kwargs):
"""Create a blueprint exception from a decorated function."""
"""
This method enables the process of creating a global exception
handler for the current blueprint under question.
:param args: List of Python exceptions to be caught by the handler
:param kwargs: Additional optional arguments to be passed to the
exception handler
:return a decorated method to handle global exceptions for any
route registered under this blueprint.
"""
def decorator(handler):
exception = FutureException(handler, args, kwargs)
@@ -319,9 +380,20 @@ class Blueprint:
def get(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **GET** *HTTP* method
:param uri: URL to be tagged to **GET** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=["GET"],
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
version=version,
@@ -337,9 +409,20 @@ class Blueprint:
version=None,
name=None,
):
"""
Add an API URL under the **POST** *HTTP* method
:param uri: URL to be tagged to **POST** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=["POST"],
methods=frozenset({"POST"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
@@ -356,9 +439,20 @@ class Blueprint:
version=None,
name=None,
):
"""
Add an API URL under the **PUT** *HTTP* method
:param uri: URL to be tagged to **PUT** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=["PUT"],
methods=frozenset({"PUT"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
@@ -369,9 +463,20 @@ class Blueprint:
def head(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **HEAD** *HTTP* method
:param uri: URL to be tagged to **HEAD** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=["HEAD"],
methods=frozenset({"HEAD"}),
host=host,
strict_slashes=strict_slashes,
version=version,
@@ -381,9 +486,20 @@ class Blueprint:
def options(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **OPTIONS** *HTTP* method
:param uri: URL to be tagged to **OPTIONS** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=["OPTIONS"],
methods=frozenset({"OPTIONS"}),
host=host,
strict_slashes=strict_slashes,
version=version,
@@ -399,9 +515,20 @@ class Blueprint:
version=None,
name=None,
):
"""
Add an API URL under the **PATCH** *HTTP* method
:param uri: URL to be tagged to **PATCH** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=["PATCH"],
methods=frozenset({"PATCH"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
@@ -412,9 +539,20 @@ class Blueprint:
def delete(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **DELETE** *HTTP* method
:param uri: URL to be tagged to **DELETE** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=["DELETE"],
methods=frozenset({"DELETE"}),
host=host,
strict_slashes=strict_slashes,
version=version,

6
sanic/compat.py Normal file
View File

@@ -0,0 +1,6 @@
from multidict import CIMultiDict
class Header(CIMultiDict):
def get_all(self, key):
return self.getall(key, default=[])

View File

@@ -2,46 +2,45 @@ import os
import types
from sanic.exceptions import PyFileError
from sanic.helpers import import_string
SANIC_PREFIX = "SANIC_"
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_BUFFER_QUEUE_SIZE": 100,
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"KEEP_ALIVE": True,
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabytes
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True,
"PROXIES_COUNT": -1,
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"REAL_IP_HEADER": "X-Real-IP",
}
class Config(dict):
def __init__(self, defaults=None, load_env=True, keep_alive=True):
super().__init__(defaults or {})
self.LOGO = """
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \\
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀
"""
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
self.REQUEST_TIMEOUT = 60 # 60 seconds
self.RESPONSE_TIMEOUT = 60 # 60 seconds
self.KEEP_ALIVE = keep_alive
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
self.WEBSOCKET_MAX_QUEUE = 32
self.WEBSOCKET_READ_LIMIT = 2 ** 16
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
self.ACCESS_LOG = True
def __init__(self, defaults=None, load_env=True, keep_alive=None):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self.LOGO = BASE_LOGO
if keep_alive is not None:
self.KEEP_ALIVE = keep_alive
if load_env:
prefix = SANIC_PREFIX if load_env is True else load_env
@@ -81,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__,
)
@@ -104,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
@@ -111,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)
@@ -129,4 +133,24 @@ class Config(dict):
try:
self[config_key] = float(v)
except ValueError:
self[config_key] = v
try:
self[config_key] = strtobool(v)
except ValueError:
self[config_key] = v
def strtobool(val):
"""
This function was borrowed from distutils.utils. While distutils
is part of stdlib, it feels odd to use distutils in main application code.
The function was modified to walk its talk and actually return bool
and not int.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return True
elif val in ("n", "no", "f", "false", "off", "0"):
return False
else:
raise ValueError("invalid truth value %r" % (val,))

View File

@@ -1,6 +1,10 @@
import re
import string
from datetime import datetime
DEFAULT_MAX_AGE = 0
# ------------------------------------------------------------ #
# SimpleCookie
@@ -103,9 +107,29 @@ class Cookie(dict):
if key not in self._keys:
raise KeyError("Unknown cookie property")
if value is not False:
if key.lower() == "max-age":
if not str(value).isdigit():
value = DEFAULT_MAX_AGE
elif key.lower() == "expires":
if not isinstance(value, datetime):
raise TypeError(
"Cookie 'expires' property must be a datetime"
)
return super().__setitem__(key, value)
def encode(self, encoding):
"""
Encode the cookie content in a specific type of encoding instructed
by the developer. Leverages the :func:`str.encode` method provided
by python.
This method can be used to encode and embed ``utf-8`` content into
the cookies.
:param encoding: Encoding to be used with the cookie
:return: Cookie encoded in a codec of choosing.
:except: UnicodeEncodeError
"""
output = ["%s=%s" % (self.key, _quote(self.value))]
for key, value in self.items():
if key == "max-age":
@@ -114,16 +138,10 @@ class Cookie(dict):
except TypeError:
output.append("%s=%s" % (self._keys[key], value))
elif key == "expires":
try:
output.append(
"%s=%s"
% (
self._keys[key],
value.strftime("%a, %d-%b-%Y %T GMT"),
)
)
except AttributeError:
output.append("%s=%s" % (self._keys[key], value))
output.append(
"%s=%s"
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
)
elif key in self._flags and self[key]:
output.append(self._keys[key])
else:

View File

@@ -123,7 +123,7 @@ _sanic_exceptions = {}
def add_status_code(code):
"""
Decorator used for adding exceptions to _sanic_exceptions.
Decorator used for adding exceptions to :class:`SanicException`.
"""
def class_decorator(cls):
@@ -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

@@ -19,6 +19,18 @@ from sanic.response import html, text
class ErrorHandler:
"""
Provide :class:`sanic.app.Sanic` application with a mechanism to handle
and process any and all uncaught exceptions in a way the application
developer will set fit.
This error handling framework is built into the core that can be extended
by the developers to perform a wide range of tasks from recording the error
stats to reporting them to an external service that can be used for
realtime alerting system.
"""
handlers = None
cached_handlers = None
_missing = object()
@@ -58,9 +70,34 @@ class ErrorHandler:
)
def add(self, exception, handler):
"""
Add a new exception handler to an already existing handler object.
:param exception: Type of exception that need to be handled
:param handler: Reference to the method that will handle the exception
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:type handler: ``function``
:return: None
"""
self.handlers.append((exception, handler))
def lookup(self, exception):
"""
Lookup the existing instance of :class:`ErrorHandler` and fetch the
registered handler for a specific type of exception.
This method leverages a dict lookup to speedup the retrieval process.
:param exception: Type of exception
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:return: Registered function if found ``None`` otherwise
"""
handler = self.cached_handlers.get(type(exception), self._missing)
if handler is self._missing:
for exception_class, handler in self.handlers:
@@ -75,9 +112,15 @@ class ErrorHandler:
"""Fetches and executes an exception handler and returns a response
object
:param request: Request
:param request: Instance of :class:`sanic.request.Request`
:param exception: Exception to handle
:return: Response object
:type request: :class:`sanic.request.Request`
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:return: Wrap the return value obtained from :func:`default`
or registered handler for that type of exception.
"""
handler = self.lookup(exception)
response = None
@@ -109,6 +152,20 @@ class ErrorHandler:
"""
def default(self, request, exception):
"""
Provide a default behavior for the objects of :class:`ErrorHandler`.
If a developer chooses to extent the :class:`ErrorHandler` they can
provide a custom implementation for this method to behave in a way
they see fit.
:param request: Incoming request
:param exception: Exception object
:type request: :class:`sanic.request.Request`
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:return:
"""
self.log(format_exc())
try:
url = repr(request.url)
@@ -133,7 +190,23 @@ class ErrorHandler:
class ContentRangeHandler:
"""Class responsible for parsing request header"""
"""
A mechanism to parse and process the incoming request headers to
extract the content range information.
:param request: Incoming api request
:param stats: Stats related to the content
:type request: :class:`sanic.request.Request`
:type stats: :class:`posix.stat_result`
:ivar start: Content Range start
:ivar end: Content Range end
:ivar size: Length of the content
:ivar total: Total size identified by the :class:`posix.stat_result`
instance
:ivar ContentRangeHandler.headers: Content range header ``dict``
"""
__slots__ = ("start", "end", "size", "total", "headers")

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

@@ -36,7 +36,15 @@ def _iter_module_files():
def _get_args_for_reloading():
"""Returns the executable."""
rv = [sys.executable]
rv.extend(sys.argv)
main_module = sys.modules["__main__"]
mod_spec = getattr(main_module, "__spec__", None)
if mod_spec:
# Parent exe was launched as a module rather than a script
rv.extend(["-m", mod_spec.name])
if len(sys.argv) > 1:
rv.extend(sys.argv[1:])
else:
rv.extend(sys.argv)
return rv
@@ -44,6 +52,7 @@ def restart_with_reloader():
"""Create a new process and a subprocess in it with the same arguments as
this one.
"""
cwd = os.getcwd()
args = _get_args_for_reloading()
new_environ = os.environ.copy()
new_environ["SANIC_SERVER_RUNNING"] = "true"
@@ -51,7 +60,7 @@ def restart_with_reloader():
worker_process = Process(
target=subprocess.call,
args=(cmd,),
kwargs=dict(shell=True, env=new_environ),
kwargs={"cwd": cwd, "shell": True, "env": new_environ},
)
worker_process.start()
return worker_process

View File

@@ -1,10 +1,13 @@
import asyncio
import email.utils
import json
import sys
import warnings
from cgi import parse_header
from collections import namedtuple
from collections import defaultdict, namedtuple
from http.cookies import SimpleCookie
from urllib.parse import parse_qs, urlunparse
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
from httptools import parse_url
@@ -24,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
@@ -47,37 +49,56 @@ class RequestParameters(dict):
return super().get(name, default)
class StreamBuffer:
def __init__(self, buffer_size=100):
self._queue = asyncio.Queue(buffer_size)
async def read(self):
""" Stop reading when gets None """
payload = await self._queue.get()
self._queue.task_done()
return payload
async def put(self, payload):
await self._queue.put(payload)
def is_full(self):
return self._queue.full()
class Request(dict):
"""Properties of an HTTP request such as URL, headers, etc."""
__slots__ = (
"app",
"headers",
"version",
"method",
"__weakref__",
"_cookies",
"transport",
"body",
"parsed_json",
"parsed_args",
"parsed_form",
"parsed_files",
"_ip",
"_parsed_url",
"uri_template",
"stream",
"_port",
"_remote_addr",
"_socket",
"_port",
"__weakref__",
"app",
"body",
"endpoint",
"headers",
"method",
"parsed_args",
"parsed_not_grouped_args",
"parsed_files",
"parsed_form",
"parsed_json",
"raw_url",
"stream",
"transport",
"uri_template",
"version",
)
def __init__(self, url_bytes, headers, version, method, transport):
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
@@ -89,14 +110,14 @@ class Request(dict):
self.parsed_json = None
self.parsed_form = None
self.parsed_files = None
self.parsed_args = None
self.parsed_args = defaultdict(RequestParameters)
self.parsed_not_grouped_args = defaultdict(list)
self.uri_template = None
self._cookies = None
self.stream = None
self.endpoint = None
def __repr__(self):
if self.method is None or not self.path:
return "<{0}>".format(self.__class__.__name__)
return "<{0}: {1} {2}>".format(
self.__class__.__name__, self.method, self.path
)
@@ -180,21 +201,127 @@ class Request(dict):
return self.parsed_files
@property
def args(self):
if self.parsed_args is None:
def get_args(
self,
keep_blank_values: bool = False,
strict_parsing: bool = False,
encoding: str = "utf-8",
errors: str = "replace",
) -> RequestParameters:
"""
Method to parse `query_string` using `urllib.parse.parse_qs`.
This methods is used by `args` property.
Can be used directly if you need to change default parameters.
:param keep_blank_values:
flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
:type keep_blank_values: bool
:param strict_parsing:
flag indicating what to do with parsing errors.
If false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
:type strict_parsing: bool
:param encoding:
specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type encoding: str
:param errors:
specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type errors: str
:return: RequestParameters
"""
if not self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
if self.query_string:
self.parsed_args = RequestParameters(
parse_qs(self.query_string)
self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
] = RequestParameters(
parse_qs(
qs=self.query_string,
keep_blank_values=keep_blank_values,
strict_parsing=strict_parsing,
encoding=encoding,
errors=errors,
)
)
else:
self.parsed_args = RequestParameters()
return self.parsed_args
return self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
]
args = property(get_args)
@property
def raw_args(self):
def raw_args(self) -> dict:
if self.app.debug: # pragma: no cover
warnings.simplefilter("default")
warnings.warn(
"Use of raw_args will be deprecated in "
"the future versions. Please use args or query_args "
"properties instead",
DeprecationWarning,
)
return {k: v[0] for k, v in self.args.items()}
def get_query_args(
self,
keep_blank_values: bool = False,
strict_parsing: bool = False,
encoding: str = "utf-8",
errors: str = "replace",
) -> list:
"""
Method to parse `query_string` using `urllib.parse.parse_qsl`.
This methods is used by `query_args` property.
Can be used directly if you need to change default parameters.
:param keep_blank_values:
flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
:type keep_blank_values: bool
:param strict_parsing:
flag indicating what to do with parsing errors.
If false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
:type strict_parsing: bool
:param encoding:
specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type encoding: str
:param errors:
specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type errors: str
:return: list
"""
if not self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
if self.query_string:
self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
] = parse_qsl(
qs=self.query_string,
keep_blank_values=keep_blank_values,
strict_parsing=strict_parsing,
encoding=encoding,
errors=errors,
)
return self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
]
query_args = property(get_query_args)
@property
def cookies(self):
if self._cookies is None:
@@ -211,12 +338,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
@@ -235,27 +368,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")[1]
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"
@@ -271,8 +470,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
@@ -301,6 +504,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"])
@@ -338,15 +566,28 @@ def parse_multipart_form(body, boundary):
)
if form_header_field == "content-disposition":
file_name = form_parameters.get("filename")
field_name = form_parameters.get("name")
file_name = form_parameters.get("filename")
# non-ASCII filenames in RFC2231, "filename*" format
if file_name is None and form_parameters.get("filename*"):
encoding, _, value = email.utils.decode_rfc2231(
form_parameters["filename*"]
)
file_name = unquote(value, encoding=encoding)
elif form_header_field == "content-type":
content_type = form_header_value
content_charset = form_parameters.get("charset", "utf-8")
if field_name:
post_data = form_part[line_index:-4]
if file_name:
if file_name is None:
value = post_data.decode(content_charset)
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
else:
form_file = File(
type=content_type, name=file_name, body=post_data
)
@@ -354,12 +595,6 @@ def parse_multipart_form(body, boundary):
files[field_name].append(form_file)
else:
files[field_name] = [form_file]
else:
value = post_data.decode(content_charset)
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
else:
logger.debug(
"Form-data field does not have a 'name' parameter "

View File

@@ -4,8 +4,8 @@ from os import path
from urllib.parse import quote_plus
from aiofiles import open as open_async
from multidict import CIMultiDict
from sanic.compat import Header
from sanic.cookies import CookieJar
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
@@ -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.headers = Header(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
)
@@ -150,7 +164,7 @@ class HTTPResponse(BaseHTTPResponse):
self.body = body_bytes
self.status = status
self.headers = CIMultiDict(headers or {})
self.headers = Header(headers or {})
self._cookies = None
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
@@ -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

@@ -17,8 +17,8 @@ Parameter = namedtuple("Parameter", ["name", "cast"])
REGEX_TYPES = {
"string": (str, r"[^/]+"),
"int": (int, r"\d+"),
"number": (float, r"[0-9\\.]+"),
"int": (int, r"-?\d+"),
"number": (float, r"-?(?:\d+(?:\.\d*)?|\.\d+)"),
"alpha": (str, r"[A-Za-z]+"),
"path": (str, r"[^/].*?"),
"uuid": (
@@ -331,6 +331,17 @@ class Router:
@staticmethod
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
"""
Check if a URL pattern exists in a list of routes provided based on
the comparison of URL pattern and the parameters.
:param pattern: URL parameter pattern
:param routes_to_check: list of dynamic routes either hashable or
unhashable routes.
:param parameters: List of :class:`Parameter` items
:return: Tuple of index and route if matching route exists else
-1 for index and None for route
"""
for ndx, route in enumerate(routes_to_check):
if route.pattern == pattern and route.parameters == parameters:
return ndx, route
@@ -395,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

@@ -12,9 +12,10 @@ from time import time
from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
from multidict import CIMultiDict
from sanic.compat import Header
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
from sanic.request import EXPECT_HEADER, Request, StreamBuffer
from sanic.response import HTTPResponse
@@ -39,7 +40,13 @@ class Signal:
class HttpProtocol(asyncio.Protocol):
"""
This class provides a basic HTTP implementation of the sanic framework.
"""
__slots__ = (
# app
"app",
# event loop, connection
"loop",
"transport",
@@ -56,6 +63,7 @@ class HttpProtocol(asyncio.Protocol):
"response_timeout",
"keep_alive_timeout",
"request_max_size",
"request_buffer_queue_size",
"request_class",
"is_request_stream",
"router",
@@ -83,14 +91,16 @@ class HttpProtocol(asyncio.Protocol):
self,
*,
loop,
app,
request_handler,
error_handler,
signal=Signal(),
connections=set(),
connections=None,
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
request_max_size=None,
request_buffer_queue_size=100,
request_class=None,
access_log=True,
keep_alive=True,
@@ -101,6 +111,7 @@ class HttpProtocol(asyncio.Protocol):
**kwargs
):
self.loop = loop
self.app = app
self.transport = None
self.request = None
self.parser = None
@@ -109,10 +120,11 @@ class HttpProtocol(asyncio.Protocol):
self.router = router
self.signal = signal
self.access_log = access_log
self.connections = connections
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
self.request_buffer_queue_size = request_buffer_queue_size
self.response_timeout = response_timeout
self.keep_alive_timeout = keep_alive_timeout
self.request_max_size = request_max_size
@@ -138,6 +150,13 @@ class HttpProtocol(asyncio.Protocol):
@property
def keep_alive(self):
"""
Check if the connection needs to be kept alive based on the params
attached to the `_keep_alive` attribute, :attr:`Signal.stopped`
and :func:`HttpProtocol.parser.should_keep_alive`
:return: ``True`` if connection is to be kept alive ``False`` else
"""
return (
self._keep_alive
and not self.signal.stopped
@@ -210,8 +229,13 @@ class HttpProtocol(asyncio.Protocol):
self.write_error(ServiceUnavailable("Response Timeout"))
def keep_alive_timeout_callback(self):
# Check if elapsed time since last response exceeds our configured
# maximum keep alive timeout value
"""
Check if elapsed time since last response exceeds our configured
maximum keep alive timeout value and if so, close the transport
pipe and let the response writer handle the error.
:return: None
"""
time_elapsed = time() - self._last_response_time
if time_elapsed < self.keep_alive_timeout:
time_left = self.keep_alive_timeout - time_elapsed
@@ -280,31 +304,61 @@ class HttpProtocol(asyncio.Protocol):
def on_headers_complete(self):
self.request = self.request_class(
url_bytes=self.url,
headers=CIMultiDict(self.headers),
headers=Header(self.headers),
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
)
if self._is_stream_handler:
self.request.stream = asyncio.Queue()
self.request.stream = StreamBuffer(
self.request_buffer_queue_size
)
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(
self.request.stream.put(body)
self.body_append(body)
)
return
self.request.body_push(body)
else:
self.request.body_push(body)
async def body_append(self, body):
if self.request.stream.is_full():
self.transport.pause_reading()
await self.request.stream.put(body)
self.transport.resume_reading()
else:
await self.request.stream.put(body)
def on_message_complete(self):
# Entire request (headers and whole body) is received.
@@ -321,6 +375,12 @@ class HttpProtocol(asyncio.Protocol):
self.execute_request_handler()
def execute_request_handler(self):
"""
Invoke the request handler defined by the
:func:`sanic.app.Sanic.handle_request` method
:return: None
"""
self._response_timeout_handler = self.loop.call_later(
self.response_timeout, self.response_timeout_callback
)
@@ -335,6 +395,17 @@ class HttpProtocol(asyncio.Protocol):
# Responding
# -------------------------------------------- #
def log_response(self, response):
"""
Helper method provided to enable the logging of responses in case if
the :attr:`HttpProtocol.access_log` is enabled.
:param response: Response generated for the current request
:type response: :class:`sanic.response.HTTPResponse` or
:class:`sanic.response.StreamingHTTPResponse`
:return: None
"""
if self.access_log:
extra = {"status": getattr(response, "status", 0)}
@@ -406,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):
@@ -489,11 +560,29 @@ class HttpProtocol(asyncio.Protocol):
logger.debug("Connection lost before server could close it.")
def bail_out(self, message, from_error=False):
if from_error or self.transport.is_closing():
"""
In case if the transport pipes are closed and the sanic app encounters
an error while writing data to the transport pipe, we log the error
with proper details.
:param message: Error message to display
:param from_error: If the bail out was invoked while handling an
exception scenario.
:type message: str
:type from_error: bool
:return: None
"""
if from_error or self.transport is None or self.transport.is_closing():
logger.error(
"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:
@@ -532,18 +621,6 @@ class HttpProtocol(asyncio.Protocol):
self.transport = None
def update_current_time(loop):
"""Cache the current time, since it is needed at the end of every
keep-alive request to update the request timeout time
:param loop:
:return:
"""
global current_time
current_time = time()
loop.call_later(1, partial(update_current_time, loop))
def trigger_events(events, loop):
"""Trigger event callbacks (functions or async)
@@ -559,6 +636,7 @@ def trigger_events(events, loop):
def serve(
host,
port,
app,
request_handler,
error_handler,
before_start=None,
@@ -572,6 +650,7 @@ def serve(
ssl=None,
sock=None,
request_max_size=None,
request_buffer_queue_size=100,
reuse_port=False,
loop=None,
protocol=HttpProtocol,
@@ -592,6 +671,7 @@ def serve(
websocket_write_limit=2 ** 16,
state=None,
graceful_shutdown_timeout=15.0,
asyncio_server_kwargs=None,
):
"""Start asynchronous HTTP Server on an individual process.
@@ -632,9 +712,12 @@ def serve(
outgoing bytes, the low-water limit is a
quarter of the high-water limit.
:param is_request_stream: disable/enable Request.stream
:param request_buffer_queue_size: streaming request buffer queue size
:param router: Router object
:param graceful_shutdown_timeout: How long take to Force close non-idle
connection
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
create_server method
:return: Nothing
"""
if not run_async:
@@ -645,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,
@@ -669,7 +755,9 @@ def serve(
state=state,
debug=debug,
)
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
)
server_coroutine = loop.create_server(
server,
host,
@@ -678,12 +766,9 @@ def serve(
reuse_port=reuse_port,
sock=sock,
backlog=backlog,
**asyncio_server_kwargs
)
# Instead of pulling time at the end of every request,
# pull it once per minute
loop.call_soon(partial(update_current_time, loop))
if run_async:
return server_coroutine
@@ -779,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:
@@ -787,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,52 +1,67 @@
from json import JSONDecodeError
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
class SanicTestClient:
def __init__(self, app, port=PORT):
"""Use port=None to bind to a random port"""
self.app = app
self.port = port
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
import aiohttp
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
url = uri
else:
url = "http://{host}:{port}{uri}".format(
host=HOST, port=self.port, uri=uri
)
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(
@@ -57,7 +72,7 @@ class SanicTestClient:
debug=False,
server_kwargs={"auto_reload": False},
*request_args,
**request_kwargs
**request_kwargs,
):
results = [None, None]
exceptions = []
@@ -79,11 +94,31 @@ class SanicTestClient:
else:
return self.app.error_handler.default(request, exception)
if self.port:
server_kwargs = dict(host=HOST, port=self.port, **server_kwargs)
host, port = HOST, self.port
else:
sock = socket()
sock.bind((HOST, 0))
server_kwargs = dict(sock=sock, **server_kwargs)
host, port = sock.getsockname()
if uri.startswith(
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
):
url = uri
else:
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
scheme = "ws" if method == "websocket" else "http"
url = "{scheme}://{host}:{port}{uri}".format(
scheme=scheme, host=host, port=port, uri=uri
)
@self.app.listener("after_server_start")
async def _collect_response(sanic, loop):
try:
response = await self._local_request(
method, uri, *request_args, **request_kwargs
method, url, *request_args, **request_kwargs
)
results[-1] = response
except Exception as e:
@@ -91,7 +126,7 @@ class SanicTestClient:
exceptions.append(e)
self.app.stop()
self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
self.app.run(debug=debug, **server_kwargs)
self.app.listeners["after_server_start"].pop()
if exceptions:
@@ -101,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
@@ -110,7 +145,7 @@ class SanicTestClient:
else:
try:
return results[-1]
except BaseException:
except BaseException: # noqa
raise ValueError(
"Request object expected, got ({})".format(results)
)
@@ -135,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

@@ -57,6 +57,7 @@ class GunicornWorker(base.Worker):
if self.app.callable.websocket_enabled
else self.http_protocol
)
self._server_settings = self.app.callable._helper(
loop=self.loop,
debug=is_debug,

59
scripts/changelog.py Executable file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python
from os import path
if __name__ == "__main__":
try:
import towncrier
import click
except ImportError:
print(
"Please make sure you have a installed towncrier and click before using this tool"
)
exit(1)
@click.command()
@click.option(
"--draft",
"draft",
default=False,
flag_value=True,
help="Render the news fragments, don't write to files, "
"don't check versions.",
)
@click.option(
"--dir", "directory", default=path.dirname(path.abspath(__file__))
)
@click.option("--name", "project_name", default=None)
@click.option(
"--version",
"project_version",
default=None,
help="Render the news fragments using given version.",
)
@click.option("--date", "project_date", default=None)
@click.option(
"--yes",
"answer_yes",
default=False,
flag_value=True,
help="Do not ask for confirmation to remove news fragments.",
)
def _main(
draft,
directory,
project_name,
project_version,
project_date,
answer_yes,
):
return towncrier.__main(
draft,
directory,
project_name,
project_version,
project_date,
answer_yes,
)
_main()

33
scripts/pyproject.toml Normal file
View File

@@ -0,0 +1,33 @@
[tool.towncrier]
package = "sanic"
package_dir = "."
filename = "../CHANGELOG.rst"
directory = "./changelogs"
underlines = ["=", "*", "~"]
issue_format = "`#{issue} <https://github.com/huge-success/sanic/issues/{issue}>`__"
title_format = "Version {version}"
[[tool.towncrier.type]]
directory = "feature"
name = "Features"
showcontent = true
[[tool.towncrier.type]]
directory = "bugfix"
name = "Bugfixes"
showcontent = true
[[tool.towncrier.type]]
directory = "doc"
name = "Improved Documentation"
showcontent = true
[[tool.towncrier.type]]
directory = "removal"
name = "Deprecations and Removals"
showcontent = true
[[tool.towncrier.type]]
directory = "misc"
name = "Miscellaneous internal changes"
showcontent = true

353
scripts/release.py Executable file
View File

@@ -0,0 +1,353 @@
#!/usr/bin/env python
from argparse import ArgumentParser, Namespace
from collections import OrderedDict
from configparser import RawConfigParser
from datetime import datetime
from json import dumps
from os import path, chdir
from subprocess import Popen, PIPE
from jinja2 import Environment, BaseLoader
from requests import patch
import towncrier
GIT_COMMANDS = {
"get_tag": ["git describe --tags --abbrev=0"],
"commit_version_change": [
"git add . && git commit -m 'Bumping up version from "
"{current_version} to {new_version}'"
],
"create_new_tag": [
"git tag -a {new_version} -m 'Bumping up version from "
"{current_version} to {new_version}'"
],
"push_tag": ["git push origin {new_version}"],
"get_change_log": [
'git log --no-merges --pretty=format:"%h::: %cn::: %s" '
"{current_version}.."
],
}
RELEASE_NOTE_TEMPLATE = """
# {{ release_name }} - {% now 'utc', '%Y-%m-%d' %}
To see the exhaustive list of pull requests included in this release see:
https://github.com/huge-success/sanic/milestone/{{milestone}}?closed=1
# Changelog
{% for row in changelogs %}
* {{ row -}}
{% endfor %}
# Credits
{% for author in authors %}
* {{ author -}}
{% endfor %}
"""
JINJA_RELEASE_NOTE_TEMPLATE = Environment(
loader=BaseLoader, extensions=["jinja2_time.TimeExtension"]
).from_string(RELEASE_NOTE_TEMPLATE)
RELEASE_NOTE_UPDATE_URL = (
"https://api.github.com/repos/huge-success/sanic/releases/tags/"
"{new_version}?access_token={token}"
)
class Directory:
def __init__(self):
self._old_path = path.dirname(path.abspath(__file__))
self._new_path = path.dirname(self._old_path)
def __enter__(self):
chdir(self._new_path)
def __exit__(self, exc_type, exc_val, exc_tb):
chdir(self._old_path)
def _run_shell_command(command: list):
try:
process = Popen(
command, stderr=PIPE, stdout=PIPE, stdin=PIPE, shell=True
)
output, error = process.communicate()
return_code = process.returncode
return output.decode("utf-8"), error, return_code
except:
return None, None, -1
def _fetch_default_calendar_release_version():
return datetime.now().strftime("%y.%m.0")
def _fetch_current_version(config_file: str) -> str:
if path.isfile(config_file):
config_parser = RawConfigParser()
with open(config_file) as cfg:
config_parser.read_file(cfg)
return (
config_parser.get("version", "current_version")
or _fetch_default_calendar_release_version()
)
else:
return _fetch_default_calendar_release_version()
def _change_micro_version(current_version: str):
version_string = current_version.split(".")
version_string[-1] = str((int(version_string[-1]) + 1))
return ".".join(version_string)
def _get_new_version(
config_file: str = "./setup.cfg",
current_version: str = None,
micro_release: bool = False,
):
if micro_release:
if current_version:
return _change_micro_version(current_version)
elif config_file:
return _change_micro_version(_fetch_current_version(config_file))
else:
return _fetch_default_calendar_release_version()
else:
return _fetch_default_calendar_release_version()
def _get_current_tag(git_command_name="get_tag"):
global GIT_COMMANDS
command = GIT_COMMANDS.get(git_command_name)
out, err, ret = _run_shell_command(command)
if len(str(out)):
return str(out).split("\n")[0]
else:
return None
def _update_release_version_for_sanic(
current_version, new_version, config_file, generate_changelog
):
config_parser = RawConfigParser()
with open(config_file) as cfg:
config_parser.read_file(cfg)
config_parser.set("version", "current_version", new_version)
version_files = config_parser.get("version", "files")
current_version_line = config_parser.get(
"version", "current_version_pattern"
).format(current_version=current_version)
new_version_line = config_parser.get(
"version", "new_version_pattern"
).format(new_version=new_version)
for version_file in version_files.split(","):
with open(version_file) as init_file:
data = init_file.read()
new_data = data.replace(current_version_line, new_version_line)
with open(version_file, "w") as init_file:
init_file.write(new_data)
with open(config_file, "w") as config:
config_parser.write(config)
if generate_changelog:
towncrier.__main(
draft=False,
directory=path.dirname(path.abspath(__file__)),
project_name=None,
project_version=new_version,
project_date=None,
answer_yes=True,
)
command = GIT_COMMANDS.get("commit_version_change")
command[0] = command[0].format(
new_version=new_version, current_version=current_version
)
_, err, ret = _run_shell_command(command)
if int(ret) != 0:
print(
"Failed to Commit Version upgrade changes to Sanic: {}".format(
err.decode("utf-8")
)
)
exit(1)
def _generate_change_log(current_version: str = None):
global GIT_COMMANDS
command = GIT_COMMANDS.get("get_change_log")
command[0] = command[0].format(current_version=current_version)
output, error, ret = _run_shell_command(command=command)
if not len(str(output)):
print("Unable to Fetch Change log details to update the Release Note")
exit(1)
commit_details = OrderedDict()
commit_details["authors"] = dict()
commit_details["commits"] = list()
for line in str(output).split("\n"):
commit, author, description = line.split(":::")
if "GitHub" not in author:
commit_details["authors"][author] = 1
commit_details["commits"].append(" - ".join([commit, description]))
return commit_details
def _generate_markdown_document(
milestone, release_name, current_version, release_version
):
global JINJA_RELEASE_NOTE_TEMPLATE
release_name = release_name or release_version
change_log = _generate_change_log(current_version=current_version)
return JINJA_RELEASE_NOTE_TEMPLATE.render(
release_name=release_name,
milestone=milestone,
changelogs=change_log["commits"],
authors=change_log["authors"].keys(),
)
def _tag_release(new_version, current_version, milestone, release_name, token):
global GIT_COMMANDS
global RELEASE_NOTE_UPDATE_URL
for command_name in ["create_new_tag", "push_tag"]:
command = GIT_COMMANDS.get(command_name)
command[0] = command[0].format(
new_version=new_version, current_version=current_version
)
out, error, ret = _run_shell_command(command=command)
if int(ret) != 0:
print("Failed to execute the command: {}".format(command[0]))
exit(1)
change_log = _generate_markdown_document(
milestone, release_name, current_version, new_version
)
body = {"name": release_name or new_version, "body": change_log}
headers = {"content-type": "application/json"}
response = patch(
RELEASE_NOTE_UPDATE_URL.format(new_version=new_version, token=token),
data=dumps(body),
headers=headers,
)
response.raise_for_status()
def release(args: Namespace):
current_tag = _get_current_tag()
current_version = _fetch_current_version(args.config)
if current_tag and current_version not in current_tag:
print(
"Tag mismatch between what's in git and what was provided by "
"--current-version. Existing: {}, Give: {}".format(
current_tag, current_version
)
)
exit(1)
new_version = args.release_version or _get_new_version(
args.config, current_version, args.micro_release
)
_update_release_version_for_sanic(
current_version=current_version,
new_version=new_version,
config_file=args.config,
generate_changelog=args.generate_changelog,
)
if args.tag_release:
_tag_release(
current_version=current_version,
new_version=new_version,
milestone=args.milestone,
release_name=args.release_name,
token=args.token,
)
if __name__ == "__main__":
cli = ArgumentParser(description="Sanic Release Manager")
cli.add_argument(
"--release-version",
"-r",
help="New Version to use for Release",
default=_fetch_default_calendar_release_version(),
required=False,
)
cli.add_argument(
"--current-version",
"-cv",
help="Current Version to default in case if you don't want to "
"use the version configuration files",
default=None,
required=False,
)
cli.add_argument(
"--config",
"-c",
help="Configuration file used for release",
default="./setup.cfg",
required=False,
)
cli.add_argument(
"--token",
"-t",
help="Git access token with necessary access to Huge Sanic Org",
required=False,
)
cli.add_argument(
"--milestone",
"-ms",
help="Git Release milestone information to include in relase note",
required=False,
)
cli.add_argument(
"--release-name",
"-n",
help="Release Name to use if any",
required=False,
)
cli.add_argument(
"--micro-release",
"-m",
help="Micro Release with patches only",
default=False,
action="store_true",
required=False,
)
cli.add_argument(
"--tag-release",
help="Tag a new release for Sanic",
default=False,
action="store_true",
required=False,
)
cli.add_argument(
"--generate-changelog",
help="Generate changelog for Sanic as part of release",
default=False,
action="store_true",
required=False,
)
args = cli.parse_args()
if args.tag_release:
for key, value in {
"--token/-t": args.token,
"--milestone/-m": args.milestone,
}.items():
if not value:
print(f"{key} is mandatory while using --tag-release")
exit(1)
with Directory():
release(args)

View File

@@ -1,11 +1,8 @@
[flake8]
# https://github.com/ambv/black#slices
# https://github.com/ambv/black#line-breaks--binary-operators
ignore = E203, W503
[isort]
atomic=true
atomic = true
default_section = THIRDPARTY
include_trailing_comma = true
known_first_party = sanic
@@ -15,3 +12,10 @@ lines_after_imports = 2
lines_between_types = 1
multi_line_output = 3
not_skip = __init__.py
[version]
current_version = 19.6.3
files = sanic/__version__.py
current_version_pattern = __version__ = "{current_version}"
new_version_pattern = __version__ = "{new_version}"

142
setup.py
View File

@@ -4,74 +4,140 @@ Sanic
import codecs
import os
import re
from distutils.errors import DistutilsPlatformError
import sys
from distutils.util import strtobool
from setuptools import setup
from setuptools.command.test import test as TestCommand
def open_local(paths, mode='r', encoding='utf8'):
path = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
*paths
)
class PyTest(TestCommand):
"""
Provide a Test runner to be used from setup.py to run unit tests
"""
user_options = [("pytest-args=", "a", "Arguments to pass to pytest")]
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ""
def run_tests(self):
import shlex
import pytest
errno = pytest.main(shlex.split(self.pytest_args))
sys.exit(errno)
def open_local(paths, mode="r", encoding="utf8"):
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), *paths)
return codecs.open(path, mode, encoding)
with open_local(['sanic', '__init__.py'], encoding='latin1') as fp:
with open_local(["sanic", "__version__.py"], encoding="latin1") as fp:
try:
version = re.findall(r"^__version__ = \"([^']+)\"\r?$",
fp.read(), re.M)[0]
version = re.findall(
r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M
)[0]
except IndexError:
raise RuntimeError('Unable to determine version.')
raise RuntimeError("Unable to determine version.")
with open_local(['README.rst']) as rm:
with open_local(["README.rst"]) as rm:
long_description = rm.read()
setup_kwargs = {
'name': 'sanic',
'version': version,
'url': 'http://github.com/channelcat/sanic/',
'license': 'MIT',
'author': 'Channel Cat',
'author_email': 'channelcat@gmail.com',
'description': (
'A microframework based on uvloop, httptools, and learnings of flask'),
'long_description': long_description,
'packages': ['sanic'],
'platforms': 'any',
'classifiers': [
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
"name": "sanic",
"version": version,
"url": "http://github.com/huge-success/sanic/",
"license": "MIT",
"author": "Sanic Community",
"author_email": "admhpkns@gmail.com",
"description": (
"A web server and web framework that's written to go fast. Build fast. Run fast."
),
"long_description": long_description,
"packages": ["sanic"],
"platforms": "any",
"classifiers": [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
],
}
env_dependency = '; sys_platform != "win32" and implementation_name == "cpython"'
ujson = 'ujson>=1.35' + env_dependency
uvloop = 'uvloop>=0.5.3' + env_dependency
env_dependency = (
'; sys_platform != "win32" ' 'and implementation_name == "cpython"'
)
ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency
requirements = [
'httptools>=0.0.10',
"httptools>=0.0.10",
uvloop,
ujson,
'aiofiles>=0.3.0',
'websockets>=6.0,<7.0',
'multidict>=4.0,<5.0',
"aiofiles>=0.3.0",
"websockets>=7.0,<8.0",
"multidict>=4.0,<5.0",
"requests-async==0.5.0",
]
tests_require = [
"pytest==4.1.0",
"multidict>=4.0,<5.0",
"gunicorn",
"pytest-cov",
"httpcore==0.3.0",
"beautifulsoup4",
uvloop,
ujson,
"pytest-sanic",
"pytest-sugar",
"pytest-benchmark",
]
docs_require = [
"sphinx>=2.1.2",
"sphinx_rtd_theme",
"recommonmark>=0.5.0",
"docutils",
"pygments",
]
dev_require = tests_require + [
"aiofiles",
"tox",
"black",
"flake8",
"bandit",
"towncrier",
]
all_require = dev_require + docs_require
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
print("Installing without uJSON")
requirements.remove(ujson)
tests_require.remove(ujson)
# 'nt' means windows OS
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
print("Installing without uvLoop")
requirements.remove(uvloop)
tests_require.remove(uvloop)
setup_kwargs['install_requires'] = requirements
extras_require = {
"test": tests_require,
"dev": dev_require,
"docs": docs_require,
"all": all_require,
}
setup_kwargs["install_requires"] = requirements
setup_kwargs["tests_require"] = tests_require
setup_kwargs["extras_require"] = extras_require
setup_kwargs["cmdclass"] = {"test": PyTest}
setup(**setup_kwargs)

View File

@@ -0,0 +1,55 @@
from random import choice, seed
from pytest import mark
import sanic.router
seed("Pack my box with five dozen liquor jugs.")
# Disable Caching for testing purpose
sanic.router.ROUTER_CACHE_SIZE = 0
class TestSanicRouteResolution:
@mark.asyncio
async def test_resolve_route_no_arg_string_path(
self, sanic_router, route_generator, benchmark
):
simple_routes = route_generator.generate_random_direct_route(
max_route_depth=4
)
router, simple_routes = sanic_router(route_details=simple_routes)
route_to_call = choice(simple_routes)
result = benchmark.pedantic(
router._get,
("/{}".format(route_to_call[-1]), route_to_call[0], "localhost"),
iterations=1000,
rounds=1000,
)
assert await result[0](None) == 1
@mark.asyncio
async def test_resolve_route_with_typed_args(
self, sanic_router, route_generator, benchmark
):
typed_routes = route_generator.add_typed_parameters(
route_generator.generate_random_direct_route(max_route_depth=4),
max_route_depth=8,
)
router, typed_routes = sanic_router(route_details=typed_routes)
route_to_call = choice(typed_routes)
url = route_generator.generate_url_for_template(
template=route_to_call[-1]
)
print("{} -> {}".format(route_to_call[-1], url))
result = benchmark.pedantic(
router._get,
("/{}".format(url), route_to_call[0], "localhost"),
iterations=1000,
rounds=1000,
)
assert await result[0](None) == 1

View File

@@ -1,12 +1,131 @@
import random
import re
import string
import sys
import uuid
import pytest
from sanic import Sanic
from sanic.router import RouteExists, Router
if sys.platform in ['win32', 'cygwin']:
random.seed("Pack my box with five dozen liquor jugs.")
if sys.platform in ["win32", "cygwin"]:
collect_ignore = ["test_worker.py"]
async def _handler(request):
"""
Dummy placeholder method used for route resolver when creating a new
route into the sanic router. This router is not actually called by the
sanic app. So do not worry about the arguments to this method.
If you change the return value of this method, make sure to propagate the
change to any test case that leverages RouteStringGenerator.
"""
return 1
TYPE_TO_GENERATOR_MAP = {
"string": lambda: "".join(
[random.choice(string.ascii_letters + string.digits) for _ in range(4)]
),
"int": lambda: random.choice(range(1000000)),
"number": lambda: random.random(),
"alpha": lambda: "".join(
[random.choice(string.ascii_letters) for _ in range(4)]
),
"uuid": lambda: str(uuid.uuid1()),
}
class RouteStringGenerator:
ROUTE_COUNT_PER_DEPTH = 100
HTTP_METHODS = ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTION"]
ROUTE_PARAM_TYPES = ["string", "int", "number", "alpha", "uuid"]
def generate_random_direct_route(self, max_route_depth=4):
routes = []
for depth in range(1, max_route_depth + 1):
for _ in range(self.ROUTE_COUNT_PER_DEPTH):
route = "/".join(
[
TYPE_TO_GENERATOR_MAP.get("string")()
for _ in range(depth)
]
)
route = route.replace(".", "", -1)
route_detail = (random.choice(self.HTTP_METHODS), route)
if route_detail not in routes:
routes.append(route_detail)
return routes
def add_typed_parameters(self, current_routes, max_route_depth=8):
routes = []
for method, route in current_routes:
current_length = len(route.split("/"))
new_route_part = "/".join(
[
"<{}:{}>".format(
TYPE_TO_GENERATOR_MAP.get("string")(),
random.choice(self.ROUTE_PARAM_TYPES),
)
for _ in range(max_route_depth - current_length)
]
)
route = "/".join([route, new_route_part])
route = route.replace(".", "", -1)
routes.append((method, route))
return routes
@staticmethod
def generate_url_for_template(template):
url = template
for pattern, param_type in re.findall(
re.compile(r"((?:<\w+:(string|int|number|alpha|uuid)>)+)"),
template,
):
value = TYPE_TO_GENERATOR_MAP.get(param_type)()
url = url.replace(pattern, str(value), -1)
return url
@pytest.fixture(scope="function")
def sanic_router():
# noinspection PyProtectedMember
def _setup(route_details: tuple) -> (Router, tuple):
router = Router()
added_router = []
for method, route in route_details:
try:
router._add(
uri="/{}".format(route),
methods=frozenset({method}),
host="localhost",
handler=_handler,
)
added_router.append((method, route))
except RouteExists:
pass
return router, added_router
return _setup
@pytest.fixture(scope="function")
def route_generator() -> RouteStringGenerator:
return RouteStringGenerator()
@pytest.fixture(scope="function")
def url_param_generator():
return TYPE_TO_GENERATOR_MAP
@pytest.fixture
def app(request):
return Sanic(request.node.name)

View File

@@ -1,18 +1,26 @@
# 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)
async def handle(request):
return web.Response(body=json.dumps({"test":True}).encode('utf-8'), content_type='application/json')
return web.Response(
body=json.dumps({"test": True}).encode("utf-8"),
content_type="application/json",
)
app = web.Application(loop=loop)
app.router.add_route('GET', '/', handle)
app.router.add_route("GET", "/", handle)
web.run_app(app, port=sys.argv[1], access_log=None)

View File

@@ -1,11 +1,13 @@
# 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('/')
@route("/")
def index():
return ujson.dumps({'test': True})
return ujson.dumps({"test": True})
app = bottle.default_app()

View File

@@ -3,9 +3,11 @@
import falcon
import ujson as json
class TestResource:
def on_get(self, req, resp):
resp.body = json.dumps({"test": True})
app = falcon.API()
app.add_route('/', TestResource())
app.add_route("/", TestResource())

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)
@@ -13,8 +16,14 @@ kyk = Kyoukai("example_app")
logger = logging.getLogger("Kyoukai")
logger.setLevel(logging.ERROR)
@kyk.route("/")
async def index(ctx: HTTPRequestContext):
return ujson.dumps({"test":True}), 200, {"Content-Type": "application/json"}
return (
ujson.dumps({"test": True}),
200,
{"Content-Type": "application/json"},
)
kyk.run()
kyk.run()

View File

@@ -1,22 +1,30 @@
import asyncpg
import sys
import os
import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../')
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 + "/../../../")
print(json({"test": True}).output())
print("Running New 100,000 times")
times = 0
total_time = 0
for n in range(6):
time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000)
time = timeit.timeit(
'json({ "test":True }).output()',
setup="from sanic.response import json",
number=100000,
)
print("Took {} seconds".format(time))
total_time += time
times += 1
@@ -26,7 +34,11 @@ print("Running Old 100,000 times")
times = 0
total_time = 0
for n in range(6):
time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000)
time = timeit.timeit(
'json({ "test":True }).output_old()',
setup="from sanic.response import json",
number=100000,
)
print("Took {} seconds".format(time))
total_time += time
times += 1

View File

@@ -1,13 +1,17 @@
import sys
import os
import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../')
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 + "/../../../")
app = Sanic("test")
@@ -15,5 +19,6 @@ app = Sanic("test")
async def test(request):
return json({"test": True})
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=sys.argv[1])

View File

@@ -1,13 +1,17 @@
import sys
import os
import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../')
import os
import sys
from sanic import Sanic
from sanic.response import json, text
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 + "/../../../")
app = Sanic("test")
@@ -17,7 +21,7 @@ async def test(request):
return json({"test": True})
@app.route("/sync", methods=['GET', 'POST'])
@app.route("/sync", methods=["GET", "POST"])
def test(request):
return json({"test": True})
@@ -44,15 +48,19 @@ def post_json(request):
@app.route("/query_string")
def query_string(request):
return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
return json(
{
"parsed": True,
"args": request.args,
"url": request.url,
"query_string": request.query_string,
}
)
import sys
app.run(host="0.0.0.0", port=sys.argv[1])
# import asyncio_redis
# import asyncpg
# async def setup(sanic, loop):

View File

@@ -1,18 +1,19 @@
# Run with: python simple_server.py
import ujson
from tornado import ioloop, web
class MainHandler(web.RequestHandler):
def get(self):
self.write(ujson.dumps({'test': True}))
self.write(ujson.dumps({"test": True}))
app = web.Application([
(r'/', MainHandler)
], debug=False,
app = web.Application(
[(r"/", MainHandler)],
debug=False,
compress_response=False,
static_hash_cache=True
static_hash_cache=True,
)
app.listen(8000)

View File

@@ -2,26 +2,28 @@
""" 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
from wheezy.web.middleware import (
bootstrap_defaults,
path_routing_middleware_factory,
)
import ujson
class WelcomeHandler(BaseHandler):
def get(self):
response = HTTPResponse(content_type='application/json; charset=UTF-8')
response.write(ujson.dumps({"test":True}))
response = HTTPResponse(content_type="application/json; charset=UTF-8")
response.write(ujson.dumps({"test": True}))
return response
all_urls = [
url('', WelcomeHandler, name='default'),
# url('', welcome, name='welcome')
url("", WelcomeHandler, name="default"),
# url('', welcome, name='welcome')
]
@@ -29,18 +31,19 @@ options = {}
main = WSGIApplication(
middleware=[
bootstrap_defaults(url_mapping=all_urls),
path_routing_middleware_factory
path_routing_middleware_factory,
],
options=options
options=options,
)
if __name__ == '__main__':
if __name__ == "__main__":
import sys
from wsgiref.simple_server import make_server
try:
print('Visit http://localhost:{}/'.format(sys.argv[-1]))
make_server('', int(sys.argv[-1]), main).serve_forever()
print("Visit http://localhost:{}/".format(sys.argv[-1]))
make_server("", int(sys.argv[-1]), main).serve_forever()
except KeyboardInterrupt:
pass
print('\nThanks!')
print("\nThanks!")

View File

@@ -1,5 +1,8 @@
import asyncio
import logging
import sys
from inspect import isawaitable
import pytest
@@ -7,15 +10,49 @@ from sanic.exceptions import SanicException
from sanic.response import text
def test_app_loop_running(app):
def uvloop_installed():
try:
import uvloop # noqa
@app.get('/test')
return True
except ImportError:
return False
def test_app_loop_running(app):
@app.get("/test")
async def handler(request):
assert isinstance(app.loop, asyncio.AbstractEventLoop)
return text('pass')
return text("pass")
request, response = app.test_client.get('/test')
assert response.text == 'pass'
request, response = app.test_client.get("/test")
assert response.text == "pass"
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
def test_create_asyncio_server(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
assert isawaitable(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is True
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
def test_asyncio_server_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
def test_app_loop_not_running(app):
@@ -23,105 +60,115 @@ def test_app_loop_not_running(app):
app.loop
assert str(excinfo.value) == (
'Loop can only be retrieved after the app has started '
'running. Not supported with `create_server` function'
"Loop can only be retrieved after the app has started "
"running. Not supported with `create_server` function"
)
def test_app_run_raise_type_error(app):
with pytest.raises(TypeError) as excinfo:
app.run(loop='loop')
app.run(loop="loop")
assert str(excinfo.value) == (
'loop is not a valid argument. To use an existing loop, '
'change to create_server().\nSee more: '
'https://sanic.readthedocs.io/en/latest/sanic/deploying.html'
'#asynchronous-support'
"loop is not a valid argument. To use an existing loop, "
"change to create_server().\nSee more: "
"https://sanic.readthedocs.io/en/latest/sanic/deploying.html"
"#asynchronous-support"
)
def test_app_route_raise_value_error(app):
with pytest.raises(ValueError) as excinfo:
@app.route('/test')
async def handler():
return text('test')
assert str(excinfo.value) == 'Required parameter `request` missing in the handler() route?'
@app.route("/test")
async def handler():
return text("test")
assert (
str(excinfo.value)
== "Required parameter `request` missing in the handler() route?"
)
def test_app_handle_request_handler_is_none(app, monkeypatch):
def mockreturn(*args, **kwargs):
return None, [], {}, ''
return None, [], {}, ""
# Not sure how to make app.router.get() return None, so use mock here.
monkeypatch.setattr(app.router, 'get', mockreturn)
monkeypatch.setattr(app.router, "get", mockreturn)
@app.get('/test')
@app.get("/test")
def handler(request):
return text('test')
return text("test")
request, response = app.test_client.get('/test')
request, response = app.test_client.get("/test")
assert response.text == 'Error: \'None\' was returned while requesting a handler from the router'
assert (
response.text
== "Error: 'None' was returned while requesting a handler from the router"
)
@pytest.mark.parametrize('websocket_enabled', [True, False])
@pytest.mark.parametrize('enable', [True, False])
@pytest.mark.parametrize("websocket_enabled", [True, False])
@pytest.mark.parametrize("enable", [True, False])
def test_app_enable_websocket(app, websocket_enabled, enable):
app.websocket_enabled = websocket_enabled
app.enable_websocket(enable=enable)
assert app.websocket_enabled == enable
@app.websocket('/ws')
@app.websocket("/ws")
async def handler(request, ws):
await ws.send('test')
await ws.send("test")
assert app.websocket_enabled == True
def test_handle_request_with_nested_exception(app, monkeypatch):
err_msg = 'Mock Exception'
err_msg = "Mock Exception"
# Not sure how to raise an exception in app.error_handler.response(), use mock here
def mock_error_handler_response(*args, **kwargs):
raise Exception(err_msg)
monkeypatch.setattr(app.error_handler, 'response', mock_error_handler_response)
monkeypatch.setattr(
app.error_handler, "response", mock_error_handler_response
)
@app.get('/')
@app.get("/")
def handler(request):
raise Exception
return text('OK')
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert response.status == 500
assert response.text == 'An error occurred while handling an error'
assert response.text == "An error occurred while handling an error"
def test_handle_request_with_nested_exception_debug(app, monkeypatch):
err_msg = 'Mock Exception'
err_msg = "Mock Exception"
# Not sure how to raise an exception in app.error_handler.response(), use mock here
def mock_error_handler_response(*args, **kwargs):
raise Exception(err_msg)
monkeypatch.setattr(app.error_handler, 'response', mock_error_handler_response)
monkeypatch.setattr(
app.error_handler, "response", mock_error_handler_response
)
@app.get('/')
@app.get("/")
def handler(request):
raise Exception
return text('OK')
request, response = app.test_client.get('/', debug=True)
request, response = app.test_client.get("/", debug=True)
assert response.status == 500
assert response.text.startswith(
'Error while handling error: {}\nStack: Traceback (most recent call last):\n'.format(err_msg)
"Error while handling error: {}\nStack: Traceback (most recent call last):\n".format(
err_msg
)
)
@@ -129,22 +176,22 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
# Not sure how to raise an exception in app.error_handler.response(), use mock here
def mock_error_handler_response(*args, **kwargs):
raise SanicException('Mock SanicException')
raise SanicException("Mock SanicException")
monkeypatch.setattr(app.error_handler, 'response', mock_error_handler_response)
monkeypatch.setattr(
app.error_handler, "response", mock_error_handler_response
)
@app.get('/')
@app.get("/")
def handler(request):
raise Exception
return text('OK')
caplog.set_level(logging.ERROR, logger="sanic.root")
with caplog.at_level(logging.ERROR):
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert response.status == 500
assert response.text == 'Error: Mock SanicException'
assert caplog.record_tuples[0] == (
'sanic.root',
assert response.text == "Error: Mock SanicException"
assert (
"sanic.root",
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'"
)
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) in caplog.record_tuples

282
tests/test_asgi.py Normal file
View File

@@ -0,0 +1,282 @@
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 json, 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"
@pytest.mark.asyncio
async def test_cookie_customization(app):
@app.get("/cookie")
def get_cookie(request):
response = text("There's a cookie up in this response")
response.cookies["test"] = "Cookie1"
response.cookies["test"]["httponly"] = True
response.cookies["c2"] = "Cookie2"
response.cookies["c2"]["httponly"] = False
return response
_, response = await app.asgi_client.get("/cookie")
cookie_map = {
"test": {"value": "Cookie1", "HttpOnly": True},
"c2": {"value": "Cookie2", "HttpOnly": False},
}
for k, v in (
response.cookies._cookies.get("mockserver.local").get("/").items()
):
assert cookie_map.get(k).get("value") == v.value
if cookie_map.get(k).get("HttpOnly"):
assert "HttpOnly" in v._rest.keys()
@pytest.mark.asyncio
async def test_json_content_type(app):
@app.get("/json")
def send_json(request):
return json({"foo": "bar"})
@app.get("/text")
def send_text(request):
return text("foobar")
@app.get("/custom")
def send_custom(request):
return text("foobar", content_type="somethingelse")
_, response = await app.asgi_client.get("/json")
assert response.headers.get("content-type") == "application/json"
_, response = await app.asgi_client.get("/text")
assert response.headers.get("content-type") == "text/plain; charset=utf-8"
_, response = await app.asgi_client.get("/custom")
assert response.headers.get("content-type") == "somethingelse"

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

@@ -4,17 +4,18 @@ import asyncio
def test_bad_request_response(app):
lines = []
@app.listener('after_server_start')
@app.listener("after_server_start")
async def _request(sanic, loop):
connect = asyncio.open_connection('127.0.0.1', 42101)
connect = asyncio.open_connection("127.0.0.1", 42101)
reader, writer = await connect
writer.write(b'not http')
writer.write(b"not http")
while True:
line = await reader.readline()
if not line:
break
lines.append(line)
app.stop()
app.run(host='127.0.0.1', port=42101, debug=False)
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
assert lines[-1] == b'Error: Bad Request'
app.run(host="127.0.0.1", port=42101, debug=False)
assert lines[0] == b"HTTP/1.1 400 Bad Request\r\n"
assert lines[-1] == b"Error: Bad Request"

View File

@@ -0,0 +1,181 @@
from pytest import raises
from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.response import HTTPResponse, text
MIDDLEWARE_INVOKE_COUNTER = {"request": 0, "response": 0}
AUTH = "dGVzdDp0ZXN0Cg=="
def test_bp_group_indexing(app: Sanic):
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
group = Blueprint.group(blueprint_1, blueprint_2)
assert group[0] == blueprint_1
with raises(expected_exception=IndexError) as e:
_ = group[3]
def test_bp_group_with_additional_route_params(app: Sanic):
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
@blueprint_1.route(
"/request_path", methods=frozenset({"PUT", "POST"}), version=2
)
def blueprint_1_v2_method_with_put_and_post(request: Request):
if request.method == "PUT":
return text("PUT_OK")
elif request.method == "POST":
return text("POST_OK")
@blueprint_2.route(
"/route/<param>", methods=frozenset({"DELETE", "PATCH"}), name="test"
)
def blueprint_2_named_method(request: Request, param):
if request.method == "DELETE":
return text("DELETE_{}".format(param))
elif request.method == "PATCH":
return text("PATCH_{}".format(param))
blueprint_group = Blueprint.group(
blueprint_1, blueprint_2, url_prefix="/api"
)
@blueprint_group.middleware("request")
def authenticate_request(request: Request):
global AUTH
auth = request.headers.get("authorization")
if auth:
# Dummy auth check. We can have anything here and it's fine.
if AUTH not in auth:
return text("Unauthorized", status=401)
else:
return text("Unauthorized", status=401)
@blueprint_group.middleware("response")
def enhance_response_middleware(request: Request, response: HTTPResponse):
response.headers.add("x-test-middleware", "value")
app.blueprint(blueprint_group)
header = {"authorization": " ".join(["Basic", AUTH])}
_, response = app.test_client.put(
"/v2/api/bp1/request_path", headers=header
)
assert response.text == "PUT_OK"
assert response.headers.get("x-test-middleware") == "value"
_, response = app.test_client.post(
"/v2/api/bp1/request_path", headers=header
)
assert response.text == "POST_OK"
_, response = app.test_client.delete("/api/bp2/route/bp2", headers=header)
assert response.text == "DELETE_bp2"
_, response = app.test_client.patch("/api/bp2/route/bp2", headers=header)
assert response.text == "PATCH_bp2"
_, response = app.test_client.get("/v2/api/bp1/request_path")
assert response.status == 401
def test_bp_group(app: Sanic):
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
blueprint_group_1 = Blueprint.group(
blueprint_1, blueprint_2, url_prefix="/bp"
)
blueprint_3 = Blueprint("blueprint_3", url_prefix="/bp3")
@blueprint_group_1.middleware("request")
def blueprint_group_1_middleware(request):
global MIDDLEWARE_INVOKE_COUNTER
MIDDLEWARE_INVOKE_COUNTER["request"] += 1
@blueprint_3.route("/")
def blueprint_3_default_route(request):
return text("BP3_OK")
blueprint_group_2 = Blueprint.group(
blueprint_group_1, blueprint_3, url_prefix="/api"
)
@blueprint_group_2.middleware("response")
def blueprint_group_2_middleware(request, response):
global MIDDLEWARE_INVOKE_COUNTER
MIDDLEWARE_INVOKE_COUNTER["response"] += 1
app.blueprint(blueprint_group_2)
@app.route("/")
def app_default_route(request):
return text("APP_OK")
_, response = app.test_client.get("/")
assert response.text == "APP_OK"
_, response = app.test_client.get("/api/bp/bp1")
assert response.text == "BP1_OK"
_, response = app.test_client.get("/api/bp/bp2")
assert response.text == "BP2_OK"
_, response = app.test_client.get("/api/bp3")
assert response.text == "BP3_OK"
assert MIDDLEWARE_INVOKE_COUNTER["response"] == 4
assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4
def test_bp_group_list_operations(app: Sanic):
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
blueprint_group_1 = Blueprint.group(
blueprint_1, blueprint_2, url_prefix="/bp"
)
blueprint_3 = Blueprint("blueprint_2", url_prefix="/bp3")
@blueprint_3.route("/second")
def blueprint_3_second_route(request):
return text("BP3_OK")
assert len(blueprint_group_1) == 2
blueprint_group_1.append(blueprint_3)
assert len(blueprint_group_1) == 3
del blueprint_group_1[2]
assert len(blueprint_group_1) == 2
blueprint_group_1[1] = blueprint_3
assert len(blueprint_group_1) == 2
assert blueprint_group_1.url_prefix == "/bp"

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
@@ -17,12 +17,13 @@ from sanic.views import CompositionView
# GET
# ------------------------------------------------------------ #
@pytest.fixture(scope='module')
@pytest.fixture(scope="module")
def static_file_directory():
"""The static directory to serve"""
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, 'static')
static_directory = os.path.join(current_directory, "static")
return static_directory
@@ -32,21 +33,23 @@ def get_file_path(static_file_directory, file_name):
def get_file_content(static_file_directory, file_name):
"""The content of the static file to check"""
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
with open(get_file_path(static_file_directory, file_name), "rb") as file:
return file.read()
@pytest.mark.parametrize('method', HTTP_METHODS)
@pytest.mark.parametrize("method", HTTP_METHODS)
def test_versioned_routes_get(app, method):
bp = Blueprint('test_text')
bp = Blueprint("test_text")
method = method.lower()
func = getattr(bp, method)
if callable(func):
@func('/{}'.format(method), version=1)
@func("/{}".format(method), version=1)
def handler(request):
return text('OK')
return text("OK")
else:
print(func)
raise Exception("{} is not callable".format(func))
@@ -55,245 +58,231 @@ def test_versioned_routes_get(app, method):
client_method = getattr(app.test_client, method)
request, response = client_method('/v1/{}'.format(method))
request, response = client_method("/v1/{}".format(method))
assert response.status == 200
def test_bp(app):
bp = Blueprint('test_text')
bp = Blueprint("test_text")
@bp.route('/')
@bp.route("/")
def handler(request):
return text('Hello')
return text("Hello")
app.blueprint(bp)
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert app.is_request_stream is False
assert response.text == 'Hello'
assert response.text == "Hello"
def test_bp_strict_slash(app):
bp = Blueprint('test_text')
bp = Blueprint("test_text")
@bp.get('/get', strict_slashes=True)
@bp.get("/get", strict_slashes=True)
def get_handler(request):
return text('OK')
return text("OK")
@bp.post('/post/', strict_slashes=True)
@bp.post("/post/", strict_slashes=True)
def post_handler(request):
return text('OK')
return text("OK")
app.blueprint(bp)
request, response = app.test_client.get('/get')
assert response.text == 'OK'
request, response = app.test_client.get("/get")
assert response.text == "OK"
assert response.json is None
request, response = app.test_client.get('/get/')
request, response = app.test_client.get("/get/")
assert response.status == 404
request, response = app.test_client.post('/post/')
assert response.text == 'OK'
request, response = app.test_client.post("/post/")
assert response.text == "OK"
request, response = app.test_client.post('/post')
request, response = app.test_client.post("/post")
assert response.status == 404
def test_bp_strict_slash_default_value(app):
bp = Blueprint('test_text', strict_slashes=True)
bp = Blueprint("test_text", strict_slashes=True)
@bp.get('/get')
@bp.get("/get")
def get_handler(request):
return text('OK')
return text("OK")
@bp.post('/post/')
@bp.post("/post/")
def post_handler(request):
return text('OK')
return text("OK")
app.blueprint(bp)
request, response = app.test_client.get('/get/')
request, response = app.test_client.get("/get/")
assert response.status == 404
request, response = app.test_client.post('/post')
request, response = app.test_client.post("/post")
assert response.status == 404
def test_bp_strict_slash_without_passing_default_value(app):
bp = Blueprint('test_text')
bp = Blueprint("test_text")
@bp.get('/get')
@bp.get("/get")
def get_handler(request):
return text('OK')
return text("OK")
@bp.post('/post/')
@bp.post("/post/")
def post_handler(request):
return text('OK')
return text("OK")
app.blueprint(bp)
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
request, response = app.test_client.get("/get/")
assert response.text == "OK"
request, response = app.test_client.post('/post')
assert response.text == 'OK'
request, response = app.test_client.post("/post")
assert response.text == "OK"
def test_bp_strict_slash_default_value_can_be_overwritten(app):
bp = Blueprint('test_text', strict_slashes=True)
bp = Blueprint("test_text", strict_slashes=True)
@bp.get('/get', strict_slashes=False)
@bp.get("/get", strict_slashes=False)
def get_handler(request):
return text('OK')
return text("OK")
@bp.post('/post/', strict_slashes=False)
@bp.post("/post/", strict_slashes=False)
def post_handler(request):
return text('OK')
return text("OK")
app.blueprint(bp)
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
request, response = app.test_client.get("/get/")
assert response.text == "OK"
request, response = app.test_client.post('/post')
assert response.text == 'OK'
request, response = app.test_client.post("/post")
assert response.text == "OK"
def test_bp_with_url_prefix(app):
bp = Blueprint('test_text', url_prefix='/test1')
bp = Blueprint("test_text", url_prefix="/test1")
@bp.route('/')
@bp.route("/")
def handler(request):
return text('Hello')
return text("Hello")
app.blueprint(bp)
request, response = app.test_client.get('/test1/')
request, response = app.test_client.get("/test1/")
assert response.text == 'Hello'
assert response.text == "Hello"
def test_several_bp_with_url_prefix(app):
bp = Blueprint('test_text', url_prefix='/test1')
bp2 = Blueprint('test_text2', url_prefix='/test2')
bp = Blueprint("test_text", url_prefix="/test1")
bp2 = Blueprint("test_text2", url_prefix="/test2")
@bp.route('/')
@bp.route("/")
def handler(request):
return text('Hello')
return text("Hello")
@bp2.route('/')
@bp2.route("/")
def handler2(request):
return text('Hello2')
return text("Hello2")
app.blueprint(bp)
app.blueprint(bp2)
request, response = app.test_client.get('/test1/')
assert response.text == 'Hello'
request, response = app.test_client.get("/test1/")
assert response.text == "Hello"
request, response = app.test_client.get('/test2/')
assert response.text == 'Hello2'
request, response = app.test_client.get("/test2/")
assert response.text == "Hello2"
def test_bp_with_host(app):
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
bp = Blueprint("test_bp_host", url_prefix="/test1", host="example.com")
@bp.route('/')
@bp.route("/")
def handler1(request):
return text('Hello')
return text("Hello")
@bp.route('/', host="sub.example.com")
@bp.route("/", host="sub.example.com")
def handler2(request):
return text('Hello subdomain!')
return text("Hello subdomain!")
app.blueprint(bp)
headers = {"Host": "example.com"}
request, response = app.test_client.get(
'/test1/',
headers=headers)
assert response.text == 'Hello'
request, response = app.test_client.get("/test1/", headers=headers)
assert response.text == "Hello"
headers = {"Host": "sub.example.com"}
request, response = app.test_client.get(
'/test1/',
headers=headers)
request, response = app.test_client.get("/test1/", headers=headers)
assert response.text == 'Hello subdomain!'
assert response.text == "Hello subdomain!"
def test_several_bp_with_host(app):
bp = Blueprint('test_text',
url_prefix='/test',
host="example.com")
bp2 = Blueprint('test_text2',
url_prefix='/test',
host="sub.example.com")
bp = Blueprint("test_text", url_prefix="/test", host="example.com")
bp2 = Blueprint("test_text2", url_prefix="/test", host="sub.example.com")
@bp.route('/')
@bp.route("/")
def handler(request):
return text('Hello')
return text("Hello")
@bp2.route('/')
@bp2.route("/")
def handler1(request):
return text('Hello2')
return text("Hello2")
@bp2.route('/other/')
@bp2.route("/other/")
def handler2(request):
return text('Hello3')
return text("Hello3")
app.blueprint(bp)
app.blueprint(bp2)
assert bp.host == "example.com"
headers = {"Host": "example.com"}
request, response = app.test_client.get(
'/test/',
headers=headers)
assert response.text == 'Hello'
request, response = app.test_client.get("/test/", headers=headers)
assert response.text == "Hello"
assert bp2.host == "sub.example.com"
headers = {"Host": "sub.example.com"}
request, response = app.test_client.get(
'/test/',
headers=headers)
request, response = app.test_client.get("/test/", headers=headers)
assert response.text == 'Hello2'
request, response = app.test_client.get(
'/test/other/',
headers=headers)
assert response.text == 'Hello3'
assert response.text == "Hello2"
request, response = app.test_client.get("/test/other/", headers=headers)
assert response.text == "Hello3"
def test_bp_middleware(app):
blueprint = Blueprint('test_middleware')
blueprint = Blueprint("test_middleware")
@blueprint.middleware('response')
@blueprint.middleware("response")
async def process_response(request, response):
return text('OK')
return text("OK")
@app.route('/')
@app.route("/")
async def handler(request):
return text('FAIL')
return text("FAIL")
app.blueprint(blueprint)
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
def test_bp_exception_handler(app):
blueprint = Blueprint('test_middleware')
blueprint = Blueprint("test_middleware")
@blueprint.route('/1')
@blueprint.route("/1")
def handler_1(request):
raise InvalidUsage("OK")
@blueprint.route('/2')
@blueprint.route("/2")
def handler_2(request):
raise ServerError("OK")
@blueprint.route('/3')
@blueprint.route("/3")
def handler_3(request):
raise NotFound("OK")
@@ -303,131 +292,131 @@ def test_bp_exception_handler(app):
app.blueprint(blueprint)
request, response = app.test_client.get('/1')
request, response = app.test_client.get("/1")
assert response.status == 400
request, response = app.test_client.get('/2')
request, response = app.test_client.get("/2")
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
request, response = app.test_client.get('/3')
request, response = app.test_client.get("/3")
assert response.status == 200
def test_bp_listeners(app):
blueprint = Blueprint('test_middleware')
blueprint = Blueprint("test_middleware")
order = []
@blueprint.listener('before_server_start')
@blueprint.listener("before_server_start")
def handler_1(sanic, loop):
order.append(1)
@blueprint.listener('after_server_start')
@blueprint.listener("after_server_start")
def handler_2(sanic, loop):
order.append(2)
@blueprint.listener('after_server_start')
@blueprint.listener("after_server_start")
def handler_3(sanic, loop):
order.append(3)
@blueprint.listener('before_server_stop')
@blueprint.listener("before_server_stop")
def handler_4(sanic, loop):
order.append(5)
@blueprint.listener('before_server_stop')
@blueprint.listener("before_server_stop")
def handler_5(sanic, loop):
order.append(4)
@blueprint.listener('after_server_stop')
@blueprint.listener("after_server_stop")
def handler_6(sanic, loop):
order.append(6)
app.blueprint(blueprint)
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert order == [1, 2, 3, 4, 5, 6]
def test_bp_static(app):
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
with open(current_file, "rb") as file:
current_file_contents = file.read()
blueprint = Blueprint('test_static')
blueprint = Blueprint("test_static")
blueprint.static('/testing.file', current_file)
blueprint.static("/testing.file", current_file)
app.blueprint(blueprint)
request, response = app.test_client.get('/testing.file')
request, response = app.test_client.get("/testing.file")
assert response.status == 200
assert response.body == current_file_contents
@pytest.mark.parametrize('file_name', ['test.html'])
@pytest.mark.parametrize("file_name", ["test.html"])
def test_bp_static_content_type(app, file_name):
# This is done here, since no other test loads a file here
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, 'static')
static_directory = os.path.join(current_directory, "static")
blueprint = Blueprint('test_static')
blueprint = Blueprint("test_static")
blueprint.static(
'/testing.file',
"/testing.file",
get_file_path(static_directory, file_name),
content_type='text/html; charset=utf-8'
content_type="text/html; charset=utf-8",
)
app.blueprint(blueprint)
request, response = app.test_client.get('/testing.file')
request, response = app.test_client.get("/testing.file")
assert response.status == 200
assert response.body == get_file_content(static_directory, file_name)
assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
assert response.headers["Content-Type"] == "text/html; charset=utf-8"
def test_bp_shorthand(app):
blueprint = Blueprint('test_shorhand_routes')
blueprint = Blueprint("test_shorhand_routes")
ev = asyncio.Event()
@blueprint.get('/get')
@blueprint.get("/get")
def handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.put('/put')
@blueprint.put("/put")
def put_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.post('/post')
@blueprint.post("/post")
def post_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.head('/head')
@blueprint.head("/head")
def head_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.options('/options')
@blueprint.options("/options")
def options_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.patch('/patch')
@blueprint.patch("/patch")
def patch_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.delete('/delete')
@blueprint.delete("/delete")
def delete_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.websocket('/ws/', strict_slashes=True)
@blueprint.websocket("/ws/", strict_slashes=True)
async def websocket_handler(request, ws):
assert request.stream is None
ev.set()
@@ -436,137 +425,138 @@ def test_bp_shorthand(app):
assert app.is_request_stream is False
request, response = app.test_client.get('/get')
assert response.text == 'OK'
request, response = app.test_client.get("/get")
assert response.text == "OK"
request, response = app.test_client.post('/get')
request, response = app.test_client.post("/get")
assert response.status == 405
request, response = app.test_client.put('/put')
assert response.text == 'OK'
request, response = app.test_client.put("/put")
assert response.text == "OK"
request, response = app.test_client.get('/post')
request, response = app.test_client.get("/post")
assert response.status == 405
request, response = app.test_client.post('/post')
assert response.text == 'OK'
request, response = app.test_client.post("/post")
assert response.text == "OK"
request, response = app.test_client.get('/post')
request, response = app.test_client.get("/post")
assert response.status == 405
request, response = app.test_client.head('/head')
request, response = app.test_client.head("/head")
assert response.status == 200
request, response = app.test_client.get('/head')
request, response = app.test_client.get("/head")
assert response.status == 405
request, response = app.test_client.options('/options')
assert response.text == 'OK'
request, response = app.test_client.options("/options")
assert response.text == "OK"
request, response = app.test_client.get('/options')
request, response = app.test_client.get("/options")
assert response.status == 405
request, response = app.test_client.patch('/patch')
assert response.text == 'OK'
request, response = app.test_client.patch("/patch")
assert response.text == "OK"
request, response = app.test_client.get('/patch')
request, response = app.test_client.get("/patch")
assert response.status == 405
request, response = app.test_client.delete('/delete')
assert response.text == 'OK'
request, response = app.test_client.delete("/delete")
assert response.text == "OK"
request, response = app.test_client.get('/delete')
request, response = app.test_client.get("/delete")
assert response.status == 405
request, response = app.test_client.get('/ws/', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'})
assert response.status == 101
request, response = app.test_client.websocket("/ws/")
assert response.opened is True
assert ev.is_set()
def test_bp_group(app):
deep_0 = Blueprint('deep_0', url_prefix='/deep')
deep_1 = Blueprint('deep_1', url_prefix='/deep1')
deep_0 = Blueprint("deep_0", url_prefix="/deep")
deep_1 = Blueprint("deep_1", url_prefix="/deep1")
@deep_0.route('/')
@deep_0.route("/")
def handler(request):
return text('D0_OK')
return text("D0_OK")
@deep_1.route('/bottom')
@deep_1.route("/bottom")
def bottom_handler(request):
return text('D1B_OK')
return text("D1B_OK")
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix="/mid")
mid_1 = Blueprint("mid_tier", url_prefix="/mid1")
@mid_1.route('/')
@mid_1.route("/")
def handler1(request):
return text('M1_OK')
return text("M1_OK")
top = Blueprint.group(mid_0, mid_1)
app.blueprint(top)
@app.route('/')
@app.route("/")
def handler2(request):
return text('TOP_OK')
return text("TOP_OK")
request, response = app.test_client.get('/')
assert response.text == 'TOP_OK'
request, response = app.test_client.get("/")
assert response.text == "TOP_OK"
request, response = app.test_client.get('/mid1')
assert response.text == 'M1_OK'
request, response = app.test_client.get("/mid1")
assert response.text == "M1_OK"
request, response = app.test_client.get('/mid/deep')
assert response.text == 'D0_OK'
request, response = app.test_client.get("/mid/deep")
assert response.text == "D0_OK"
request, response = app.test_client.get('/mid/deep1/bottom')
assert response.text == 'D1B_OK'
request, response = app.test_client.get("/mid/deep1/bottom")
assert response.text == "D1B_OK"
def test_bp_group_with_default_url_prefix(app):
from sanic.response import json
bp_resources = Blueprint('bp_resources')
@bp_resources.get('/')
bp_resources = Blueprint("bp_resources")
@bp_resources.get("/")
def list_resources_handler(request):
resource = {}
return json([resource])
bp_resource = Blueprint('bp_resource', url_prefix='/<resource_id>')
bp_resource = Blueprint("bp_resource", url_prefix="/<resource_id>")
@bp_resource.get('/')
@bp_resource.get("/")
def get_resource_hander(request, resource_id):
resource = {'resource_id': resource_id}
resource = {"resource_id": resource_id}
return json(resource)
bp_resources_group = Blueprint.group(bp_resources, bp_resource,
url_prefix='/resources')
bp_api_v1 = Blueprint('bp_api_v1')
bp_resources_group = Blueprint.group(
bp_resources, bp_resource, url_prefix="/resources"
)
bp_api_v1 = Blueprint("bp_api_v1")
@bp_api_v1.get('/info')
@bp_api_v1.get("/info")
def api_v1_info(request):
return text('api_version: v1')
return text("api_version: v1")
bp_api_v1_group = Blueprint.group(bp_api_v1, bp_resources_group,
url_prefix='/v1')
bp_api_group = Blueprint.group(bp_api_v1_group, url_prefix='/api')
bp_api_v1_group = Blueprint.group(
bp_api_v1, bp_resources_group, url_prefix="/v1"
)
bp_api_group = Blueprint.group(bp_api_v1_group, url_prefix="/api")
app.blueprint(bp_api_group)
request, response = app.test_client.get('/api/v1/info')
assert response.text == 'api_version: v1'
request, response = app.test_client.get("/api/v1/info")
assert response.text == "api_version: v1"
request, response = app.test_client.get('/api/v1/resources')
request, response = app.test_client.get("/api/v1/resources")
assert response.json == [{}]
from uuid import uuid4
resource_id = str(uuid4())
request, response = app.test_client.get(
'/api/v1/resources/{0}'.format(resource_id))
assert response.json == {'resource_id': resource_id}
"/api/v1/resources/{0}".format(resource_id)
)
assert response.json == {"resource_id": resource_id}
def test_blueprint_middleware_with_args(app: Sanic):
@@ -588,20 +578,22 @@ def test_blueprint_middleware_with_args(app: Sanic):
app.blueprint(bp)
_, response = app.test_client.get("/wa", headers={"content-type": "application/json"})
_, response = app.test_client.get(
"/wa", headers={"content-type": "application/json"}
)
assert response.text == "value"
_, response = app.test_client.get("/wa", headers={"content-type": "plain/text"})
_, response = app.test_client.get(
"/wa", headers={"content-type": "plain/text"}
)
assert response.json.get("test") == "value"
d = {}
@pytest.mark.parametrize('file_name',
['test.file'])
@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()
with open(current_file, "rb") as file:
file.read()
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
@@ -609,11 +601,12 @@ def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
"/test.file/",
get_file_path(static_file_directory, file_name),
name="static.testing",
strict_slashes=True)
strict_slashes=True,
)
app.blueprint(bp)
uri = app.url_for('static', name='static.testing')
uri = app.url_for("static", name="static.testing")
assert uri == "/static/test.file"
_, response = app.test_client.get("/static/test.file")
@@ -627,9 +620,7 @@ def test_route_handler_add(app: Sanic):
view = CompositionView()
async def get_handler(request):
return json({
"response": "OK"
})
return json({"response": "OK"})
view.add(["GET"], get_handler, stream=False)
@@ -637,10 +628,7 @@ def test_route_handler_add(app: Sanic):
return text("OK")
bp = Blueprint(name="handler", url_prefix="/handler")
bp.add_route(
default_handler,
uri="/default/",
strict_slashes=True)
bp.add_route(default_handler, uri="/default/", strict_slashes=True)
bp.add_route(view, uri="/view", name="test")
@@ -665,18 +653,13 @@ 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()
def test_duplicate_blueprint(app):
bp_name = 'bp'
bp_name = "bp"
bp = Blueprint(bp_name)
bp1 = Blueprint(bp_name)
@@ -687,20 +670,66 @@ def test_duplicate_blueprint(app):
assert str(excinfo.value) == (
'A blueprint with the name "{}" is already registered. '
'Blueprint names must be unique.'
"Blueprint names must be unique."
).format(bp_name)
@pytest.mark.parametrize('debug', [True, False, None])
@pytest.mark.parametrize("debug", [True, False, None])
def test_register_blueprint(app, debug):
bp = Blueprint('bp')
bp = Blueprint("bp")
app.debug = debug
with pytest.warns(DeprecationWarning) as record:
app.register_blueprint(bp)
assert record[0].message.args[0] == (
"Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method"
" instead"
)
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,11 +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 DEFAULT_CONFIG, Config
from sanic.exceptions import PyFileError
@@ -13,18 +15,31 @@ from sanic.exceptions import PyFileError
def temp_path():
""" a simple cross platform replacement for NamedTemporaryFile """
with TemporaryDirectory() as td:
yield Path(td, 'file')
yield Path(td, "file")
class ConfigTest:
not_for_config = "should not be used"
CONFIG_VALUE = "should be used"
def test_load_from_object(app):
class Config:
not_for_config = 'should not be used'
CONFIG_VALUE = 'should be used'
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
app.config.from_object(Config)
assert 'CONFIG_VALUE' in app.config
assert app.config.CONFIG_VALUE == 'should be used'
assert 'not_for_config' not in app.config
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():
@@ -34,16 +49,23 @@ def test_auto_load_env():
del environ["SANIC_TEST_ANSWER"]
def test_auto_load_bool_env():
environ["SANIC_TEST_ANSWER"] = "True"
app = Sanic()
assert app.config.TEST_ANSWER == True
del environ["SANIC_TEST_ANSWER"]
def test_dont_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(load_env=False)
assert getattr(app.config, 'TEST_ANSWER', None) is None
assert getattr(app.config, "TEST_ANSWER", None) is None
del environ["SANIC_TEST_ANSWER"]
def test_load_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(load_env='MYAPP_')
app = Sanic(load_env="MYAPP_")
assert app.config.TEST_ANSWER == 42
del environ["MYAPP_TEST_ANSWER"]
@@ -63,43 +85,47 @@ def test_load_env_prefix_string_value():
def test_load_from_file(app):
config = dedent("""
config = dedent(
"""
VALUE = 'some value'
condition = 1 == 1
if condition:
CONDITIONAL = 'should be set'
""")
"""
)
with temp_path() as config_path:
config_path.write_text(config)
app.config.from_pyfile(str(config_path))
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
assert 'CONDITIONAL' in app.config
assert app.config.CONDITIONAL == 'should be set'
assert 'condition' not in app.config
assert "VALUE" in app.config
assert app.config.VALUE == "some value"
assert "CONDITIONAL" in app.config
assert app.config.CONDITIONAL == "should be set"
assert "condition" not in app.config
def test_load_from_missing_file(app):
with pytest.raises(IOError):
app.config.from_pyfile('non-existent file')
app.config.from_pyfile("non-existent file")
def test_load_from_envvar(app):
config = "VALUE = 'some value'"
with temp_path() as config_path:
config_path.write_text(config)
environ['APP_CONFIG'] = str(config_path)
app.config.from_envvar('APP_CONFIG')
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
environ["APP_CONFIG"] = str(config_path)
app.config.from_envvar("APP_CONFIG")
assert "VALUE" in app.config
assert app.config.VALUE == "some value"
def test_load_from_missing_envvar(app):
with pytest.raises(RuntimeError) as e:
app.config.from_envvar('non-existent variable')
assert str(e.value) == ("The environment variable 'non-existent "
"variable' is not set and thus configuration "
"could not be loaded.")
app.config.from_envvar("non-existent variable")
assert str(e.value) == (
"The environment variable 'non-existent "
"variable' is not set and thus configuration "
"could not be loaded."
)
def test_load_config_from_file_invalid_syntax(app):
@@ -132,6 +158,116 @@ def test_overwrite_exisiting_config_ignore_lowercase(app):
def test_missing_config(app):
with pytest.raises(AttributeError) as e:
app.config.NON_EXISTENT
assert str(e.value) == ("Config has no 'NON_EXISTENT'")
with pytest.raises(
AttributeError, match="Config has no 'NON_EXISTENT'"
) as e:
_ = app.config.NON_EXISTENT
def test_config_defaults():
"""
load DEFAULT_CONFIG
"""
conf = Config()
for key, value in DEFAULT_CONFIG.items():
assert getattr(conf, key) == value
def test_config_custom_defaults():
"""
we should have all the variables from defaults rewriting them with custom defaults passed in
Config
"""
custom_defaults = {
"REQUEST_MAX_SIZE": 1,
"KEEP_ALIVE": False,
"ACCESS_LOG": False,
}
conf = Config(defaults=custom_defaults)
for key, value in DEFAULT_CONFIG.items():
if key in custom_defaults.keys():
value = custom_defaults[key]
assert getattr(conf, key) == value
def test_config_custom_defaults_with_env():
"""
test that environment variables has higher priority than DEFAULT_CONFIG and passed defaults dict
"""
custom_defaults = {
"REQUEST_MAX_SIZE123": 1,
"KEEP_ALIVE123": False,
"ACCESS_LOG123": False,
}
environ_defaults = {
"SANIC_REQUEST_MAX_SIZE123": "2",
"SANIC_KEEP_ALIVE123": "True",
"SANIC_ACCESS_LOG123": "False",
}
for key, value in environ_defaults.items():
environ[key] = value
conf = Config(defaults=custom_defaults)
for key, value in DEFAULT_CONFIG.items():
if "SANIC_" + key in environ_defaults.keys():
value = environ_defaults["SANIC_" + key]
try:
value = int(value)
except ValueError:
if value in ["True", "False"]:
value = value == "True"
assert getattr(conf, key) == value
for key, value in environ_defaults.items():
del environ[key]
def test_config_access_log_passing_in_run(app):
assert app.config.ACCESS_LOG == True
@app.listener("after_server_start")
async def _request(sanic, loop):
app.stop()
app.run(port=1340, access_log=False)
assert app.config.ACCESS_LOG == False
app.run(port=1340, access_log=True)
assert app.config.ACCESS_LOG == True
@pytest.mark.asyncio
async def test_config_access_log_passing_in_create_server(app):
assert app.config.ACCESS_LOG == True
@app.listener("after_server_start")
async def _request(sanic, loop):
app.stop()
await app.create_server(
port=1341, access_log=False, return_asyncio_server=True
)
assert app.config.ACCESS_LOG == False
await app.create_server(
port=1342, access_log=True, return_asyncio_server=True
)
assert app.config.ACCESS_LOG == True
def test_config_rewrite_keep_alive():
config = Config()
assert config.KEEP_ALIVE == DEFAULT_CONFIG["KEEP_ALIVE"]
config = Config(keep_alive=True)
assert config.KEEP_ALIVE == True
config = Config(keep_alive=False)
assert config.KEEP_ALIVE == False
# use defaults
config = Config(defaults={"KEEP_ALIVE": False})
assert config.KEEP_ALIVE == False
config = Config(defaults={"KEEP_ALIVE": True})
assert config.KEEP_ALIVE == True

View File

@@ -1,116 +1,127 @@
from datetime import datetime, timedelta
from http.cookies import SimpleCookie
from sanic.response import text
import pytest
from sanic.cookies import Cookie
from sanic.response import text
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
def test_cookies(app):
@app.route('/')
@app.route("/")
def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test']))
response.cookies['right_back'] = 'at you'
response = text("Cookies are: {}".format(request.cookies["test"]))
response.cookies["right_back"] = "at you"
return response
request, response = app.test_client.get('/', cookies={"test": "working!"})
request, response = app.test_client.get("/", cookies={"test": "working!"})
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
response_cookies.load(response.headers.get("Set-Cookie", {}))
assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you'
assert response.text == "Cookies are: working!"
assert response_cookies["right_back"].value == "at you"
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
])
@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('/')
@app.route("/")
def handler(request):
response = text('hello cookies')
response.cookies['hello'] = 'world'
response.cookies['hello']['httponly'] = httponly
return text(response.cookies['hello'].encode('utf8'))
response = text("hello cookies")
response.cookies["hello"] = "world"
response.cookies["hello"]["httponly"] = httponly
return text(response.cookies["hello"].encode("utf8"))
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert ('HttpOnly' in response.text) == expected
assert ("HttpOnly" in response.text) == expected
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
])
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
def test_false_cookies(app, httponly, expected):
@app.route('/')
@app.route("/")
def handler(request):
response = text('hello cookies')
response.cookies['right_back'] = 'at you'
response.cookies['right_back']['httponly'] = httponly
response = text("hello cookies")
response.cookies["right_back"] = "at you"
response.cookies["right_back"]["httponly"] = httponly
return response
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
response_cookies.load(response.headers.get("Set-Cookie", {}))
assert ('HttpOnly' in response_cookies['right_back'].output()) == expected
assert ("HttpOnly" in response_cookies["right_back"].output()) == expected
def test_http2_cookies(app):
@app.route('/')
@app.route("/")
async def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test']))
response = text("Cookies are: {}".format(request.cookies["test"]))
return response
headers = {'cookie': 'test=working!'}
request, response = app.test_client.get('/', headers=headers)
headers = {"cookie": "test=working!"}
request, response = app.test_client.get("/", headers=headers)
assert response.text == 'Cookies are: working!'
assert response.text == "Cookies are: working!"
def test_cookie_options(app):
@app.route('/')
@app.route("/")
def handler(request):
response = text("OK")
response.cookies['test'] = 'at you'
response.cookies['test']['httponly'] = True
response.cookies['test']['expires'] = (datetime.now() +
timedelta(seconds=10))
response.cookies["test"] = "at you"
response.cookies["test"]["httponly"] = True
response.cookies["test"]["expires"] = datetime.now() + timedelta(
seconds=10
)
return response
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
response_cookies.load(response.headers.get("Set-Cookie", {}))
assert response_cookies['test'].value == 'at you'
assert response_cookies['test']['httponly'] is True
assert response_cookies["test"].value == "at you"
assert response_cookies["test"]["httponly"] is True
def test_cookie_deletion(app):
@app.route('/')
@app.route("/")
def handler(request):
response = text("OK")
del response.cookies['i_want_to_die']
response.cookies['i_never_existed'] = 'testing'
del response.cookies['i_never_existed']
del response.cookies["i_want_to_die"]
response.cookies["i_never_existed"] = "testing"
del response.cookies["i_never_existed"]
return response
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
response_cookies.load(response.headers.get("Set-Cookie", {}))
assert int(response_cookies['i_want_to_die']['max-age']) == 0
assert int(response_cookies["i_want_to_die"]["max-age"]) == 0
with pytest.raises(KeyError):
response.cookies['i_never_existed']
response.cookies["i_never_existed"]
def test_cookie_reserved_cookie():
@@ -134,58 +145,91 @@ def test_cookie_set_unknown_property():
def test_cookie_set_same_key(app):
cookies = {'test': 'wait'}
cookies = {"test": "wait"}
@app.get('/')
@app.get("/")
def handler(request):
response = text('pass')
response.cookies['test'] = 'modified'
response.cookies['test'] = 'pass'
response = text("pass")
response.cookies["test"] = "modified"
response.cookies["test"] = "pass"
return response
request, response = app.test_client.get('/', cookies=cookies)
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'])
@pytest.mark.parametrize("max_age", ["0", 30, 30.0, 30.1, "30", "test"])
def test_cookie_max_age(app, max_age):
cookies = {'test': 'wait'}
cookies = {"test": "wait"}
@app.get('/')
@app.get("/")
def handler(request):
response = text('pass')
response.cookies['test'] = 'pass'
response.cookies['test']['max-age'] = max_age
response = text("pass")
response.cookies["test"] = "pass"
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'
assert response.cookies['test']['max-age'] == str(max_age)
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)
# 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 cookie is None
@pytest.mark.parametrize('expires', [
datetime.now() + timedelta(seconds=60),
'Fri, 21-Dec-2018 15:30:00 GMT'
])
@pytest.mark.parametrize(
"expires", [datetime.utcnow() + timedelta(seconds=60)]
)
def test_cookie_expires(app, expires):
cookies = {'test': 'wait'}
expires = expires.replace(microsecond=0)
cookies = {"test": "wait"}
@app.get('/')
@app.get("/")
def handler(request):
response = text('pass')
response.cookies['test'] = 'pass'
response.cookies['test']['expires'] = expires
response = text("pass")
response.cookies["test"] = "pass"
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"] == "pass"
assert cookie_expires == expires
assert response.cookies['test'].value == 'pass'
if isinstance(expires, datetime):
expires = expires.strftime("%a, %d-%b-%Y %T GMT")
assert response.cookies['test']['expires'] == expires
@pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"])
def test_cookie_expires_illegal_instance_type(expires):
c = Cookie("test_cookie", "value")
with pytest.raises(expected_exception=TypeError) as e:
c["expires"] = expires
assert e.message == "Cookie 'expires' property must be a datetime"

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):
@@ -13,26 +15,26 @@ def test_create_task(app):
app.add_task(coro)
@app.route('/early')
@app.route("/early")
def not_set(request):
return text(e.is_set())
@app.route('/late')
@app.route("/late")
async def set(request):
await asyncio.sleep(0.1)
return text(e.is_set())
request, response = app.test_client.get('/early')
assert response.body == b'False'
request, response = app.test_client.get("/early")
assert response.body == b"False"
request, response = app.test_client.get('/late')
assert response.body == b'True'
request, response = app.test_client.get("/late")
assert response.body == b"True"
def test_create_task_with_app_arg(app):
q = Queue()
@app.route('/')
@app.route("/")
def not_set(request):
return "hello"
@@ -41,5 +43,5 @@ def test_create_task_with_app_arg(app):
app.add_task(coro)
request, response = app.test_client.get('/')
assert q.get() == 'test_create_task_with_app_arg'
request, response = app.test_client.get("/")
assert q.get() == "test_create_task_with_app_arg"

Some files were not shown because too many files have changed in this diff Show More