Compare commits

..

271 Commits
0.8.2 ... 19.3

Author SHA1 Message Date
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
Daniel Thorn
d5813152ab Allow sanic test client to bind to a random port (#1376) 2019-03-04 15:23:03 -06: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
章昕
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
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
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
7
aa7f2759a6 Merge pull request #1438 from yunstanford/master
18.12 Release
2018-12-27 11:15:07 -08:00
Yun Xu
9b9dd67797 adopt CalVer: MM.YY.MICRO, 18.12.0 release 2018-12-27 11:00:38 -08:00
7
3f73bc075a Merge pull request #1437 from FlouieInCl/master
Fix typo in exceptions.md
2018-12-27 09:59:00 -08:00
Yun Xu
56989a017b 18.12 release 2018-12-27 08:55:17 -08:00
JeongKyungSeo
ada5918bc8 Fix typo in exceptions.md 2018-12-27 16:11:37 +09:00
Jacob
4efd450b32 Add tests (#1433)
* Add tests for remove_route()

* Add tests for sanic/router.py

* Add tests for sanic/cookies.py

* Disable reset logging in test_logging.py

* Add tests for sanic/request.py

* Add tests for ContentRangeHandler

* Add tests for exception at response middleware

* Fix cached_handlers for ErrorHandler.lookup()

* Add test for websocket request timeout

* Add tests for getting cookies of StreamResponse, Remove some unused variables in tests/test_cookies.py

* Add tests for nested error handle
2018-12-22 09:21:45 -06:00
Omar Ryhan
d2670664ba Update exceptions.md (#1431)
Documented error handling from ``app.error_handler.add``
Documented custom error handling by subclassing.
2018-12-22 09:21:03 -06:00
7
fa7405fe9c Merge pull request #1422 from ashleysommer/server_slots
Add in some server.py __slots__ attribute names that are missing.
2018-12-15 13:58:22 -08:00
Jacob
33297f48a5 Add tests (#1430) 2018-12-13 11:50:50 -06: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
Ashley Sommer
06297a1918 Add in some server.py __slots__ property names that are missing. 2018-12-03 11:22:17 +10:00
Harsha Narayana
aa0874b6d8 100% Coverage for Sanic Blueprint (#1419)
* add unit tests to completely cover blueprints

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

* fix typo in the unit test code

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-25 13:56:34 -06:00
7
822ced6294 Merge pull request #1416 from chenjr0719/add_tests_for_static
Add tests for static and update document
2018-11-21 23:01:37 +08:00
jacob
1a59614f79 Add stream_large_files and host examples in static_file document 2018-11-20 14:28:08 +08:00
jacob
f2d528e52a Add tests for static 2018-11-20 12:28:00 +08:00
Hasan Ramezani
f7adc5f84c Fix remove_entity_headers helper function (#1415)
* Fix `remove_entity_headers` helper function

* Add test for `remove_entity_headers` helper function
2018-11-19 09:30:53 -06:00
7
e955e833c4 Merge pull request #33 from huge-success/master
Merge upstream master branch
2018-11-16 13:02:16 +08:00
Tim&Anna
096c44b910 Update extensions.md (#1263)
* Update extensions.md

add an extension: sanic-script

* Update extensions.md
2018-11-14 07:16:43 -06:00
Nir Galon
efb9a42045 Change deprecated verify_ssl to ssl (#1155) 2018-11-14 07:16:14 -06:00
7
296cda7801 Merge pull request #1411 from devArtoria/patch-2
ADD: Sanic-JWT-Extended extension to extension docs
2018-11-13 13:49:35 +08:00
Lewis
90b9d73244 ADD: Sanic-JWT-Extended extension 2018-11-13 14:39:29 +09:00
Richard K
c8b0e7f2a7 Created methods to append and finish body content on Request (#1379)
* created methods to append and finish body content on request.py so the underlying body instance can have certain flexibility; modified server.py to reflect these changes

* - made some adjustments (including the Request.body_init method) as requested by @ahopkins;
- created a new test with a custom Request class implementation of the flexibility provided by the new methods;
2018-11-12 09:11:41 -06:00
7
6ce88ab5a4 Merge pull request #1400 from chenjr0719/add_tests_for_log
Add test for sanic.root logger and update the docs of logging
2018-11-12 20:45:05 +08:00
7
e13ab805df Merge pull request #1409 from yunstanford/windows-ci
CI Support for Windows
2018-11-12 20:05:21 +08:00
Yun Xu
e58ea8c7b4 fix unit test for windows ci
fix unit tests for windows ci

add appveyor build status badge

add readthedoc build status badge
2018-11-12 01:04:53 -08:00
jacob
dd5bac61cb Update document for logging 2018-11-12 16:09:12 +08:00
Jacob
6270b27a97 Merge branch 'master' into add_tests_for_log 2018-11-12 09:53:44 +08:00
Hasan Ramezani
f89ba1d39f Add tests for is_entity_header and is_hop_by_hop_header helper functions (#1410) 2018-11-11 10:57:57 -06:00
Yun Xu
8b5d137d8f fix .appveyor.yml 2018-11-10 06:11:01 -08:00
Yun Xu
2629fab649 add .appveyor.yml for windows ci support 2018-11-10 05:50:22 -08:00
7
92cd10c6a8 Merge pull request #32 from huge-success/master
merge upstream master branch
2018-11-10 21:26:37 +08:00
7
cc3edb90dc Merge pull request #1408 from harshanarayana/feature/Unit_Test_Enhancements
Additional Unit Tests
2018-11-10 20:46:51 +08:00
Harsha Narayana
c60ba81984 cleanup stale test for cookie object
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-10 16:54:24 +05:30
Harsha Narayana
ece3cdaa2e add unit tests for App Config, Cokkies and Request handler
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-10 16:50:30 +05:30
7
4cb40f2042 Merge pull request #1403 from harshanarayana/fix/GIT-1398-Http_Response_Content_Length_Mismatch
Fix Content-Length Mismatch while using json and ujson
2018-11-10 00:14:03 +08:00
7
0e9f350982 Merge pull request #1405 from hramezani/test_has_message_body
Add test for has_message_body helper function.
2018-11-08 22:20:07 +08:00
Hasan Ramezani
cf439f01f8 Add test for has_message_body helper function. 2018-11-07 21:29:12 +01:00
Harsha Narayana
f1f1b8a630 add additional test cases to validate Content-Length header
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-07 22:07:28 +05:30
Harsha Narayana
d4d1df03c9 fix content length mismatch in windows and other platform
The current implementation of `sanic` attempts to make use of `ujson` if
it's available in the system and if not, it will default to the inbuilt
`json` module provided by python.

The current implementation of `ujson` does not provide a mechanism to
provide a custom `seperators` parameter as part of the `dumps` method
invocation and the default behavior of the module is to strip all the
spaces around seperators such as `:` and `,`. This leads to an
inconsistency in the response length when the response is generated
using the `ujson` and in built `json` module provided by python.

To maintain the consistency, this commit overrides the default behavior
of the `dumps` method provided by the `json` module to add a `seperators`
argument that will strip the white spaces around these character like
the default behavior of `ujson`

This addresses the issue referenced in #1398

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-07 21:38:32 +05:30
Harsha Narayana
92b73a6f4f fix Range header handling for static files (#1402)
This commit fixes the issue in the `Range` header handling that was done
while serving the file contents.

As per the HTTP response standards, a status code of 206 will be used in
case if the Range is returning a partial value and default of 200 in
other cases.

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-07 07:36:56 -06:00
Meng Wang
b63c06c75a fix the logger and make it work (#1397)
* fix the logger and make it work

* modify test_logging parameters and add a new unit test
2018-11-06 08:39:38 -06:00
jacob
3e3bce422e Add test for sanic.root logger and update the docs of logging 2018-11-06 21:27:01 +08:00
Stephen Sadowski
e3a27c2cc4 Merge pull request #1391 from AndresSan6/loop_exception
Handle "loop" exception in app.py
2018-11-05 08:19:01 -06:00
Stephen Sadowski
f13f451084 Merge pull request #1385 from lixxu/master
update doc for latest blueprint code
2018-11-05 07:40:12 -06:00
Stephen Sadowski
df0e3de911 Merge pull request #1393 from ashleysommer/pickleable-app-blueprint
Fix pickling blueprints Fixes #1392
2018-11-05 07:24:15 -06:00
Ashley Sommer
8466be8728 Fix type pikcle->pickle in multiprocessing test 2018-11-04 15:27:25 +10:00
Ashley Sommer
5cf2144b3f 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).
2018-11-04 15:04:12 +10:00
Andres Sanchez
7c182f63c8 Indentation fix 2018-11-01 10:59:45 -06:00
Andres Sanchez
056180782c Removed unnecessary changes to request and router files, changes to fix lint test 2018-11-01 10:53:53 -06:00
Andres Sanchez
ff0d5870e9 Merge branch 'lintfix' into loop_exception
Made changes unnecesarry changes in request and router files, went back to previous commit and made correct changes to fix lint
2018-11-01 10:40:47 -06:00
Andres Sanchez
b70176f8c7 Fixed character limit per line in requested changes for app.py 2018-11-01 10:36:34 -06:00
Andres Sanchez
e3655b525d Modifications to ruequest and router files to fix linting issues. 2018-11-01 10:04:40 -06:00
Andres Sanchez
e63d0091af Assert was chnaged for an if and updated error messages 2018-10-31 15:23:29 -06:00
Andres Sanchez
7b0af2d80d Handle loop exception in app.py 2018-10-31 13:35:03 -06:00
Stephen Sadowski
7d79a86d4d Merge pull request #1387 from huge-success/docbuild
Resolve build of latex documentation relating to markdown lists
2018-10-30 16:13:29 -05:00
Adam Hopkins
ba46aff069 Resolve build of latex documentation relating to markdown lists 2018-10-30 22:39:17 +02:00
lixxu
7a65471ba5 update doc for latest blueprint code 2018-10-29 16:54:34 +08:00
Stephen Sadowski
c7c46da975 Merge pull request #1383 from huge-success/docbuild
Fix documentation build errors
2018-10-26 08:19:10 -05:00
Adam Hopkins
c708e8425f Fix documentation build errors 2018-10-26 11:57:28 +03:00
Eli Uriegas
905c51bef0 Merge pull request #1371 from yunstanford/integrate-isort
codestyle: Integrate isort
2018-10-23 16:05:36 -07:00
Eli Uriegas
bd87098b7e Merge pull request #1368 from yunstanford/fix-redirect
Add '%' to quote_plus's `safe` parameter in response.redirect
2018-10-23 15:12:02 -07:00
Eli Uriegas
5f486cc25f Merge pull request #1378 from hramezani/fix_some_lint_error
Fix some test files lint errors.
2018-10-23 15:10:15 -07:00
Eli Uriegas
f79fb72a33 Merge pull request #1377 from yunstanford/fix-readthedoc-build
Fix readthedoc build
2018-10-23 15:07:25 -07:00
Yun Xu
0505aa2dda refactor import 2018-10-23 14:53:39 -07:00
Hasan Ramezani
485ff32e42 Fix all test files lint errors. 2018-10-23 11:04:17 +02:00
Stephen Sadowski
5ead67972f Merge pull request #1375 from sjsadowski/master
Added documentation for AF_INET6 and AF_UNIX socket usage
2018-10-21 15:28:40 -05:00
Yun Xu
9c860dbff3 fix readthedoc build 2018-10-21 01:56:48 -07:00
Stephen Sadowski
a20ad99638 Added documentation for AF_INET6 and AF_UNIX socket usage 2018-10-19 13:33:01 -05:00
Yun Xu
8ef7bf8e7b integrate with isort 2018-10-17 21:20:16 -07:00
7
0d5be1969a Merge pull request #31 from huge-success/master
Merge Upstream master branch
2018-10-17 21:02:44 -07:00
Adam Hopkins
d06ea9bfc3 Merge pull request #1370 from huge-success/ahopkins-patch-1
Update issue templates
2018-10-17 09:47:22 +03:00
Adam Hopkins
57e79882e1 Update issue templates 2018-10-16 15:42:52 +03:00
Yun Xu
20d1ab60c7 remove unused json import 2018-10-15 22:13:42 -07:00
Yun Xu
277c2ce2d2 fix redirect with quoted param 2018-10-15 21:53:11 -07:00
7
34e51f01d1 Merge pull request #30 from huge-success/master
Merge Upstream Master Branch
2018-10-15 20:04:57 -07:00
7
f4b4e3a58c Merge pull request #1366 from hramezani/lint_test_blueprints
Fix some lint errors and warnings in `tests/test_blueprints.py`
2018-10-14 21:02:48 -07:00
7
def2e033c8 Merge pull request #1365 from yunstanford/codestyle-black
Codestyle black
2018-10-14 10:07:09 -07:00
Hasan Ramezani
dfec18278b Fix some lint errors and warnings in tests/test_blueprints.py. 2018-10-14 16:09:47 +02:00
Yun Xu
cd5bdecda3 add codestyle badge in README 2018-10-13 18:33:02 -07:00
Yun Xu
9b6217ba41 fix travisci 2018-10-13 18:19:08 -07:00
Yun Xu
272f6e195d added black for lint check 2018-10-13 18:10:43 -07:00
Yun Xu
aa9bf04dfe run black against sanic module 2018-10-13 17:55:33 -07:00
7
9ae6dfb6d2 Merge pull request #29 from huge-success/master
merge upstream master branch
2018-10-13 17:28:32 -07:00
7
619bb79a2f Merge pull request #1336 from untitaker/logging-refactor
Try not to stringify exception in logging messages
2018-10-13 16:54:57 -07:00
7
0cad831eca Merge pull request #1364 from yunstanford/raise-exception-when-param-conflicts
Raise exception when param conflicts
2018-10-13 16:28:59 -07:00
Yun Xu
f15a7fb588 fix flake8 2018-10-12 23:06:43 -07:00
Yun Xu
1bdf9ca057 add py37 in setup.py 2018-10-12 22:58:49 -07:00
Yun Xu
c8c370b784 raise exception when param conflicts in route 2018-10-12 22:57:56 -07:00
7
63182f55f7 Merge pull request #28 from huge-success/master
Merge upstream master branch
2018-10-12 22:38:37 -07:00
Stephen Sadowski
41759248e2 Merge pull request #1361 from yunstanford/cancel-request-when-connection-lost
Cancel request when connection lost
2018-10-12 07:25:10 -05:00
Yun Xu
3149d5a66d add unit test for request_stream 2018-10-11 23:12:33 -07:00
Yun Xu
8b13597099 add unit tests for verifying 2018-10-11 23:02:21 -07:00
Yun Xu
36032cc26e cancel task when connection_lost 2018-10-11 22:38:26 -07:00
7
4cb107aedc Merge pull request #27 from huge-success/master
Merge upstream master branch
2018-10-11 22:34:09 -07:00
7
176f8d1981 Merge pull request #1358 from hramezani/fix_config_tests
Change the config test to remove `NamedTemporaryFile`
2018-10-11 21:39:48 -07:00
Hasan Ramezani
9a26030bd5 Change the config test to remove NamedTemporaryFile 2018-10-11 17:34:46 +02:00
Stephen Sadowski
6778f4d9e0 Merge pull request #1342 from hramezani/load_config_file_syntax_error
Handle syntax error in load config file.
2018-10-11 08:56:48 -05:00
Stephen Sadowski
fd61b9e3e2 Merge pull request #1327 from hatarist/fix-1323
Rename the `http` module to `helpers`
2018-10-11 07:56:51 -05:00
Stephen Sadowski
298d5cdf24 Merge pull request #1334 from chenjr0719/master
Fix TypeError when use Blueprint.group() to group blueprint with defa…
2018-10-11 07:28:10 -05:00
7
1bf1c9d006 Merge pull request #26 from huge-success/master
Merge upstream master branch
2018-10-10 20:33:57 -07:00
7
7dc62be5cf Merge pull request #1335 from abuckenheimer/fix_windows_unittests
unittests passing on windows again
2018-10-10 20:15:35 -07:00
jacob
be580a6a5b Clean up files created by pytest-html 2018-10-11 10:06:05 +08:00
7
8ce519668b Merge pull request #1353 from abn/fix-unhandled-exception
Simplify request ip and port retrieval logic
2018-10-09 23:33:51 -07:00
jacob
801258c46a Merge branch 'master' of github.com:chenjr0719/sanic 2018-10-10 14:04:45 +08:00
jacob
32a1db3622 Remove normpath 2018-10-10 14:04:21 +08:00
7
ed1f3daacc Merge pull request #1352 from devArtoria/patch-1
Fix missing quotes in decorator example
2018-10-08 21:57:34 -07:00
Alec Buckenheimer
b7d74c82ba simplified aiohttp version diffs, reverted worker import policy 2018-10-08 22:48:21 -04:00
Arun Babu Neelicattu
c3b31a6fb0 Simplify request ip and port retrieval logic
This change also ensures that cases where transport stream is
already closed is handled gracefully.
2018-10-08 21:25:47 +02:00
Hasan Ramezani
f4c55bbc07 Handle config error in load config file. 2018-10-08 19:17:06 +02:00
Lewis
a16842f7bc Fix missing quotes in decorator example 2018-10-08 18:59:15 +09:00
7
439a38664f Merge pull request #25 from huge-success/master
Merge upstream master branch
2018-10-07 20:32:52 -07:00
7
5cc12fd945 Merge pull request #1348 from hramezani/add_config_test
Add test for `config.from_object`.
2018-10-07 19:53:58 -07:00
7
fe116fff5a Merge pull request #1350 from hramezani/config_documentation
Add missed documentation for config section.
2018-10-07 13:58:06 -07:00
Stephen Sadowski
06aaaf4727 Merge pull request #1351 from yunstanford/integrate-with-codecov
Integrate with codecov
2018-10-07 10:13:31 -05:00
Yun Xu
6deb9b49b2 correct Codecov badge url 2018-10-06 21:39:04 -07:00
Yun Xu
d59e92d3e5 integrate with codecov 2018-10-06 21:31:04 -07:00
7
cc83c1f0cf Merge pull request #24 from huge-success/master
merge upstream master branch
2018-10-06 21:22:54 -07:00
Hasan Ramezani
1fe7306af8 Add missed documentation for config section. 2018-10-07 01:32:36 +02:00
Hasan Ramezani
c796d73fc3 Add test for config.from_object. 2018-10-07 00:14:37 +02:00
Markus Unterwaditzer
eb93f884f3 fix: Missing import 2018-10-05 16:47:12 +02:00
Markus Unterwaditzer
3673feb256 fix: typo 2018-10-05 16:33:46 +02:00
Markus Unterwaditzer
7c9c783e9d deprecate Handler.log 2018-10-05 16:31:01 +02:00
Stephen Sadowski
74a4b9efaa Merge pull request #1345 from huge-success/ahopkins-patch-1
Update README.rst
2018-10-04 18:45:47 -05:00
Stephen Sadowski
4466e8cce1 Merge pull request #1304 from ignatenkobrain/fedora
Switch to websockets 6.0
2018-10-04 18:45:22 -05:00
Adam Hopkins
b689037984 Update README.rst 2018-10-04 12:31:57 +03:00
Stephen Sadowski
db1ba21d88 Merge pull request #1343 from vltr/httptools_pinned
pinned httptools requirement to version 0.0.10+
2018-10-03 19:27:25 -05:00
Eli Uriegas
50d270ef7c Merge pull request #1316 from sjsadowski/master
Updated changelog.md for 0.8.x
2018-10-03 15:19:21 -07:00
Richard Kuesters
d1a578b555 pinned httptools requirement to version 0.0.10+ 2018-10-03 12:22:29 -03:00
Stephen Sadowski
76e9859cf8 Merge branch 'master' into master 2018-10-03 09:56:29 -05:00
Stephen Sadowski
add9d363c5 Merge branch 'master' into logging-refactor 2018-10-03 09:55:01 -05:00
Stephen Sadowski
1498baab0f Merge pull request #1338 from hramezani/improve_config_test
Check error message and fix some lint error in test config.
2018-10-03 09:18:46 -05:00
Stephen Sadowski
df7f63d45d Merge branch 'master' into improve_config_test 2018-10-03 06:30:44 -05:00
Stephen Sadowski
f7425126a1 Merge pull request #1341 from ashleysommer/unnecessary_code
Fixes #1340
2018-10-03 06:30:22 -05:00
Ashley Sommer
790047e450 Fixes #1340 2018-10-03 10:59:24 +10:00
Stephen Sadowski
9198b5b0be Merge branch 'master' into improve_config_test 2018-10-02 13:21:23 -05:00
Stephen Sadowski
d534acb79d Merge branch 'master' into logging-refactor 2018-10-01 15:41:07 -05:00
Hasan Ramezani
d100f54551 Check error message and fix some lint error in test config. 2018-10-01 20:36:21 +02:00
Stephen Sadowski
7a9e100b0f Merge branch 'master' into fix_windows_unittests 2018-10-01 10:10:48 -05:00
Stephen Sadowski
fafe23d7c2 Merge pull request #1337 from cmcaine/fix-error-msg
Fix whitespace in error message
2018-10-01 09:31:45 -05:00
Alec Buckenheimer
9a08bdae4a fix flake8 linelength errors 2018-10-01 09:46:18 -04:00
Colin Caine
bcc11fa7fe Fix whitespace in error message 2018-09-30 09:36:55 +01:00
Markus Unterwaditzer
7d0c0fdf7c fix: Namespacing of sanic logger 2018-09-29 22:40:05 +02:00
Markus Unterwaditzer
0e33d46ead Try not to stringify exception in logging messages
This just fixes the worst offenders that trip up error reporting tools
like Sentry.io
2018-09-29 22:32:51 +02:00
Alec Buckenheimer
efbacc17cf unittests passing on windows again 2018-09-29 13:54:47 -04:00
jacob
bd6dbd9090 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 2018-09-29 18:23:16 +08:00
Eli Uriegas
076cf51fb2 Merge pull request #1305 from Stranger6667/app-fixture
Reuse app fixture in tests
2018-09-26 18:30:46 -07:00
Igor Hatarist
f8a6af1e28 Rename the http module to helpers to prevent conflicts with the built-in Python http library (fixes #1323) 2018-09-25 20:46:40 +03:00
Stephen Sadowski
96912f436d Corrected Raphael Deem's name in changelog - sorry @r0fls! 2018-09-24 09:05:58 -05:00
Raphael Deem
f0e162442f Merge branch 'master' into app-fixture 2018-09-21 15:16:00 -07:00
Eli Uriegas
04b8dd989f Merge pull request #1315 from seemethere/multidocs
Add multidict to readthedocs environment.yml
2018-09-15 19:03:56 +02:00
Stephen Sadowski
5851c8bd91 revised formatting for CHANGELOG.md 2018-09-14 13:30:57 -05:00
Stephen Sadowski
78efcf93f8 Updated changelog for all accepted PRs from 0.7.0 to Current 2018-09-14 10:56:32 -05:00
Eli Uriegas
bb35bc3898 Add multidict to readthedocs environment.yml
Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
2018-09-14 16:00:29 +02:00
Stephen Sadowski
f38783bdef Merge pull request #1 from huge-success/master
Merge from head
2018-09-14 08:20:37 -05:00
Channel Cat
d8f9986089 Re-releasing with updated credentials 2018-09-13 02:24:31 -07:00
Channel Cat
3e616b599a update encrypted creds for new org 2018-09-13 02:17:27 -07:00
Igor Gnatenko
c578974246 Switch to websockets 6.0
Signed-off-by: Igor Gnatenko <i.gnatenko.brain@gmail.com>
2018-09-02 09:23:30 +02:00
dmitry.dygalo
fec81ffe73 Reuse app fixture in tests 2018-08-26 16:43:14 +02:00
7
a7dd73c657 Merge pull request #23 from channelcat/master
py37 (#1256)
2018-07-03 22:12:02 -07:00
7
f770e16f6d Merge pull request #22 from channelcat/master
merge upstream master branch
2018-06-26 23:33:35 -07:00
7
c1222175b3 Merge pull request #21 from channelcat/master
remote tracking
2018-06-10 20:17:27 -07:00
7
7928b9b3a2 Merge pull request #20 from channelcat/master
merge upstream master branch
2018-04-29 21:50:07 -07: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
126 changed files with 10421 additions and 4862 deletions

32
.appveyor.yml Normal file
View File

@@ -0,0 +1,32 @@
version: "{branch}.{build}"
environment:
matrix:
- TOXENV: py35-no-ext
PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5.x"
PYTHON_ARCH: "64"
- TOXENV: py36-no-ext
PYTHON: "C:\\Python36-x64"
PYTHON_VERSION: "3.6.x"
PYTHON_ARCH: "64"
- TOXENV: py37-no-ext
PYTHON: "C:\\Python37-x64"
PYTHON_VERSION: "3.7.x"
PYTHON_ARCH: "64"
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
install:
- pip install tox
build: off
test_script: tox
notifications:
- provider: Email
on_build_success: false
on_build_status_changed: false

25
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,25 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks.
**Code snippet**
Relevant source code, make sure to remove what is not necessary.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 0.8.3]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,16 @@
---
name: Feature request
about: Suggest an idea for Sanic
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or sample code about the feature request here.

13
.github/ISSUE_TEMPLATE/help-wanted.md vendored Normal file
View File

@@ -0,0 +1,13 @@
---
name: Help wanted
about: Do you need help? Try community.sanicframework.org
---
*DELETE ALL BEFORE POSTING*
*Post your HELP WANTED questions on [the community forum](https://community.sanicframework.org/)*.
Checkout the community forum before posting any question here.
We prefer if you put these kinds of questions here:
https://community.sanicframework.org/c/questions-and-help

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ docs/_build/
docs/_api/
build/*
.DS_Store
dist/*

View File

@@ -21,17 +21,21 @@ matrix:
python: 3.7
dist: xenial
sudo: true
- env: TOX_ENV=flake8
- env: TOX_ENV=lint
python: 3.6
- env: TOX_ENV=check
python: 3.6
install: pip install -U tox
install:
- pip install -U tox
- pip install codecov
script: travis_retry tox -e $TOX_ENV
after_success:
- codecov
deploy:
provider: pypi
user: channelcat
password:
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
on:
tags: true
distributions: "sdist bdist_wheel"

View File

@@ -1,3 +1,116 @@
Version 18.12
-------------
18.12.0
- Changes:
- Improved codebase test coverage from 81% to 91%.
- Added stream_large_files and host examples in static_file document
- Added methods to append and finish body content on Request (#1379)
- Integrated with .appveyor.yml for windows ci support
- Added documentation for AF_INET6 and AF_UNIX socket usage
- Adopt black/isort for codestyle
- Cancel task when connection_lost
- Simplify request ip and port retrieval logic
- Handle config error in load config file.
- Integrate with codecov for CI
- Add missed documentation for config section.
- Deprecate Handler.log
- Pinned httptools requirement to version 0.0.10+
- Fixes:
- Fix `remove_entity_headers` helper function (#1415)
- Fix TypeError when use Blueprint.group() to group blueprint with default url_prefix, Use os.path.normpath to avoid invalid url_prefix like api//v1
f8a6af1 Rename the `http` module to `helpers` to prevent conflicts with the built-in Python http library (fixes #1323)
- Fix unittests on windows
- Fix Namespacing of sanic logger
- Fix missing quotes in decorator example
- Fix redirect with quoted param
- Fix doc for latest blueprint code
- Fix build of latex documentation relating to markdown lists
- Fix loop exception handling in app.py
- Fix content length mismatch in windows and other platform
- Fix Range header handling for static files (#1402)
- Fix the logger and make it work (#1397)
- Fix type pikcle->pickle in multiprocessing test
- Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
- Fix document for logging
Version 0.8
-----------
0.8.3
- Changes:
- Ownership changed to org 'huge-success'
0.8.0
- Changes:
- Add Server-Sent Events extension (Innokenty Lebedev)
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
- Sanitize URL before redirection (aveao)
- Add url_bytes to request (johndoe46)
- py37 support for travisci (yunstanford)
- Auto reloader support for OSX (garyo)
- Add UUID route support (Volodymyr Maksymiv)
- Add pausable response streams (Ashley Sommer)
- Add weakref to request slots (vopankov)
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
- Allow streaming handlers in add_route (kinware)
- use travis_retry for tox (Raphael Deem)
- update aiohttp version for test client (yunstanford)
- add redirect import for clarity (yingshaoxo)
- Update HTTP Entity headers (Arnulfo Solís)
- Add register_listener method (Stephan Fitzpatrick)
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
- Content-length header on 204/304 responses (Arnulfo Solís)
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
- Update development status from pre-alpha to beta (Maksim Anisenkov)
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
- Add support for blueprint groups and nesting (Elias Tarhini)
- Remove uvloop for windows setup (Aleksandr Kurlov)
- Auto Reload (Yaser Amari)
- Documentation updates/fixups (multiple contributors)
- Fixes:
- Fix: auto_reload in Linux (Ashley Sommer)
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
- Fix: disable auto_reload by default on windows (abuckenheimer)
- Fix (1143): Turn off access log with gunicorn (hqy)
- Fix (1268): Support status code for file response (Cosmo Borsky)
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
- Fix (1242): Responses for CI header (yunstanford)
- Fix (1237): add version constraint for websockets (yunstanford)
- Fix (1231): memory leak - always release resource (Phillip Xu)
- Fix (1221): make request truthy if transport exists (Raphael Deem)
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
- Fix try_everything examples (PyManiacGR, kot83)
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
- Fix: raw requires bytes-like object (cloudship)
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
- Fix: Bug in multipart/form-data parser (DirkGuijt)
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
- Fix: Parameter check (Howie Hu)
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
- Fix: single quote in readme.rst (Cosven)
- Fix: method typos (Dmitry Dygalo)
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
- Fix (1042): Exception Handling (Raphael Deem)
- Fix: Chinese URIs (Howie Hu)
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
- Fix (1065): allow add_task after server starts (Raphael Deem)
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
- Fix (1062): inject the app in add_task method (Raphael Deem)
- Fix: update environment.yml for readthedocs (Eli Uriegas)
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
Note: Changelog was unmaintained between 0.1 and 0.7
Version 0.1
-----------
- 0.1.7
@@ -5,18 +118,18 @@ Version 0.1
- 0.1.6
- Static files
- Lazy Cookie Loading
- 0.1.5
- 0.1.5
- Cookies
- Blueprint listeners and ordering
- Faster Router
- Fix: Incomplete file reads on medium+ sized post requests
- Breaking: after_start and before_stop now pass sanic as their first argument
- 0.1.4
- 0.1.4
- Multiprocessing
- 0.1.3
- Blueprint support
- Faster Response processing
- 0.1.1 - 0.1.2
- 0.1.1 - 0.1.2
- Struggling to update pypi via CI
- 0.1.0
- Released to public
- 0.1.0
- Released to public

View File

@@ -18,9 +18,22 @@ So assume you have already cloned the repo and are in the working directory with
a virtual environment already set up, then run:
```bash
python setup.py develop && pip install -r requirements-dev.txt
pip3 install -e . "[.dev]"
```
# Dependency Changes
`Sanic` doesn't use `requirements*.txt` files to manage any kind of dependencies related to it in order to simplify the
effort required in managing the dependencies. Please make sure you have read and understood the following section of
the document that explains the way `sanic` manages dependencies inside the `setup.py` file.
| Dependency Type | Usage | Installation |
| ------------------------------------------| -------------------------------------------------------------------------- | --------------------------- |
| requirements | Bare minimum dependencies required for sanic to function | pip3 install -e . |
| tests_require / extras_require['test'] | Dependencies required to run the Unit Tests for `sanic` | pip3 install -e '[.test]' |
| extras_require['dev'] | Additional Development requirements to add contributing | pip3 install -e '[.dev]' |
| extras_require['docs'] | Dependencies required to enable building and enhancing sanic documentation | pip3 install -e '[.docs]' |
## Running tests
To run the tests for sanic it is recommended to use tox like so:

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,58 @@
test:
find . -name "*.pyc" -delete
.PHONY: help test test-coverage install docker-test black fix-import beautify
.DEFAULT: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo "test"
@echo " Run Sanic Unit Tests"
@echo "test-coverage"
@echo " Run Sanic Unit Tests with Coverage"
@echo "install"
@echo " Install Sanic"
@echo "docker-test"
@echo " Run Sanic Unit Tests using Docker"
@echo "black"
@echo " Analyze and fix linting issues using Black"
@echo "fix-import"
@echo " Analyze and fix import order using isort"
@echo "beautify [sort_imports=1] [include_tests=1]"
@echo " Analyze and fix linting issue using black and optionally fix import sort using isort"
@echo ""
clean:
find . ! -path "./.eggs/*" -name "*.pyc" -exec rm {} \;
find . ! -path "./.eggs/*" -name "*.pyo" -exec rm {} \;
find . ! -path "./.eggs/*" -name ".coverage" -exec rm {} \;
rm -rf build/* > /dev/null 2>&1
rm -rf dist/* > /dev/null 2>&1
test: clean
python setup.py test
test-coverage: clean
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
install:
python setup.py install
docker-test: clean
docker build -t sanic/test-image -f docker/Dockerfile .
docker run -t sanic/test-image tox
beautify: black
ifdef sort_imports
ifdef include_tests
$(warning It is suggested that you do not run sort import on tests)
isort -rc sanic tests
else
$(info Sorting Imports)
isort -rc sanic
endif
endif
black:
black --config ./pyproject.toml sanic tests
fix-import: black
isort -rc sanic

View File

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

0
docs/_static/.gitkeep vendored Normal file
View File

View File

@@ -38,7 +38,7 @@ master_doc = 'index'
# General information about the project.
project = 'Sanic'
copyright = '2016, Sanic contributors'
copyright = '2018, Sanic contributors'
author = 'Sanic contributors'
# The version info for the project you're documenting, acts as replacement for

View File

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

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.AnstractServer
This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance:
.. code:: python
import asyncio
import socket
import os
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/")
async def test(request):
return json({"hello": "world"})
server_socket = '/tmp/sanic.sock'
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
os.remove(server_socket)
finally:
sock.bind(server_socket)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
srv_coro = app.create_server(
sock=sock,
return_asyncio_server=True,
asyncio_server_kwargs=dict(
start_serving=False
)
)
srv = loop.run_until_complete(srv_coro)
try:
assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
loop.run_until_complete(srv.serve_forever())
except KeyboardInterrupt:
srv.close()
loop.close()
Please note that uvloop does not support these features yet.

View File

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

135
docs/sanic/changelog.md Normal file
View File

@@ -0,0 +1,135 @@
Version 18.12
-------------
18.12.0
- Changes:
- Improved codebase test coverage from 81% to 91%.
- Added stream_large_files and host examples in static_file document
- Added methods to append and finish body content on Request (#1379)
- Integrated with .appveyor.yml for windows ci support
- Added documentation for AF_INET6 and AF_UNIX socket usage
- Adopt black/isort for codestyle
- Cancel task when connection_lost
- Simplify request ip and port retrieval logic
- Handle config error in load config file.
- Integrate with codecov for CI
- Add missed documentation for config section.
- Deprecate Handler.log
- Pinned httptools requirement to version 0.0.10+
- Fixes:
- Fix `remove_entity_headers` helper function (#1415)
- Fix TypeError when use Blueprint.group() to group blueprint with default url_prefix, Use os.path.normpath to avoid invalid url_prefix like api//v1
f8a6af1 Rename the `http` module to `helpers` to prevent conflicts with the built-in Python http library (fixes #1323)
- Fix unittests on windows
- Fix Namespacing of sanic logger
- Fix missing quotes in decorator example
- Fix redirect with quoted param
- Fix doc for latest blueprint code
- Fix build of latex documentation relating to markdown lists
- Fix loop exception handling in app.py
- Fix content length mismatch in windows and other platform
- Fix Range header handling for static files (#1402)
- Fix the logger and make it work (#1397)
- Fix type pikcle->pickle in multiprocessing test
- Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
- Fix document for logging
Version 0.8
-----------
0.8.3
- Changes:
- Ownership changed to org 'huge-success'
0.8.0
- Changes:
- Add Server-Sent Events extension (Innokenty Lebedev)
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
- Sanitize URL before redirection (aveao)
- Add url_bytes to request (johndoe46)
- py37 support for travisci (yunstanford)
- Auto reloader support for OSX (garyo)
- Add UUID route support (Volodymyr Maksymiv)
- Add pausable response streams (Ashley Sommer)
- Add weakref to request slots (vopankov)
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
- Allow streaming handlers in add_route (kinware)
- use travis_retry for tox (Raphael Deem)
- update aiohttp version for test client (yunstanford)
- add redirect import for clarity (yingshaoxo)
- Update HTTP Entity headers (Arnulfo Solís)
- Add register_listener method (Stephan Fitzpatrick)
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
- Content-length header on 204/304 responses (Arnulfo Solís)
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
- Update development status from pre-alpha to beta (Maksim Anisenkov)
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
- Add support for blueprint groups and nesting (Elias Tarhini)
- Remove uvloop for windows setup (Aleksandr Kurlov)
- Auto Reload (Yaser Amari)
- Documentation updates/fixups (multiple contributors)
- Fixes:
- Fix: auto_reload in Linux (Ashley Sommer)
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
- Fix: disable auto_reload by default on windows (abuckenheimer)
- Fix (1143): Turn off access log with gunicorn (hqy)
- Fix (1268): Support status code for file response (Cosmo Borsky)
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
- Fix (1242): Responses for CI header (yunstanford)
- Fix (1237): add version constraint for websockets (yunstanford)
- Fix (1231): memory leak - always release resource (Phillip Xu)
- Fix (1221): make request truthy if transport exists (Raphael Deem)
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
- Fix try_everything examples (PyManiacGR, kot83)
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
- Fix: raw requires bytes-like object (cloudship)
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
- Fix: Bug in multipart/form-data parser (DirkGuijt)
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
- Fix: Parameter check (Howie Hu)
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
- Fix: single quote in readme.rst (Cosven)
- Fix: method typos (Dmitry Dygalo)
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
- Fix (1042): Exception Handling (Raphael Deem)
- Fix: Chinese URIs (Howie Hu)
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
- Fix (1065): allow add_task after server starts (Raphael Deem)
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
- Fix (1062): inject the app in add_task method (Raphael Deem)
- Fix: update environment.yml for readthedocs (Eli Uriegas)
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
Note: Changelog was unmaintained between 0.1 and 0.7
Version 0.1
-----------
- 0.1.7
- Reversed static url and directory arguments to meet spec
- 0.1.6
- Static files
- Lazy Cookie Loading
- 0.1.5
- Cookies
- Blueprint listeners and ordering
- Faster Router
- Fix: Incomplete file reads on medium+ sized post requests
- Breaking: after_start and before_stop now pass sanic as their first argument
- 0.1.4
- Multiprocessing
- 0.1.3
- Blueprint support
- Faster Response processing
- 0.1.1 - 0.1.2
- Struggling to update pypi via CI
- 0.1.0
- Released to public

View File

@@ -85,27 +85,53 @@ DB_USER = 'appuser'
Out of the box there are just a few predefined values which can be overwritten when creating the application.
| Variable | Default | Description |
| ------------------ | --------- | --------------------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
| Variable | Default | Description |
| ------------------------- | --------- | --------------------------------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
| ACCESS_LOG | True | Disable or enable access log |
### The different Timeout variables:
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly.
#### `REQUEST_TIMEOUT`
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response.
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the
Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the
`REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates an `HTTP 408` response
and sends that to the client. Set this parameter's value higher if your clients routinely pass very large request payloads
or upload requests very slowly.
### What is Keep Alive? And what does the Keep Alive Timeout value do?
#### `RESPONSE_TIMEOUT`
Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server.
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the
Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT`
value (in seconds), this is considered a Server Error so Sanic generates an `HTTP 503` response and sends that to the
client. Set this parameter's value higher if your application is likely to have long-running process that delay the
generation of a response.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request.
#### `KEEP_ALIVE_TIMEOUT`
The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long.
##### What is Keep Alive? And what does the Keep Alive Timeout value do?
`Keep-Alive` is a HTTP feature introduced in `HTTP 1.1`. When sending a HTTP request, the client (usually a web browser application)
can set a `Keep-Alive` header to indicate the http server (Sanic) to not close the TCP connection after it has send the response.
This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient
network traffic for both the client and the server.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application,
set it to `False` to cause all client connections to close immediately after a response is sent, regardless of
the `Keep-Alive` header on the request.
The amount of time the server holds the TCP connection open is decided by the server itself.
In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds.
This is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for
the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless
you know your clients are using a browser which supports TCP connections held open for that long.
For reference:
```

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:
1. All pull requests must pass unit tests
* All pull requests must be reviewed and approved by at least
one current collaborator on the project
* All pull requests must pass flake8 checks
* If you decide to remove/change anything from any common interface
a deprecation message should accompany it.
* If you implement a new feature you should have at least one unit
test to accompany it.
## Documentation
Sanic's documentation is built
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
Markdown and can be found in the `docs` folder, while the module reference is
automatically generated using `sphinx-apidoc`.
To generate the documentation from scratch:
```bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
```
The HTML documentation will be created in the `docs/_build` folder.
## Warning
One of the main goals of Sanic is speed. Code that lowers the performance of
Sanic without significant gains in usability, security, or features may not be
merged. Please don't let this intimidate you! If you have any concerns about an
idea, open an issue for discussion and help.

View File

@@ -0,0 +1,89 @@
Contributing
============
Thank you for your interest! Sanic is always looking for contributors.
If you dont feel comfortable contributing code, adding docstrings to
the source files is very appreciated.
Installation
------------
To develop on sanic (and mainly to just run the tests) it is highly
recommend to install from sources.
So assume you have already cloned the repo and are in the working
directory with a virtual environment already set up, then run:
.. code:: bash
pip3 install -e '.[dev]'
Dependency Changes
------------------
``Sanic`` doesn't use ``requirements*.txt`` files to manage any kind of dependencies related to it in order to simplify the
effort required in managing the dependencies. Please make sure you have read and understood the following section of
the document that explains the way ``sanic`` manages dependencies inside the ``setup.py`` file.
+------------------------+-----------------------------------------------+--------------------------------+
| Dependency Type | Usage | Installation |
+========================+===============================================+================================+
| requirements | Bare minimum dependencies required for sanic | ``pip3 install -e .`` |
| | to function | |
+------------------------+-----------------------------------------------+--------------------------------+
| tests_require / | Dependencies required to run the Unit Tests | ``pip3 install -e '.[test]'`` |
| extras_require['test'] | for ``sanic`` | |
+------------------------+-----------------------------------------------+--------------------------------+
| extras_require['dev'] | Additional Development requirements to add | ``pip3 install -e '.[dev]'`` |
| | for contributing | |
+------------------------+-----------------------------------------------+--------------------------------+
| extras_require['docs'] | Dependencies required to enable building and | ``pip3 install -e '.[docs]'`` |
| | enhancing sanic documentation | |
+------------------------+-----------------------------------------------+--------------------------------+
Running tests
-------------
To run the tests for sanic it is recommended to use tox like so:
.. code:: bash
tox
See its 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
-------------
Sanics documentation is built using `sphinx`_. Guides are written in
Markdown and can be found in the ``docs`` folder, while the module
reference is automatically generated using ``sphinx-apidoc``.
To generate the documentation from scratch:
.. code:: bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
The HTML documentation will be created in the ``docs/_build`` folder.
.. warning::
One of the main goals of Sanic is speed. Code that lowers the
performance of Sanic without significant gains in usability, security,
or features may not be merged. Please dont let this intimidate you! If
you have any concerns about an idea, open an issue for discussion and
help.
.. _sphinx: http://www.sphinx-doc.org/en/1.5.1/

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

@@ -34,6 +34,6 @@ def authorized():
@app.route("/")
@authorized()
async def test(request):
return json({status: 'authorized'})
return json({'status': 'authorized'})
```

View File

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

167
docs/sanic/examples.rst Normal file
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 ``reqeust`` object can be manipulated as a regular ``dict`` object.
.. literalinclude:: ../../examples/amending_request_object.py
For more examples and useful samples please visit the `Huge-Sanic's GitHub Page <https://github.com/huge-success/sanic/tree/master/examples>`_

View File

@@ -47,6 +47,36 @@ async def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
```
You can also add an exception handler as such:
```python
from sanic import Sanic
async def server_error_handler(request, exception):
return text("Oops, server error", status=500)
app = Sanic()
app.error_handler.add(Exception, server_error_handler)
```
In some cases, you might want to add some more error handling
functionality to what is provided by default. In that case, you
can subclass Sanic's default error handler as such:
```python
from sanic import Sanic
from sanic.handlers import ErrorHandler
class CustomErrorHandler(ErrorHandler):
def default(self, request, exception):
''' handles errors that have no error handlers assigned '''
# You custom error handling logic...
return super().default(request, exception)
app = Sanic()
app.error_handler = CustomErrorHandler()
```
## Useful exceptions
Some of the most useful exceptions are presented below:

View File

@@ -1,34 +1,75 @@
# Extensions
A list of Sanic extensions created by the community.
## Extension and Plugin Development
- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins.
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
Allows using redis, memcache or an in memory store.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
## Security
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic.
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
- [Secure](https://github.com/cakinney/secure): Secure 🔒 is a lightweight package that adds optional security headers and cookie attributes for Python web frameworks.
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. Allows using redis, memcache or an in memory store.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
`Babel` library
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
- [sanic-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/))
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
## Documentation
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
## ORM and Database Integration
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/))
- [Databases](https://github.com/encode/databases): Async database access for SQLAlchemy core, with support for PostgreSQL, MySQL, and SQLite.
## Unit Testing
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
## Project Creation Template
- [cookiecutter-sanic](https://github.com/harshanarayana/cookiecutter-sanic): Get your sanic application up and running in a matter of second in a well defined project structure.
Batteries included for deployment, unit testing, automated release management and changelog generation.
## Templating
- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
## API Helper Utilities
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
## i18n/l10n Support
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the `Babel` library
## Custom Middlewares
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
## Monitoring and Reporting
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
- [sanic-zipkin](https://github.com/kevinqqnj/sanic-zipkin): Easily report request/function/RPC traces to zipkin/jaeger, through aiozipkin.
## Sample Applications
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.

View File

@@ -4,8 +4,21 @@ Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
syntax, so earlier versions of python won't work.
1. Install Sanic: `python3 -m pip install sanic`
2. Create a file called `main.py` with the following code:
## 1. Install Sanic
```
pip3 install sanic
```
To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`)
to true will stop that features installation.
```bash
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic
```
## 2. Create a file called `main.py`
```python
from sanic import Sanic
@@ -20,9 +33,16 @@ syntax, so earlier versions of python won't work.
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
```
3. Run the server: `python3 main.py`
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
the message *Hello world!*.
## 3. Run the server
```
python3 main.py
```
## 4. Check your browser
Open the address `http://0.0.0.0:8000` in your web browser. You should see
the message *Hello world!*.
You now have a working Sanic server!

View File

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

103
docs/sanic/logging.rst Normal file
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

@@ -17,7 +17,7 @@ string representing its type: `'request'` or `'response'`.
The simplest middleware doesn't modify the request or response at all:
```python
```
@app.middleware('request')
async def print_on_request(request):
print("I print when a request is received by the server")
@@ -33,23 +33,34 @@ Middleware can modify the request or response parameter it is given, *as long
as it does not return it*. The following example shows a practical use-case for
this.
```python
```
app = Sanic(__name__)
@app.middleware('request')
async def add_key(request):
# Add a key to request object like dict object
request['foo'] = 'bar'
@app.middleware('response')
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
app.run(host="0.0.0.0", port=8000)
```
The above code will apply the two middleware in order. First, the middleware
The above code will apply the three middleware in order. The first middleware
**add_key** will add a new key `foo` into `request` object. This worked because
`request` object can be manipulated like `dict` object. Then, the second middleware
**custom_banner** will change the HTTP response header *Server* to
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
*Fake-Server*, and the last middleware **prevent_xss** will add the HTTP
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
are invoked *after* a user function returns a response.
@@ -60,7 +71,7 @@ and the response will be returned. If this occurs to a request before the
relevant user route handler is reached, the handler will never be called.
Returning a response will also prevent any further middleware from running.
```python
```
@app.middleware('request')
async def halt_request(request):
return text('I halted the request')
@@ -79,11 +90,11 @@ If you want to execute startup/teardown code as your server starts or closes, yo
- `before_server_stop`
- `after_server_stop`
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
For example:
```python
```
@app.listener('before_server_start')
async def setup_db(app, loop):
app.db = await db_setup()
@@ -101,16 +112,16 @@ async def close_db(app, loop):
await app.db.close()
```
It's also possible to register a listener using the `register_listener` method.
It's also possible to register a listener using the `register_listener` method.
This may be useful if you define your listeners in another module besides
the one you instantiate your app in.
```python
```
app = Sanic()
async def setup_db(app, loop):
app.db = await db_setup()
app.register_listener(setup_db, 'before_server_start')
```
@@ -118,7 +129,7 @@ app.register_listener(setup_db, 'before_server_start')
If you want to schedule a background task to run after the loop has started,
Sanic provides the `add_task` method to easily do so.
```python
```
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
@@ -128,7 +139,7 @@ app.add_task(notify_server_started_after_five_seconds())
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
```python
```
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
@@ -138,7 +149,7 @@ app.add_task(notify_server_started_after_five_seconds)
Or you can pass the app explicitly for the same effect:
```python
```
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)

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
@@ -106,6 +153,51 @@ The following variables are accessible as properties on `Request` objects:
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
## Changing the default parsing rules of the queryset
The default parameters that are using internally in `args` and `query_args` properties to parse queryset:
- `keep_blank_values` (bool): `False` - flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
- `strict_parsing` (bool): `False` - flag indicating what to do with parsing errors. If
false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
- `encoding` and `errors` (str): 'utf-8' and 'replace' - specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
If you would like to change that default parameters you could call `get_args` and `get_query_args` methods
with the new values.
For the queryset `/?test1=value1&test2=&test3=value3`:
```python
from sanic.response import json
@app.route("/query_string")
def query_string(request):
args_with_blank_values = request.get_args(keep_blank_values=True)
return json({
"parsed": True,
"url": request.url,
"args_with_blank_values": args_with_blank_values,
"query_string": request.query_string
})
```
The output will be:
```
{
"parsed": true,
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3",
"args_with_blank_values": {"test1": ["value1""], "test2": "", "test3": ["value3"]},
"query_string": "test1=value1&test2=&test3=value3"
}
```
## Accessing values using `get` and `getlist`
The request properties which return a dictionary actually return a subclass of
@@ -126,3 +218,40 @@ args.get('titles') # => 'Post 1'
args.getlist('titles') # => ['Post 1', 'Post 2']
```
## Accessing the handler name with the request.endpoint attribute
The `request.endpoint` attribute holds the handler's name. For instance, the below
route will return "hello".
```python
from sanic.response import text
from sanic import Sanic
app = Sanic()
@app.get("/")
def hello(request):
return text(request.endpoint)
```
Or, with a blueprint it will be include both, separated by a period. For example,
the below route would return foo.bar:
```python
from sanic import Sanic
from sanic import Blueprint
from sanic.response import text
app = Sanic(__name__)
blueprint = Blueprint('foo')
@blueprint.get('/')
async def bar(request):
return text(request.endpoint)
app.blueprint(blueprint)
app.run(host="0.0.0.0", port=8000, debug=True)
```

View File

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

66
docs/sanic/sockets.rst Normal file
View File

@@ -0,0 +1,66 @@
Sockets
=======
Sanic can use the python
`socket module <https://docs.python.org/3/library/socket.html>`_ to accommodate
non IPv4 sockets.
IPv6 example:
.. code:: python
from sanic import Sanic
from sanic.response import json
import socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('::', 7777))
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(sock=sock)
to test IPv6 ``curl -g -6 "http://[::1]:7777/"``
UNIX socket example:
.. code:: python
import signal
import sys
import socket
import os
from sanic import Sanic
from sanic.response import json
server_socket = '/tmp/sanic.sock'
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_socket)
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
def signal_handler(sig, frame):
print('Exiting')
os.unlink(server_socket)
sys.exit(0)
if __name__ == "__main__":
app.run(sock=sock)
to test UNIX: ``curl -v --unix-socket /tmp/sanic.sock http://localhost/hello``

View File

@@ -1,7 +1,7 @@
# Static Files
Static files and directories, such as an image file, are served by Sanic when
registered with the `app.static` method. The method takes an endpoint URL and a
registered with the `app.static()` method. The method takes an endpoint URL and a
filename. The file specified will then be accessible via the given endpoint.
```python
@@ -43,3 +43,41 @@ app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
app.run(host="0.0.0.0", port=8000)
```
> **Note:** Sanic does not provide directory index when you serve a static directory.
## Virtual Host
The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example:
```python
from sanic import Sanic
app = Sanic(__name__)
app.static('/static', './static')
app.static('/example_static', './example_static', host='www.example.com')
```
## Streaming Large File
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
Here is an example:
```python
from sanic import Sanic
app = Sanic(__name__)
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
```
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
```python
from sanic import Sanic
app = Sanic(__name__)
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
```

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')

View File

@@ -59,6 +59,23 @@ the available arguments to aiohttp can be found
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
## Using a random port
If you need to test using a free unpriveleged port chosen by the kernel
instead of the default with `SanicTestClient`, you can do so by specifying
`port=None`. On most systems the port will be in the range 1024 to 65535.
```python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
from sanic.testing import SanicTestClient
def test_index_returns_200():
request, response = SanicTestClient(app, port=None).get('/')
assert response.status == 200
```
## pytest-sanic
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously.

View File

@@ -1,7 +1,7 @@
WebSocket
=========
Sanic supports websockets, to setup a WebSocket:
Sanic provides an easy to user abstraction on top of `websockets`. To setup a WebSocket:
.. code:: python
@@ -35,7 +35,7 @@ decorator:
app.add_websocket_route(feed, '/feed')
Handlers for a WebSocket route are passed the request as first argument, and a
Handlers for a WebSocket route is invoked with the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has ``send``
and ``recv`` methods to send and receive data respectively.
@@ -43,6 +43,7 @@ and ``recv`` methods to send and receive data respectively.
You could setup your own WebSocket configuration through ``app.config``, like
.. code:: python
app.config.WEBSOCKET_MAX_SIZE = 2 ** 20
app.config.WEBSOCKET_MAX_QUEUE = 32
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16

View File

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

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

@@ -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)
)

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)
)

2
pyproject.toml Normal file
View File

@@ -0,0 +1,2 @@
[tool.black]
line-length = 79

304
release.py Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,23 @@
from argparse import ArgumentParser
from importlib import import_module
from sanic.log import logger
from sanic.app import Sanic
from sanic.log import logger
if __name__ == "__main__":
parser = ArgumentParser(prog='sanic')
parser.add_argument('--host', dest='host', type=str, default='127.0.0.1')
parser.add_argument('--port', dest='port', type=int, default=8000)
parser.add_argument('--cert', dest='cert', type=str,
help='location of certificate for SSL')
parser.add_argument('--key', dest='key', type=str,
help='location of keyfile for SSL.')
parser.add_argument('--workers', dest='workers', type=int, default=1, )
parser.add_argument('--debug', dest='debug', action="store_true")
parser.add_argument('module')
parser = ArgumentParser(prog="sanic")
parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
parser.add_argument("--port", dest="port", type=int, default=8000)
parser.add_argument(
"--cert", dest="cert", type=str, help="location of certificate for SSL"
)
parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL."
)
parser.add_argument("--workers", dest="workers", type=int, default=1)
parser.add_argument("--debug", dest="debug", action="store_true")
parser.add_argument("module")
args = parser.parse_args()
try:
@@ -25,20 +28,29 @@ if __name__ == "__main__":
module = import_module(module_name)
app = getattr(module, app_name, None)
if not isinstance(app, Sanic):
raise ValueError("Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?"
.format(type(app).__name__, args.module))
raise ValueError(
"Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?".format(
type(app).__name__, args.module
)
)
if args.cert is not None or args.key is not None:
ssl = {'cert': args.cert, 'key': args.key}
ssl = {"cert": args.cert, "key": args.key}
else:
ssl = None
app.run(host=args.host, port=args.port,
workers=args.workers, debug=args.debug, ssl=ssl)
app.run(
host=args.host,
port=args.port,
workers=args.workers,
debug=args.debug,
ssl=ssl,
)
except ImportError as e:
logger.error("No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
.format(e.name))
except ValueError as e:
logger.error("{}".format(e))
logger.error(
"No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app".format(e.name)
)
except ValueError:
logger.exception("Failed to run app")

File diff suppressed because it is too large Load Diff

120
sanic/blueprint_group.py Normal file
View File

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

View File

@@ -1,28 +1,55 @@
from collections import defaultdict, namedtuple
from sanic.blueprint_group import BlueprintGroup
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
FutureRoute = namedtuple('Route',
['handler', 'uri', 'methods', 'host',
'strict_slashes', 'stream', 'version', 'name'])
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
FutureStatic = namedtuple('Route',
['uri', 'file_or_directory', 'args', 'kwargs'])
FutureRoute = namedtuple(
"FutureRoute",
[
"handler",
"uri",
"methods",
"host",
"strict_slashes",
"stream",
"version",
"name",
],
)
FutureListener = namedtuple(
"FutureListener", ["handler", "uri", "methods", "host"]
)
FutureMiddleware = namedtuple(
"FutureMiddleware", ["middleware", "args", "kwargs"]
)
FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"])
FutureStatic = namedtuple(
"FutureStatic", ["uri", "file_or_directory", "args", "kwargs"]
)
class Blueprint:
def __init__(self, name,
url_prefix=None,
host=None, version=None,
strict_slashes=False):
"""Create a new blueprint
def __init__(
self,
name,
url_prefix=None,
host=None,
version=None,
strict_slashes=False,
):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
URLs that perform a specific set of tasks which can be identified by
a unique name.
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param strict_slashes: strict to trailing slash
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
training */*
"""
self.name = name
self.url_prefix = url_prefix
@@ -38,30 +65,44 @@ class Blueprint:
self.strict_slashes = strict_slashes
@staticmethod
def group(*blueprints, url_prefix=''):
"""Create a list of blueprints, optionally
grouping them under a general URL prefix.
def group(*blueprints, url_prefix=""):
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.
:param blueprints: blueprints to be registered as a group
:param url_prefix: URL route to be prepended to all sub-prefixes
"""
def chain(nested):
"""itertools.chain() but leaves strings untouched"""
for i in nested:
if isinstance(i, (list, tuple)):
yield from chain(i)
elif isinstance(i, BlueprintGroup):
yield from i.blueprints
else:
yield i
bps = []
bps = BlueprintGroup(url_prefix=url_prefix)
for bp in chain(blueprints):
if bp.url_prefix is None:
bp.url_prefix = ""
bp.url_prefix = url_prefix + bp.url_prefix
bps.append(bp)
return bps
def register(self, app, options):
"""Register the blueprint to the sanic app."""
"""
Register the blueprint to the sanic app.
url_prefix = options.get('url_prefix', self.url_prefix)
:param app: Instance of :class:`sanic.app.Sanic` class
:param options: Options to be used while registering the
blueprint into the app.
*url_prefix* - URL Prefix to override the blueprint prefix
"""
url_prefix = options.get("url_prefix", self.url_prefix)
# Routes
for future in self.routes:
@@ -73,14 +114,15 @@ class Blueprint:
version = future.version or self.version
app.route(uri=uri[1:] if uri.startswith('//') else uri,
methods=future.methods,
host=future.host or self.host,
strict_slashes=future.strict_slashes,
stream=future.stream,
version=version,
name=future.name,
)(future.handler)
app.route(
uri=uri[1:] if uri.startswith("//") else uri,
methods=future.methods,
host=future.host or self.host,
strict_slashes=future.strict_slashes,
stream=future.stream,
version=version,
name=future.name,
)(future.handler)
for future in self.websocket_routes:
# attach the blueprint name to the handler so that it can be
@@ -88,18 +130,19 @@ class Blueprint:
future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
app.websocket(uri=uri,
host=future.host or self.host,
strict_slashes=future.strict_slashes,
name=future.name,
)(future.handler)
app.websocket(
uri=uri,
host=future.host or self.host,
strict_slashes=future.strict_slashes,
name=future.name,
)(future.handler)
# Middleware
for future in self.middlewares:
if future.args or future.kwargs:
app.register_middleware(future.middleware,
*future.args,
**future.kwargs)
app.register_middleware(
future.middleware, *future.args, **future.kwargs
)
else:
app.register_middleware(future.middleware)
@@ -111,48 +154,85 @@ class Blueprint:
for future in self.statics:
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
app.static(uri, future.file_or_directory,
*future.args, **future.kwargs)
app.static(
uri, future.file_or_directory, *future.args, **future.kwargs
)
# Event listeners
for event, listeners in self.listeners.items():
for listener in listeners:
app.listener(event)(listener)
def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=None, stream=False, version=None, name=None):
def route(
self,
uri,
methods=frozenset({"GET"}),
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
"""Create a blueprint route from a decorated function.
:param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods.
:param host: IP Address of FQDN for the sanic server to use.
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param stream: If the route should provide a streaming support
:param version: Blueprint Version
:param name: Unique name to identify the Route
:return a decorated method that when invoked will return an object
of type :class:`FutureRoute`
"""
if strict_slashes is None:
strict_slashes = self.strict_slashes
def decorator(handler):
route = FutureRoute(
handler, uri, methods, host, strict_slashes, stream, version,
name)
handler,
uri,
methods,
host,
strict_slashes,
stream,
version,
name,
)
self.routes.append(route)
return handler
return decorator
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=None, version=None, name=None):
def add_route(
self,
handler,
uri,
methods=frozenset({"GET"}),
host=None,
strict_slashes=None,
version=None,
name=None,
stream=False,
):
"""Create a blueprint route from a function.
:param handler: function for handling uri requests. Accepts function,
or class instance with a view_class method.
:param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods.
:param host:
:param strict_slashes:
:param version:
:param host: IP Address of FQDN for the sanic server to use.
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param version: Blueprint Version
:param name: user defined route name for url_for
:param stream: boolean specifying if the handler is a stream handler
:return: function or class instance
"""
# Handle HTTPMethodView differently
if hasattr(handler, 'view_class'):
if hasattr(handler, "view_class"):
methods = set()
for method in HTTP_METHODS:
@@ -166,34 +246,52 @@ class Blueprint:
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes, version=version,
name=name)(handler)
self.route(
uri=uri,
methods=methods,
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)(handler)
return handler
def websocket(self, uri, host=None, strict_slashes=None, version=None,
name=None):
def websocket(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible.
:param host: IP Address of FQDN for the sanic server to use.
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param version: Blueprint Version
:param name: Unique name to identify the Websocket Route
"""
if strict_slashes is None:
strict_slashes = self.strict_slashes
def decorator(handler):
route = FutureRoute(handler, uri, [], host, strict_slashes,
False, version, name)
route = FutureRoute(
handler, uri, [], host, strict_slashes, False, version, name
)
self.websocket_routes.append(route)
return handler
return decorator
def add_websocket_route(self, handler, uri, host=None, version=None,
name=None):
def add_websocket_route(
self, handler, uri, host=None, version=None, name=None
):
"""Create a blueprint websocket route from a function.
:param handler: function for handling uri requests. Accepts function,
or class instance with a view_class method.
:param uri: endpoint at which the route will be accessible.
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param name: Unique name to identify the Websocket Route
:return: function or class instance
"""
self.websocket(uri=uri, host=host, version=version, name=name)(handler)
@@ -204,13 +302,23 @@ class Blueprint:
:param event: Event to listen to.
"""
def decorator(listener):
self.listeners[event].append(listener)
return listener
return decorator
def middleware(self, *args, **kwargs):
"""Create a blueprint middleware from a decorated function."""
"""
Create a blueprint middleware from a decorated function.
:param args: Positional arguments to be used while invoking the
middleware
:param kwargs: optional keyword args that can be used with the
middleware.
"""
def register_middleware(_middleware):
future_middleware = FutureMiddleware(_middleware, args, kwargs)
self.middlewares.append(future_middleware)
@@ -222,14 +330,32 @@ class Blueprint:
args = []
return register_middleware(middleware)
else:
return register_middleware
if kwargs.get("bp_group") and callable(args[0]):
middleware = args[0]
args = args[1:]
kwargs.pop("bp_group")
return register_middleware(middleware)
else:
return register_middleware
def exception(self, *args, **kwargs):
"""Create a blueprint exception from a decorated function."""
"""
This method enables the process of creating a global exception
handler for the current blueprint under question.
:param args: List of Python exceptions to be caught by the handler
:param kwargs: Additional optional arguments to be passed to the
exception handler
:return a decorated method to handle global exceptions for any
route registered under this blueprint.
"""
def decorator(handler):
exception = FutureException(handler, args, kwargs)
self.exceptions.append(exception)
return handler
return decorator
def static(self, uri, file_or_directory, *args, **kwargs):
@@ -238,12 +364,12 @@ class Blueprint:
:param uri: endpoint at which the route will be accessible.
:param file_or_directory: Static asset.
"""
name = kwargs.pop('name', 'static')
if not name.startswith(self.name + '.'):
name = '{}.{}'.format(self.name, name)
name = kwargs.pop("name", "static")
if not name.startswith(self.name + "."):
name = "{}.{}".format(self.name, name)
kwargs.update(name=name)
strict_slashes = kwargs.get('strict_slashes')
strict_slashes = kwargs.get("strict_slashes")
if strict_slashes is None and self.strict_slashes is not None:
kwargs.update(strict_slashes=self.strict_slashes)
@@ -251,44 +377,184 @@ class Blueprint:
self.statics.append(static)
# Shorthand method decorators
def get(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["GET"], host=host,
strict_slashes=strict_slashes, version=version,
name=name)
def get(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **GET** *HTTP* method
def post(self, uri, host=None, strict_slashes=None, stream=False,
version=None, name=None):
return self.route(uri, methods=["POST"], host=host,
strict_slashes=strict_slashes, stream=stream,
version=version, name=name)
:param uri: URL to be tagged to **GET** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def put(self, uri, host=None, strict_slashes=None, stream=False,
version=None, name=None):
return self.route(uri, methods=["PUT"], host=host,
strict_slashes=strict_slashes, stream=stream,
version=version, name=name)
def post(
self,
uri,
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
"""
Add an API URL under the **POST** *HTTP* method
def head(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["HEAD"], host=host,
strict_slashes=strict_slashes, version=version,
name=name)
:param uri: URL to be tagged to **POST** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"POST"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)
def options(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["OPTIONS"], host=host,
strict_slashes=strict_slashes, version=version,
name=name)
def put(
self,
uri,
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
"""
Add an API URL under the **PUT** *HTTP* method
def patch(self, uri, host=None, strict_slashes=None, stream=False,
version=None, name=None):
return self.route(uri, methods=["PATCH"], host=host,
strict_slashes=strict_slashes, stream=stream,
version=version, name=name)
:param uri: URL to be tagged to **PUT** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"PUT"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)
def delete(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["DELETE"], host=host,
strict_slashes=strict_slashes, version=version,
name=name)
def head(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **HEAD** *HTTP* method
:param uri: URL to be tagged to **HEAD** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"HEAD"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def options(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **OPTIONS** *HTTP* method
:param uri: URL to be tagged to **OPTIONS** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"OPTIONS"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def patch(
self,
uri,
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
"""
Add an API URL under the **PATCH** *HTTP* method
:param uri: URL to be tagged to **PATCH** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"PATCH"}),
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)
def delete(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **DELETE** *HTTP* method
:param uri: URL to be tagged to **DELETE** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"DELETE"}),
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)

View File

@@ -1,45 +1,44 @@
import os
import types
from distutils.util import strtobool
SANIC_PREFIX = 'SANIC_'
from sanic.exceptions import PyFileError
SANIC_PREFIX = "SANIC_"
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_BUFFER_QUEUE_SIZE": 100,
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"KEEP_ALIVE": True,
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabytes
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True,
}
class Config(dict):
def __init__(self, defaults=None, load_env=True, keep_alive=True):
super().__init__(defaults or {})
self.LOGO = """
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \\
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀
"""
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
self.REQUEST_TIMEOUT = 60 # 60 seconds
self.RESPONSE_TIMEOUT = 60 # 60 seconds
self.KEEP_ALIVE = keep_alive
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
self.WEBSOCKET_MAX_QUEUE = 32
self.WEBSOCKET_READ_LIMIT = 2 ** 16
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
self.ACCESS_LOG = True
def __init__(self, defaults=None, load_env=True, keep_alive=None):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self.LOGO = BASE_LOGO
if keep_alive is not None:
self.KEEP_ALIVE = keep_alive
if load_env:
prefix = SANIC_PREFIX if load_env is True else load_env
@@ -63,9 +62,10 @@ class Config(dict):
"""
config_file = os.environ.get(variable_name)
if not config_file:
raise RuntimeError('The environment variable %r is not set and '
'thus configuration could not be loaded.' %
variable_name)
raise RuntimeError(
"The environment variable %r is not set and "
"thus configuration could not be loaded." % variable_name
)
return self.from_pyfile(config_file)
def from_pyfile(self, filename):
@@ -74,15 +74,20 @@ class Config(dict):
:param filename: an absolute path to the config file
"""
module = types.ModuleType('config')
module = types.ModuleType("config")
module.__file__ = filename
try:
with open(filename) as config_file:
exec(compile(config_file.read(), filename, 'exec'),
module.__dict__)
exec(
compile(config_file.read(), filename, "exec"),
module.__dict__,
)
except IOError as e:
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
e.strerror = "Unable to load configuration file (%s)" % e.strerror
raise
except Exception as e:
raise PyFileError(filename) from e
self.from_object(module)
return True
@@ -121,4 +126,7 @@ class Config(dict):
try:
self[config_key] = float(v)
except ValueError:
self[config_key] = v
try:
self[config_key] = bool(strtobool(v))
except ValueError:
self[config_key] = v

View File

@@ -1 +1 @@
HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE')
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")

View File

@@ -1,6 +1,11 @@
import re
import string
from datetime import datetime
DEFAULT_MAX_AGE = 0
# ------------------------------------------------------------ #
# SimpleCookie
# ------------------------------------------------------------ #
@@ -8,18 +13,16 @@ import string
# Straight up copied this section of dark magic from SimpleCookie
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
_UnescapedChars = _LegalChars + " ()/<=>?@[]{}"
_Translator = {n: '\\%03o' % n
for n in set(range(256)) - set(map(ord, _UnescapedChars))}
_Translator.update({
ord('"'): '\\"',
ord('\\'): '\\\\',
})
_Translator = {
n: "\\%03o" % n for n in set(range(256)) - set(map(ord, _UnescapedChars))
}
_Translator.update({ord('"'): '\\"', ord("\\"): "\\\\"})
def _quote(str):
"""Quote a string for use in a cookie header.
r"""Quote a string for use in a cookie header.
If the string does not need to be double-quoted, then just return the
string. Otherwise, surround the string in doublequotes and quote
(with a \) special characters.
@@ -30,7 +33,7 @@ def _quote(str):
return '"' + str.translate(_Translator) + '"'
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
_is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
# ------------------------------------------------------------ #
# Custom SimpleCookie
@@ -53,7 +56,7 @@ class CookieJar(dict):
# If this cookie doesn't exist, add it to the header keys
if not self.cookie_headers.get(key):
cookie = Cookie(key, value)
cookie['path'] = '/'
cookie["path"] = "/"
self.cookie_headers[key] = self.header_key
self.headers.add(self.header_key, cookie)
return super().__setitem__(key, cookie)
@@ -62,8 +65,8 @@ class CookieJar(dict):
def __delitem__(self, key):
if key not in self.cookie_headers:
self[key] = ''
self[key]['max-age'] = 0
self[key] = ""
self[key]["max-age"] = 0
else:
cookie_header = self.cookie_headers[key]
# remove it from header
@@ -77,6 +80,7 @@ class CookieJar(dict):
class Cookie(dict):
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
_keys = {
"expires": "expires",
"path": "Path",
@@ -88,7 +92,7 @@ class Cookie(dict):
"version": "Version",
"samesite": "SameSite",
}
_flags = {'secure', 'httponly'}
_flags = {"secure", "httponly"}
def __init__(self, key, value):
if key in self._keys:
@@ -103,27 +107,44 @@ class Cookie(dict):
if key not in self._keys:
raise KeyError("Unknown cookie property")
if value is not False:
if key.lower() == "max-age":
if not str(value).isdigit():
value = DEFAULT_MAX_AGE
elif key.lower() == "expires":
if not isinstance(value, datetime):
raise TypeError(
"Cookie 'expires' property must be a datetime"
)
return super().__setitem__(key, value)
def encode(self, encoding):
output = ['%s=%s' % (self.key, _quote(self.value))]
"""
Encode the cookie content in a specific type of encoding instructed
by the developer. Leverages the :func:`str.encode` method provided
by python.
This method can be used to encode and embed ``utf-8`` content into
the cookies.
:param encoding: Encoding to be used with the cookie
:return: Cookie encoded in a codec of choosing.
:except: UnicodeEncodeError
"""
output = ["%s=%s" % (self.key, _quote(self.value))]
for key, value in self.items():
if key == 'max-age':
if key == "max-age":
try:
output.append('%s=%d' % (self._keys[key], value))
output.append("%s=%d" % (self._keys[key], value))
except TypeError:
output.append('%s=%s' % (self._keys[key], value))
elif key == 'expires':
try:
output.append('%s=%s' % (
self._keys[key],
value.strftime("%a, %d-%b-%Y %T GMT")
))
except AttributeError:
output.append('%s=%s' % (self._keys[key], value))
output.append("%s=%s" % (self._keys[key], value))
elif key == "expires":
output.append(
"%s=%s"
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
)
elif key in self._flags and self[key]:
output.append(self._keys[key])
else:
output.append('%s=%s' % (self._keys[key], value))
output.append("%s=%s" % (self._keys[key], value))
return "; ".join(output).encode(encoding)

View File

@@ -1,6 +1,7 @@
from sanic.http import STATUS_CODES
from sanic.helpers import STATUS_CODES
TRACEBACK_STYLE = '''
TRACEBACK_STYLE = """
<style>
body {
padding: 20px;
@@ -61,9 +62,9 @@ TRACEBACK_STYLE = '''
font-size: 14px;
}
</style>
'''
"""
TRACEBACK_WRAPPER_HTML = '''
TRACEBACK_WRAPPER_HTML = """
<html>
<head>
{style}
@@ -78,27 +79,27 @@ TRACEBACK_WRAPPER_HTML = '''
</div>
</body>
</html>
'''
"""
TRACEBACK_WRAPPER_INNER_HTML = '''
TRACEBACK_WRAPPER_INNER_HTML = """
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
</div>
'''
"""
TRACEBACK_BORDER = '''
TRACEBACK_BORDER = """
<div class="tb-border">
<b><i>
The above exception was the direct cause of the
following exception:
</i></b>
</div>
'''
"""
TRACEBACK_LINE_HTML = '''
TRACEBACK_LINE_HTML = """
<div class="frame-line">
<p class="frame-descriptor">
File {0.filename}, line <i>{0.lineno}</i>,
@@ -106,15 +107,15 @@ TRACEBACK_LINE_HTML = '''
</p>
<p class="frame-code"><code>{0.line}</code></p>
</div>
'''
"""
INTERNAL_SERVER_ERROR_HTML = '''
INTERNAL_SERVER_ERROR_HTML = """
<h1>Internal Server Error</h1>
<p>
The server encountered an internal error and cannot complete
your request.
</p>
'''
"""
_sanic_exceptions = {}
@@ -122,17 +123,18 @@ _sanic_exceptions = {}
def add_status_code(code):
"""
Decorator used for adding exceptions to _sanic_exceptions.
Decorator used for adding exceptions to :class:`SanicException`.
"""
def class_decorator(cls):
cls.status_code = code
_sanic_exceptions[code] = cls
return cls
return class_decorator
class SanicException(Exception):
def __init__(self, message, status_code=None):
super().__init__(message)
@@ -156,8 +158,8 @@ class MethodNotSupported(SanicException):
super().__init__(message)
self.headers = dict()
self.headers["Allow"] = ", ".join(allowed_methods)
if method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
self.headers['Content-Length'] = 0
if method in ["HEAD", "PATCH", "PUT", "DELETE"]:
self.headers["Content-Length"] = 0
@add_status_code(500)
@@ -169,6 +171,7 @@ class ServerError(SanicException):
class ServiceUnavailable(SanicException):
"""The server is currently unavailable (because it is overloaded or
down for maintenance). Generally, this is a temporary state."""
pass
@@ -192,6 +195,7 @@ class RequestTimeout(SanicException):
the connection. The socket connection has actually been lost - the Web
server has 'timed out' on that particular socket connection.
"""
pass
@@ -209,8 +213,8 @@ class ContentRangeError(SanicException):
def __init__(self, message, content_range):
super().__init__(message)
self.headers = {
'Content-Type': 'text/plain',
"Content-Range": "bytes */%s" % (content_range.total,)
"Content-Type": "text/plain",
"Content-Range": "bytes */%s" % (content_range.total,),
}
@@ -223,6 +227,11 @@ class InvalidRangeType(ContentRangeError):
pass
class PyFileError(Exception):
def __init__(self, file):
super().__init__("could not execute config file %s", file)
@add_status_code(401)
class Unauthorized(SanicException):
"""
@@ -258,13 +267,14 @@ class Unauthorized(SanicException):
scheme="Bearer",
realm="Restricted Area")
"""
def __init__(self, message, status_code=None, scheme=None, **kwargs):
super().__init__(message, status_code)
# if auth-scheme is specified, set "WWW-Authenticate" header
if scheme is not None:
values = ['{!s}="{!s}"'.format(k, v) for k, v in kwargs.items()]
challenge = ', '.join(values)
challenge = ", ".join(values)
self.headers = {
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
@@ -283,6 +293,6 @@ def abort(status_code, message=None):
if message is None:
message = STATUS_CODES.get(status_code)
# These are stored as bytes in the STATUS_CODES dict
message = message.decode('utf8')
message = message.decode("utf8")
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
raise sanic_exception(message=message, status_code=status_code)

View File

@@ -1,22 +1,36 @@
import sys
from traceback import format_exc, extract_tb
from traceback import extract_tb, format_exc
from sanic.exceptions import (
ContentRangeError,
HeaderNotFound,
INTERNAL_SERVER_ERROR_HTML,
InvalidRangeType,
SanicException,
TRACEBACK_BORDER,
TRACEBACK_LINE_HTML,
TRACEBACK_STYLE,
TRACEBACK_WRAPPER_HTML,
TRACEBACK_WRAPPER_INNER_HTML,
TRACEBACK_BORDER)
ContentRangeError,
HeaderNotFound,
InvalidRangeType,
SanicException,
)
from sanic.log import logger
from sanic.response import text, html
from sanic.response import html, text
class ErrorHandler:
"""
Provide :class:`sanic.app.Sanic` application with a mechanism to handle
and process any and all uncaught exceptions in a way the application
developer will set fit.
This error handling framework is built into the core that can be extended
by the developers to perform a wide range of tasks from recording the error
stats to reporting them to an external service that can be used for
realtime alerting system.
"""
handlers = None
cached_handlers = None
_missing = object()
@@ -36,7 +50,8 @@ class ErrorHandler:
return TRACEBACK_WRAPPER_INNER_HTML.format(
exc_name=exception.__class__.__name__,
exc_value=exception,
frame_html=''.join(frame_html))
frame_html="".join(frame_html),
)
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
@@ -51,13 +66,39 @@ class ErrorHandler:
exc_name=exception.__class__.__name__,
exc_value=exception,
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
path=request.path)
path=request.path,
)
def add(self, exception, handler):
"""
Add a new exception handler to an already existing handler object.
:param exception: Type of exception that need to be handled
:param handler: Reference to the method that will handle the exception
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:type handler: ``function``
:return: None
"""
self.handlers.append((exception, handler))
def lookup(self, exception):
handler = self.cached_handlers.get(exception, self._missing)
"""
Lookup the existing instance of :class:`ErrorHandler` and fetch the
registered handler for a specific type of exception.
This method leverages a dict lookup to speedup the retrieval process.
:param exception: Type of exception
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:return: Registered function if found ``None`` otherwise
"""
handler = self.cached_handlers.get(type(exception), self._missing)
if handler is self._missing:
for exception_class, handler in self.handlers:
if isinstance(exception, exception_class):
@@ -71,9 +112,15 @@ class ErrorHandler:
"""Fetches and executes an exception handler and returns a response
object
:param request: Request
:param request: Instance of :class:`sanic.request.Request`
:param exception: Exception to handle
:return: Response object
:type request: :class:`sanic.request.Request`
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:return: Wrap the return value obtained from :func:`default`
or registered handler for that type of exception.
"""
handler = self.lookup(exception)
response = None
@@ -84,88 +131,130 @@ class ErrorHandler:
response = self.default(request, exception)
except Exception:
self.log(format_exc())
if self.debug:
url = getattr(request, 'url', 'unknown')
response_message = ('Exception raised in exception handler '
'"%s" for uri: "%s"\n%s')
logger.error(response_message,
handler.__name__, url, format_exc())
try:
url = repr(request.url)
except AttributeError:
url = "unknown"
response_message = (
"Exception raised in exception handler " '"%s" for uri: %s'
)
logger.exception(response_message, handler.__name__, url)
return text(response_message % (
handler.__name__, url, format_exc()), 500)
if self.debug:
return text(response_message % (handler.__name__, url), 500)
else:
return text('An error occurred while handling an error', 500)
return text("An error occurred while handling an error", 500)
return response
def log(self, message, level='error'):
def log(self, message, level="error"):
"""
Override this method in an ErrorHandler subclass to prevent
logging exceptions.
Deprecated, do not use.
"""
getattr(logger, level)(message)
def default(self, request, exception):
"""
Provide a default behavior for the objects of :class:`ErrorHandler`.
If a developer chooses to extent the :class:`ErrorHandler` they can
provide a custom implementation for this method to behave in a way
they see fit.
:param request: Incoming request
:param exception: Exception object
:type request: :class:`sanic.request.Request`
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:return:
"""
self.log(format_exc())
try:
url = repr(request.url)
except AttributeError:
url = "unknown"
response_message = "Exception occurred while handling uri: %s"
logger.exception(response_message, url)
if issubclass(type(exception), SanicException):
return text(
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500),
headers=getattr(exception, 'headers', dict())
"Error: {}".format(exception),
status=getattr(exception, "status_code", 500),
headers=getattr(exception, "headers", dict()),
)
elif self.debug:
html_output = self._render_traceback_html(exception, request)
response_message = ('Exception occurred while handling uri: '
'"%s"\n%s')
logger.error(response_message, request.url, format_exc())
return html(html_output, status=500)
else:
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
class ContentRangeHandler:
"""Class responsible for parsing request header"""
__slots__ = ('start', 'end', 'size', 'total', 'headers')
"""
A mechanism to parse and process the incoming request headers to
extract the content range information.
:param request: Incoming api request
:param stats: Stats related to the content
:type request: :class:`sanic.request.Request`
:type stats: :class:`posix.stat_result`
:ivar start: Content Range start
:ivar end: Content Range end
:ivar size: Length of the content
:ivar total: Total size identified by the :class:`posix.stat_result`
instance
:ivar ContentRangeHandler.headers: Content range header ``dict``
"""
__slots__ = ("start", "end", "size", "total", "headers")
def __init__(self, request, stats):
self.total = stats.st_size
_range = request.headers.get('Range')
_range = request.headers.get("Range")
if _range is None:
raise HeaderNotFound('Range Header Not Found')
unit, _, value = tuple(map(str.strip, _range.partition('=')))
if unit != 'bytes':
raise HeaderNotFound("Range Header Not Found")
unit, _, value = tuple(map(str.strip, _range.partition("=")))
if unit != "bytes":
raise InvalidRangeType(
'%s is not a valid Range Type' % (unit,), self)
start_b, _, end_b = tuple(map(str.strip, value.partition('-')))
"%s is not a valid Range Type" % (unit,), self
)
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
try:
self.start = int(start_b) if start_b else None
except ValueError:
raise ContentRangeError(
'\'%s\' is invalid for Content Range' % (start_b,), self)
"'%s' is invalid for Content Range" % (start_b,), self
)
try:
self.end = int(end_b) if end_b else None
except ValueError:
raise ContentRangeError(
'\'%s\' is invalid for Content Range' % (end_b,), self)
"'%s' is invalid for Content Range" % (end_b,), self
)
if self.end is None:
if self.start is None:
raise ContentRangeError(
'Invalid for Content Range parameters', self)
"Invalid for Content Range parameters", self
)
else:
# this case represents `Content-Range: bytes 5-`
self.end = self.total
self.end = self.total - 1
else:
if self.start is None:
# this case represents `Content-Range: bytes -5`
self.start = self.total - self.end
self.end = self.total
self.end = self.total - 1
if self.start >= self.end:
raise ContentRangeError(
'Invalid for Content Range parameters', self)
self.size = self.end - self.start
"Invalid for Content Range parameters", self
)
self.size = self.end - self.start + 1
self.headers = {
'Content-Range': "bytes %s-%s/%s" % (
self.start, self.end, self.total)}
"Content-Range": "bytes %s-%s/%s"
% (self.start, self.end, self.total)
}
def __bool__(self):
return self.size > 0

133
sanic/helpers.py Normal file
View File

@@ -0,0 +1,133 @@
"""Defines basics of HTTP standard."""
STATUS_CODES = {
100: b"Continue",
101: b"Switching Protocols",
102: b"Processing",
200: b"OK",
201: b"Created",
202: b"Accepted",
203: b"Non-Authoritative Information",
204: b"No Content",
205: b"Reset Content",
206: b"Partial Content",
207: b"Multi-Status",
208: b"Already Reported",
226: b"IM Used",
300: b"Multiple Choices",
301: b"Moved Permanently",
302: b"Found",
303: b"See Other",
304: b"Not Modified",
305: b"Use Proxy",
307: b"Temporary Redirect",
308: b"Permanent Redirect",
400: b"Bad Request",
401: b"Unauthorized",
402: b"Payment Required",
403: b"Forbidden",
404: b"Not Found",
405: b"Method Not Allowed",
406: b"Not Acceptable",
407: b"Proxy Authentication Required",
408: b"Request Timeout",
409: b"Conflict",
410: b"Gone",
411: b"Length Required",
412: b"Precondition Failed",
413: b"Request Entity Too Large",
414: b"Request-URI Too Long",
415: b"Unsupported Media Type",
416: b"Requested Range Not Satisfiable",
417: b"Expectation Failed",
418: b"I'm a teapot",
422: b"Unprocessable Entity",
423: b"Locked",
424: b"Failed Dependency",
426: b"Upgrade Required",
428: b"Precondition Required",
429: b"Too Many Requests",
431: b"Request Header Fields Too Large",
451: b"Unavailable For Legal Reasons",
500: b"Internal Server Error",
501: b"Not Implemented",
502: b"Bad Gateway",
503: b"Service Unavailable",
504: b"Gateway Timeout",
505: b"HTTP Version Not Supported",
506: b"Variant Also Negotiates",
507: b"Insufficient Storage",
508: b"Loop Detected",
510: b"Not Extended",
511: b"Network Authentication Required",
}
# According to https://tools.ietf.org/html/rfc2616#section-7.1
_ENTITY_HEADERS = frozenset(
[
"allow",
"content-encoding",
"content-language",
"content-length",
"content-location",
"content-md5",
"content-range",
"content-type",
"expires",
"last-modified",
"extension-header",
]
)
# According to https://tools.ietf.org/html/rfc2616#section-13.5.1
_HOP_BY_HOP_HEADERS = frozenset(
[
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailers",
"transfer-encoding",
"upgrade",
]
)
def has_message_body(status):
"""
According to the following RFC message body and length SHOULD NOT
be included in responses status 1XX, 204 and 304.
https://tools.ietf.org/html/rfc2616#section-4.4
https://tools.ietf.org/html/rfc2616#section-4.3
"""
return status not in (204, 304) and not (100 <= status < 200)
def is_entity_header(header):
"""Checks if the given header is an Entity Header"""
return header.lower() in _ENTITY_HEADERS
def is_hop_by_hop_header(header):
"""Checks if the given header is a Hop By Hop header"""
return header.lower() in _HOP_BY_HOP_HEADERS
def remove_entity_headers(headers, allowed=("content-location", "expires")):
"""
Removes all the entity headers present in the headers given.
According to RFC 2616 Section 10.3.5,
Content-Location and Expires are allowed as for the
"strong cache validator".
https://tools.ietf.org/html/rfc2616#section-10.3.5
returns the headers without the entity headers
"""
allowed = set([h.lower() for h in allowed])
headers = {
header: value
for header, value in headers.items()
if not is_entity_header(header) or header.lower() in allowed
}
return headers

View File

@@ -1,128 +0,0 @@
"""Defines basics of HTTP standard."""
STATUS_CODES = {
100: b'Continue',
101: b'Switching Protocols',
102: b'Processing',
200: b'OK',
201: b'Created',
202: b'Accepted',
203: b'Non-Authoritative Information',
204: b'No Content',
205: b'Reset Content',
206: b'Partial Content',
207: b'Multi-Status',
208: b'Already Reported',
226: b'IM Used',
300: b'Multiple Choices',
301: b'Moved Permanently',
302: b'Found',
303: b'See Other',
304: b'Not Modified',
305: b'Use Proxy',
307: b'Temporary Redirect',
308: b'Permanent Redirect',
400: b'Bad Request',
401: b'Unauthorized',
402: b'Payment Required',
403: b'Forbidden',
404: b'Not Found',
405: b'Method Not Allowed',
406: b'Not Acceptable',
407: b'Proxy Authentication Required',
408: b'Request Timeout',
409: b'Conflict',
410: b'Gone',
411: b'Length Required',
412: b'Precondition Failed',
413: b'Request Entity Too Large',
414: b'Request-URI Too Long',
415: b'Unsupported Media Type',
416: b'Requested Range Not Satisfiable',
417: b'Expectation Failed',
418: b'I\'m a teapot',
422: b'Unprocessable Entity',
423: b'Locked',
424: b'Failed Dependency',
426: b'Upgrade Required',
428: b'Precondition Required',
429: b'Too Many Requests',
431: b'Request Header Fields Too Large',
451: b'Unavailable For Legal Reasons',
500: b'Internal Server Error',
501: b'Not Implemented',
502: b'Bad Gateway',
503: b'Service Unavailable',
504: b'Gateway Timeout',
505: b'HTTP Version Not Supported',
506: b'Variant Also Negotiates',
507: b'Insufficient Storage',
508: b'Loop Detected',
510: b'Not Extended',
511: b'Network Authentication Required'
}
# According to https://tools.ietf.org/html/rfc2616#section-7.1
_ENTITY_HEADERS = frozenset([
'allow',
'content-encoding',
'content-language',
'content-length',
'content-location',
'content-md5',
'content-range',
'content-type',
'expires',
'last-modified',
'extension-header'
])
# According to https://tools.ietf.org/html/rfc2616#section-13.5.1
_HOP_BY_HOP_HEADERS = frozenset([
'connection',
'keep-alive',
'proxy-authenticate',
'proxy-authorization',
'te',
'trailers',
'transfer-encoding',
'upgrade'
])
def has_message_body(status):
"""
According to the following RFC message body and length SHOULD NOT
be included in responses status 1XX, 204 and 304.
https://tools.ietf.org/html/rfc2616#section-4.4
https://tools.ietf.org/html/rfc2616#section-4.3
"""
return status not in (204, 304) and not (100 <= status < 200)
def is_entity_header(header):
"""Checks if the given header is an Entity Header"""
return header.lower() in _ENTITY_HEADERS
def is_hop_by_hop_header(header):
"""Checks if the given header is a Hop By Hop header"""
return header.lower() in _HOP_BY_HOP_HEADERS
def remove_entity_headers(headers,
allowed=('content-location', 'expires')):
"""
Removes all the entity headers present in the headers given.
According to RFC 2616 Section 10.3.5,
Content-Location and Expires are allowed as for the
"strong cache validator".
https://tools.ietf.org/html/rfc2616#section-10.3.5
returns the headers without the entity headers
"""
allowed = set([h.lower() for h in allowed])
headers = {header: value for header, value in headers.items()
if not is_entity_header(header)
and header.lower() not in allowed}
return headers

View File

@@ -5,59 +5,54 @@ import sys
LOGGING_CONFIG_DEFAULTS = dict(
version=1,
disable_existing_loggers=False,
loggers={
"root": {
"level": "INFO",
"handlers": ["console"]
},
"sanic.root": {"level": "INFO", "handlers": ["console"]},
"sanic.error": {
"level": "INFO",
"handlers": ["error_console"],
"propagate": True,
"qualname": "sanic.error"
"qualname": "sanic.error",
},
"sanic.access": {
"level": "INFO",
"handlers": ["access_console"],
"propagate": True,
"qualname": "sanic.access"
}
"qualname": "sanic.access",
},
},
handlers={
"console": {
"class": "logging.StreamHandler",
"formatter": "generic",
"stream": sys.stdout
"stream": sys.stdout,
},
"error_console": {
"class": "logging.StreamHandler",
"formatter": "generic",
"stream": sys.stderr
"stream": sys.stderr,
},
"access_console": {
"class": "logging.StreamHandler",
"formatter": "access",
"stream": sys.stdout
"stream": sys.stdout,
},
},
formatters={
"generic": {
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
"class": "logging.Formatter"
"class": "logging.Formatter",
},
"access": {
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " +
"%(request)s %(message)s %(status)d %(byte)d",
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
+ "%(request)s %(message)s %(status)d %(byte)d",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
"class": "logging.Formatter"
"class": "logging.Formatter",
},
}
},
)
logger = logging.getLogger('root')
error_logger = logging.getLogger('sanic.error')
access_logger = logging.getLogger('sanic.access')
logger = logging.getLogger("sanic.root")
error_logger = logging.getLogger("sanic.error")
access_logger = logging.getLogger("sanic.access")

View File

@@ -1,9 +1,10 @@
import os
import sys
import signal
import subprocess
from time import sleep
import sys
from multiprocessing import Process
from time import sleep
def _iter_module_files():
@@ -18,7 +19,7 @@ def _iter_module_files():
for module in list(sys.modules.values()):
if module is None:
continue
filename = getattr(module, '__file__', None)
filename = getattr(module, "__file__", None)
if filename:
old = None
while not os.path.isfile(filename):
@@ -27,7 +28,7 @@ def _iter_module_files():
if filename == old:
break
else:
if filename[-4:] in ('.pyc', '.pyo'):
if filename[-4:] in (".pyc", ".pyo"):
filename = filename[:-1]
yield filename
@@ -35,7 +36,15 @@ def _iter_module_files():
def _get_args_for_reloading():
"""Returns the executable."""
rv = [sys.executable]
rv.extend(sys.argv)
main_module = sys.modules["__main__"]
mod_spec = getattr(main_module, "__spec__", None)
if mod_spec:
# Parent exe was launched as a module rather than a script
rv.extend(["-m", mod_spec.name])
if len(sys.argv) > 1:
rv.extend(sys.argv[1:])
else:
rv.extend(sys.argv)
return rv
@@ -43,13 +52,16 @@ def restart_with_reloader():
"""Create a new process and a subprocess in it with the same arguments as
this one.
"""
cwd = os.getcwd()
args = _get_args_for_reloading()
new_environ = os.environ.copy()
new_environ['SANIC_SERVER_RUNNING'] = 'true'
cmd = ' '.join(args)
new_environ["SANIC_SERVER_RUNNING"] = "true"
cmd = " ".join(args)
worker_process = Process(
target=subprocess.call, args=(cmd,),
kwargs=dict(shell=True, env=new_environ))
target=subprocess.call,
args=(cmd,),
kwargs={"cwd": cwd, "shell": True, "env": new_environ},
)
worker_process.start()
return worker_process
@@ -67,8 +79,10 @@ def kill_process_children_unix(pid):
children_list_pid = children_list_file.read().split()
for child_pid in children_list_pid:
children_proc_path = "/proc/%s/task/%s/children" % \
(child_pid, child_pid)
children_proc_path = "/proc/%s/task/%s/children" % (
child_pid,
child_pid,
)
if not os.path.isfile(children_proc_path):
continue
with open(children_proc_path) as children_list_file_2:
@@ -90,7 +104,7 @@ def kill_process_children_osx(pid):
:param pid: PID of parent process (process ID)
:return: Nothing
"""
subprocess.run(['pkill', '-P', str(pid)])
subprocess.run(["pkill", "-P", str(pid)])
def kill_process_children(pid):
@@ -99,12 +113,12 @@ def kill_process_children(pid):
:param pid: PID of parent process (process ID)
:return: Nothing
"""
if sys.platform == 'darwin':
if sys.platform == "darwin":
kill_process_children_osx(pid)
elif sys.platform == 'linux':
elif sys.platform == "linux":
kill_process_children_unix(pid)
else:
pass # should signal error here
pass # should signal error here
def kill_program_completly(proc):
@@ -127,9 +141,11 @@ def watchdog(sleep_interval):
mtimes = {}
worker_process = restart_with_reloader()
signal.signal(
signal.SIGTERM, lambda *args: kill_program_completly(worker_process))
signal.SIGTERM, lambda *args: kill_program_completly(worker_process)
)
signal.signal(
signal.SIGINT, lambda *args: kill_program_completly(worker_process))
signal.SIGINT, lambda *args: kill_program_completly(worker_process)
)
while True:
for filename in _iter_module_files():
try:

View File

@@ -1,24 +1,32 @@
import sys
import asyncio
import email.utils
import json
import socket
import sys
import warnings
from cgi import parse_header
from collections import namedtuple
from collections import defaultdict, namedtuple
from http.cookies import SimpleCookie
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
from httptools import parse_url
from urllib.parse import parse_qs, urlunparse
from sanic.exceptions import InvalidUsage
from sanic.log import error_logger, logger
try:
from ujson import loads as json_loads
except ImportError:
if sys.version_info[:2] == (3, 5):
def json_loads(data):
# on Python 3.5 json.loads only supports str not bytes
return json.loads(data.decode())
else:
json_loads = json.loads
from sanic.exceptions import InvalidUsage
from sanic.log import error_logger, logger
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
@@ -42,13 +50,49 @@ class RequestParameters(dict):
return super().get(name, default)
class StreamBuffer:
def __init__(self, buffer_size=100):
self._queue = asyncio.Queue(buffer_size)
async def read(self):
""" Stop reading when gets None """
payload = await self._queue.get()
self._queue.task_done()
return payload
async def put(self, payload):
await self._queue.put(payload)
def is_full(self):
return self._queue.full()
class Request(dict):
"""Properties of an HTTP request such as URL, headers, etc."""
__slots__ = (
'app', 'headers', 'version', 'method', '_cookies', 'transport',
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr',
'_socket', '_port', '__weakref__', 'raw_url'
"__weakref__",
"_cookies",
"_ip",
"_parsed_url",
"_port",
"_remote_addr",
"_socket",
"app",
"body",
"endpoint",
"headers",
"method",
"parsed_args",
"parsed_not_grouped_args",
"parsed_files",
"parsed_form",
"parsed_json",
"raw_url",
"stream",
"transport",
"uri_template",
"version",
)
def __init__(self, url_bytes, headers, version, method, transport):
@@ -63,27 +107,36 @@ class Request(dict):
self.transport = transport
# Init but do not inhale
self.body = []
self.body_init()
self.parsed_json = None
self.parsed_form = None
self.parsed_files = None
self.parsed_args = None
self.parsed_args = defaultdict(RequestParameters)
self.parsed_not_grouped_args = defaultdict(list)
self.uri_template = None
self._cookies = None
self.stream = None
self.endpoint = None
def __repr__(self):
if self.method is None or not self.path:
return '<{0}>'.format(self.__class__.__name__)
return '<{0}: {1} {2}>'.format(self.__class__.__name__,
self.method,
self.path)
return "<{0}: {1} {2}>".format(
self.__class__.__name__, self.method, self.path
)
def __bool__(self):
if self.transport:
return True
return False
def body_init(self):
self.body = []
def body_push(self, data):
self.body.append(data)
def body_finish(self):
self.body = b"".join(self.body)
@property
def json(self):
if self.parsed_json is None:
@@ -107,8 +160,8 @@ class Request(dict):
:return: token related to request
"""
prefixes = ('Bearer', 'Token')
auth_header = self.headers.get('Authorization')
prefixes = ("Bearer", "Token")
auth_header = self.headers.get("Authorization")
if auth_header is not None:
for prefix in prefixes:
@@ -123,17 +176,20 @@ class Request(dict):
self.parsed_form = RequestParameters()
self.parsed_files = RequestParameters()
content_type = self.headers.get(
'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
)
content_type, parameters = parse_header(content_type)
try:
if content_type == 'application/x-www-form-urlencoded':
if content_type == "application/x-www-form-urlencoded":
self.parsed_form = RequestParameters(
parse_qs(self.body.decode('utf-8')))
elif content_type == 'multipart/form-data':
parse_qs(self.body.decode("utf-8"))
)
elif content_type == "multipart/form-data":
# TODO: Stream this instead of reading to/from memory
boundary = parameters['boundary'].encode('utf-8')
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
boundary = parameters["boundary"].encode("utf-8")
self.parsed_form, self.parsed_files = parse_multipart_form(
self.body, boundary
)
except Exception:
error_logger.exception("Failed when parsing form")
@@ -146,64 +202,156 @@ class Request(dict):
return self.parsed_files
@property
def args(self):
if self.parsed_args is None:
def get_args(
self,
keep_blank_values: bool = False,
strict_parsing: bool = False,
encoding: str = "utf-8",
errors: str = "replace",
) -> RequestParameters:
"""
Method to parse `query_string` using `urllib.parse.parse_qs`.
This methods is used by `args` property.
Can be used directly if you need to change default parameters.
:param keep_blank_values: flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
:type keep_blank_values: bool
:param strict_parsing: flag indicating what to do with parsing errors.
If false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
:type strict_parsing: bool
:param encoding: specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type encoding: str
:param errors: specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type errors: str
:return: RequestParameters
"""
if not self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
if self.query_string:
self.parsed_args = RequestParameters(
parse_qs(self.query_string))
else:
self.parsed_args = RequestParameters()
return self.parsed_args
self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
] = RequestParameters(
parse_qs(
qs=self.query_string,
keep_blank_values=keep_blank_values,
strict_parsing=strict_parsing,
encoding=encoding,
errors=errors,
)
)
return self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
]
args = property(get_args)
@property
def raw_args(self):
def raw_args(self) -> dict:
if self.app.debug: # pragma: no cover
warnings.simplefilter("default")
warnings.warn(
"Use of raw_args will be deprecated in "
"the future versions. Please use args or query_args "
"properties instead",
DeprecationWarning,
)
return {k: v[0] for k, v in self.args.items()}
def get_query_args(
self,
keep_blank_values: bool = False,
strict_parsing: bool = False,
encoding: str = "utf-8",
errors: str = "replace",
) -> list:
"""
Method to parse `query_string` using `urllib.parse.parse_qsl`.
This methods is used by `query_args` property.
Can be used directly if you need to change default parameters.
:param keep_blank_values: flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
:type keep_blank_values: bool
:param strict_parsing: flag indicating what to do with parsing errors.
If false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
:type strict_parsing: bool
:param encoding: specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type encoding: str
:param errors: specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type errors: str
:return: list
"""
if not self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
if self.query_string:
self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
] = parse_qsl(
qs=self.query_string,
keep_blank_values=keep_blank_values,
strict_parsing=strict_parsing,
encoding=encoding,
errors=errors,
)
return self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
]
query_args = property(get_query_args)
@property
def cookies(self):
if self._cookies is None:
cookie = self.headers.get('Cookie')
cookie = self.headers.get("Cookie")
if cookie is not None:
cookies = SimpleCookie()
cookies.load(cookie)
self._cookies = {name: cookie.value
for name, cookie in cookies.items()}
self._cookies = {
name: cookie.value for name, cookie in cookies.items()
}
else:
self._cookies = {}
return self._cookies
@property
def ip(self):
if not hasattr(self, '_socket'):
if not hasattr(self, "_socket"):
self._get_address()
return self._ip
@property
def port(self):
if not hasattr(self, '_socket'):
if not hasattr(self, "_socket"):
self._get_address()
return self._port
@property
def socket(self):
if not hasattr(self, '_socket'):
if not hasattr(self, "_socket"):
self._get_address()
return self._socket
def _get_address(self):
sock = self.transport.get_extra_info('socket')
if sock.family == socket.AF_INET:
self._socket = (self.transport.get_extra_info('peername') or
(None, None))
self._ip, self._port = self._socket
elif sock.family == socket.AF_INET6:
self._socket = (self.transport.get_extra_info('peername') or
(None, None, None, None))
self._ip, self._port, *_ = self._socket
else:
self._ip, self._port = (None, None)
self._socket = self.transport.get_extra_info("peername") or (
None,
None,
)
self._ip = self._socket[0]
self._port = self._socket[1]
@property
def remote_addr(self):
@@ -211,29 +359,31 @@ class Request(dict):
:return: original client ip.
"""
if not hasattr(self, '_remote_addr'):
forwarded_for = self.headers.get('X-Forwarded-For', '').split(',')
if not hasattr(self, "_remote_addr"):
forwarded_for = self.headers.get("X-Forwarded-For", "").split(",")
remote_addrs = [
addr for addr in [
addr.strip() for addr in forwarded_for
] if addr
]
addr
for addr in [addr.strip() for addr in forwarded_for]
if addr
]
if len(remote_addrs) > 0:
self._remote_addr = remote_addrs[0]
else:
self._remote_addr = ''
self._remote_addr = ""
return self._remote_addr
@property
def scheme(self):
if self.app.websocket_enabled \
and self.headers.get('upgrade') == 'websocket':
scheme = 'ws'
if (
self.app.websocket_enabled
and self.headers.get("upgrade") == "websocket"
):
scheme = "ws"
else:
scheme = 'http'
scheme = "http"
if self.transport.get_extra_info('sslcontext'):
scheme += 's'
if self.transport.get_extra_info("sslcontext"):
scheme += "s"
return scheme
@@ -241,11 +391,11 @@ class Request(dict):
def host(self):
# it appears that httptools doesn't return the host
# so pull it from the headers
return self.headers.get('Host', '')
return self.headers.get("Host", "")
@property
def content_type(self):
return self.headers.get('Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
@property
def match_info(self):
@@ -254,27 +404,23 @@ class Request(dict):
@property
def path(self):
return self._parsed_url.path.decode('utf-8')
return self._parsed_url.path.decode("utf-8")
@property
def query_string(self):
if self._parsed_url.query:
return self._parsed_url.query.decode('utf-8')
return self._parsed_url.query.decode("utf-8")
else:
return ''
return ""
@property
def url(self):
return urlunparse((
self.scheme,
self.host,
self.path,
None,
self.query_string,
None))
return urlunparse(
(self.scheme, self.host, self.path, None, self.query_string, None)
)
File = namedtuple('File', ['type', 'body', 'name'])
File = namedtuple("File", ["type", "body", "name"])
def parse_multipart_form(body, boundary):
@@ -290,49 +436,59 @@ def parse_multipart_form(body, boundary):
form_parts = body.split(boundary)
for form_part in form_parts[1:-1]:
file_name = None
content_type = 'text/plain'
content_charset = 'utf-8'
content_type = "text/plain"
content_charset = "utf-8"
field_name = None
line_index = 2
line_end_index = 0
while not line_end_index == -1:
line_end_index = form_part.find(b'\r\n', line_index)
form_line = form_part[line_index:line_end_index].decode('utf-8')
line_end_index = form_part.find(b"\r\n", line_index)
form_line = form_part[line_index:line_end_index].decode("utf-8")
line_index = line_end_index + 2
if not form_line:
break
colon_index = form_line.index(':')
colon_index = form_line.index(":")
form_header_field = form_line[0:colon_index].lower()
form_header_value, form_parameters = parse_header(
form_line[colon_index + 2:])
form_line[colon_index + 2 :]
)
if form_header_field == 'content-disposition':
file_name = form_parameters.get('filename')
field_name = form_parameters.get('name')
elif form_header_field == 'content-type':
if form_header_field == "content-disposition":
field_name = form_parameters.get("name")
file_name = form_parameters.get("filename")
# non-ASCII filenames in RFC2231, "filename*" format
if file_name is None and form_parameters.get("filename*"):
encoding, _, value = email.utils.decode_rfc2231(
form_parameters["filename*"]
)
file_name = unquote(value, encoding=encoding)
elif form_header_field == "content-type":
content_type = form_header_value
content_charset = form_parameters.get('charset', 'utf-8')
content_charset = form_parameters.get("charset", "utf-8")
if field_name:
post_data = form_part[line_index:-4]
if file_name:
form_file = File(type=content_type,
name=file_name,
body=post_data)
if field_name in files:
files[field_name].append(form_file)
else:
files[field_name] = [form_file]
else:
if file_name is None:
value = post_data.decode(content_charset)
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
else:
form_file = File(
type=content_type, name=file_name, body=post_data
)
if field_name in files:
files[field_name].append(form_file)
else:
files[field_name] = [form_file]
else:
logger.debug('Form-data field does not have a \'name\' parameter \
in the Content-Disposition header')
logger.debug(
"Form-data field does not have a 'name' parameter "
"in the Content-Disposition header"
)
return fields, files

View File

@@ -1,17 +1,23 @@
from functools import partial
from mimetypes import guess_type
from os import path
from urllib.parse import quote_plus
try:
from ujson import dumps as json_dumps
except BaseException:
from json import dumps as json_dumps
from aiofiles import open as open_async
from multidict import CIMultiDict
from sanic import http
from sanic.cookies import CookieJar
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
try:
from ujson import dumps as json_dumps
except BaseException:
from json import dumps
# This is done in order to ensure that the JSON response is
# kept consistent across both ujson and inbuilt json usage.
json_dumps = partial(dumps, separators=(",", ":"))
class BaseHTTPResponse:
@@ -24,16 +30,18 @@ class BaseHTTPResponse:
return str(data).encode()
def _parse_headers(self):
headers = b''
headers = b""
for name, value in self.headers.items():
try:
headers += (
b'%b: %b\r\n' % (
name.encode(), value.encode('utf-8')))
headers += b"%b: %b\r\n" % (
name.encode(),
value.encode("utf-8"),
)
except AttributeError:
headers += (
b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
headers += b"%b: %b\r\n" % (
str(name).encode(),
str(value).encode("utf-8"),
)
return headers
@@ -46,12 +54,17 @@ class BaseHTTPResponse:
class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = (
'protocol', 'streaming_fn', 'status',
'content_type', 'headers', '_cookies'
"protocol",
"streaming_fn",
"status",
"content_type",
"headers",
"_cookies",
)
def __init__(self, streaming_fn, status=200, headers=None,
content_type='text/plain'):
def __init__(
self, streaming_fn, status=200, headers=None, content_type="text/plain"
):
self.content_type = content_type
self.streaming_fn = streaming_fn
self.status = status
@@ -66,61 +79,69 @@ class StreamingHTTPResponse(BaseHTTPResponse):
if type(data) != bytes:
data = self._encode_body(data)
self.protocol.push_data(
b"%x\r\n%b\r\n" % (len(data), data))
self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
await self.protocol.drain()
async def stream(
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
self, version="1.1", keep_alive=False, keep_alive_timeout=None
):
"""Streams headers, runs the `streaming_fn` callback that writes
content to the response body, then finalizes the response body.
"""
headers = self.get_headers(
version, keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout)
version,
keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout,
)
self.protocol.push_data(headers)
await self.protocol.drain()
await self.streaming_fn(self)
self.protocol.push_data(b'0\r\n\r\n')
self.protocol.push_data(b"0\r\n\r\n")
# no need to await drain here after this write, because it is the
# very last thing we write and nothing needs to wait for it.
def get_headers(
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
self, version="1.1", keep_alive=False, keep_alive_timeout=None
):
# This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python
timeout_header = b''
timeout_header = b""
if keep_alive and keep_alive_timeout is not None:
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
self.headers['Transfer-Encoding'] = 'chunked'
self.headers.pop('Content-Length', None)
self.headers['Content-Type'] = self.headers.get(
'Content-Type', self.content_type)
self.headers["Transfer-Encoding"] = "chunked"
self.headers.pop("Content-Length", None)
self.headers["Content-Type"] = self.headers.get(
"Content-Type", self.content_type
)
headers = self._parse_headers()
if self.status is 200:
status = b'OK'
if self.status == 200:
status = b"OK"
else:
status = http.STATUS_CODES.get(self.status)
status = STATUS_CODES.get(self.status)
return (b'HTTP/%b %d %b\r\n'
b'%b'
b'%b\r\n') % (
version.encode(),
self.status,
status,
timeout_header,
headers
)
return (b"HTTP/%b %d %b\r\n" b"%b" b"%b\r\n") % (
version.encode(),
self.status,
status,
timeout_header,
headers,
)
class HTTPResponse(BaseHTTPResponse):
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
__slots__ = ("body", "status", "content_type", "headers", "_cookies")
def __init__(self, body=None, status=200, headers=None,
content_type='text/plain', body_bytes=b''):
def __init__(
self,
body=None,
status=200,
headers=None,
content_type="text/plain",
body_bytes=b"",
):
self.content_type = content_type
if body is not None:
@@ -132,46 +153,45 @@ class HTTPResponse(BaseHTTPResponse):
self.headers = CIMultiDict(headers or {})
self._cookies = None
def output(
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
# This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python
timeout_header = b''
timeout_header = b""
if keep_alive and keep_alive_timeout is not None:
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
body = b''
if http.has_message_body(self.status):
body = b""
if has_message_body(self.status):
body = self.body
self.headers['Content-Length'] = self.headers.get(
'Content-Length', len(self.body))
self.headers["Content-Length"] = self.headers.get(
"Content-Length", len(self.body)
)
self.headers['Content-Type'] = self.headers.get(
'Content-Type', self.content_type)
self.headers["Content-Type"] = self.headers.get(
"Content-Type", self.content_type
)
if self.status in (304, 412):
self.headers = http.remove_entity_headers(self.headers)
self.headers = remove_entity_headers(self.headers)
headers = self._parse_headers()
if self.status is 200:
status = b'OK'
if self.status == 200:
status = b"OK"
else:
status = http.STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
return (b'HTTP/%b %d %b\r\n'
b'Connection: %b\r\n'
b'%b'
b'%b\r\n'
b'%b') % (
version.encode(),
self.status,
status,
b'keep-alive' if keep_alive else b'close',
timeout_header,
headers,
body
)
return (
b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b"
) % (
version.encode(),
self.status,
status,
b"keep-alive" if keep_alive else b"close",
timeout_header,
headers,
body,
)
@property
def cookies(self):
@@ -180,9 +200,14 @@ class HTTPResponse(BaseHTTPResponse):
return self._cookies
def json(body, status=200, headers=None,
content_type="application/json", dumps=json_dumps,
**kwargs):
def json(
body,
status=200,
headers=None,
content_type="application/json",
dumps=json_dumps,
**kwargs
):
"""
Returns response object with body in json format.
@@ -191,12 +216,17 @@ def json(body, status=200, headers=None,
:param headers: Custom Headers.
:param kwargs: Remaining arguments that are passed to the json encoder.
"""
return HTTPResponse(dumps(body, **kwargs), headers=headers,
status=status, content_type=content_type)
return HTTPResponse(
dumps(body, **kwargs),
headers=headers,
status=status,
content_type=content_type,
)
def text(body, status=200, headers=None,
content_type="text/plain; charset=utf-8"):
def text(
body, status=200, headers=None, content_type="text/plain; charset=utf-8"
):
"""
Returns response object with body in text format.
@@ -206,12 +236,13 @@ def text(body, status=200, headers=None,
:param content_type: the content type (string) of the response
"""
return HTTPResponse(
body, status=status, headers=headers,
content_type=content_type)
body, status=status, headers=headers, content_type=content_type
)
def raw(body, status=200, headers=None,
content_type="application/octet-stream"):
def raw(
body, status=200, headers=None, content_type="application/octet-stream"
):
"""
Returns response object without encoding the body.
@@ -220,8 +251,12 @@ def raw(body, status=200, headers=None,
:param headers: Custom Headers.
:param content_type: the content type (string) of the response.
"""
return HTTPResponse(body_bytes=body, status=status, headers=headers,
content_type=content_type)
return HTTPResponse(
body_bytes=body,
status=status,
headers=headers,
content_type=content_type,
)
def html(body, status=200, headers=None):
@@ -232,12 +267,22 @@ def html(body, status=200, headers=None):
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8")
return HTTPResponse(
body,
status=status,
headers=headers,
content_type="text/html; charset=utf-8",
)
async def file(location, status=200, mime_type=None, headers=None,
filename=None, _range=None):
async def file(
location,
status=200,
mime_type=None,
headers=None,
filename=None,
_range=None,
):
"""Return a response object with file data.
:param location: Location of file on system.
@@ -249,28 +294,41 @@ async def file(location, status=200, mime_type=None, headers=None,
headers = headers or {}
if filename:
headers.setdefault(
'Content-Disposition',
'attachment; filename="{}"'.format(filename))
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
)
filename = filename or path.split(location)[-1]
async with open_async(location, mode='rb') as _file:
async with open_async(location, mode="rb") as _file:
if _range:
await _file.seek(_range.start)
out_stream = await _file.read(_range.size)
headers['Content-Range'] = 'bytes %s-%s/%s' % (
_range.start, _range.end, _range.total)
headers["Content-Range"] = "bytes %s-%s/%s" % (
_range.start,
_range.end,
_range.total,
)
status = 206
else:
out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
return HTTPResponse(status=status,
headers=headers,
content_type=mime_type,
body_bytes=out_stream)
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
return HTTPResponse(
status=status,
headers=headers,
content_type=mime_type,
body_bytes=out_stream,
)
async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
headers=None, filename=None, _range=None):
async def file_stream(
location,
status=200,
chunk_size=4096,
mime_type=None,
headers=None,
filename=None,
_range=None,
):
"""Return a streaming response object with file data.
:param location: Location of file on system.
@@ -283,11 +341,11 @@ async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
headers = headers or {}
if filename:
headers.setdefault(
'Content-Disposition',
'attachment; filename="{}"'.format(filename))
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
)
filename = filename or path.split(location)[-1]
_file = await open_async(location, mode='rb')
_file = await open_async(location, mode="rb")
async def _streaming_fn(response):
nonlocal _file, chunk_size
@@ -312,19 +370,28 @@ async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
await _file.close()
return # Returning from this fn closes the stream
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
if _range:
headers['Content-Range'] = 'bytes %s-%s/%s' % (
_range.start, _range.end, _range.total)
return StreamingHTTPResponse(streaming_fn=_streaming_fn,
status=status,
headers=headers,
content_type=mime_type)
headers["Content-Range"] = "bytes %s-%s/%s" % (
_range.start,
_range.end,
_range.total,
)
status = 206
return StreamingHTTPResponse(
streaming_fn=_streaming_fn,
status=status,
headers=headers,
content_type=mime_type,
)
def stream(
streaming_fn, status=200, headers=None,
content_type="text/plain; charset=utf-8"):
streaming_fn,
status=200,
headers=None,
content_type="text/plain; charset=utf-8",
):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
@@ -344,15 +411,13 @@ def stream(
:param headers: Custom Headers.
"""
return StreamingHTTPResponse(
streaming_fn,
headers=headers,
content_type=content_type,
status=status
streaming_fn, headers=headers, content_type=content_type, status=status
)
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
def redirect(
to, headers=None, status=302, content_type="text/html; charset=utf-8"
):
"""Abort execution and cause a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to
@@ -364,12 +429,11 @@ def redirect(to, headers=None, status=302,
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/#?&=@[]!$&'()*+,;")
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers['Location'] = safe_to
headers["Location"] = safe_to
return HTTPResponse(
status=status,
headers=headers,
content_type=content_type)
status=status, headers=headers, content_type=content_type
)

View File

@@ -1,33 +1,38 @@
import re
import uuid
from collections import defaultdict, namedtuple
from collections.abc import Iterable
from functools import lru_cache
from urllib.parse import unquote
from sanic.exceptions import NotFound, MethodNotSupported
from sanic.exceptions import MethodNotSupported, NotFound
from sanic.views import CompositionView
Route = namedtuple(
'Route',
['handler', 'methods', 'pattern', 'parameters', 'name', 'uri'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
"Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
)
Parameter = namedtuple("Parameter", ["name", "cast"])
REGEX_TYPES = {
'string': (str, r'[^/]+'),
'int': (int, r'\d+'),
'number': (float, r'[0-9\\.]+'),
'alpha': (str, r'[A-Za-z]+'),
'path': (str, r'[^/].*?'),
'uuid': (uuid.UUID, r'[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-'
r'[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}')
"string": (str, r"[^/]+"),
"int": (int, r"-?\d+"),
"number": (float, r"-?[0-9\\.]+"),
"alpha": (str, r"[A-Za-z]+"),
"path": (str, r"[^/].*?"),
"uuid": (
uuid.UUID,
r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-"
r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}",
),
}
ROUTER_CACHE_SIZE = 1024
def url_hash(url):
return url.count('/')
return url.count("/")
class RouteExists(Exception):
@@ -38,6 +43,10 @@ class RouteDoesNotExist(Exception):
pass
class ParameterNameConflicts(Exception):
pass
class Router:
"""Router supports basic routing with parameters and method checks
@@ -64,10 +73,11 @@ class Router:
also be passed in as the type. The argument given to the function will
always be a string, independent of the type.
"""
routes_static = None
routes_dynamic = None
routes_always_check = None
parameter_pattern = re.compile(r'<(.+?)>')
parameter_pattern = re.compile(r"<(.+?)>")
def __init__(self):
self.routes_all = {}
@@ -94,9 +104,9 @@ class Router:
"""
# We could receive NAME or NAME:PATTERN
name = parameter_string
pattern = 'string'
if ':' in parameter_string:
name, pattern = parameter_string.split(':', 1)
pattern = "string"
if ":" in parameter_string:
name, pattern = parameter_string.split(":", 1)
if not name:
raise ValueError(
"Invalid parameter syntax: {}".format(parameter_string)
@@ -108,8 +118,16 @@ class Router:
return name, _type, pattern
def add(self, uri, methods, handler, host=None, strict_slashes=False,
version=None, name=None):
def add(
self,
uri,
methods,
handler,
host=None,
strict_slashes=False,
version=None,
name=None,
):
"""Add a handler to the route list
:param uri: path to match
@@ -123,8 +141,8 @@ class Router:
:return: Nothing
"""
if version is not None:
version = re.escape(str(version).strip('/').lstrip('v'))
uri = "/".join(["/v{}".format(version), uri.lstrip('/')])
version = re.escape(str(version).strip("/").lstrip("v"))
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
# add regular version
self._add(uri, methods, handler, host, name)
@@ -139,28 +157,26 @@ class Router:
return
# Add versions with and without trailing /
slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
unslashed_methods = self.routes_all.get(uri[:-1], frozenset({}))
if isinstance(methods, Iterable):
_slash_is_missing = all(method in slashed_methods for
method in methods)
_without_slash_is_missing = all(method in unslashed_methods for
method in methods)
_slash_is_missing = all(
method in slashed_methods for method in methods
)
_without_slash_is_missing = all(
method in unslashed_methods for method in methods
)
else:
_slash_is_missing = methods in slashed_methods
_without_slash_is_missing = methods in unslashed_methods
slash_is_missing = (
not uri[-1] == '/' and not _slash_is_missing
)
slash_is_missing = not uri[-1] == "/" and not _slash_is_missing
without_slash_is_missing = (
uri[-1] == '/' and not
_without_slash_is_missing and not
uri == '/'
uri[-1] == "/" and not _without_slash_is_missing and not uri == "/"
)
# add version with trailing slash
if slash_is_missing:
self._add(uri + '/', methods, handler, host, name)
self._add(uri + "/", methods, handler, host, name)
# add version without trailing slash
elif without_slash_is_missing:
self._add(uri[:-1], methods, handler, host, name)
@@ -183,8 +199,10 @@ class Router:
else:
if not isinstance(host, Iterable):
raise ValueError("Expected either string or Iterable of "
"host strings, not {!r}".format(host))
raise ValueError(
"Expected either string or Iterable of "
"host strings, not {!r}".format(host)
)
for host_ in host:
self.add(uri, methods, handler, host_, name)
@@ -195,40 +213,48 @@ class Router:
methods = frozenset(methods)
parameters = []
parameter_names = set()
properties = {"unhashable": None}
def add_parameter(match):
name = match.group(1)
name, _type, pattern = self.parse_parameter_string(name)
parameter = Parameter(
name=name, cast=_type)
if name in parameter_names:
raise ParameterNameConflicts(
"Multiple parameter named <{name}> "
"in route uri {uri}".format(name=name, uri=uri)
)
parameter_names.add(name)
parameter = Parameter(name=name, cast=_type)
parameters.append(parameter)
# Mark the whole route as unhashable if it has the hash key in it
if re.search(r'(^|[^^]){1}/', pattern):
properties['unhashable'] = True
if re.search(r"(^|[^^]){1}/", pattern):
properties["unhashable"] = True
# Mark the route as unhashable if it matches the hash key
elif re.search(r'/', pattern):
properties['unhashable'] = True
elif re.search(r"/", pattern):
properties["unhashable"] = True
return '({})'.format(pattern)
return "({})".format(pattern)
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
pattern = re.compile(r'^{}$'.format(pattern_string))
pattern = re.compile(r"^{}$".format(pattern_string))
def merge_route(route, methods, handler):
# merge to the existing route when possible.
if not route.methods or not methods:
# method-unspecified routes are not mergeable.
raise RouteExists(
"Route already registered: {}".format(uri))
raise RouteExists("Route already registered: {}".format(uri))
elif route.methods.intersection(methods):
# already existing method is not overloadable.
duplicated = methods.intersection(route.methods)
raise RouteExists(
"Route already registered: {} [{}]".format(
uri, ','.join(list(duplicated))))
uri, ",".join(list(duplicated))
)
)
if isinstance(route.handler, CompositionView):
view = route.handler
else:
@@ -236,19 +262,22 @@ class Router:
view.add(route.methods, route.handler)
view.add(methods, handler)
route = route._replace(
handler=view, methods=methods.union(route.methods))
handler=view, methods=methods.union(route.methods)
)
return route
if parameters:
# TODO: This is too complex, we need to reduce the complexity
if properties['unhashable']:
if properties["unhashable"]:
routes_to_check = self.routes_always_check
ndx, route = self.check_dynamic_route_exists(
pattern, routes_to_check, parameters)
pattern, routes_to_check, parameters
)
else:
routes_to_check = self.routes_dynamic[url_hash(uri)]
ndx, route = self.check_dynamic_route_exists(
pattern, routes_to_check, parameters)
pattern, routes_to_check, parameters
)
if ndx != -1:
# Pop the ndx of the route, no dups of the same route
routes_to_check.pop(ndx)
@@ -259,35 +288,41 @@ class Router:
# if available
# special prefix for static files
is_static = False
if name and name.startswith('_static_'):
if name and name.startswith("_static_"):
is_static = True
name = name.split('_static_', 1)[-1]
name = name.split("_static_", 1)[-1]
if hasattr(handler, '__blueprintname__'):
handler_name = '{}.{}'.format(
handler.__blueprintname__, name or handler.__name__)
if hasattr(handler, "__blueprintname__"):
handler_name = "{}.{}".format(
handler.__blueprintname__, name or handler.__name__
)
else:
handler_name = name or getattr(handler, '__name__', None)
handler_name = name or getattr(handler, "__name__", None)
if route:
route = merge_route(route, methods, handler)
else:
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters, name=handler_name, uri=uri)
handler=handler,
methods=methods,
pattern=pattern,
parameters=parameters,
name=handler_name,
uri=uri,
)
self.routes_all[uri] = route
if is_static:
pair = self.routes_static_files.get(handler_name)
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
self.routes_static_files[handler_name] = (uri, route)
else:
pair = self.routes_names.get(handler_name)
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
self.routes_names[handler_name] = (uri, route)
if properties['unhashable']:
if properties["unhashable"]:
self.routes_always_check.append(route)
elif parameters:
self.routes_dynamic[url_hash(uri)].append(route)
@@ -296,6 +331,17 @@ class Router:
@staticmethod
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
"""
Check if a URL pattern exists in a list of routes provided based on
the comparison of URL pattern and the parameters.
:param pattern: URL parameter pattern
:param routes_to_check: list of dynamic routes either hashable or
unhashable routes.
:param parameters: List of :class:`Parameter` items
:return: Tuple of index and route if matching route exists else
-1 for index and None for route
"""
for ndx, route in enumerate(routes_to_check):
if route.pattern == pattern and route.parameters == parameters:
return ndx, route
@@ -322,8 +368,10 @@ class Router:
if route in self.routes_always_check:
self.routes_always_check.remove(route)
elif url_hash(uri) in self.routes_dynamic \
and route in self.routes_dynamic[url_hash(uri)]:
elif (
url_hash(uri) in self.routes_dynamic
and route in self.routes_dynamic[url_hash(uri)]
):
self.routes_dynamic[url_hash(uri)].remove(route)
else:
self.routes_static.pop(uri)
@@ -342,7 +390,7 @@ class Router:
if not view_name:
return (None, None)
if view_name == 'static' or view_name.endswith('.static'):
if view_name == "static" or view_name.endswith(".static"):
return self.routes_static_files.get(name, (None, None))
return self.routes_names.get(view_name, (None, None))
@@ -356,14 +404,15 @@ class Router:
"""
# No virtual hosts specified; default behavior
if not self.hosts:
return self._get(request.path, request.method, '')
return self._get(request.path, request.method, "")
# virtual hosts specified; try to match route to the host header
try:
return self._get(request.path, request.method,
request.headers.get("Host", ''))
return self._get(
request.path, request.method, request.headers.get("Host", "")
)
# try default hosts
except NotFound:
return self._get(request.path, request.method, '')
return self._get(request.path, request.method, "")
def get_supported_methods(self, url):
"""Get a list of supported methods for a url and optional host.
@@ -373,7 +422,7 @@ class Router:
"""
route = self.routes_all.get(url)
# if methods are None then this logic will prevent an error
return getattr(route, 'methods', None) or frozenset()
return getattr(route, "methods", None) or frozenset()
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method, host):
@@ -388,9 +437,10 @@ class Router:
# Check against known static routes
route = self.routes_static.get(url)
method_not_supported = MethodNotSupported(
'Method {} not allowed for URL {}'.format(method, url),
"Method {} not allowed for URL {}".format(method, url),
method=method,
allowed_methods=self.get_supported_methods(url))
allowed_methods=self.get_supported_methods(url),
)
if route:
if route.methods and method not in route.methods:
raise method_not_supported
@@ -416,13 +466,14 @@ class Router:
# Route was found but the methods didn't match
if route_found:
raise method_not_supported
raise NotFound('Requested URL {} not found'.format(url))
raise NotFound("Requested URL {} not found".format(url))
kwargs = {p.name: p.cast(value)
for value, p
in zip(match.groups(1), route.parameters)}
kwargs = {
p.name: p.cast(value)
for value, p in zip(match.groups(1), route.parameters)
}
route_handler = route.handler
if hasattr(route_handler, 'handlers'):
if hasattr(route_handler, "handlers"):
route_handler = route_handler.handlers[method]
return route_handler, [], kwargs, route.uri
@@ -435,7 +486,8 @@ class Router:
handler = self.get(request)[0]
except (NotFound, MethodNotSupported):
return False
if (hasattr(handler, 'view_class') and
hasattr(handler.view_class, request.method.lower())):
if hasattr(handler, "view_class") and hasattr(
handler.view_class, request.method.lower()
):
handler = getattr(handler.view_class, request.method.lower())
return hasattr(handler, 'is_stream')
return hasattr(handler, "is_stream")

View File

@@ -1,69 +1,111 @@
import asyncio
import os
import traceback
from functools import partial
from inspect import isawaitable
from multiprocessing import Process
from signal import (
SIGTERM, SIGINT, SIG_IGN,
signal as signal_func,
Signals
)
from socket import (
socket,
SOL_SOCKET,
SO_REUSEADDR,
)
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from socket import SO_REUSEADDR, SOL_SOCKET, socket
from time import time
from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
from multidict import CIMultiDict
from sanic.exceptions import (
InvalidUsage,
PayloadTooLarge,
RequestTimeout,
ServerError,
ServiceUnavailable,
)
from sanic.log import access_logger, logger
from sanic.request import Request, StreamBuffer
from sanic.response import HTTPResponse
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
from sanic.log import logger, access_logger
from sanic.response import HTTPResponse
from sanic.request import Request
from sanic.exceptions import (
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError,
ServiceUnavailable)
current_time = None
class Signal:
stopped = False
class HttpProtocol(asyncio.Protocol):
"""
This class provides a basic HTTP implementation of the sanic framework.
"""
__slots__ = (
# event loop, connection
'loop', 'transport', 'connections', 'signal',
"loop",
"transport",
"connections",
"signal",
# request params
'parser', 'request', 'url', 'headers',
"parser",
"request",
"url",
"headers",
# request config
'request_handler', 'request_timeout', 'response_timeout',
'keep_alive_timeout', 'request_max_size', 'request_class',
'is_request_stream', 'router',
"request_handler",
"request_timeout",
"response_timeout",
"keep_alive_timeout",
"request_max_size",
"request_buffer_queue_size",
"request_class",
"is_request_stream",
"router",
"error_handler",
# enable or disable access log purpose
'access_log',
"access_log",
# connection management
'_total_request_size', '_request_timeout_handler',
'_response_timeout_handler', '_keep_alive_timeout_handler',
'_last_request_time', '_last_response_time', '_is_stream_handler',
'_not_paused')
"_total_request_size",
"_request_timeout_handler",
"_response_timeout_handler",
"_keep_alive_timeout_handler",
"_last_request_time",
"_last_response_time",
"_is_stream_handler",
"_not_paused",
"_request_handler_task",
"_request_stream_task",
"_keep_alive",
"_header_fragment",
"state",
"_debug",
)
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections=set(), request_timeout=60,
response_timeout=60, keep_alive_timeout=5,
request_max_size=None, request_class=None, access_log=True,
keep_alive=True, is_request_stream=False, router=None,
state=None, debug=False, **kwargs):
def __init__(
self,
*,
loop,
request_handler,
error_handler,
signal=Signal(),
connections=None,
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
request_max_size=None,
request_buffer_queue_size=100,
request_class=None,
access_log=True,
keep_alive=True,
is_request_stream=False,
router=None,
state=None,
debug=False,
**kwargs
):
self.loop = loop
self.transport = None
self.request = None
@@ -73,10 +115,11 @@ class HttpProtocol(asyncio.Protocol):
self.router = router
self.signal = signal
self.access_log = access_log
self.connections = connections
self.connections = connections or set()
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
self.request_buffer_queue_size = request_buffer_queue_size
self.response_timeout = response_timeout
self.keep_alive_timeout = keep_alive_timeout
self.request_max_size = request_max_size
@@ -93,19 +136,27 @@ class HttpProtocol(asyncio.Protocol):
self._request_handler_task = None
self._request_stream_task = None
self._keep_alive = keep_alive
self._header_fragment = b''
self._header_fragment = b""
self.state = state if state else {}
if 'requests_count' not in self.state:
self.state['requests_count'] = 0
if "requests_count" not in self.state:
self.state["requests_count"] = 0
self._debug = debug
self._not_paused.set()
@property
def keep_alive(self):
"""
Check if the connection needs to be kept alive based on the params
attached to the `_keep_alive` attribute, :attr:`Signal.stopped`
and :func:`HttpProtocol.parser.should_keep_alive`
:return: ``True`` if connection is to be kept alive ``False`` else
"""
return (
self._keep_alive and
not self.signal.stopped and
self.parser.should_keep_alive())
self._keep_alive
and not self.signal.stopped
and self.parser.should_keep_alive()
)
# -------------------------------------------- #
# Connection
@@ -114,12 +165,17 @@ class HttpProtocol(asyncio.Protocol):
def connection_made(self, transport):
self.connections.add(self)
self._request_timeout_handler = self.loop.call_later(
self.request_timeout, self.request_timeout_callback)
self.request_timeout, self.request_timeout_callback
)
self.transport = transport
self._last_request_time = current_time
self._last_request_time = time()
def connection_lost(self, exc):
self.connections.discard(self)
if self._request_handler_task:
self._request_handler_task.cancel()
if self._request_stream_task:
self._request_stream_task.cancel()
if self._request_timeout_handler:
self._request_timeout_handler.cancel()
if self._response_timeout_handler:
@@ -138,55 +194,51 @@ class HttpProtocol(asyncio.Protocol):
# exactly what this timeout is checking for.
# Check if elapsed time since request initiated exceeds our
# configured maximum request timeout value
time_elapsed = current_time - self._last_request_time
time_elapsed = time() - self._last_request_time
if time_elapsed < self.request_timeout:
time_left = self.request_timeout - time_elapsed
self._request_timeout_handler = (
self.loop.call_later(time_left,
self.request_timeout_callback)
self._request_timeout_handler = self.loop.call_later(
time_left, self.request_timeout_callback
)
else:
if self._request_stream_task:
self._request_stream_task.cancel()
if self._request_handler_task:
self._request_handler_task.cancel()
try:
raise RequestTimeout('Request Timeout')
except RequestTimeout as exception:
self.write_error(exception)
self.write_error(RequestTimeout("Request Timeout"))
def response_timeout_callback(self):
# Check if elapsed time since response was initiated exceeds our
# configured maximum request timeout value
time_elapsed = current_time - self._last_request_time
time_elapsed = time() - self._last_request_time
if time_elapsed < self.response_timeout:
time_left = self.response_timeout - time_elapsed
self._response_timeout_handler = (
self.loop.call_later(time_left,
self.response_timeout_callback)
self._response_timeout_handler = self.loop.call_later(
time_left, self.response_timeout_callback
)
else:
if self._request_stream_task:
self._request_stream_task.cancel()
if self._request_handler_task:
self._request_handler_task.cancel()
try:
raise ServiceUnavailable('Response Timeout')
except ServiceUnavailable as exception:
self.write_error(exception)
self.write_error(ServiceUnavailable("Response Timeout"))
def keep_alive_timeout_callback(self):
# Check if elapsed time since last response exceeds our configured
# maximum keep alive timeout value
time_elapsed = current_time - self._last_response_time
"""
Check if elapsed time since last response exceeds our configured
maximum keep alive timeout value and if so, close the transport
pipe and let the response writer handle the error.
:return: None
"""
time_elapsed = time() - self._last_response_time
if time_elapsed < self.keep_alive_timeout:
time_left = self.keep_alive_timeout - time_elapsed
self._keep_alive_timeout_handler = (
self.loop.call_later(time_left,
self.keep_alive_timeout_callback)
self._keep_alive_timeout_handler = self.loop.call_later(
time_left, self.keep_alive_timeout_callback
)
else:
logger.debug('KeepAlive Timeout. Closing connection.')
logger.debug("KeepAlive Timeout. Closing connection.")
self.transport.close()
self.transport = None
@@ -199,8 +251,7 @@ class HttpProtocol(asyncio.Protocol):
# memory limits
self._total_request_size += len(data)
if self._total_request_size > self.request_max_size:
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
self.write_error(PayloadTooLarge("Payload Too Large"))
# Create parser if this is the first time we're receiving data
if self.parser is None:
@@ -209,17 +260,16 @@ class HttpProtocol(asyncio.Protocol):
self.parser = HttpRequestParser(self)
# requests count
self.state['requests_count'] = self.state['requests_count'] + 1
self.state["requests_count"] = self.state["requests_count"] + 1
# Parse request chunk or close connection
try:
self.parser.feed_data(data)
except HttpParserError:
message = 'Bad Request'
message = "Bad Request"
if self._debug:
message += '\n' + traceback.format_exc()
exception = InvalidUsage(message)
self.write_error(exception)
message += "\n" + traceback.format_exc()
self.write_error(InvalidUsage(message))
def on_url(self, url):
if not self.url:
@@ -231,18 +281,20 @@ class HttpProtocol(asyncio.Protocol):
self._header_fragment += name
if value is not None:
if self._header_fragment == b'Content-Length' \
and int(value) > self.request_max_size:
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
if (
self._header_fragment == b"Content-Length"
and int(value) > self.request_max_size
):
self.write_error(PayloadTooLarge("Payload Too Large"))
try:
value = value.decode()
except UnicodeDecodeError:
value = value.decode('latin_1')
value = value.decode("latin_1")
self.headers.append(
(self._header_fragment.decode().casefold(), value))
(self._header_fragment.decode().casefold(), value)
)
self._header_fragment = b''
self._header_fragment = b""
def on_headers_complete(self):
self.request = self.request_class(
@@ -250,7 +302,7 @@ class HttpProtocol(asyncio.Protocol):
headers=CIMultiDict(self.headers),
version=self.parser.get_http_version(),
method=self.parser.get_method().decode(),
transport=self.transport
transport=self.transport,
)
# Remove any existing KeepAlive handler here,
# It will be recreated if required on the new request.
@@ -259,17 +311,29 @@ class HttpProtocol(asyncio.Protocol):
self._keep_alive_timeout_handler = None
if self.is_request_stream:
self._is_stream_handler = self.router.is_stream_handler(
self.request)
self.request
)
if self._is_stream_handler:
self.request.stream = asyncio.Queue()
self.request.stream = StreamBuffer(
self.request_buffer_queue_size
)
self.execute_request_handler()
def on_body(self, body):
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
self.request.stream.put(body))
return
self.request.body.append(body)
self.body_append(body)
)
else:
self.request.body_push(body)
async def body_append(self, body):
if self.request.stream.is_full():
self.transport.pause_reading()
await self.request.stream.put(body)
self.transport.resume_reading()
else:
await self.request.stream.put(body)
def on_message_complete(self):
# Entire request (headers and whole body) is received.
@@ -279,47 +343,66 @@ class HttpProtocol(asyncio.Protocol):
self._request_timeout_handler = None
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
self.request.stream.put(None))
self.request.stream.put(None)
)
return
self.request.body = b''.join(self.request.body)
self.request.body_finish()
self.execute_request_handler()
def execute_request_handler(self):
"""
Invoke the request handler defined by the
:func:`sanic.app.Sanic.handle_request` method
:return: None
"""
self._response_timeout_handler = self.loop.call_later(
self.response_timeout, self.response_timeout_callback)
self._last_request_time = current_time
self.response_timeout, self.response_timeout_callback
)
self._last_request_time = time()
self._request_handler_task = self.loop.create_task(
self.request_handler(
self.request,
self.write_response,
self.stream_response))
self.request, self.write_response, self.stream_response
)
)
# -------------------------------------------- #
# Responding
# -------------------------------------------- #
def log_response(self, response):
"""
Helper method provided to enable the logging of responses in case if
the :attr:`HttpProtocol.access_log` is enabled.
:param response: Response generated for the current request
:type response: :class:`sanic.response.HTTPResponse` or
:class:`sanic.response.StreamingHTTPResponse`
:return: None
"""
if self.access_log:
extra = {
'status': getattr(response, 'status', 0),
}
extra = {"status": getattr(response, "status", 0)}
if isinstance(response, HTTPResponse):
extra['byte'] = len(response.body)
extra["byte"] = len(response.body)
else:
extra['byte'] = -1
extra["byte"] = -1
extra['host'] = 'UNKNOWN'
extra["host"] = "UNKNOWN"
if self.request is not None:
if self.request.ip:
extra['host'] = '{0}:{1}'.format(self.request.ip,
self.request.port)
extra["host"] = "{0}:{1}".format(
self.request.ip, self.request.port
)
extra['request'] = '{0} {1}'.format(self.request.method,
self.request.url)
extra["request"] = "{0} {1}".format(
self.request.method, self.request.url
)
else:
extra['request'] = 'nil'
extra["request"] = "nil"
access_logger.info('', extra=extra)
access_logger.info("", extra=extra)
def write_response(self, response):
"""
@@ -332,32 +415,38 @@ class HttpProtocol(asyncio.Protocol):
keep_alive = self.keep_alive
self.transport.write(
response.output(
self.request.version, keep_alive,
self.keep_alive_timeout))
self.request.version, keep_alive, self.keep_alive_timeout
)
)
self.log_response(response)
except AttributeError:
logger.error('Invalid response object for url %s, '
'Expected Type: HTTPResponse, Actual Type: %s',
self.url, type(response))
self.write_error(ServerError('Invalid response type'))
logger.error(
"Invalid response object for url %s, "
"Expected Type: HTTPResponse, Actual Type: %s",
self.url,
type(response),
)
self.write_error(ServerError("Invalid response type"))
except RuntimeError:
if self._debug:
logger.error('Connection lost before response written @ %s',
self.request.ip)
logger.error(
"Connection lost before response written @ %s",
self.request.ip,
)
keep_alive = False
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(
repr(e)))
"Writing response failed, connection closed {}".format(repr(e))
)
finally:
if not keep_alive:
self.transport.close()
self.transport = None
else:
self._keep_alive_timeout_handler = self.loop.call_later(
self.keep_alive_timeout,
self.keep_alive_timeout_callback)
self._last_response_time = current_time
self.keep_alive_timeout, self.keep_alive_timeout_callback
)
self._last_response_time = time()
self.cleanup()
async def drain(self):
@@ -380,31 +469,37 @@ class HttpProtocol(asyncio.Protocol):
keep_alive = self.keep_alive
response.protocol = self
await response.stream(
self.request.version, keep_alive, self.keep_alive_timeout)
self.request.version, keep_alive, self.keep_alive_timeout
)
self.log_response(response)
except AttributeError:
logger.error('Invalid response object for url %s, '
'Expected Type: HTTPResponse, Actual Type: %s',
self.url, type(response))
self.write_error(ServerError('Invalid response type'))
logger.error(
"Invalid response object for url %s, "
"Expected Type: HTTPResponse, Actual Type: %s",
self.url,
type(response),
)
self.write_error(ServerError("Invalid response type"))
except RuntimeError:
if self._debug:
logger.error('Connection lost before response written @ %s',
self.request.ip)
logger.error(
"Connection lost before response written @ %s",
self.request.ip,
)
keep_alive = False
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(
repr(e)))
"Writing response failed, connection closed {}".format(repr(e))
)
finally:
if not keep_alive:
self.transport.close()
self.transport = None
else:
self._keep_alive_timeout_handler = self.loop.call_later(
self.keep_alive_timeout,
self.keep_alive_timeout_callback)
self._last_response_time = current_time
self.keep_alive_timeout, self.keep_alive_timeout_callback
)
self._last_response_time = time()
self.cleanup()
def write_error(self, exception):
@@ -416,35 +511,53 @@ class HttpProtocol(asyncio.Protocol):
response = None
try:
response = self.error_handler.response(self.request, exception)
version = self.request.version if self.request else '1.1'
version = self.request.version if self.request else "1.1"
self.transport.write(response.output(version))
except RuntimeError:
if self._debug:
logger.error('Connection lost before error written @ %s',
self.request.ip if self.request else 'Unknown')
logger.error(
"Connection lost before error written @ %s",
self.request.ip if self.request else "Unknown",
)
except Exception as e:
self.bail_out(
"Writing error failed, connection closed {}".format(
repr(e)), from_error=True
"Writing error failed, connection closed {}".format(repr(e)),
from_error=True,
)
finally:
if self.parser and (self.keep_alive
or getattr(response, 'status', 0) == 408):
if self.parser and (
self.keep_alive or getattr(response, "status", 0) == 408
):
self.log_response(response)
try:
self.transport.close()
except AttributeError as e:
logger.debug('Connection lost before server could close it.')
except AttributeError:
logger.debug("Connection lost before server could close it.")
def bail_out(self, message, from_error=False):
"""
In case if the transport pipes are closed and the sanic app encounters
an error while writing data to the transport pipe, we log the error
with proper details.
:param message: Error message to display
:param from_error: If the bail out was invoked while handling an
exception scenario.
:type message: str
:type from_error: bool
:return: None
"""
if from_error or self.transport.is_closing():
logger.error("Transport closed @ %s and exception "
"experienced during error handling",
self.transport.get_extra_info('peername'))
logger.debug('Exception:\n%s', traceback.format_exc())
logger.error(
"Transport closed @ %s and exception "
"experienced during error handling",
self.transport.get_extra_info("peername"),
)
logger.debug("Exception:", exc_info=True)
else:
exception = ServerError(message)
self.write_error(exception)
self.write_error(ServerError(message))
logger.error(message)
def cleanup(self):
@@ -479,18 +592,6 @@ class HttpProtocol(asyncio.Protocol):
self.transport = None
def update_current_time(loop):
"""Cache the current time, since it is needed at the end of every
keep-alive request to update the request timeout time
:param loop:
:return:
"""
global current_time
current_time = time()
loop.call_later(1, partial(update_current_time, loop))
def trigger_events(events, loop):
"""Trigger event callbacks (functions or async)
@@ -503,17 +604,45 @@ def trigger_events(events, loop):
loop.run_until_complete(result)
def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, debug=False,
request_timeout=60, response_timeout=60, keep_alive_timeout=5,
ssl=None, sock=None, request_max_size=None, reuse_port=False,
loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_multiple=False, run_async=False,
connections=None, signal=Signal(), request_class=None,
access_log=True, keep_alive=True, is_request_stream=False,
router=None, websocket_max_size=None, websocket_max_queue=None,
websocket_read_limit=2 ** 16, websocket_write_limit=2 ** 16,
state=None, graceful_shutdown_timeout=15.0):
def serve(
host,
port,
request_handler,
error_handler,
before_start=None,
after_start=None,
before_stop=None,
after_stop=None,
debug=False,
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
ssl=None,
sock=None,
request_max_size=None,
request_buffer_queue_size=100,
reuse_port=False,
loop=None,
protocol=HttpProtocol,
backlog=100,
register_sys_signals=True,
run_multiple=False,
run_async=False,
connections=None,
signal=Signal(),
request_class=None,
access_log=True,
keep_alive=True,
is_request_stream=False,
router=None,
websocket_max_size=None,
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16,
state=None,
graceful_shutdown_timeout=15.0,
asyncio_server_kwargs=None,
):
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
@@ -553,7 +682,12 @@ def serve(host, port, request_handler, error_handler, before_start=None,
outgoing bytes, the low-water limit is a
quarter of the high-water limit.
:param is_request_stream: disable/enable Request.stream
:param request_buffer_queue_size: streaming request buffer queue size
:param router: Router object
:param graceful_shutdown_timeout: How long take to Force close non-idle
connection
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
create_server method
:return: Nothing
"""
if not run_async:
@@ -588,7 +722,9 @@ def serve(host, port, request_handler, error_handler, before_start=None,
state=state,
debug=debug,
)
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
)
server_coroutine = loop.create_server(
server,
host,
@@ -596,13 +732,10 @@ def serve(host, port, request_handler, error_handler, before_start=None,
ssl=ssl,
reuse_port=reuse_port,
sock=sock,
backlog=backlog
backlog=backlog,
**asyncio_server_kwargs
)
# Instead of pulling time at the end of every request,
# pull it once per minute
loop.call_soon(partial(update_current_time, loop))
if run_async:
return server_coroutine
@@ -627,11 +760,13 @@ def serve(host, port, request_handler, error_handler, before_start=None,
try:
loop.add_signal_handler(_signal, loop.stop)
except NotImplementedError:
logger.warning('Sanic tried to use loop.add_signal_handler '
'but it is not implemented on this platform.')
logger.warning(
"Sanic tried to use loop.add_signal_handler "
"but it is not implemented on this platform."
)
pid = os.getpid()
try:
logger.info('Starting worker [%s]', pid)
logger.info("Starting worker [%s]", pid)
loop.run_forever()
finally:
logger.info("Stopping worker [%s]", pid)
@@ -662,9 +797,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
coros = []
for conn in connections:
if hasattr(conn, "websocket") and conn.websocket:
coros.append(
conn.websocket.close_connection()
)
coros.append(conn.websocket.close_connection())
else:
conn.close()
@@ -685,18 +818,18 @@ def serve_multiple(server_settings, workers):
:param stop_event: if provided, is used as a stop signal
:return:
"""
server_settings['reuse_port'] = True
server_settings['run_multiple'] = True
server_settings["reuse_port"] = True
server_settings["run_multiple"] = True
# Handling when custom socket is not provided.
if server_settings.get('sock') is None:
if server_settings.get("sock") is None:
sock = socket()
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind((server_settings['host'], server_settings['port']))
sock.bind((server_settings["host"], server_settings["port"]))
sock.set_inheritable(True)
server_settings['sock'] = sock
server_settings['host'] = None
server_settings['port'] = None
server_settings["sock"] = sock
server_settings["host"] = None
server_settings["port"] = None
def sig_handler(signal, frame):
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
@@ -720,4 +853,4 @@ def serve_multiple(server_settings, workers):
# the above processes will block this until they're stopped
for process in processes:
process.terminate()
server_settings.get('sock').close()
server_settings.get("sock").close()

View File

@@ -1,7 +1,7 @@
from mimetypes import guess_type
from os import path
from re import sub
from time import strftime, gmtime
from time import gmtime, strftime
from urllib.parse import unquote
from aiofiles.os import stat
@@ -13,13 +13,22 @@ from sanic.exceptions import (
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler
from sanic.response import file, file_stream, HTTPResponse
from sanic.response import HTTPResponse, file, file_stream
def register(app, uri, file_or_directory, pattern,
use_modified_since, use_content_range,
stream_large_files, name='static', host=None,
strict_slashes=None, content_type=None):
def register(
app,
uri,
file_or_directory,
pattern,
use_modified_since,
use_content_range,
stream_large_files,
name="static",
host=None,
strict_slashes=None,
content_type=None,
):
# TODO: Though sanic is not a file server, I feel like we should at least
# make a good effort here. Modified-since is nice, but we could
# also look into etags, expires, and caching
@@ -46,12 +55,12 @@ def register(app, uri, file_or_directory, pattern,
# If we're not trying to match a file directly,
# serve from the folder
if not path.isfile(file_or_directory):
uri += '<file_uri:' + pattern + '>'
uri += "<file_uri:" + pattern + ">"
async def _handler(request, file_uri=None):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if file_uri and '../' in file_uri:
if file_uri and "../" in file_uri:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
@@ -59,15 +68,16 @@ def register(app, uri, file_or_directory, pattern,
root_path = file_path = file_or_directory
if file_uri:
file_path = path.join(
file_or_directory, sub('^[/]*', '', file_uri))
file_or_directory, sub("^[/]*", "", file_uri)
)
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))):
raise FileNotFound('File not found',
path=file_or_directory,
relative_url=file_uri)
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
try:
headers = {}
# Check if the client has been sent this file before
@@ -76,33 +86,35 @@ def register(app, uri, file_or_directory, pattern,
if use_modified_since:
stats = await stat(file_path)
modified_since = strftime(
'%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
if request.headers.get('If-Modified-Since') == modified_since:
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
)
if request.headers.get("If-Modified-Since") == modified_since:
return HTTPResponse(status=304)
headers['Last-Modified'] = modified_since
headers["Last-Modified"] = modified_since
_range = None
if use_content_range:
_range = None
if not stats:
stats = await stat(file_path)
headers['Accept-Ranges'] = 'bytes'
headers['Content-Length'] = str(stats.st_size)
if request.method != 'HEAD':
headers["Accept-Ranges"] = "bytes"
headers["Content-Length"] = str(stats.st_size)
if request.method != "HEAD":
try:
_range = ContentRangeHandler(request, stats)
except HeaderNotFound:
pass
else:
del headers['Content-Length']
del headers["Content-Length"]
for key, value in _range.headers.items():
headers[key] = value
headers['Content-Type'] = content_type \
or guess_type(file_path)[0] or 'text/plain'
if request.method == 'HEAD':
headers["Content-Type"] = (
content_type or guess_type(file_path)[0] or "text/plain"
)
if request.method == "HEAD":
return HTTPResponse(headers=headers)
else:
if stream_large_files:
if isinstance(stream_large_files, int):
if type(stream_large_files) == int:
threshold = stream_large_files
else:
threshold = 1024 * 1024
@@ -110,19 +122,25 @@ def register(app, uri, file_or_directory, pattern,
if not stats:
stats = await stat(file_path)
if stats.st_size >= threshold:
return await file_stream(file_path, headers=headers,
_range=_range)
return await file_stream(
file_path, headers=headers, _range=_range
)
return await file(file_path, headers=headers, _range=_range)
except ContentRangeError:
raise
except Exception:
raise FileNotFound('File not found',
path=file_or_directory,
relative_url=file_uri)
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
# special prefix for static files
if not name.startswith('_static_'):
name = '_static_{}'.format(name)
if not name.startswith("_static_"):
name = "_static_{}".format(name)
app.route(uri, methods=['GET', 'HEAD'], name=name, host=host,
strict_slashes=strict_slashes)(_handler)
app.route(
uri,
methods=["GET", "HEAD"],
name=name,
host=host,
strict_slashes=strict_slashes,
)(_handler)

View File

@@ -1,85 +1,109 @@
import traceback
from json import JSONDecodeError
from sanic.log import logger
from socket import socket
from sanic.exceptions import MethodNotSupported
from sanic.log import logger
from sanic.response import text
HOST = '127.0.0.1'
HOST = "127.0.0.1"
PORT = 42101
class SanicTestClient:
def __init__(self, app, port=PORT):
"""Use port=None to bind to a random port"""
self.app = app
self.port = port
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
async def _local_request(self, method, url, cookies=None, *args, **kwargs):
import aiohttp
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=self.port, uri=uri)
logger.info(url)
conn = aiohttp.TCPConnector(verify_ssl=False)
conn = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(
cookies=cookies, connector=conn) as session:
async with getattr(
session, method.lower())(url, *args, **kwargs) as response:
cookies=cookies, connector=conn
) as session:
async with getattr(session, method.lower())(
url, *args, **kwargs
) as response:
try:
response.text = await response.text()
except UnicodeDecodeError as e:
except UnicodeDecodeError:
response.text = None
try:
response.json = await response.json()
except (JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError):
except (
JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError,
):
response.json = None
response.body = await response.read()
return response
def _sanic_endpoint_test(
self, method='get', uri='/', gather_request=True,
debug=False, server_kwargs={"auto_reload": False},
*request_args, **request_kwargs):
self,
method="get",
uri="/",
gather_request=True,
debug=False,
server_kwargs={"auto_reload": False},
*request_args,
**request_kwargs
):
results = [None, None]
exceptions = []
if gather_request:
def _collect_request(request):
if results[0] is None:
results[0] = request
self.app.request_middleware.appendleft(_collect_request)
@self.app.exception(MethodNotSupported)
async def error_handler(request, exception):
if request.method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
if request.method in ["HEAD", "PATCH", "PUT", "DELETE"]:
return text(
'', exception.status_code, headers=exception.headers
"", exception.status_code, headers=exception.headers
)
else:
return self.app.error_handler.default(request, exception)
@self.app.listener('after_server_start')
if self.port:
server_kwargs = dict(host=HOST, port=self.port, **server_kwargs)
host, port = HOST, self.port
else:
sock = socket()
sock.bind((HOST, 0))
server_kwargs = dict(sock=sock, **server_kwargs)
host, port = sock.getsockname()
if uri.startswith(("http:", "https:", "ftp:", "ftps://", "//")):
url = uri
else:
url = "http://{host}:{port}{uri}".format(
host=host, port=port, uri=uri
)
@self.app.listener("after_server_start")
async def _collect_response(sanic, loop):
try:
response = await self._local_request(
method, uri, *request_args,
**request_kwargs)
method, url, *request_args, **request_kwargs
)
results[-1] = response
except Exception as e:
logger.error(
'Exception:\n{}'.format(traceback.format_exc()))
logger.exception("Exception")
exceptions.append(e)
self.app.stop()
self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
self.app.listeners['after_server_start'].pop()
self.app.run(debug=debug, **server_kwargs)
self.app.listeners["after_server_start"].pop()
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
@@ -91,31 +115,34 @@ class SanicTestClient:
except BaseException:
raise ValueError(
"Request and response object expected, got ({})".format(
results))
results
)
)
else:
try:
return results[-1]
except BaseException:
raise ValueError(
"Request object expected, got ({})".format(results))
"Request object expected, got ({})".format(results)
)
def get(self, *args, **kwargs):
return self._sanic_endpoint_test('get', *args, **kwargs)
return self._sanic_endpoint_test("get", *args, **kwargs)
def post(self, *args, **kwargs):
return self._sanic_endpoint_test('post', *args, **kwargs)
return self._sanic_endpoint_test("post", *args, **kwargs)
def put(self, *args, **kwargs):
return self._sanic_endpoint_test('put', *args, **kwargs)
return self._sanic_endpoint_test("put", *args, **kwargs)
def delete(self, *args, **kwargs):
return self._sanic_endpoint_test('delete', *args, **kwargs)
return self._sanic_endpoint_test("delete", *args, **kwargs)
def patch(self, *args, **kwargs):
return self._sanic_endpoint_test('patch', *args, **kwargs)
return self._sanic_endpoint_test("patch", *args, **kwargs)
def options(self, *args, **kwargs):
return self._sanic_endpoint_test('options', *args, **kwargs)
return self._sanic_endpoint_test("options", *args, **kwargs)
def head(self, *args, **kwargs):
return self._sanic_endpoint_test('head', *args, **kwargs)
return self._sanic_endpoint_test("head", *args, **kwargs)

View File

@@ -1,5 +1,5 @@
from sanic.exceptions import InvalidUsage
from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage
class HTTPMethodView:
@@ -48,6 +48,7 @@ class HTTPMethodView:
"""Return view function for use with the routing system, that
dispatches request to appropriate handler method.
"""
def view(*args, **kwargs):
self = view.view_class(*class_args, **class_kwargs)
return self.dispatch_request(*args, **kwargs)
@@ -94,11 +95,13 @@ class CompositionView:
for method in methods:
if method not in HTTP_METHODS:
raise InvalidUsage(
'{} is not a valid HTTP method.'.format(method))
"{} is not a valid HTTP method.".format(method)
)
if method in self.handlers:
raise InvalidUsage(
'Method {} is already registered.'.format(method))
"Method {} is already registered.".format(method)
)
self.handlers[method] = handler
def __call__(self, request, *args, **kwargs):

View File

@@ -1,16 +1,22 @@
from httptools import HttpParserUpgrade
from websockets import ConnectionClosed # noqa
from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake
from sanic.exceptions import InvalidUsage
from sanic.server import HttpProtocol
from httptools import HttpParserUpgrade
from websockets import handshake, WebSocketCommonProtocol, InvalidHandshake
from websockets import ConnectionClosed # noqa
class WebSocketProtocol(HttpProtocol):
def __init__(self, *args, websocket_timeout=10,
websocket_max_size=None,
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16, **kwargs):
def __init__(
self,
*args,
websocket_timeout=10,
websocket_max_size=None,
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16,
**kwargs
):
super().__init__(*args, **kwargs)
self.websocket = None
self.websocket_timeout = websocket_timeout
@@ -57,36 +63,32 @@ class WebSocketProtocol(HttpProtocol):
async def websocket_handshake(self, request, subprotocols=None):
# let the websockets package do the handshake with the client
headers = []
def get_header(k):
return request.headers.get(k, '')
def set_header(k, v):
headers.append((k, v))
headers = {}
try:
key = handshake.check_request(get_header)
handshake.build_response(set_header, key)
key = handshake.check_request(request.headers)
handshake.build_response(headers, key)
except InvalidHandshake:
raise InvalidUsage('Invalid websocket request')
raise InvalidUsage("Invalid websocket request")
subprotocol = None
if subprotocols and 'Sec-Websocket-Protocol' in request.headers:
if subprotocols and "Sec-Websocket-Protocol" in request.headers:
# select a subprotocol
client_subprotocols = [p.strip() for p in request.headers[
'Sec-Websocket-Protocol'].split(',')]
client_subprotocols = [
p.strip()
for p in request.headers["Sec-Websocket-Protocol"].split(",")
]
for p in client_subprotocols:
if p in subprotocols:
subprotocol = p
set_header('Sec-Websocket-Protocol', subprotocol)
headers["Sec-Websocket-Protocol"] = subprotocol
break
# write the 101 response back to the client
rv = b'HTTP/1.1 101 Switching Protocols\r\n'
for k, v in headers:
rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n'
rv += b'\r\n'
rv = b"HTTP/1.1 101 Switching Protocols\r\n"
for k, v in headers.items():
rv += k.encode("utf-8") + b": " + v.encode("utf-8") + b"\r\n"
rv += b"\r\n"
request.transport.write(rv)
# hook up the websocket protocol
@@ -95,7 +97,7 @@ class WebSocketProtocol(HttpProtocol):
max_size=self.websocket_max_size,
max_queue=self.websocket_max_queue,
read_limit=self.websocket_read_limit,
write_limit=self.websocket_write_limit
write_limit=self.websocket_write_limit,
)
self.websocket.subprotocol = subprotocol
self.websocket.connection_made(request.transport)

View File

@@ -1,10 +1,16 @@
import os
import sys
import signal
import asyncio
import logging
import os
import signal
import sys
import traceback
import gunicorn.workers.base as base
from sanic.server import HttpProtocol, Signal, serve, trigger_events
from sanic.websocket import WebSocketProtocol
try:
import ssl
except ImportError:
@@ -12,13 +18,10 @@ except ImportError:
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
import gunicorn.workers.base as base
from sanic.server import trigger_events, serve, HttpProtocol, Signal
from sanic.websocket import WebSocketProtocol
class GunicornWorker(base.Worker):
@@ -50,36 +53,44 @@ class GunicornWorker(base.Worker):
def run(self):
is_debug = self.log.loglevel == logging.DEBUG
protocol = (
self.websocket_protocol if self.app.callable.websocket_enabled
else self.http_protocol)
self.websocket_protocol
if self.app.callable.websocket_enabled
else self.http_protocol
)
self._server_settings = self.app.callable._helper(
loop=self.loop,
debug=is_debug,
protocol=protocol,
ssl=self.ssl_context,
run_async=True)
self._server_settings['signal'] = self.signal
self._server_settings.pop('sock')
trigger_events(self._server_settings.get('before_start', []),
self.loop)
self._server_settings['before_start'] = ()
run_async=True,
)
self._server_settings["signal"] = self.signal
self._server_settings.pop("sock")
trigger_events(
self._server_settings.get("before_start", []), self.loop
)
self._server_settings["before_start"] = ()
self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
try:
self.loop.run_until_complete(self._runner)
self.app.callable.is_running = True
trigger_events(self._server_settings.get('after_start', []),
self.loop)
trigger_events(
self._server_settings.get("after_start", []), self.loop
)
self.loop.run_until_complete(self._check_alive())
trigger_events(self._server_settings.get('before_stop', []),
self.loop)
trigger_events(
self._server_settings.get("before_stop", []), self.loop
)
self.loop.run_until_complete(self.close())
except BaseException:
traceback.print_exc()
finally:
try:
trigger_events(self._server_settings.get('after_stop', []),
self.loop)
trigger_events(
self._server_settings.get("after_stop", []), self.loop
)
except BaseException:
traceback.print_exc()
finally:
@@ -90,8 +101,11 @@ class GunicornWorker(base.Worker):
async def close(self):
if self.servers:
# stop accepting connections
self.log.info("Stopping server: %s, connections: %s",
self.pid, len(self.connections))
self.log.info(
"Stopping server: %s, connections: %s",
self.pid,
len(self.connections),
)
for server in self.servers:
server.close()
await server.wait_closed()
@@ -105,8 +119,9 @@ class GunicornWorker(base.Worker):
# gracefully shutdown timeout
start_shutdown = 0
graceful_shutdown_timeout = self.cfg.graceful_timeout
while self.connections and \
(start_shutdown < graceful_shutdown_timeout):
while self.connections and (
start_shutdown < graceful_shutdown_timeout
):
await asyncio.sleep(0.1)
start_shutdown = start_shutdown + 0.1
@@ -115,9 +130,7 @@ class GunicornWorker(base.Worker):
coros = []
for conn in self.connections:
if hasattr(conn, "websocket") and conn.websocket:
coros.append(
conn.websocket.close_connection()
)
coros.append(conn.websocket.close_connection())
else:
conn.close()
_shutdown = asyncio.gather(*coros, loop=self.loop)
@@ -148,8 +161,9 @@ class GunicornWorker(base.Worker):
)
if self.max_requests and req_count > self.max_requests:
self.alive = False
self.log.info("Max requests exceeded, shutting down: %s",
self)
self.log.info(
"Max requests exceeded, shutting down: %s", self
)
elif pid == os.getpid() and self.ppid != os.getppid():
self.alive = False
self.log.info("Parent changed, shutting down: %s", self)
@@ -175,23 +189,29 @@ class GunicornWorker(base.Worker):
def init_signals(self):
# Set up signals through the event loop API.
self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit,
signal.SIGQUIT, None)
self.loop.add_signal_handler(
signal.SIGQUIT, self.handle_quit, signal.SIGQUIT, None
)
self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit,
signal.SIGTERM, None)
self.loop.add_signal_handler(
signal.SIGTERM, self.handle_exit, signal.SIGTERM, None
)
self.loop.add_signal_handler(signal.SIGINT, self.handle_quit,
signal.SIGINT, None)
self.loop.add_signal_handler(
signal.SIGINT, self.handle_quit, signal.SIGINT, None
)
self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch,
signal.SIGWINCH, None)
self.loop.add_signal_handler(
signal.SIGWINCH, self.handle_winch, signal.SIGWINCH, None
)
self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1,
signal.SIGUSR1, None)
self.loop.add_signal_handler(
signal.SIGUSR1, self.handle_usr1, signal.SIGUSR1, None
)
self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort,
signal.SIGABRT, None)
self.loop.add_signal_handler(
signal.SIGABRT, self.handle_abort, signal.SIGABRT, None
)
# Don't let SIGTERM and SIGUSR1 disturb active requests
# by interrupting system calls

20
setup.cfg Normal file
View File

@@ -0,0 +1,20 @@
[flake8]
ignore = E203, W503
[isort]
atomic = true
default_section = THIRDPARTY
include_trailing_comma = true
known_first_party = sanic
known_third_party = pytest
line_length = 79
lines_after_imports = 2
lines_between_types = 1
multi_line_output = 3
not_skip = __init__.py
[version]
current_version = 0.8.3
file = sanic/__init__.py
current_version_pattern = __version__ = "{current_version}"
new_version_pattern = __version__ = "{new_version}"

127
setup.py
View File

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

View File

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

130
tests/conftest.py Normal file
View File

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

View File

@@ -9,10 +9,15 @@ import ujson as json
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)
async def handle(request):
return web.Response(body=json.dumps({"test":True}).encode('utf-8'), content_type='application/json')
return web.Response(
body=json.dumps({"test": True}).encode("utf-8"),
content_type="application/json",
)
app = web.Application(loop=loop)
app.router.add_route('GET', '/', handle)
app.router.add_route("GET", "/", handle)
web.run_app(app, port=sys.argv[1], access_log=None)

View File

@@ -4,8 +4,9 @@ from bottle import route, run
import ujson
@route('/')
@route("/")
def index():
return ujson.dumps({'test': True})
return ujson.dumps({"test": True})
app = bottle.default_app()

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

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

View File

@@ -3,8 +3,10 @@ import sys
import os
import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../')
currentdir = os.path.dirname(
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
sys.path.insert(0, currentdir + "/../../../")
import timeit
@@ -16,7 +18,11 @@ print("Running New 100,000 times")
times = 0
total_time = 0
for n in range(6):
time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000)
time = timeit.timeit(
'json({ "test":True }).output()',
setup="from sanic.response import json",
number=100000,
)
print("Took {} seconds".format(time))
total_time += time
times += 1
@@ -26,7 +32,11 @@ print("Running Old 100,000 times")
times = 0
total_time = 0
for n in range(6):
time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000)
time = timeit.timeit(
'json({ "test":True }).output_old()',
setup="from sanic.response import json",
number=100000,
)
print("Took {} seconds".format(time))
total_time += time
times += 1

View File

@@ -2,8 +2,10 @@ import sys
import os
import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../')
currentdir = os.path.dirname(
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
sys.path.insert(0, currentdir + "/../../../")
from sanic import Sanic
from sanic.response import json
@@ -15,5 +17,6 @@ app = Sanic("test")
async def test(request):
return json({"test": True})
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=sys.argv[1])

View File

@@ -2,8 +2,10 @@ import sys
import os
import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../')
currentdir = os.path.dirname(
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
sys.path.insert(0, currentdir + "/../../../")
from sanic import Sanic
from sanic.response import json, text
@@ -17,7 +19,7 @@ async def test(request):
return json({"test": True})
@app.route("/sync", methods=['GET', 'POST'])
@app.route("/sync", methods=["GET", "POST"])
def test(request):
return json({"test": True})
@@ -44,7 +46,14 @@ def post_json(request):
@app.route("/query_string")
def query_string(request):
return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
return json(
{
"parsed": True,
"args": request.args,
"url": request.url,
"query_string": request.query_string,
}
)
import sys
@@ -52,7 +61,6 @@ import sys
app.run(host="0.0.0.0", port=sys.argv[1])
# import asyncio_redis
# import asyncpg
# async def setup(sanic, loop):

View File

@@ -5,14 +5,14 @@ from tornado import ioloop, web
class MainHandler(web.RequestHandler):
def get(self):
self.write(ujson.dumps({'test': True}))
self.write(ujson.dumps({"test": True}))
app = web.Application([
(r'/', MainHandler)
], debug=False,
app = web.Application(
[(r"/", MainHandler)],
debug=False,
compress_response=False,
static_hash_cache=True
static_hash_cache=True,
)
app.listen(8000)

View File

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

196
tests/test_app.py Normal file
View File

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

View File

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

View File

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

View File

@@ -1,290 +1,288 @@
import asyncio
import inspect
import os
import pytest
from sanic import Sanic
from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json, text
from sanic.exceptions import NotFound, ServerError, InvalidUsage
from sanic.constants import HTTP_METHODS
from sanic.exceptions import NotFound, ServerError, InvalidUsage
from sanic.request import Request
from sanic.response import text, json
from sanic.views import CompositionView
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
@pytest.fixture(scope="module")
def static_file_directory():
"""The static directory to serve"""
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, "static")
return static_directory
def get_file_path(static_file_directory, file_name):
return os.path.join(static_file_directory, file_name)
def get_file_content(static_file_directory, file_name):
"""The content of the static file to check"""
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
with open(get_file_path(static_file_directory, file_name), "rb") as file:
return file.read()
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(method):
app = Sanic('test_shorhand_routes_get')
bp = Blueprint('test_text')
@pytest.mark.parametrize("method", HTTP_METHODS)
def test_versioned_routes_get(app, method):
bp = Blueprint("test_text")
method = method.lower()
func = getattr(bp, method)
if callable(func):
@func('/{}'.format(method), version=1)
@func("/{}".format(method), version=1)
def handler(request):
return text('OK')
return text("OK")
else:
print(func)
raise
raise Exception("{} is not callable".format(func))
app.blueprint(bp)
client_method = getattr(app.test_client, method)
request, response = client_method('/v1/{}'.format(method))
request, response = client_method("/v1/{}".format(method))
assert response.status == 200
def test_bp():
app = Sanic('test_text')
bp = Blueprint('test_text')
def test_bp(app):
bp = Blueprint("test_text")
@bp.route('/')
@bp.route("/")
def handler(request):
return text('Hello')
return text("Hello")
app.blueprint(bp)
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert app.is_request_stream is False
assert response.text == 'Hello'
assert response.text == "Hello"
def test_bp_strict_slash():
app = Sanic('test_route_strict_slash')
bp = Blueprint('test_text')
@bp.get('/get', strict_slashes=True)
def handler(request):
return text('OK')
def test_bp_strict_slash(app):
bp = Blueprint("test_text")
@bp.post('/post/', strict_slashes=True)
def handler(request):
return text('OK')
@bp.get("/get", strict_slashes=True)
def get_handler(request):
return text("OK")
@bp.post("/post/", strict_slashes=True)
def post_handler(request):
return text("OK")
app.blueprint(bp)
request, response = app.test_client.get('/get')
assert response.text == 'OK'
assert response.json == None
request, response = app.test_client.get("/get")
assert response.text == "OK"
assert response.json is None
request, response = app.test_client.get('/get/')
request, response = app.test_client.get("/get/")
assert response.status == 404
request, response = app.test_client.post('/post/')
assert response.text == 'OK'
request, response = app.test_client.post("/post/")
assert response.text == "OK"
request, response = app.test_client.post('/post')
request, response = app.test_client.post("/post")
assert response.status == 404
def test_bp_strict_slash_default_value():
app = Sanic('test_route_strict_slash')
bp = Blueprint('test_text', strict_slashes=True)
@bp.get('/get')
def handler(request):
return text('OK')
def test_bp_strict_slash_default_value(app):
bp = Blueprint("test_text", strict_slashes=True)
@bp.post('/post/')
def handler(request):
return text('OK')
@bp.get("/get")
def get_handler(request):
return text("OK")
@bp.post("/post/")
def post_handler(request):
return text("OK")
app.blueprint(bp)
request, response = app.test_client.get('/get/')
request, response = app.test_client.get("/get/")
assert response.status == 404
request, response = app.test_client.post('/post')
request, response = app.test_client.post("/post")
assert response.status == 404
def test_bp_strict_slash_without_passing_default_value():
app = Sanic('test_route_strict_slash')
bp = Blueprint('test_text')
@bp.get('/get')
def handler(request):
return text('OK')
def test_bp_strict_slash_without_passing_default_value(app):
bp = Blueprint("test_text")
@bp.post('/post/')
def handler(request):
return text('OK')
@bp.get("/get")
def get_handler(request):
return text("OK")
@bp.post("/post/")
def post_handler(request):
return text("OK")
app.blueprint(bp)
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
request, response = app.test_client.get("/get/")
assert response.text == "OK"
request, response = app.test_client.post('/post')
assert response.text == 'OK'
request, response = app.test_client.post("/post")
assert response.text == "OK"
def test_bp_strict_slash_default_value_can_be_overwritten():
app = Sanic('test_route_strict_slash')
bp = Blueprint('test_text', strict_slashes=True)
@bp.get('/get', strict_slashes=False)
def handler(request):
return text('OK')
def test_bp_strict_slash_default_value_can_be_overwritten(app):
bp = Blueprint("test_text", strict_slashes=True)
@bp.post('/post/', strict_slashes=False)
def handler(request):
return text('OK')
@bp.get("/get", strict_slashes=False)
def get_handler(request):
return text("OK")
@bp.post("/post/", strict_slashes=False)
def post_handler(request):
return text("OK")
app.blueprint(bp)
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
request, response = app.test_client.get("/get/")
assert response.text == "OK"
request, response = app.test_client.post('/post')
assert response.text == 'OK'
request, response = app.test_client.post("/post")
assert response.text == "OK"
def test_bp_with_url_prefix():
app = Sanic('test_text')
bp = Blueprint('test_text', url_prefix='/test1')
@bp.route('/')
def test_bp_with_url_prefix(app):
bp = Blueprint("test_text", url_prefix="/test1")
@bp.route("/")
def handler(request):
return text('Hello')
return text("Hello")
app.blueprint(bp)
request, response = app.test_client.get('/test1/')
request, response = app.test_client.get("/test1/")
assert response.text == 'Hello'
assert response.text == "Hello"
def test_several_bp_with_url_prefix():
app = Sanic('test_text')
bp = Blueprint('test_text', url_prefix='/test1')
bp2 = Blueprint('test_text2', url_prefix='/test2')
def test_several_bp_with_url_prefix(app):
bp = Blueprint("test_text", url_prefix="/test1")
bp2 = Blueprint("test_text2", url_prefix="/test2")
@bp.route('/')
@bp.route("/")
def handler(request):
return text('Hello')
return text("Hello")
@bp2.route('/')
@bp2.route("/")
def handler2(request):
return text('Hello2')
return text("Hello2")
app.blueprint(bp)
app.blueprint(bp2)
request, response = app.test_client.get('/test1/')
assert response.text == 'Hello'
request, response = app.test_client.get("/test1/")
assert response.text == "Hello"
request, response = app.test_client.get('/test2/')
assert response.text == 'Hello2'
request, response = app.test_client.get("/test2/")
assert response.text == "Hello2"
def test_bp_with_host():
app = Sanic('test_bp_host')
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
@bp.route('/')
def handler(request):
return text('Hello')
def test_bp_with_host(app):
bp = Blueprint("test_bp_host", url_prefix="/test1", host="example.com")
@bp.route('/', host="sub.example.com")
def handler(request):
return text('Hello subdomain!')
@bp.route("/")
def handler1(request):
return text("Hello")
@bp.route("/", host="sub.example.com")
def handler2(request):
return text("Hello subdomain!")
app.blueprint(bp)
headers = {"Host": "example.com"}
request, response = app.test_client.get(
'/test1/',
headers=headers)
assert response.text == 'Hello'
request, response = app.test_client.get("/test1/", headers=headers)
assert response.text == "Hello"
headers = {"Host": "sub.example.com"}
request, response = app.test_client.get(
'/test1/',
headers=headers)
request, response = app.test_client.get("/test1/", headers=headers)
assert response.text == 'Hello subdomain!'
assert response.text == "Hello subdomain!"
def test_several_bp_with_host():
app = Sanic('test_text')
bp = Blueprint('test_text',
url_prefix='/test',
host="example.com")
bp2 = Blueprint('test_text2',
url_prefix='/test',
host="sub.example.com")
def test_several_bp_with_host(app):
bp = Blueprint("test_text", url_prefix="/test", host="example.com")
bp2 = Blueprint("test_text2", url_prefix="/test", host="sub.example.com")
@bp.route('/')
@bp.route("/")
def handler(request):
return text('Hello')
return text("Hello")
@bp2.route('/')
@bp2.route("/")
def handler1(request):
return text("Hello2")
@bp2.route("/other/")
def handler2(request):
return text('Hello2')
@bp2.route('/other/')
def handler2(request):
return text('Hello3')
return text("Hello3")
app.blueprint(bp)
app.blueprint(bp2)
assert bp.host == "example.com"
headers = {"Host": "example.com"}
request, response = app.test_client.get(
'/test/',
headers=headers)
assert response.text == 'Hello'
request, response = app.test_client.get("/test/", headers=headers)
assert response.text == "Hello"
assert bp2.host == "sub.example.com"
headers = {"Host": "sub.example.com"}
request, response = app.test_client.get(
'/test/',
headers=headers)
request, response = app.test_client.get("/test/", headers=headers)
assert response.text == 'Hello2'
request, response = app.test_client.get(
'/test/other/',
headers=headers)
assert response.text == 'Hello3'
assert response.text == "Hello2"
request, response = app.test_client.get("/test/other/", headers=headers)
assert response.text == "Hello3"
def test_bp_middleware():
app = Sanic('test_middleware')
blueprint = Blueprint('test_middleware')
@blueprint.middleware('response')
def test_bp_middleware(app):
blueprint = Blueprint("test_middleware")
@blueprint.middleware("response")
async def process_response(request, response):
return text('OK')
return text("OK")
@app.route('/')
@app.route("/")
async def handler(request):
return text('FAIL')
return text("FAIL")
app.blueprint(blueprint)
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
def test_bp_exception_handler():
app = Sanic('test_middleware')
blueprint = Blueprint('test_middleware')
@blueprint.route('/1')
def test_bp_exception_handler(app):
blueprint = Blueprint("test_middleware")
@blueprint.route("/1")
def handler_1(request):
raise InvalidUsage("OK")
@blueprint.route('/2')
@blueprint.route("/2")
def handler_2(request):
raise ServerError("OK")
@blueprint.route('/3')
@blueprint.route("/3")
def handler_3(request):
raise NotFound("OK")
@@ -294,133 +292,132 @@ def test_bp_exception_handler():
app.blueprint(blueprint)
request, response = app.test_client.get('/1')
request, response = app.test_client.get("/1")
assert response.status == 400
request, response = app.test_client.get('/2')
request, response = app.test_client.get("/2")
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
request, response = app.test_client.get('/3')
request, response = app.test_client.get("/3")
assert response.status == 200
def test_bp_listeners():
app = Sanic('test_middleware')
blueprint = Blueprint('test_middleware')
def test_bp_listeners(app):
blueprint = Blueprint("test_middleware")
order = []
@blueprint.listener('before_server_start')
@blueprint.listener("before_server_start")
def handler_1(sanic, loop):
order.append(1)
@blueprint.listener('after_server_start')
@blueprint.listener("after_server_start")
def handler_2(sanic, loop):
order.append(2)
@blueprint.listener('after_server_start')
@blueprint.listener("after_server_start")
def handler_3(sanic, loop):
order.append(3)
@blueprint.listener('before_server_stop')
@blueprint.listener("before_server_stop")
def handler_4(sanic, loop):
order.append(5)
@blueprint.listener('before_server_stop')
@blueprint.listener("before_server_stop")
def handler_5(sanic, loop):
order.append(4)
@blueprint.listener('after_server_stop')
@blueprint.listener("after_server_stop")
def handler_6(sanic, loop):
order.append(6)
app.blueprint(blueprint)
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert order == [1,2,3,4,5,6]
assert order == [1, 2, 3, 4, 5, 6]
def test_bp_static():
def test_bp_static(app):
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
with open(current_file, "rb") as file:
current_file_contents = file.read()
app = Sanic('test_static')
blueprint = Blueprint('test_static')
blueprint = Blueprint("test_static")
blueprint.static('/testing.file', current_file)
blueprint.static("/testing.file", current_file)
app.blueprint(blueprint)
request, response = app.test_client.get('/testing.file')
request, response = app.test_client.get("/testing.file")
assert response.status == 200
assert response.body == current_file_contents
@pytest.mark.parametrize('file_name', ['test.html'])
def test_bp_static_content_type(file_name):
@pytest.mark.parametrize("file_name", ["test.html"])
def test_bp_static_content_type(app, file_name):
# This is done here, since no other test loads a file here
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, 'static')
static_directory = os.path.join(current_directory, "static")
app = Sanic('test_static')
blueprint = Blueprint('test_static')
blueprint = Blueprint("test_static")
blueprint.static(
'/testing.file',
"/testing.file",
get_file_path(static_directory, file_name),
content_type='text/html; charset=utf-8'
content_type="text/html; charset=utf-8",
)
app.blueprint(blueprint)
request, response = app.test_client.get('/testing.file')
request, response = app.test_client.get("/testing.file")
assert response.status == 200
assert response.body == get_file_content(static_directory, file_name)
assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
assert response.headers["Content-Type"] == "text/html; charset=utf-8"
def test_bp_shorthand():
app = Sanic('test_shorhand_routes')
blueprint = Blueprint('test_shorhand_routes')
def test_bp_shorthand(app):
blueprint = Blueprint("test_shorhand_routes")
ev = asyncio.Event()
@blueprint.get('/get')
@blueprint.get("/get")
def handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.put('/put')
def handler(request):
@blueprint.put("/put")
def put_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.post('/post')
def handler(request):
@blueprint.post("/post")
def post_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.head('/head')
def handler(request):
@blueprint.head("/head")
def head_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.options('/options')
def handler(request):
@blueprint.options("/options")
def options_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.patch('/patch')
def handler(request):
@blueprint.patch("/patch")
def patch_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.delete('/delete')
def handler(request):
@blueprint.delete("/delete")
def delete_handler(request):
assert request.stream is None
return text('OK')
return text("OK")
@blueprint.websocket('/ws')
async def handler(request, ws):
@blueprint.websocket("/ws/", strict_slashes=True)
async def websocket_handler(request, ws):
assert request.stream is None
ev.set()
@@ -428,93 +425,282 @@ def test_bp_shorthand():
assert app.is_request_stream is False
request, response = app.test_client.get('/get')
assert response.text == 'OK'
request, response = app.test_client.get("/get")
assert response.text == "OK"
request, response = app.test_client.post('/get')
request, response = app.test_client.post("/get")
assert response.status == 405
request, response = app.test_client.put('/put')
assert response.text == 'OK'
request, response = app.test_client.put("/put")
assert response.text == "OK"
request, response = app.test_client.get('/post')
request, response = app.test_client.get("/post")
assert response.status == 405
request, response = app.test_client.post('/post')
assert response.text == 'OK'
request, response = app.test_client.post("/post")
assert response.text == "OK"
request, response = app.test_client.get('/post')
request, response = app.test_client.get("/post")
assert response.status == 405
request, response = app.test_client.head('/head')
request, response = app.test_client.head("/head")
assert response.status == 200
request, response = app.test_client.get('/head')
request, response = app.test_client.get("/head")
assert response.status == 405
request, response = app.test_client.options('/options')
assert response.text == 'OK'
request, response = app.test_client.options("/options")
assert response.text == "OK"
request, response = app.test_client.get('/options')
request, response = app.test_client.get("/options")
assert response.status == 405
request, response = app.test_client.patch('/patch')
assert response.text == 'OK'
request, response = app.test_client.patch("/patch")
assert response.text == "OK"
request, response = app.test_client.get('/patch')
request, response = app.test_client.get("/patch")
assert response.status == 405
request, response = app.test_client.delete('/delete')
assert response.text == 'OK'
request, response = app.test_client.delete("/delete")
assert response.text == "OK"
request, response = app.test_client.get('/delete')
request, response = app.test_client.get("/delete")
assert response.status == 405
request, response = app.test_client.get('/ws', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'})
request, response = app.test_client.get(
"/ws/",
headers={
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
},
)
assert response.status == 101
assert ev.is_set()
def test_bp_group():
app = Sanic('test_nested_bp_groups')
deep_0 = Blueprint('deep_0', url_prefix='/deep')
deep_1 = Blueprint('deep_1', url_prefix = '/deep1')
def test_bp_group(app):
deep_0 = Blueprint("deep_0", url_prefix="/deep")
deep_1 = Blueprint("deep_1", url_prefix="/deep1")
@deep_0.route('/')
@deep_0.route("/")
def handler(request):
return text('D0_OK')
return text("D0_OK")
@deep_1.route('/bottom')
def handler(request):
return text('D1B_OK')
@deep_1.route("/bottom")
def bottom_handler(request):
return text("D1B_OK")
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix="/mid")
mid_1 = Blueprint("mid_tier", url_prefix="/mid1")
@mid_1.route('/')
def handler(request):
return text('M1_OK')
@mid_1.route("/")
def handler1(request):
return text("M1_OK")
top = Blueprint.group(mid_0, mid_1)
app.blueprint(top)
@app.route('/')
def handler(request):
return text('TOP_OK')
@app.route("/")
def handler2(request):
return text("TOP_OK")
request, response = app.test_client.get('/')
assert response.text == 'TOP_OK'
request, response = app.test_client.get("/")
assert response.text == "TOP_OK"
request, response = app.test_client.get('/mid1')
assert response.text == 'M1_OK'
request, response = app.test_client.get("/mid1")
assert response.text == "M1_OK"
request, response = app.test_client.get('/mid/deep')
assert response.text == 'D0_OK'
request, response = app.test_client.get("/mid/deep")
assert response.text == "D0_OK"
request, response = app.test_client.get('/mid/deep1/bottom')
assert response.text == 'D1B_OK'
request, response = app.test_client.get("/mid/deep1/bottom")
assert response.text == "D1B_OK"
def test_bp_group_with_default_url_prefix(app):
from sanic.response import json
bp_resources = Blueprint("bp_resources")
@bp_resources.get("/")
def list_resources_handler(request):
resource = {}
return json([resource])
bp_resource = Blueprint("bp_resource", url_prefix="/<resource_id>")
@bp_resource.get("/")
def get_resource_hander(request, resource_id):
resource = {"resource_id": resource_id}
return json(resource)
bp_resources_group = Blueprint.group(
bp_resources, bp_resource, url_prefix="/resources"
)
bp_api_v1 = Blueprint("bp_api_v1")
@bp_api_v1.get("/info")
def api_v1_info(request):
return text("api_version: v1")
bp_api_v1_group = Blueprint.group(
bp_api_v1, bp_resources_group, url_prefix="/v1"
)
bp_api_group = Blueprint.group(bp_api_v1_group, url_prefix="/api")
app.blueprint(bp_api_group)
request, response = app.test_client.get("/api/v1/info")
assert response.text == "api_version: v1"
request, response = app.test_client.get("/api/v1/resources")
assert response.json == [{}]
from uuid import uuid4
resource_id = str(uuid4())
request, response = app.test_client.get(
"/api/v1/resources/{0}".format(resource_id)
)
assert response.json == {"resource_id": resource_id}
def test_blueprint_middleware_with_args(app: Sanic):
bp = Blueprint(name="with_args_bp", url_prefix="/wa")
@bp.middleware
def middleware_with_no_tag(request: Request):
if request.headers.get("content-type") == "application/json":
request.headers["accepts"] = "plain/text"
else:
request.headers["accepts"] = "application/json"
@bp.route("/")
def default_route(request):
if request.headers.get("accepts") == "application/json":
return json({"test": "value"})
else:
return text("value")
app.blueprint(bp)
_, response = app.test_client.get(
"/wa", headers={"content-type": "application/json"}
)
assert response.text == "value"
_, response = app.test_client.get(
"/wa", headers={"content-type": "plain/text"}
)
assert response.json.get("test") == "value"
d = {}
@pytest.mark.parametrize("file_name", ["test.file"])
def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, "rb") as file:
current_file_contents = file.read()
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
bp.static(
"/test.file/",
get_file_path(static_file_directory, file_name),
name="static.testing",
strict_slashes=True,
)
app.blueprint(bp)
uri = app.url_for("static", name="static.testing")
assert uri == "/static/test.file"
_, response = app.test_client.get("/static/test.file")
assert response.status == 404
_, response = app.test_client.get("/static/test.file/")
assert response.status == 200
def test_route_handler_add(app: Sanic):
view = CompositionView()
async def get_handler(request):
return json({"response": "OK"})
view.add(["GET"], get_handler, stream=False)
async def default_handler(request):
return text("OK")
bp = Blueprint(name="handler", url_prefix="/handler")
bp.add_route(default_handler, uri="/default/", strict_slashes=True)
bp.add_route(view, uri="/view", name="test")
app.blueprint(bp)
_, response = app.test_client.get("/handler/default/")
assert response.text == "OK"
_, response = app.test_client.get("/handler/view")
assert response.json["response"] == "OK"
def test_websocket_route(app: Sanic):
event = asyncio.Event()
async def websocket_handler(request, ws):
assert ws.subprotocol is None
event.set()
bp = Blueprint(name="handler", url_prefix="/ws")
bp.add_websocket_route(websocket_handler, "/test", name="test")
app.blueprint(bp)
_, response = app.test_client.get(
"/ws/test",
headers={
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
},
)
assert response.status == 101
assert event.is_set()
def test_duplicate_blueprint(app):
bp_name = "bp"
bp = Blueprint(bp_name)
bp1 = Blueprint(bp_name)
app.blueprint(bp)
with pytest.raises(AssertionError) as excinfo:
app.blueprint(bp1)
assert str(excinfo.value) == (
'A blueprint with the name "{}" is already registered. '
"Blueprint names must be unique."
).format(bp_name)
@pytest.mark.parametrize("debug", [True, False, None])
def test_register_blueprint(app, debug):
bp = Blueprint("bp")
app.debug = debug
with pytest.warns(DeprecationWarning) as record:
app.register_blueprint(bp)
assert record[0].message.args[0] == (
"Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method"
" instead"
)

View File

@@ -1,20 +1,32 @@
from os import environ
from pathlib import Path
from contextlib import contextmanager
from tempfile import TemporaryDirectory
from textwrap import dedent
import pytest
from tempfile import NamedTemporaryFile
from sanic import Sanic
from sanic.config import Config, DEFAULT_CONFIG
from sanic.exceptions import PyFileError
def test_load_from_object():
app = Sanic('test_load_from_object')
@contextmanager
def temp_path():
""" a simple cross platform replacement for NamedTemporaryFile """
with TemporaryDirectory() as td:
yield Path(td, "file")
def test_load_from_object(app):
class Config:
not_for_config = 'should not be used'
CONFIG_VALUE = 'should be used'
not_for_config = "should not be used"
CONFIG_VALUE = "should be used"
app.config.from_object(Config)
assert 'CONFIG_VALUE' in app.config
assert app.config.CONFIG_VALUE == 'should be used'
assert 'not_for_config' not in app.config
assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config
def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
@@ -22,64 +34,98 @@ def test_auto_load_env():
assert app.config.TEST_ANSWER == 42
del environ["SANIC_TEST_ANSWER"]
def test_auto_load_bool_env():
environ["SANIC_TEST_ANSWER"] = "True"
app = Sanic()
assert app.config.TEST_ANSWER == True
del environ["SANIC_TEST_ANSWER"]
def test_dont_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(load_env=False)
assert getattr(app.config, 'TEST_ANSWER', None) == None
assert getattr(app.config, "TEST_ANSWER", None) is None
del environ["SANIC_TEST_ANSWER"]
def test_load_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(load_env='MYAPP_')
app = Sanic(load_env="MYAPP_")
assert app.config.TEST_ANSWER == 42
del environ["MYAPP_TEST_ANSWER"]
def test_load_from_file():
app = Sanic('test_load_from_file')
config = b"""
VALUE = 'some value'
condition = 1 == 1
if condition:
CONDITIONAL = 'should be set'
def test_load_env_prefix_float_values():
environ["MYAPP_TEST_ROI"] = "2.3"
app = Sanic(load_env="MYAPP_")
assert app.config.TEST_ROI == 2.3
del environ["MYAPP_TEST_ROI"]
def test_load_env_prefix_string_value():
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
app = Sanic(load_env="MYAPP_")
assert app.config.TEST_TOKEN == "somerandomtesttoken"
del environ["MYAPP_TEST_TOKEN"]
def test_load_from_file(app):
config = dedent(
"""
VALUE = 'some value'
condition = 1 == 1
if condition:
CONDITIONAL = 'should be set'
"""
with NamedTemporaryFile() as config_file:
config_file.write(config)
config_file.seek(0)
app.config.from_pyfile(config_file.name)
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
assert 'CONDITIONAL' in app.config
assert app.config.CONDITIONAL == 'should be set'
assert 'condition' not in app.config
)
with temp_path() as config_path:
config_path.write_text(config)
app.config.from_pyfile(str(config_path))
assert "VALUE" in app.config
assert app.config.VALUE == "some value"
assert "CONDITIONAL" in app.config
assert app.config.CONDITIONAL == "should be set"
assert "condition" not in app.config
def test_load_from_missing_file():
app = Sanic('test_load_from_missing_file')
def test_load_from_missing_file(app):
with pytest.raises(IOError):
app.config.from_pyfile('non-existent file')
app.config.from_pyfile("non-existent file")
def test_load_from_envvar():
app = Sanic('test_load_from_envvar')
config = b"VALUE = 'some value'"
with NamedTemporaryFile() as config_file:
config_file.write(config)
config_file.seek(0)
environ['APP_CONFIG'] = config_file.name
app.config.from_envvar('APP_CONFIG')
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
def test_load_from_envvar(app):
config = "VALUE = 'some value'"
with temp_path() as config_path:
config_path.write_text(config)
environ["APP_CONFIG"] = str(config_path)
app.config.from_envvar("APP_CONFIG")
assert "VALUE" in app.config
assert app.config.VALUE == "some value"
def test_load_from_missing_envvar():
app = Sanic('test_load_from_missing_envvar')
with pytest.raises(RuntimeError):
app.config.from_envvar('non-existent variable')
def test_load_from_missing_envvar(app):
with pytest.raises(RuntimeError) as e:
app.config.from_envvar("non-existent variable")
assert str(e.value) == (
"The environment variable 'non-existent "
"variable' is not set and thus configuration "
"could not be loaded."
)
def test_overwrite_exisiting_config():
app = Sanic('test_overwrite_exisiting_config')
def test_load_config_from_file_invalid_syntax(app):
config = "VALUE = some value"
with temp_path() as config_path:
config_path.write_text(config)
with pytest.raises(PyFileError):
app.config.from_pyfile(config_path)
def test_overwrite_exisiting_config(app):
app.config.DEFAULT = 1
class Config:
DEFAULT = 2
@@ -87,7 +133,126 @@ def test_overwrite_exisiting_config():
assert app.config.DEFAULT == 2
def test_missing_config():
app = Sanic('test_missing_config')
with pytest.raises(AttributeError):
app.config.NON_EXISTENT
def test_overwrite_exisiting_config_ignore_lowercase(app):
app.config.default = 1
class Config:
default = 2
app.config.from_object(Config)
assert app.config.default == 1
def test_missing_config(app):
with pytest.raises(
AttributeError, match="Config has no 'NON_EXISTENT'"
) as e:
_ = app.config.NON_EXISTENT
def test_config_defaults():
"""
load DEFAULT_CONFIG
"""
conf = Config()
for key, value in DEFAULT_CONFIG.items():
assert getattr(conf, key) == value
def test_config_custom_defaults():
"""
we should have all the variables from defaults rewriting them with custom defaults passed in
Config
"""
custom_defaults = {
"REQUEST_MAX_SIZE": 1,
"KEEP_ALIVE": False,
"ACCESS_LOG": False,
}
conf = Config(defaults=custom_defaults)
for key, value in DEFAULT_CONFIG.items():
if key in custom_defaults.keys():
value = custom_defaults[key]
assert getattr(conf, key) == value
def test_config_custom_defaults_with_env():
"""
test that environment variables has higher priority than DEFAULT_CONFIG and passed defaults dict
"""
custom_defaults = {
"REQUEST_MAX_SIZE123": 1,
"KEEP_ALIVE123": False,
"ACCESS_LOG123": False,
}
environ_defaults = {
"SANIC_REQUEST_MAX_SIZE123": "2",
"SANIC_KEEP_ALIVE123": "True",
"SANIC_ACCESS_LOG123": "False",
}
for key, value in environ_defaults.items():
environ[key] = value
conf = Config(defaults=custom_defaults)
for key, value in DEFAULT_CONFIG.items():
if "SANIC_" + key in environ_defaults.keys():
value = environ_defaults["SANIC_" + key]
try:
value = int(value)
except ValueError:
if value in ["True", "False"]:
value = value == "True"
assert getattr(conf, key) == value
for key, value in environ_defaults.items():
del environ[key]
def test_config_access_log_passing_in_run(app):
assert app.config.ACCESS_LOG == True
@app.listener("after_server_start")
async def _request(sanic, loop):
app.stop()
app.run(port=1340, access_log=False)
assert app.config.ACCESS_LOG == False
app.run(port=1340, access_log=True)
assert app.config.ACCESS_LOG == True
async def test_config_access_log_passing_in_create_server(app):
assert app.config.ACCESS_LOG == True
@app.listener("after_server_start")
async def _request(sanic, loop):
app.stop()
await app.create_server(
port=1341, access_log=False, return_asyncio_server=True
)
assert app.config.ACCESS_LOG == False
await app.create_server(
port=1342, access_log=True, return_asyncio_server=True
)
assert app.config.ACCESS_LOG == True
def test_config_rewrite_keep_alive():
config = Config()
assert config.KEEP_ALIVE == DEFAULT_CONFIG["KEEP_ALIVE"]
config = Config(keep_alive=True)
assert config.KEEP_ALIVE == True
config = Config(keep_alive=False)
assert config.KEEP_ALIVE == False
# use defaults
config = Config(defaults={"KEEP_ALIVE": False})
assert config.KEEP_ALIVE == False
config = Config(defaults={"KEEP_ALIVE": True})
assert config.KEEP_ALIVE == True

View File

@@ -1,116 +1,190 @@
from datetime import datetime, timedelta
from http.cookies import SimpleCookie
from sanic import Sanic
from sanic.response import json, text
from sanic.response import text
import pytest
from sanic.cookies import Cookie, DEFAULT_MAX_AGE
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
def test_cookies():
app = Sanic('test_text')
@app.route('/')
def test_cookies(app):
@app.route("/")
def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test']))
response.cookies['right_back'] = 'at you'
response = text("Cookies are: {}".format(request.cookies["test"]))
response.cookies["right_back"] = "at you"
return response
request, response = app.test_client.get('/', cookies={"test": "working!"})
request, response = app.test_client.get("/", cookies={"test": "working!"})
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
response_cookies.load(response.headers.get("Set-Cookie", {}))
assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you'
assert response.text == "Cookies are: working!"
assert response_cookies["right_back"].value == "at you"
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
])
def test_false_cookies_encoded(httponly, expected):
app = Sanic('test_text')
@app.route('/')
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
def test_false_cookies_encoded(app, httponly, expected):
@app.route("/")
def handler(request):
response = text('hello cookies')
response.cookies['hello'] = 'world'
response.cookies['hello']['httponly'] = httponly
return text(response.cookies['hello'].encode('utf8'))
response = text("hello cookies")
response.cookies["hello"] = "world"
response.cookies["hello"]["httponly"] = httponly
return text(response.cookies["hello"].encode("utf8"))
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
assert ('HttpOnly' in response.text) == expected
assert ("HttpOnly" in response.text) == expected
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
])
def test_false_cookies(httponly, expected):
app = Sanic('test_text')
@app.route('/')
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
def test_false_cookies(app, httponly, expected):
@app.route("/")
def handler(request):
response = text('hello cookies')
response.cookies['right_back'] = 'at you'
response.cookies['right_back']['httponly'] = httponly
response = text("hello cookies")
response.cookies["right_back"] = "at you"
response.cookies["right_back"]["httponly"] = httponly
return response
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
response_cookies.load(response.headers.get("Set-Cookie", {}))
assert ('HttpOnly' in response_cookies['right_back'].output()) == expected
assert ("HttpOnly" in response_cookies["right_back"].output()) == expected
def test_http2_cookies():
app = Sanic('test_http2_cookies')
@app.route('/')
def test_http2_cookies(app):
@app.route("/")
async def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test']))
response = text("Cookies are: {}".format(request.cookies["test"]))
return response
headers = {'cookie': 'test=working!'}
request, response = app.test_client.get('/', headers=headers)
headers = {"cookie": "test=working!"}
request, response = app.test_client.get("/", headers=headers)
assert response.text == 'Cookies are: working!'
assert response.text == "Cookies are: working!"
def test_cookie_options():
app = Sanic('test_text')
@app.route('/')
def test_cookie_options(app):
@app.route("/")
def handler(request):
response = text("OK")
response.cookies['test'] = 'at you'
response.cookies['test']['httponly'] = True
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
response.cookies["test"] = "at you"
response.cookies["test"]["httponly"] = True
response.cookies["test"]["expires"] = datetime.now() + timedelta(
seconds=10
)
return response
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
response_cookies.load(response.headers.get("Set-Cookie", {}))
assert response_cookies['test'].value == 'at you'
assert response_cookies['test']['httponly'] == True
assert response_cookies["test"].value == "at you"
assert response_cookies["test"]["httponly"] is True
def test_cookie_deletion():
app = Sanic('test_text')
@app.route('/')
def test_cookie_deletion(app):
@app.route("/")
def handler(request):
response = text("OK")
del response.cookies['i_want_to_die']
response.cookies['i_never_existed'] = 'testing'
del response.cookies['i_never_existed']
del response.cookies["i_want_to_die"]
response.cookies["i_never_existed"] = "testing"
del response.cookies["i_never_existed"]
return response
request, response = app.test_client.get('/')
request, response = app.test_client.get("/")
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
response_cookies.load(response.headers.get("Set-Cookie", {}))
assert int(response_cookies['i_want_to_die']['max-age']) == 0
assert int(response_cookies["i_want_to_die"]["max-age"]) == 0
with pytest.raises(KeyError):
hold_my_beer = response.cookies['i_never_existed']
_ = response.cookies["i_never_existed"]
def test_cookie_reserved_cookie():
with pytest.raises(expected_exception=KeyError) as e:
Cookie("domain", "testdomain.com")
assert e.message == "Cookie name is a reserved word"
def test_cookie_illegal_key_format():
with pytest.raises(expected_exception=KeyError) as e:
Cookie("testå", "test")
assert e.message == "Cookie key contains illegal characters"
def test_cookie_set_unknown_property():
c = Cookie("test_cookie", "value")
with pytest.raises(expected_exception=KeyError) as e:
c["invalid"] = "value"
assert e.message == "Unknown cookie property"
def test_cookie_set_same_key(app):
cookies = {"test": "wait"}
@app.get("/")
def handler(request):
response = text("pass")
response.cookies["test"] = "modified"
response.cookies["test"] = "pass"
return response
request, response = app.test_client.get("/", cookies=cookies)
assert response.status == 200
assert response.cookies["test"].value == "pass"
@pytest.mark.parametrize("max_age", ["0", 30, 30.0, 30.1, "30", "test"])
def test_cookie_max_age(app, max_age):
cookies = {"test": "wait"}
@app.get("/")
def handler(request):
response = text("pass")
response.cookies["test"] = "pass"
response.cookies["test"]["max-age"] = max_age
return response
request, response = app.test_client.get("/", cookies=cookies)
assert response.status == 200
assert response.cookies["test"].value == "pass"
if str(max_age).isdigit() and int(max_age) == float(max_age):
assert response.cookies["test"]["max-age"] == str(max_age)
else:
assert response.cookies["test"]["max-age"] == str(DEFAULT_MAX_AGE)
@pytest.mark.parametrize("expires", [datetime.now() + timedelta(seconds=60)])
def test_cookie_expires(app, expires):
cookies = {"test": "wait"}
@app.get("/")
def handler(request):
response = text("pass")
response.cookies["test"] = "pass"
response.cookies["test"]["expires"] = expires
return response
request, response = app.test_client.get("/", cookies=cookies)
assert response.status == 200
assert response.cookies["test"].value == "pass"
if isinstance(expires, datetime):
expires = expires.strftime("%a, %d-%b-%Y %T GMT")
assert response.cookies["test"]["expires"] == expires
@pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"])
def test_cookie_expires_illegal_instance_type(expires):
c = Cookie("test_cookie", "value")
with pytest.raises(expected_exception=TypeError) as e:
c["expires"] = expires
assert e.message == "Cookie 'expires' property must be a datetime"

View File

@@ -1,40 +1,38 @@
from sanic import Sanic
from sanic.response import text
from threading import Event
import asyncio
from queue import Queue
def test_create_task():
def test_create_task(app):
e = Event()
async def coro():
await asyncio.sleep(0.05)
e.set()
app = Sanic('test_create_task')
app.add_task(coro)
@app.route('/early')
@app.route("/early")
def not_set(request):
return text(e.is_set())
@app.route('/late')
@app.route("/late")
async def set(request):
await asyncio.sleep(0.1)
return text(e.is_set())
request, response = app.test_client.get('/early')
assert response.body == b'False'
request, response = app.test_client.get("/early")
assert response.body == b"False"
request, response = app.test_client.get('/late')
assert response.body == b'True'
request, response = app.test_client.get("/late")
assert response.body == b"True"
def test_create_task_with_app_arg():
app = Sanic('test_add_task')
def test_create_task_with_app_arg(app):
q = Queue()
@app.route('/')
@app.route("/")
def not_set(request):
return "hello"
@@ -43,5 +41,5 @@ def test_create_task_with_app_arg():
app.add_task(coro)
request, response = app.test_client.get('/')
assert q.get() == 'test_add_task'
request, response = app.test_client.get("/")
assert q.get() == "test_create_task_with_app_arg"

View File

@@ -1,31 +1,21 @@
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic('test_custom_porotocol')
class CustomHttpProtocol(HttpProtocol):
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.write(response.output(self.request.version))
self.transport.close()
@app.route('/1')
async def handler_1(request):
return 'OK'
def test_use_custom_protocol(app):
@app.route("/1")
async def handler_1(request):
return "OK"
def test_use_custom_protocol():
server_kwargs = {
'protocol': CustomHttpProtocol
}
request, response = app.test_client.get(
'/1', server_kwargs=server_kwargs)
server_kwargs = {"protocol": CustomHttpProtocol}
request, response = app.test_client.get("/1", server_kwargs=server_kwargs)
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"

View File

@@ -0,0 +1,53 @@
from io import BytesIO
from sanic import Sanic
from sanic.request import Request
from sanic.response import json_dumps, text
class CustomRequest(Request):
__slots__ = ("body_buffer",)
def body_init(self):
self.body_buffer = BytesIO()
def body_push(self, data):
self.body_buffer.write(data)
def body_finish(self):
self.body = self.body_buffer.getvalue()
self.body_buffer.close()
def test_custom_request():
app = Sanic(request_class=CustomRequest)
@app.route("/post", methods=["POST"])
async def post_handler(request):
return text("OK")
@app.route("/get")
async def get_handler(request):
return text("OK")
payload = {"test": "OK"}
headers = {"content-type": "application/json"}
request, response = app.test_client.post(
"/post", data=json_dumps(payload), headers=headers
)
assert isinstance(request.body_buffer, BytesIO)
assert request.body_buffer.closed
assert request.body == b'{"test":"OK"}'
assert request.json.get("test") == "OK"
assert response.text == "OK"
assert response.status == 200
request, response = app.test_client.get("/get")
assert isinstance(request.body_buffer, BytesIO)
assert request.body_buffer.closed
assert request.body == b""
assert response.text == "OK"
assert response.status == 200

View File

@@ -1,44 +1,43 @@
from sanic import Sanic
from sanic.response import text
from sanic.router import RouteExists
import pytest
@pytest.mark.parametrize("method,attr, expected", [
("get", "text", "OK1 test"),
("post", "text", "OK2 test"),
("put", "text", "OK2 test"),
("delete", "status", 405),
])
def test_overload_dynamic_routes(method, attr, expected):
app = Sanic('test_dynamic_route')
@app.route('/overload/<param>', methods=['GET'])
@pytest.mark.parametrize(
"method,attr, expected",
[
("get", "text", "OK1 test"),
("post", "text", "OK2 test"),
("put", "text", "OK2 test"),
("delete", "status", 405),
],
)
def test_overload_dynamic_routes(app, method, attr, expected):
@app.route("/overload/<param>", methods=["GET"])
async def handler1(request, param):
return text('OK1 ' + param)
return text("OK1 " + param)
@app.route('/overload/<param>', methods=['POST', 'PUT'])
@app.route("/overload/<param>", methods=["POST", "PUT"])
async def handler2(request, param):
return text('OK2 ' + param)
return text("OK2 " + param)
request, response = getattr(app.test_client, method)('/overload/test')
request, response = getattr(app.test_client, method)("/overload/test")
assert getattr(response, attr) == expected
def test_overload_dynamic_routes_exist():
app = Sanic('test_dynamic_route')
@app.route('/overload/<param>', methods=['GET'])
def test_overload_dynamic_routes_exist(app):
@app.route("/overload/<param>", methods=["GET"])
async def handler1(request, param):
return text('OK1 ' + param)
return text("OK1 " + param)
@app.route('/overload/<param>', methods=['POST', 'PUT'])
@app.route("/overload/<param>", methods=["POST", "PUT"])
async def handler2(request, param):
return text('OK2 ' + param)
return text("OK2 " + param)
# if this doesn't raise an error, than at least the below should happen:
# assert response.text == 'Duplicated'
with pytest.raises(RouteExists):
@app.route('/overload/<param>', methods=['PUT', 'DELETE'])
async def handler3(request):
return text('Duplicated')
@app.route("/overload/<param>", methods=["PUT", "DELETE"])
async def handler3(request, param):
return text("Duplicated")

View File

@@ -11,194 +11,204 @@ class SanicExceptionTestException(Exception):
pass
@pytest.fixture(scope='module')
@pytest.fixture(scope="module")
def exception_app():
app = Sanic('test_exceptions')
app = Sanic("test_exceptions")
@app.route('/')
@app.route("/")
def handler(request):
return text('OK')
return text("OK")
@app.route('/error')
@app.route("/error")
def handler_error(request):
raise ServerError("OK")
@app.route('/404')
@app.route("/404")
def handler_404(request):
raise NotFound("OK")
@app.route('/403')
@app.route("/403")
def handler_403(request):
raise Forbidden("Forbidden")
@app.route('/401')
@app.route("/401")
def handler_401(request):
raise Unauthorized("Unauthorized")
@app.route('/401/basic')
@app.route("/401/basic")
def handler_401_basic(request):
raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic")
@app.route('/401/digest')
@app.route("/401/digest")
def handler_401_digest(request):
raise Unauthorized("Unauthorized",
scheme="Digest",
realm="Sanic",
qop="auth, auth-int",
algorithm="MD5",
nonce="abcdef",
opaque="zyxwvu")
raise Unauthorized(
"Unauthorized",
scheme="Digest",
realm="Sanic",
qop="auth, auth-int",
algorithm="MD5",
nonce="abcdef",
opaque="zyxwvu",
)
@app.route('/401/bearer')
@app.route("/401/bearer")
def handler_401_bearer(request):
raise Unauthorized("Unauthorized", scheme="Bearer")
@app.route('/invalid')
@app.route("/invalid")
def handler_invalid(request):
raise InvalidUsage("OK")
@app.route('/abort/401')
@app.route("/abort/401")
def handler_401_error(request):
abort(401)
@app.route('/abort')
@app.route("/abort")
def handler_500_error(request):
abort(500)
return text("OK")
@app.route('/divide_by_zero')
def handle_unhandled_exception(request):
1 / 0
@app.route("/abort/message")
def handler_abort_message(request):
abort(500, message="Abort")
@app.route('/error_in_error_handler_handler')
@app.route("/divide_by_zero")
def handle_unhandled_exception(request):
_ = 1 / 0
@app.route("/error_in_error_handler_handler")
def custom_error_handler(request):
raise SanicExceptionTestException('Dummy message!')
raise SanicExceptionTestException("Dummy message!")
@app.exception(SanicExceptionTestException)
def error_in_error_handler_handler(request, exception):
1 / 0
_ = 1 / 0
return app
def test_catch_exception_list():
app = Sanic('exception_list')
def test_catch_exception_list(app):
@app.exception([SanicExceptionTestException, NotFound])
def exception_list(request, exception):
return text("ok")
@app.route('/')
@app.route("/")
def exception(request):
raise SanicExceptionTestException("You won't see me")
request, response = app.test_client.get('/random')
assert response.text == 'ok'
request, response = app.test_client.get("/random")
assert response.text == "ok"
request, response = app.test_client.get('/')
assert response.text == 'ok'
request, response = app.test_client.get("/")
assert response.text == "ok"
def test_no_exception(exception_app):
"""Test that a route works without an exception"""
request, response = exception_app.test_client.get('/')
request, response = exception_app.test_client.get("/")
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
def test_server_error_exception(exception_app):
"""Test the built-in ServerError exception works"""
request, response = exception_app.test_client.get('/error')
request, response = exception_app.test_client.get("/error")
assert response.status == 500
def test_invalid_usage_exception(exception_app):
"""Test the built-in InvalidUsage exception works"""
request, response = exception_app.test_client.get('/invalid')
request, response = exception_app.test_client.get("/invalid")
assert response.status == 400
def test_not_found_exception(exception_app):
"""Test the built-in NotFound exception works"""
request, response = exception_app.test_client.get('/404')
request, response = exception_app.test_client.get("/404")
assert response.status == 404
def test_forbidden_exception(exception_app):
"""Test the built-in Forbidden exception"""
request, response = exception_app.test_client.get('/403')
request, response = exception_app.test_client.get("/403")
assert response.status == 403
def test_unauthorized_exception(exception_app):
"""Test the built-in Unauthorized exception"""
request, response = exception_app.test_client.get('/401')
request, response = exception_app.test_client.get("/401")
assert response.status == 401
request, response = exception_app.test_client.get('/401/basic')
request, response = exception_app.test_client.get("/401/basic")
assert response.status == 401
assert response.headers.get('WWW-Authenticate') is not None
assert response.headers.get('WWW-Authenticate') == 'Basic realm="Sanic"'
assert response.headers.get("WWW-Authenticate") is not None
assert response.headers.get("WWW-Authenticate") == 'Basic realm="Sanic"'
request, response = exception_app.test_client.get('/401/digest')
request, response = exception_app.test_client.get("/401/digest")
assert response.status == 401
auth_header = response.headers.get('WWW-Authenticate')
auth_header = response.headers.get("WWW-Authenticate")
assert auth_header is not None
assert auth_header.startswith('Digest')
assert auth_header.startswith("Digest")
assert 'qop="auth, auth-int"' in auth_header
assert 'algorithm="MD5"' in auth_header
assert 'nonce="abcdef"' in auth_header
assert 'opaque="zyxwvu"' in auth_header
request, response = exception_app.test_client.get('/401/bearer')
request, response = exception_app.test_client.get("/401/bearer")
assert response.status == 401
assert response.headers.get('WWW-Authenticate') == "Bearer"
assert response.headers.get("WWW-Authenticate") == "Bearer"
def test_handled_unhandled_exception(exception_app):
"""Test that an exception not built into sanic is handled"""
request, response = exception_app.test_client.get('/divide_by_zero')
request, response = exception_app.test_client.get("/divide_by_zero")
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
assert soup.h1.text == 'Internal Server Error'
soup = BeautifulSoup(response.body, "html.parser")
assert soup.h1.text == "Internal Server Error"
message = " ".join(soup.p.text.split())
assert message == (
"The server encountered an internal error and "
"cannot complete your request.")
"cannot complete your request."
)
def test_exception_in_exception_handler(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = exception_app.test_client.get(
'/error_in_error_handler_handler')
"/error_in_error_handler_handler"
)
assert response.status == 500
assert response.body == b'An error occurred while handling an error'
assert response.body == b"An error occurred while handling an error"
def test_exception_in_exception_handler_debug_off(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = exception_app.test_client.get(
'/error_in_error_handler_handler',
debug=False)
"/error_in_error_handler_handler", debug=False
)
assert response.status == 500
assert response.body == b'An error occurred while handling an error'
assert response.body == b"An error occurred while handling an error"
def test_exception_in_exception_handler_debug_on(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = exception_app.test_client.get(
'/error_in_error_handler_handler',
debug=True)
"/error_in_error_handler_handler", debug=True
)
assert response.status == 500
assert response.body.startswith(b'Exception raised in exception ')
assert response.body.startswith(b"Exception raised in exception ")
def test_abort(exception_app):
"""Test the abort function"""
request, response = exception_app.test_client.get('/abort/401')
request, response = exception_app.test_client.get("/abort/401")
assert response.status == 401
request, response = exception_app.test_client.get('/abort')
request, response = exception_app.test_client.get("/abort")
assert response.status == 500
request, response = exception_app.test_client.get("/abort/message")
assert response.status == 500
assert response.text == "Error: Abort"

View File

@@ -4,38 +4,39 @@ from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.handlers import ErrorHandler
from bs4 import BeautifulSoup
exception_handler_app = Sanic('test_exception_handler')
exception_handler_app = Sanic("test_exception_handler")
@exception_handler_app.route('/1')
@exception_handler_app.route("/1")
def handler_1(request):
raise InvalidUsage("OK")
@exception_handler_app.route('/2')
@exception_handler_app.route("/2")
def handler_2(request):
raise ServerError("OK")
@exception_handler_app.route('/3')
@exception_handler_app.route("/3")
def handler_3(request):
raise NotFound("OK")
@exception_handler_app.route('/4')
@exception_handler_app.route("/4")
def handler_4(request):
foo = bar # noqa -- F821 undefined name 'bar' is done to throw exception
foo = bar # noqa -- F821 undefined name 'bar' is done to throw exception
return text(foo)
@exception_handler_app.route('/5')
@exception_handler_app.route("/5")
def handler_5(request):
class CustomServerError(ServerError):
pass
raise CustomServerError('Custom server error')
raise CustomServerError("Custom server error")
@exception_handler_app.route('/6/<arg:int>')
@exception_handler_app.route("/6/<arg:int>")
def handler_6(request, arg):
try:
foo = 1 / arg
@@ -50,67 +51,67 @@ def handler_exception(request, exception):
def test_invalid_usage_exception_handler():
request, response = exception_handler_app.test_client.get('/1')
request, response = exception_handler_app.test_client.get("/1")
assert response.status == 400
def test_server_error_exception_handler():
request, response = exception_handler_app.test_client.get('/2')
request, response = exception_handler_app.test_client.get("/2")
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
def test_not_found_exception_handler():
request, response = exception_handler_app.test_client.get('/3')
request, response = exception_handler_app.test_client.get("/3")
assert response.status == 200
def test_text_exception__handler():
request, response = exception_handler_app.test_client.get('/random')
request, response = exception_handler_app.test_client.get("/random")
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
def test_html_traceback_output_in_debug_mode():
request, response = exception_handler_app.test_client.get(
'/4', debug=True)
request, response = exception_handler_app.test_client.get("/4", debug=True)
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
soup = BeautifulSoup(response.body, "html.parser")
html = str(soup)
assert 'response = handler(request, *args, **kwargs)' in html
assert 'handler_4' in html
assert 'foo = bar' in html
assert "response = handler(request, *args, **kwargs)" in html
assert "handler_4" in html
assert "foo = bar" in html
summary_text = " ".join(soup.select('.summary')[0].text.split())
summary_text = " ".join(soup.select(".summary")[0].text.split())
assert (
"NameError: name 'bar' "
"is not defined while handling path /4") == summary_text
"NameError: name 'bar' " "is not defined while handling path /4"
) == summary_text
def test_inherited_exception_handler():
request, response = exception_handler_app.test_client.get('/5')
request, response = exception_handler_app.test_client.get("/5")
assert response.status == 200
def test_chained_exception_handler():
request, response = exception_handler_app.test_client.get(
'/6/0', debug=True)
"/6/0", debug=True
)
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
soup = BeautifulSoup(response.body, "html.parser")
html = str(soup)
assert 'response = handler(request, *args, **kwargs)' in html
assert 'handler_6' in html
assert 'foo = 1 / arg' in html
assert 'ValueError' in html
assert 'The above exception was the direct cause' in html
assert "response = handler(request, *args, **kwargs)" in html
assert "handler_6" in html
assert "foo = 1 / arg" in html
assert "ValueError" in html
assert "The above exception was the direct cause" in html
summary_text = " ".join(soup.select('.summary')[0].text.split())
summary_text = " ".join(soup.select(".summary")[0].text.split())
assert (
"ZeroDivisionError: division by zero "
"while handling path /6/0") == summary_text
"ZeroDivisionError: division by zero " "while handling path /6/0"
) == summary_text
def test_exception_handler_lookup():
@@ -131,7 +132,8 @@ def test_exception_handler_lookup():
try:
ModuleNotFoundError
except:
except Exception:
class ModuleNotFoundError(ImportError):
pass
@@ -143,12 +145,12 @@ def test_exception_handler_lookup():
assert handler.lookup(ImportError()) == import_error_handler
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
assert handler.lookup(CustomError()) == custom_error_handler
assert handler.lookup(ServerError('Error')) == server_error_handler
assert handler.lookup(CustomServerError('Error')) == server_error_handler
assert handler.lookup(ServerError("Error")) == server_error_handler
assert handler.lookup(CustomServerError("Error")) == server_error_handler
# once again to ensure there is no caching bug
assert handler.lookup(ImportError()) == import_error_handler
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
assert handler.lookup(CustomError()) == custom_error_handler
assert handler.lookup(ServerError('Error')) == server_error_handler
assert handler.lookup(CustomServerError('Error')) == server_error_handler
assert handler.lookup(ServerError("Error")) == server_error_handler
assert handler.lookup(CustomServerError("Error")) == server_error_handler

58
tests/test_helpers.py Normal file
View File

@@ -0,0 +1,58 @@
from sanic import helpers
def test_has_message_body():
tests = (
(100, False),
(102, False),
(204, False),
(200, True),
(304, False),
(400, True),
)
for status_code, expected in tests:
assert helpers.has_message_body(status_code) is expected
def test_is_entity_header():
tests = (
("allow", True),
("extension-header", True),
("", False),
("test", False),
)
for header, expected in tests:
assert helpers.is_entity_header(header) is expected
def test_is_hop_by_hop_header():
tests = (
("connection", True),
("upgrade", True),
("", False),
("test", False),
)
for header, expected in tests:
assert helpers.is_hop_by_hop_header(header) is expected
def test_remove_entity_headers():
tests = (
({}, {}),
({"Allow": "GET, POST, HEAD"}, {}),
(
{
"Content-Type": "application/json",
"Expires": "Wed, 21 Oct 2015 07:28:00 GMT",
"Foo": "Bar",
},
{"Expires": "Wed, 21 Oct 2015 07:28:00 GMT", "Foo": "Bar"},
),
(
{"Allow": "GET, POST, HEAD", "Content-Location": "/test"},
{"Content-Location": "/test"},
),
)
for header, expected in tests:
assert helpers.remove_entity_headers(header) == expected

View File

@@ -3,66 +3,32 @@ from sanic import Sanic
import asyncio
from asyncio import sleep as aio_sleep
from sanic.response import text
from sanic.config import Config
from sanic import server
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
try:
try:
import packaging # direct use
except ImportError:
# setuptools v39.0 and above.
try:
from setuptools.extern import packaging
except ImportError:
# Before setuptools v39.0
from pkg_resources.extern import packaging
version = packaging.version
except ImportError:
raise RuntimeError("The 'packaging' library is missing.")
aiohttp_version = version.parse(aiohttp.__version__)
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
class ReuseableTCPConnector(TCPConnector):
def __init__(self, *args, **kwargs):
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
self.old_proto = None
if aiohttp_version >= version.parse('3.3.0'):
async def connect(self, req, traces, timeout):
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req, traces, timeout)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
"We got a new connection, wanted the same one!")
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
elif aiohttp_version >= version.parse('3.0.0'):
async def connect(self, req, traces=None):
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req, traces=traces)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
"We got a new connection, wanted the same one!")
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
else:
async def connect(self, req):
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
"We got a new connection, wanted the same one!")
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
async def connect(self, req, *args, **kwargs):
new_conn = await super(ReuseableTCPConnector, self).connect(
req, *args, **kwargs
)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
"We got a new connection, wanted the same one!"
)
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
class ReuseableSanicTestClient(SanicTestClient):
@@ -78,31 +44,39 @@ class ReuseableSanicTestClient(SanicTestClient):
# Copied from SanicTestClient, but with some changes to reuse the
# same loop for the same app.
def _sanic_endpoint_test(
self, method='get', uri='/', gather_request=True,
debug=False, server_kwargs={},
*request_args, **request_kwargs):
self,
method="get",
uri="/",
gather_request=True,
debug=False,
server_kwargs={"return_asyncio_server": True},
*request_args,
**request_kwargs
):
loop = self._loop
results = [None, None]
exceptions = []
do_kill_server = request_kwargs.pop('end_server', False)
do_kill_server = request_kwargs.pop("end_server", False)
if gather_request:
def _collect_request(request):
if results[0] is None:
results[0] = request
self.app.request_middleware.appendleft(_collect_request)
@self.app.listener('after_server_start')
@self.app.listener("after_server_start")
async def _collect_response(loop):
try:
if do_kill_server:
request_kwargs['end_session'] = True
request_kwargs["end_session"] = True
response = await self._local_request(
method, uri, *request_args,
**request_kwargs)
method, uri, *request_args, **request_kwargs
)
results[-1] = response
except Exception as e2:
import traceback
traceback.print_tb(e2.__traceback__)
exceptions.append(e2)
# Don't stop here! self.app.stop()
@@ -110,23 +84,25 @@ class ReuseableSanicTestClient(SanicTestClient):
if self._server is not None:
_server = self._server
else:
_server_co = self.app.create_server(host=HOST, debug=debug,
port=PORT, **server_kwargs)
_server_co = self.app.create_server(
host=HOST, debug=debug, port=PORT, **server_kwargs
)
server.trigger_events(
self.app.listeners['before_server_start'], loop)
self.app.listeners["before_server_start"], loop
)
try:
loop._stopping = False
http_server = loop.run_until_complete(_server_co)
except Exception as e1:
import traceback
traceback.print_tb(e1.__traceback__)
raise e1
self._server = _server = http_server
server.trigger_events(
self.app.listeners['after_server_start'], loop)
self.app.listeners['after_server_start'].pop()
server.trigger_events(self.app.listeners["after_server_start"], loop)
self.app.listeners["after_server_start"].pop()
if do_kill_server:
try:
@@ -136,60 +112,66 @@ class ReuseableSanicTestClient(SanicTestClient):
self.app.stop()
except Exception as e3:
import traceback
traceback.print_tb(e3.__traceback__)
exceptions.append(e3)
if exceptions:
raise ValueError(
"Exception during request: {}".format(exceptions))
raise ValueError("Exception during request: {}".format(exceptions))
if gather_request:
self.app.request_middleware.pop()
try:
request, response = results
return request, response
except:
except Exception:
raise ValueError(
"Request and response object expected, got ({})".format(
results))
results
)
)
else:
try:
return results[-1]
except:
except Exception:
raise ValueError(
"Request object expected, got ({})".format(results))
"Request object expected, got ({})".format(results)
)
# Copied from SanicTestClient, but with some changes to reuse the
# same TCPConnection and the sane ClientSession more than once.
# Note, you cannot use the same session if you are in a _different_
# loop, so the changes above are required too.
async def _local_request(self, method, uri, cookies=None, *args,
**kwargs):
request_keepalive = kwargs.pop('request_keepalive',
Config.KEEP_ALIVE_TIMEOUT)
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
request_keepalive = kwargs.pop(
"request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"]
)
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=self.port, uri=uri)
do_kill_session = kwargs.pop('end_session', False)
url = "http://{host}:{port}{uri}".format(
host=HOST, port=self.port, uri=uri
)
do_kill_session = kwargs.pop("end_session", False)
if self._session:
session = self._session
else:
if self._tcp_connector:
conn = self._tcp_connector
else:
conn = ReuseableTCPConnector(verify_ssl=False,
loop=self._loop,
keepalive_timeout=
request_keepalive)
conn = ReuseableTCPConnector(
ssl=False,
loop=self._loop,
keepalive_timeout=request_keepalive,
)
self._tcp_connector = conn
session = aiohttp.ClientSession(cookies=cookies,
connector=conn,
loop=self._loop)
session = aiohttp.ClientSession(
cookies=cookies, connector=conn, loop=self._loop
)
self._session = session
async with getattr(session, method.lower())(
url, *args, **kwargs) as response:
url, *args, **kwargs
) as response:
try:
response.text = await response.text()
except UnicodeDecodeError:
@@ -197,9 +179,11 @@ class ReuseableSanicTestClient(SanicTestClient):
try:
response.json = await response.json()
except (JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError):
except (
JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError,
):
response.json = None
response.body = await response.read()
@@ -209,26 +193,28 @@ class ReuseableSanicTestClient(SanicTestClient):
return response
Config.KEEP_ALIVE_TIMEOUT = 2
Config.KEEP_ALIVE = True
keep_alive_timeout_app_reuse = Sanic('test_ka_timeout_reuse')
keep_alive_app_client_timeout = Sanic('test_ka_client_timeout')
keep_alive_app_server_timeout = Sanic('test_ka_server_timeout')
keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse")
keep_alive_app_client_timeout = Sanic("test_ka_client_timeout")
keep_alive_app_server_timeout = Sanic("test_ka_server_timeout")
keep_alive_timeout_app_reuse.config.update(CONFIG_FOR_TESTS)
keep_alive_app_client_timeout.config.update(CONFIG_FOR_TESTS)
keep_alive_app_server_timeout.config.update(CONFIG_FOR_TESTS)
@keep_alive_timeout_app_reuse.route('/1')
@keep_alive_timeout_app_reuse.route("/1")
async def handler1(request):
return text('OK')
return text("OK")
@keep_alive_app_client_timeout.route('/1')
@keep_alive_app_client_timeout.route("/1")
async def handler2(request):
return text('OK')
return text("OK")
@keep_alive_app_server_timeout.route('/1')
@keep_alive_app_server_timeout.route("/1")
async def handler3(request):
return text('OK')
return text("OK")
def test_keep_alive_timeout_reuse():
@@ -238,16 +224,14 @@ def test_keep_alive_timeout_reuse():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop)
headers = {
'Connection': 'keep-alive'
}
request, response = client.get('/1', headers=headers)
headers = {"Connection": "keep-alive"}
request, response = client.get("/1", headers=headers)
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
loop.run_until_complete(aio_sleep(1))
request, response = client.get('/1', end_server=True)
request, response = client.get("/1", end_server=True)
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
def test_keep_alive_client_timeout():
@@ -255,20 +239,17 @@ def test_keep_alive_client_timeout():
keep-alive timeout, client will try to create a new connection here."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_client_timeout,
loop)
headers = {
'Connection': 'keep-alive'
}
request, response = client.get('/1', headers=headers,
request_keepalive=1)
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
headers = {"Connection": "keep-alive"}
request, response = client.get("/1", headers=headers, request_keepalive=1)
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
loop.run_until_complete(aio_sleep(2))
exception = None
try:
request, response = client.get('/1', end_server=True,
request_keepalive=1)
request, response = client.get(
"/1", end_server=True, request_keepalive=1
)
except ValueError as e:
exception = e
assert exception is not None
@@ -283,23 +264,22 @@ def test_keep_alive_server_timeout():
broken server connection."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout,
loop)
headers = {
'Connection': 'keep-alive'
}
request, response = client.get('/1', headers=headers,
request_keepalive=60)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
headers = {"Connection": "keep-alive"}
request, response = client.get("/1", headers=headers, request_keepalive=60)
assert response.status == 200
assert response.text == 'OK'
assert response.text == "OK"
loop.run_until_complete(aio_sleep(3))
exception = None
try:
request, response = client.get('/1', request_keepalive=60,
end_server=True)
request, response = client.get(
"/1", request_keepalive=60, end_server=True
)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert "Connection reset" in exception.args[0] or \
"got a new connection" in exception.args[0]
assert (
"Connection reset" in exception.args[0]
or "got a new connection" in exception.args[0]
)

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