Compare commits

...

198 Commits
0.4.1 ... 0.5.0

Author SHA1 Message Date
Eli Uriegas
e5fdc7fdd0 Merge pull request #626 from seemethere/increment_050
Increment to 0.5.0
2017-04-11 16:05:26 -05:00
Eli Uriegas
015c87b5e1 Add traceback for better debugging 2017-04-11 16:02:57 -05:00
Eli Uriegas
d20a49e500 Lock chardet for now... 2017-04-11 16:02:49 -05:00
Eli Uriegas
084f0d27a3 Increment to 0.5.0 2017-04-11 15:19:00 -05:00
Eli Uriegas
522a0beec0 Merge pull request #622 from aryeh/provide_request_object
Allow a custom Request class to be passed in to Sanic
2017-04-11 15:13:05 -05:00
Raphael Deem
77a51c1e05 Merge pull request #617 from Sniedes722/master
Update Examples for 0.4.2
2017-04-10 19:07:29 -07:00
Raphael Deem
144f215705 Merge pull request #623 from qwIvan/master
Fixed #615
2017-04-10 18:58:31 -07:00
ivan
51b01b6b44 Merge branch 'master' of github.com:qwIvan/sanic 2017-04-10 18:33:43 +08:00
ivan
09885534c6 fixed #615 2017-04-10 18:31:28 +08:00
aryeh
b9dfec38c2 Break long line (> 80 chars) into 2 lines 2017-04-09 13:38:36 -04:00
aryeh
2ef8120073 Allow a custom Request class to be passed in to Sonic
Allowing a custom Request class to be defined would enable either a different Request class or a subclass of Request to be used, providing more flexibility.
2017-04-09 13:29:21 -04:00
Raphael Deem
52ff2e0e63 Merge pull request #621 from r0fls/620
fix python -m method of running
2017-04-08 13:32:17 -07:00
Raphael Deem
8cf7dce33f fix python -m method of running 2017-04-08 13:31:17 -07:00
Shawn Niederriter
9d3bb4a37a Updated examples in-line with response docs 2017-04-06 19:47:25 +00:00
Shawn Niederriter
c30437448b Updated aiohttp & run_async examples, added redirect 2017-04-06 19:42:05 +00:00
Raphael Deem
7e3496f8aa Merge pull request #614 from dkruchinin/middleware
Response middleware should be called even if server replies with an error
2017-04-06 11:49:44 -07:00
Shawn Niederriter
46ac79f4dc Update run_async demo 2017-04-06 14:39:54 -04:00
Shawn Niederriter
833b14e353 Updated aiohttp example. 2017-04-06 13:33:29 -04:00
Eli Uriegas
e9eca25792 Merge pull request #616 from r0fls/610
use socket.set_inheritable instead of os version
2017-04-04 15:17:31 -05:00
Raphael Deem
1854ad133c use socket.set_inheritable instead of os version 2017-04-04 13:13:52 -07:00
Eli Uriegas
2b5e723ea5 Merge pull request #519 from youknowone/recall-379
Add #379 again and make related test rework
2017-04-04 15:08:57 -05:00
Raphael Deem
9a18906edd Merge pull request #612 from dkruchinin/prom
Add prometheus extension
2017-04-04 11:06:43 -07:00
Raphael Deem
93cb7582c2 Merge branch 'master' into prom 2017-04-04 11:06:32 -07:00
Raphael Deem
b4529639f6 Merge pull request #611 from ashleysommer/master
Add Sanic-RestPlus to extensions.
2017-04-04 11:06:02 -07:00
Dan Kruchinin
f0a59fccf8 flake8-related fixes 2017-04-04 17:19:45 +01:00
Dan Kruchinin
46dbaf95a6 Response middleware should be called even if server replies with error 2017-04-04 15:55:43 +01:00
Dan Kruchinin
d418b03708 Add prometheus extension 2017-04-04 14:22:31 +01:00
Ashley Sommer
765e90ecfa update extensions
Add Sanic-RestPlus!
2017-04-04 13:40:59 +10:00
Ashley Sommer
ff1e88dde6 Merge pull request #2 from channelcat/master
update to latest sanic
2017-04-04 13:39:21 +10:00
Eli Uriegas
62ebcba647 Add graphql integration extension
Closes #579
2017-04-03 14:45:18 -05:00
Jeong YunWon
429e90183b Add #379 again and make related test rework
Original PR: https://github.com/channelcat/sanic/pull/379
2017-04-03 18:56:39 +09:00
Raphael Deem
875790e862 Merge pull request #606 from monobot/master
Fix URL Parse Error #599
2017-04-02 12:16:24 -07:00
monobot
25edbe6805 update docs 2017-04-02 02:28:16 +01:00
monobot
e148b50d6a Merge remote-tracking branch 'upstream/master' 2017-04-02 00:02:49 +01:00
Raphael Deem
06d46d56cd Merge pull request #602 from jkbbwr/fix/env-install
Flake8 cleanup. Setup environmental variables.
2017-03-31 10:42:21 -07:00
Jakob Bowyer
edd8770c67 Restored tests to upstream/master 2017-03-31 08:53:46 +01:00
Jakob Bowyer
daedda8547 Checked out original tests 2017-03-31 08:51:12 +01:00
Raphael Deem
df9d897e75 Merge pull request #607 from nosahama/hotfix/docs-cookie-typo-fix
Typo Fix in docs/sanic/cookies.md
2017-03-30 16:24:58 -07:00
nosaevb
fcd8e5e5ad Typo Fix in docs/sanic/cookies.md 2017-03-30 23:02:46 +01:00
monobot
6c003f71f4 Merge remote-tracking branch 'upstream/master' 2017-03-29 23:54:11 +01:00
monobot
5b704478d9 raw_args for request objects 2017-03-29 22:06:54 +01:00
Eli Uriegas
60eb528d68 Merge pull request #491 from r0fls/remove-stop-event
remove stop_event
2017-03-29 07:08:55 -05:00
Jakob Bowyer
1cf730d957 Added usage documentation for optional installs 2017-03-29 10:12:24 +01:00
Raphael Deem
171110b445 Merge pull request #604 from seemethere/add_docker_unittest_support
Fixing the unittests
2017-03-29 02:03:54 -07:00
Jakob Bowyer
22699db855 Moved skips to seperate pull request 2017-03-29 09:16:53 +01:00
Eli Uriegas
18405b3908 There was a line missing here? 2017-03-28 22:57:58 -05:00
Eli Uriegas
f0a55b5cbb Fix line length again... 2017-03-28 22:51:23 -05:00
Eli Uriegas
04a0774ee5 Fix line length 2017-03-28 22:51:23 -05:00
Eli Uriegas
3a8cfb1f45 Make these tests not so far apart 2017-03-28 22:51:23 -05:00
Eli Uriegas
dcc19d17d4 Lock to aiohttp 1.3.5 for now 2017-03-28 22:51:23 -05:00
Eli Uriegas
1ef69adc6f Simplify this as well, it replicated effort 2017-03-28 22:51:23 -05:00
Eli Uriegas
75a4df0f32 Simplify this, it had a lot of fluff 2017-03-28 22:51:23 -05:00
Eli Uriegas
8ba1b5fc35 Add docker support for local unit testing
Addresses consistency across different OS's by making it very similar to
the base Travis image.
2017-03-28 22:51:23 -05:00
Eli Uriegas
a09471ac6c Merge pull request #600 from monobot/master
added asyncorm example
2017-03-28 22:50:20 -05:00
Eli Uriegas
a916eea684 Merge pull request #601 from SakuraSound/master
Detailed example with logging, database access, environment variables, and basic middleware
2017-03-28 22:49:26 -05:00
Eli Uriegas
511998d8e1 Merge pull request #573 from r0fls/env-config
allow setting config from individual env variables
2017-03-28 22:24:37 -05:00
Joir-dan Gumbs
e3cf50f791 Changed out redis middleware for redis listeners (open/close). Fleshed out the payloads of both endpoints. Added comment about required packages. 2017-03-28 15:00:23 -07:00
Jakob Bowyer
42ba5298a7 Flake8 cleanup. Setup environmental variables.
Skipping broken tests unrelated.
2017-03-28 10:50:09 +01:00
Joir-dan Gumbs
ee79750a22 Cleaned up functions. Added extra middleware function to log endpoint being called. Added documentation to make easier to understand. 2017-03-28 01:22:36 -07:00
Raphael Deem
1787f8617f Merge pull request #592 from weargoggles/patch-1
Document synchronous response.write in streaming
2017-03-27 20:14:20 -07:00
Joir-dan Gumbs
748ca28185 Created detailed example of using sanic. Adds configurations based on various environment variables, handles database access (using aioredis), uses middleware to check for db object and attach it to request object, and logs events to a logfile (which is set using environment variables). 2017-03-27 15:42:13 -07:00
monobot
9c68d713ba added asyncorm example 2017-03-27 22:47:35 +01:00
Raphael Deem
fc69678206 Merge remote-tracking branch 'upstream/master' into remove-stop-event 2017-03-26 15:59:31 -07:00
Raphael Deem
aebd717039 fix merge conflict 2017-03-26 15:49:58 -07:00
Raphael Deem
1ddb01ac44 remove stop_event 2017-03-26 15:48:41 -07:00
Raphael Deem
3e279cd670 Merge pull request #593 from itielshwartz/master
add sanic-nginx-docker-example to extensions.md
2017-03-26 11:50:45 -07:00
Raphael Deem
724c03630a Update extensions.md 2017-03-26 11:49:15 -07:00
itiel
b00b2561e5 add sanic-nginx-docker-example to extensions.md 2017-03-26 21:16:03 +03:00
Raphael Deem
c5b50fe3cf allow setting config from individual env variables 2017-03-25 17:45:55 -07:00
Raphael Deem
df9884de3c Merge pull request #576 from matuusu/master
fix http status code not propagating in response
2017-03-24 19:23:20 -07:00
Pete Wildsmith
65ae7669f9 Document synchronous response.write in streaming
The Streaming section of the docs was updated to make clear that a synchronous write should be used in the callback, but this section was not updated.
2017-03-24 10:11:30 +00:00
Raphael Deem
179606feb1 Merge pull request #590 from r0fls/blueprint-strict-slash
add blueprint strict_slashes
2017-03-23 18:42:21 -07:00
Raphael Deem
536140340e Merge pull request #585 from subyraman/listener-docs
add docs for server lifecycle listeners and `add_task`
2017-03-23 18:38:48 -07:00
Raphael Deem
5d293df64b add blueprint strict_slashes 2017-03-23 18:37:06 -07:00
Suby Raman
6188891a53 add-listeners-docs 2017-03-23 15:49:23 -04:00
Raphael Deem
9774661cfe Merge pull request #580 from skytoup/master
Fix testing not support binary file
2017-03-23 12:22:08 -07:00
Raphael Deem
563bc34fb5 Merge pull request #578 from messense/feature/gunicorn-deploy-doc
Add documentation for Gunicorn worker
2017-03-23 12:20:00 -07:00
Raphael Deem
9c95ab3a28 Merge pull request #584 from subyraman/add-decorators-info
add decorator docs
2017-03-23 12:19:23 -07:00
Suby Raman
b776c37b36 add decorator docs 2017-03-23 15:16:46 -04:00
Eli Uriegas
5b3f92b70f Merge pull request #577 from seemethere/fix_tests_after_aiohttp_2
Hotfixes tests failing from URL object change
2017-03-23 13:56:16 -05:00
ivan
1562b81522 add arg load_body in testing 2017-03-23 20:48:57 +08:00
ivan
be1016ace6 merge upstream 2017-03-23 20:08:19 +08:00
ivan
ee27c689e1 commented aiohttp load response body in testing 2017-03-23 20:06:39 +08:00
skytoup
fdbf452ced 1. try...catch aiohttp encode response body to text in test_client
2. add tests static binary file
2017-03-23 15:22:00 +08:00
messense
3d9927dee0 Add documentation for Gunicorn worker 2017-03-23 09:12:23 +08:00
Raphael Deem
1456b128d2 Merge pull request #572 from sourcepirate/master
Removed raw string in cookies documentation
2017-03-22 14:55:20 -07:00
Eli Uriegas
166f77cb86 Merge pull request #545 from messense/feature/gunicorn-worker
Addition of a gunicorn worker
2017-03-22 16:27:06 -05:00
Eli Uriegas
5577838905 Hotfixes tests failing from URL object change
aiohttp decided to use yarl for their new URL objects so that they
aren't plain strings anymore which means that this single test fails.
Not a huge change but this should fix the testing suite.
2017-03-22 16:21:35 -05:00
matuusu
9c15982299 Update response.py
fix status code not propagating from response.stream to response.StreamingHTTPResponse
2017-03-22 12:40:40 +01:00
sourcepirate
63c24122db Removed raw string in cookies documentation 2017-03-22 06:39:23 +05:30
messense
1396ca903d Fix before_stop event 2017-03-20 14:27:02 +08:00
messense
d1fb5bdc30 Fix async before_server_start hook bug 2017-03-20 14:25:32 +08:00
messense
e27812bf3e Set signal.stopped = True on closing 2017-03-20 14:25:32 +08:00
messense
11a3cf9b99 Add signal handling 2017-03-20 14:25:32 +08:00
messense
a90d70feae Check connections is not None 2017-03-20 14:25:32 +08:00
messense
466b34735c Set app.is_running to True 2017-03-20 14:25:32 +08:00
messense
7ca9116e37 Trigger before_stop before closing server, after_stop before closing
loop
2017-03-20 14:25:31 +08:00
messense
decd3e737c Flake8 fix 2017-03-20 14:25:31 +08:00
messense
f35442ad1b Fix RuntimeError: this event loop is already running 2017-03-20 14:25:31 +08:00
messense
2b296435b3 Trigger events 2017-03-20 14:25:31 +08:00
messense
19ee1dfecc Gunicorn worker 2017-03-20 14:25:31 +08:00
Raphael Deem
7da4596ef8 Merge pull request #567 from Sniedes722/master
Added Sanic-OAuth to extensions.
2017-03-19 14:20:19 -07:00
Shawn Niederriter
a379ef6781 Added Sanic-OAuth to extensions. 2017-03-18 23:56:11 -04:00
Raphael Deem
7beb065be3 Merge pull request #562 from lixxu/master
update function name as it not halt request actually
2017-03-18 13:17:24 -07:00
Raphael Deem
38b9091513 Merge pull request #443 from sourcepirate/master
Fixed sanic peewee example.
2017-03-17 13:12:39 -07:00
Raphael Deem
96db3c9601 Merge pull request #564 from messense/feature/aioredis-example
Add an aioredis example
2017-03-17 13:11:19 -07:00
Raphael Deem
43c4fc8e33 Merge pull request #559 from r0fls/557
accept strict_slash routes
2017-03-17 13:10:26 -07:00
messense
986ff101ce Add an aioredis example 2017-03-17 14:16:13 +08:00
lixxu
94c83c445f fix broken table 2017-03-17 14:01:54 +08:00
lixxu
625865412f update function name as it not halt request actually 2017-03-17 13:12:17 +08:00
Raphael Deem
46677e69ce accept strict_slash routes 2017-03-16 11:46:07 -07:00
Eli Uriegas
5fbca5b823 Merge pull request #561 from kdelwat/master
Fix ReadTheDocs build errors
2017-03-16 09:02:39 -05:00
Eli Uriegas
879fab120f Merge pull request #560 from miguelgrinberg/cancel-websocket-tasks
cancel websocket tasks if server is stopped
2017-03-16 09:02:30 -05:00
Cadel Watson
391b24bc17 Add websockets dependency to ReadTheDocs environment 2017-03-16 17:01:49 +11:00
Cadel Watson
d713533d26 Fix docstring formatting errors 2017-03-16 16:52:18 +11:00
Cadel Watson
24f745a334 Fix formatting errors in RST files 2017-03-16 16:51:02 +11:00
Cadel Watson
86f3101861 Add autodoc extension to Sphinx configuration 2017-03-16 16:50:33 +11:00
Miguel Grinberg
fd823c63ab cancel websocket tasks if server is stopped 2017-03-15 22:45:19 -07:00
Raphael Deem
fa69892f70 Merge pull request #558 from Zheaoli/master
Add a new example by using aiomysql
2017-03-15 20:05:37 -07:00
lizheao
cfc53d0d26 Change some code in sanic aiomysql code 2017-03-15 14:42:22 +08:00
lizheao
97c2056e4a Change some code in sanic aiomysql code 2017-03-15 14:41:54 +08:00
lizheao
0ad0164171 Change some code in sanic aiomysql code 2017-03-15 14:41:38 +08:00
lizheao
df0e285b6f Add a new example 2017-03-15 14:12:37 +08:00
Raphael Deem
e92f1b8c28 Merge pull request #556 from AntonDnepr/fix-414
Small changes to the docs and tests for the #414
2017-03-14 21:07:55 -07:00
Anton Zhyrnyi
410f86c960 fix for docs&tests 2017-03-14 20:53:58 +02:00
Raphael Deem
85f27320e7 Merge pull request #553 from ashleysommer/ashleysommer-add-dispatch-extension
Add Dispatcher Extension to Extensions page in Docs
2017-03-13 17:00:46 -07:00
Raphael Deem
9a3fac90e1 Merge pull request #551 from messense/feature/documentation-links
Add hyperlinks in response documentation
2017-03-13 17:00:00 -07:00
Raphael Deem
6984f6eec4 Merge pull request #549 from ai0/websocket-scheme
add websocket scheme in request
2017-03-13 16:59:47 -07:00
Ashley Sommer
d05f502fc8 Add Dispatcher Extension
Adds a link to the extension
https://github.com/ashleysommer/sanic-dispatcher
And a very short description
2017-03-14 08:37:53 +10:00
Jing Su
ba41ab8f67 fix typo 2017-03-13 18:36:22 +08:00
Jing Su
250bb7e29d add websocket secure scheme in request @messense 2017-03-13 18:34:43 +08:00
messense
48a26fd5df Add hyperlinks in response documentation 2017-03-13 16:20:12 +08:00
Jing Su
3af26540ec add websocket scheme in request 2017-03-13 13:28:35 +08:00
Raphael Deem
7d9de068d9 Merge pull request #544 from messense/feature/document-response
Add response documentation
2017-03-11 22:49:53 -08:00
messense
d174917a07 Add response documentation 2017-03-12 14:31:51 +08:00
Eli Uriegas
af398fc4c4 Merge pull request #543 from r0fls/windows-setup
windows setup
2017-03-11 21:57:15 -08:00
Raphael Deem
878ef446a2 refactor redundant print logic 2017-03-11 21:54:07 -08:00
Raphael Deem
668f6477bb fix spacing 2017-03-11 21:46:31 -08:00
Raphael Deem
01a770cbca windows setup 2017-03-11 19:32:38 -08:00
Raphael Deem
23a1174aa2 Merge pull request #542 from nszceta/master
Faster asyncpg queries via a global connection pool
2017-03-11 16:07:16 -08:00
Raphael Deem
414020e75b Merge pull request #541 from r0fls/speedup
remove default host attribute in router
2017-03-11 16:07:05 -08:00
Adam Gradzki
ed74bccad6 Faster asyncpg queries via a global connection pool
In my benchmarks I was able to obtain a 17% performance
improvement over the current asyncpg demo code with a shared
connection pool.

Resolves: #540
See also: #531
2017-03-11 17:55:57 -06:00
Raphael Deem
0eedde445c remove default host attribute in router 2017-03-11 15:39:48 -08:00
Raphael Deem
88bf78213f Merge pull request #512 from subyraman/fix-url-building
Fix `request.url` and other url properties
2017-03-10 00:38:16 -08:00
Raphael Deem
d342461a51 Merge pull request #535 from ai0/master
Update blueprints example
2017-03-10 00:36:02 -08:00
Raphael Deem
dffaaf8751 Merge pull request #533 from 38elements/patch-1
Fix bail_out()
2017-03-10 00:35:54 -08:00
Raphael Deem
313edadf47 Merge pull request #528 from r0fls/523
allow running with SSL via commandline
2017-03-10 00:35:46 -08:00
Raphael Deem
c9ce33dfe6 Merge pull request #524 from r0fls/exception-list
allow exceptions to be a list
2017-03-10 00:35:38 -08:00
Raphael Deem
0f50ac7205 Merge pull request #517 from r0fls/empty-json
return valid json in request.json
2017-03-10 00:35:26 -08:00
Raphael Deem
e807c08275 Merge pull request #536 from lixxu/master
add sanic-babel extension
2017-03-09 12:28:22 -08:00
Lix Xu
893977365c Update extensions.md
add babel extension
2017-03-09 23:58:02 +08:00
Jing Su
0cac45809f add blueprints websocket example 2017-03-09 18:33:34 +08:00
Jing Su
489ca3c207 use blueprint method instead of deprecated register_blueprint 2017-03-09 18:31:19 +08:00
38elements
313535c599 Fix bail_out() 2017-03-09 13:36:01 +09:00
Raphael Deem
7f1e0557c9 Merge pull request #532 from r0fls/example-asyncpg-close
close connection in asyncpg example
2017-03-08 18:51:44 -08:00
Raphael Deem
0860f84a39 close connection in asyncpg example 2017-03-08 18:50:41 -08:00
Raphael Deem
2fe9e78b6d Merge pull request #525 from r0fls/518
rename TestClient
2017-03-08 18:24:13 -08:00
Raphael Deem
2ba30f2022 allow running with SSL via commandline 2017-03-07 19:57:10 -08:00
Raphael Deem
b3b27cab34 Merge pull request #527 from r0fls/asyncpg-example
update asyncpg example
2017-03-07 19:34:10 -08:00
Raphael Deem
694207a86d update asyncpg example 2017-03-07 19:32:26 -08:00
Raphael Deem
90138c4bae return valid json in request.json 2017-03-07 18:03:45 -08:00
Raphael Deem
58a833e987 rename TestClient 2017-03-07 17:13:23 -08:00
Raphael Deem
86c5a569d5 allow exceptions to be a list 2017-03-07 16:22:23 -08:00
Eli Uriegas
19592e8eea Merge pull request #473 from subyraman/explore-streams-v2
Add `stream` method for streaming content, add docs and examples
2017-03-05 17:51:44 -08:00
Eli Uriegas
8e6678d526 Merge pull request #469 from miguelgrinberg/websocket-support
websocket support
2017-03-05 17:42:49 -08:00
Suby Raman
e792a1e030 add host test 2017-03-03 14:51:13 -05:00
Suby Raman
f0e818a28c add host test 2017-03-03 13:32:32 -05:00
Suby Raman
69bd63b742 add docs 2017-03-03 11:59:33 -05:00
Suby Raman
b40f30f2e5 fix tests 2017-03-03 11:49:35 -05:00
Suby Raman
1fbde87ec2 initial commit 2017-03-03 11:44:50 -05:00
Eli Uriegas
f9dc34c8fa Merge pull request #510 from jamesstidard/patch-2
Sanic EnvConfig
2017-03-02 22:21:16 -06:00
James Stidard
f7186f5331 Sanic EnvConfig
Services like Heroku force your hand into using environment variables. Made this to help.
2017-03-02 21:22:07 +00:00
Eli Uriegas
6a680e4db0 Merge pull request #509 from kcy1019/master
Fix exception_monitoring example
2017-03-02 10:48:05 -06:00
고창영(Chang-young Koh)
f6b69f412f Fix exception_monitoring example 2017-03-03 00:55:08 +09:00
Eli Uriegas
5aed18862d Merge pull request #506 from zenixls2/bugfix/bind-listener
special handling when sock is provided and number of workers > 1
2017-03-02 09:08:59 -06:00
(Zenix) Han-Sheng Huang
62bf213a6e pass flake8 2017-03-02 13:42:55 +09:00
(Zenix) Han-Sheng Huang
e5c32e9b48 special handling when sock is provided and number of workers > 1 2017-03-02 13:27:52 +09:00
Raphael Deem
b87dc37fbb Merge pull request #500 from r0fls/revert-498
Revert "add reuse_port to create_server"
2017-02-28 19:29:29 -08:00
Raphael Deem
002d4cb37c Revert "add reuse_port to create_server"
This reverts commit c3386dec84.
2017-02-28 19:27:42 -08:00
Raphael Deem
ff321fc355 Merge pull request #499 from r0fls/498
add reuse_port to create_server
2017-02-28 19:24:35 -08:00
Raphael Deem
c3386dec84 add reuse_port to create_server 2017-02-28 19:21:53 -08:00
Eli Uriegas
927d2761f7 Merge pull request #496 from bohea/master
rate limiting for sanic
2017-02-28 09:03:03 -06:00
bohea
3289e8403a Update extensions.md 2017-02-28 17:34:40 +08:00
James Stidard
104a7c7d05 Added app to websocket request (#1) 2017-02-27 22:35:28 -08:00
Miguel Grinberg
7560660ec7 handle timeouts and disconnects properly 2017-02-27 22:35:28 -08:00
Miguel Grinberg
40ccb4a0dd websocket documentation 2017-02-27 22:35:28 -08:00
Miguel Grinberg
f90288f5dc websocket routes in blueprints 2017-02-27 22:35:28 -08:00
Miguel Grinberg
3bf79898d9 websocket unit test 2017-02-27 22:35:28 -08:00
Miguel Grinberg
1d6e11ca10 addressed feedback 2017-02-27 22:35:28 -08:00
Miguel Grinberg
6e903ee7d5 websocket support, using websockets package 2017-02-27 22:35:28 -08:00
Raphael Deem
2dca53a696 remove stop_event 2017-02-26 16:37:48 -08:00
Suby Raman
d8a6d7e02f response.write should be synchronous for performance reasons 2017-02-22 10:42:16 -05:00
Suby Raman
1a8961587c more info in docs 2017-02-21 11:38:57 -05:00
Suby Raman
fa13ad8849 clean up imports 2017-02-21 11:36:45 -05:00
Suby Raman
8b23dec322 improve performance 2017-02-21 11:28:45 -05:00
Suby Raman
4e8aac4b41 rebase 2017-02-21 11:05:06 -05:00
sourcepirate
e6a828572a Changed method name from create to new 2017-02-16 11:07:49 +05:30
plasmashadow
6ea43d8e6d Fixed sanic_peewee error and added a simple interface to query and create 2017-02-16 10:58:52 +05:30
75 changed files with 2331 additions and 403 deletions

6
Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM python:3.6
ADD . /app
WORKDIR /app
RUN pip install tox

4
Makefile Normal file
View File

@@ -0,0 +1,4 @@
test:
find . -name "*.pyc" -delete
docker build -t sanic/test-image .
docker run -t sanic/test-image tox

View File

@@ -59,6 +59,13 @@ Installation
- ``python -m pip install sanic``
To install sanic without uvloop or json 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.
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic``
Documentation
-------------

View File

@@ -22,7 +22,7 @@ import sanic
# -- General configuration ------------------------------------------------
extensions = []
extensions = ['sphinx.ext.autodoc']
templates_path = ['_templates']
@@ -68,7 +68,6 @@ pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
@@ -80,13 +79,11 @@ html_theme = 'sphinx_rtd_theme'
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'Sanicdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
@@ -110,21 +107,14 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Sanic.tex', 'Sanic Documentation',
'Sanic contributors', 'manual'),
]
latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation',
'Sanic contributors', 'manual'), ]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'sanic', 'Sanic Documentation',
[author], 1)
]
man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
# -- Options for Texinfo output -------------------------------------------
@@ -132,13 +122,10 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Sanic', 'Sanic Documentation',
author, 'Sanic', 'One line description of project.',
'Miscellaneous'),
(master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic',
'One line description of project.', 'Miscellaneous'),
]
# -- Options for Epub output ----------------------------------------------
# Bibliographic Dublin Core info.
@@ -150,8 +137,6 @@ epub_copyright = copyright
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
# -- Custom Settings -------------------------------------------------------
suppress_warnings = ['image.nonlocal_uri']

View File

@@ -9,12 +9,14 @@ Guides
sanic/getting_started
sanic/routing
sanic/request_data
sanic/response
sanic/static_files
sanic/exceptions
sanic/middleware
sanic/blueprints
sanic/config
sanic/cookies
sanic/streaming
sanic/class_based_views
sanic/custom_protocol
sanic/ssl

View File

@@ -55,13 +55,18 @@ will look like:
Blueprints have much the same functionality as an application instance.
### WebSocket routes
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
decorator or `bp.add_websocket_route` method.
### Middleware
Using blueprints allows you to also register middleware globally.
```python
@bp.middleware
async def halt_request(request):
async def print_on_request(request):
print("I am a spy")
@bp.middleware('request')
@@ -111,7 +116,7 @@ bp = Blueprint('my_blueprint')
async def setup_connection(app, loop):
global database
database = mysql.connect(host='127.0.0.1'...)
@bp.listener('after_server_stop')
async def close_connection(app, loop):
await database.close()
@@ -137,7 +142,7 @@ blueprint_v2 = Blueprint('v2', url_prefix='/v2')
@blueprint_v1.route('/')
async def api_v1_root(request):
return text('Welcome to version 1 of our documentation')
@blueprint_v2.route('/')
async def api_v2_root(request):
return text('Welcome to version 2 of our documentation')

View File

@@ -48,6 +48,24 @@ app.add_route(SimpleView.as_view(), '/')
```
You can also use `async` syntax.
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
class SimpleAsyncView(HTTPMethodView):
async def get(self, request):
return text('I am async get method')
app.add_route(SimpleAsyncView.as_view(), '/')
```
## URL parameters
If you need any URL parameters, as discussed in the routing guide, include them
@@ -128,4 +146,4 @@ view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
app.add_route(view, '/')
```
Note: currently you cannot build a URL for a CompositionView using `url_for`.
Note: currently you cannot build a URL for a CompositionView using `url_for`.

View File

@@ -29,6 +29,14 @@ In general the convention is to only have UPPERCASE configuration parameters. Th
There are several ways how to load configuration.
### From environment variables.
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_vars` boolean to the Sanic constructor to override that:
```python
app = Sanic(load_vars=False)
```
### From an Object
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
@@ -71,8 +79,7 @@ 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 (sec) |
| Variable | Default | Description |
| ----------------- | --------- | --------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |

View File

@@ -5,7 +5,7 @@ both read and write cookies, which are stored as key-value pairs.
## Reading cookies
A user's cookies can be accessed `Request` object's `cookie` dictionary.
A user's cookies can be accessed via the `Request` object's `cookies` dictionary.
```python
from sanic.response import text
@@ -42,20 +42,20 @@ from sanic.response import text
@app.route("/cookie")
async def test(request):
response = text("Time to eat some cookies muahaha")
# This cookie will be set to expire in 0 seconds
del response.cookies['kill_me']
# This cookie will self destruct in 5 seconds
response.cookies['short_life'] = 'Glad to be here'
response.cookies['short_life']['max-age'] = 5
del response.cookies['favorite_color']
# This cookie will remain unchanged
response.cookies['favorite_color'] = 'blue'
response.cookies['favorite_color'] = 'pink'
del response.cookies['favorite_color']
return response
```

39
docs/sanic/decorators.md Normal file
View File

@@ -0,0 +1,39 @@
# Handler Decorators
Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.
## Authorization Decorator
Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response.
```python
from functools import wraps
from sanic.response import json
def authorized():
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
# run some method that checks the request
# for the client's authorization status
is_authorized = check_request_for_authorization_status(request)
if is_authorized:
# the user is authorized.
# run the handler method and return the response
response = await f(request, *args, **kwargs)
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return decorated_function
return decorator
@app.route("/")
@authorized()
async def test(request):
return json({status: 'authorized'})
```

View File

@@ -44,3 +44,15 @@ directly run by the interpreter.
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
```
## Running via Gunicorn
[Gunicorn](http://gunicorn.org/) Green Unicorn is a WSGI HTTP Server for UNIX.
Its a pre-fork worker model ported from Rubys Unicorn project.
In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker`
for Gunicorn `worker-class` argument:
```
gunicorn --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
```

View File

@@ -12,3 +12,13 @@ A list of Sanic extensions created by the community.
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
`Babel` library
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
- [Sanic-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.

View File

@@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
Sanic aspires to be simple:
-------------------
Sanic aspires to be simple
---------------------------
.. code:: python

View File

@@ -1,9 +1,13 @@
# Middleware
# Middleware And Listeners
Middleware are functions which are executed before or after requests to the
server. They can be used to modify the *request to* or *response from*
user-defined handler functions.
Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle.
## Middleware
There are two types of middleware: request and response. Both are declared
using the `@app.middleware` decorator, with the decorator's parameter being a
string representing its type: `'request'` or `'response'`. Response middleware
@@ -64,3 +68,45 @@ async def halt_request(request):
async def halt_response(request, response):
return text('I halted the response')
```
## Listeners
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
- `before_server_start`
- `after_server_start`
- `before_server_stop`
- `after_server_stop`
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
For example:
```python
@app.listener('before_server_start')
async def setup_db(app, loop):
app.db = await db_setup()
@app.listener('after_server_start')
async def notify_server_started(app, loop):
print('Server successfully started!')
@app.listener('before_server_stop')
async def notify_server_stopping(app, loop):
print('Server shutting down!')
@app.listener('after_server_stop')
async def close_db(app, loop):
await app.db.close()
```
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!')
app.add_task(notify_server_started_after_five_seconds())
```

View File

@@ -9,30 +9,34 @@ The following variables are accessible as properties on `Request` objects:
```python
from sanic.response import json
@app.route("/json")
def post_json(request):
return json({ "received": True, "message": request.json })
```
- `args` (dict) - Query string variables. A query string is the section of a
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
The request's `query_string` variable holds the unparsed string value.
```python
from sanic.response import json
@app.route("/query_string")
def query_string(request):
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
```
- `raw_args` (dict) - On many cases you would need to access the url arguments in
a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
`raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
```python
from sanic.response import json
@app.route("/files")
def post_json(request):
test_file = request.files.get('test')
@@ -50,7 +54,7 @@ The following variables are accessible as properties on `Request` objects:
```python
from sanic.response import json
@app.route("/form")
def post_json(request):
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
@@ -58,15 +62,15 @@ The following variables are accessible as properties on `Request` objects:
- `body` (bytes) - Posted raw body. This property allows retrieval of the
request's raw data, regardless of content type.
```python
from sanic.response import text
@app.route("/users", methods=["POST",])
def create_user(request):
return text("You are trying to create a user with the following POST: %s" % request.body)
```
- `ip` (str) - IP address of the requester.
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
@@ -85,6 +89,12 @@ The following variables are accessible as properties on `Request` objects:
return json({'status': 'production'})
```
- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar`
- `scheme`: The URL scheme associated with the request: `http` or `https`
- `host`: The host associated with the request: `localhost:8080`
- `path`: The path of the request: `/posts/1/`
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
## Accessing values using `get` and `getlist`

102
docs/sanic/response.md Normal file
View File

@@ -0,0 +1,102 @@
# Response
Use functions in `sanic.response` module to create responses.
## Plain Text
```python
from sanic import response
@app.route('/text')
def handle_request(request):
return response.text('Hello world!')
```
## HTML
```python
from sanic import response
@app.route('/html')
def handle_request(request):
return response.html('<p>Hello world!</p>')
```
## JSON
```python
from sanic import response
@app.route('/json')
def handle_request(request):
return response.json({'message': 'Hello world!'})
```
## File
```python
from sanic import response
@app.route('/file')
async def handle_request(request):
return await response.file('/srv/www/whatever.png')
```
## Streaming
```python
from sanic import response
@app.route("/streaming")
async def index(request):
async def streaming_fn(response):
response.write('foo')
response.write('bar')
return response.stream(streaming_fn, content_type='text/plain')
```
## Redirect
```python
from sanic import response
@app.route('/redirect')
def handle_request(request):
return response.redirect('/json')
```
## Raw
Response without encoding the body
```python
from sanic import response
@app.route('/raw')
def handle_request(request):
return response.raw('raw data')
```
## Modify headers or status
To modify headers or status code, pass the `headers` or `status` argument to those functions:
```python
from sanic import response
@app.route('/json')
def handle_request(request):
return response.json(
{'message': 'Hello world!'},
headers={'X-Served-By': 'sanic'},
status=200
)
```

View File

@@ -181,3 +181,37 @@ url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2,
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
```
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.
## WebSocket routes
Routes for the WebSocket protocol can be defined with the `@app.websocket`
decorator:
```python
@app.websocket('/feed')
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
```
Alternatively, the `app.add_websocket_route` method can be used instead of the
decorator:
```python
async def feed(request, ws):
pass
app.add_websocket_route(my_websocket_handler, '/feed')
```
Handlers for a WebSocket route are passed the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has `send`
and `recv` methods to send and receive data respectively.
WebSocket support requires the [websockets](https://github.com/aaugustin/websockets)
package by Aymeric Augustin.

View File

@@ -9,4 +9,12 @@ Optionally pass in an SSLContext:
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile")
app.run(host="0.0.0.0", port=8443, ssl=context)
app.run(host="0.0.0.0", port=8443, ssl=context)
You can also pass in the locations of a certificate and key as a dictionary:
.. code:: python
ssl = {'cert': "/path/to/cert", 'key': "/path/to/keyfile"}
app.run(host="0.0.0.0", port=8443, ssl=ssl)

32
docs/sanic/streaming.md Normal file
View File

@@ -0,0 +1,32 @@
# Streaming
Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows:
```python
from sanic import Sanic
from sanic.response import stream
app = Sanic(__name__)
@app.route("/")
async def test(request):
async def sample_streaming_fn(response):
response.write('foo,')
response.write('bar')
return stream(sample_streaming_fn, content_type='text/csv')
```
This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides:
```python
@app.route("/")
async def index(request):
async def stream_from_db(response):
conn = await asyncpg.connect(database='test')
async with conn.transaction():
async for record in conn.cursor('SELECT generate_series(0, 10)'):
response.write(record[0])
return stream(stream_from_db)
```

View File

@@ -15,4 +15,5 @@ dependencies:
- httptools>=0.0.9
- ujson>=1.35
- aiofiles>=0.3.0
- websockets>=3.2
- https://github.com/channelcat/docutils-fork/zipball/master

View File

@@ -1,5 +1,5 @@
from sanic import Sanic
from sanic.response import json
from sanic import response
import aiohttp
@@ -9,20 +9,18 @@ async def fetch(session, url):
"""
Use session object to perform 'get' request on url
"""
async with session.get(url) as response:
return await response.json()
async with session.get(url) as result:
return await result.json()
@app.route("/")
async def test(request):
"""
Download and serve example JSON
"""
@app.route('/')
async def handle_request(request):
url = "https://api.github.com/repos/channelcat/sanic"
async with aiohttp.ClientSession() as session:
response = await fetch(session, url)
return json(response)
result = await fetch(session, url)
return response.json(result)
app.run(host="0.0.0.0", port=8000, workers=2)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, workers=2)

View File

View File

@@ -0,0 +1,140 @@
from sanic import Sanic
from sanic.exceptions import NotFound
from sanic.response import json
from sanic.views import HTTPMethodView
from asyncorm import configure_orm
from asyncorm.exceptions import QuerysetError
from library.models import Book
from library.serializer import BookSerializer
app = Sanic(name=__name__)
@app.listener('before_server_start')
def orm_configure(sanic, loop):
db_config = {'database': 'sanic_example',
'host': 'localhost',
'user': 'sanicdbuser',
'password': 'sanicDbPass',
}
# configure_orm needs a dictionary with:
# * the database configuration
# * the application/s where the models are defined
orm_app = configure_orm({'loop': loop, # always use the sanic loop!
'db_config': db_config,
'modules': ['library', ], # list of apps
})
# orm_app is the object that orchestrates the whole ORM
# sync_db should be run only once, better do that as external command
# it creates the tables in the database!!!!
# orm_app.sync_db()
# for all the 404 lets handle the exceptions
@app.exception(NotFound)
def ignore_404s(request, exception):
return json({'method': request.method,
'status': exception.status_code,
'error': exception.args[0],
'results': None,
})
# now the propper sanic workflow
class BooksView(HTTPMethodView):
def arg_parser(self, request):
parsed_args = {}
for k, v in request.args.items():
parsed_args[k] = v[0]
return parsed_args
async def get(self, request):
filtered_by = self.arg_parser(request)
if filtered_by:
q_books = await Book.objects.filter(**filtered_by)
else:
q_books = await Book.objects.all()
books = [BookSerializer.serialize(book) for book in q_books]
return json({'method': request.method,
'status': 200,
'results': books or None,
'count': len(books),
})
async def post(self, request):
# populate the book with the data in the request
book = Book(**request.json)
# and await on save
await book.save()
return json({'method': request.method,
'status': 201,
'results': BookSerializer.serialize(book),
})
class BookView(HTTPMethodView):
async def get_object(self, request, book_id):
try:
# await on database consults
book = await Book.objects.get(**{'id': book_id})
except QuerysetError as e:
raise NotFound(e.args[0])
return book
async def get(self, request, book_id):
# await on database consults
book = await self.get_object(request, book_id)
return json({'method': request.method,
'status': 200,
'results': BookSerializer.serialize(book),
})
async def put(self, request, book_id):
# await on database consults
book = await self.get_object(request, book_id)
# await on save
await book.save(**request.json)
return json({'method': request.method,
'status': 200,
'results': BookSerializer.serialize(book),
})
async def patch(self, request, book_id):
# await on database consults
book = await self.get_object(request, book_id)
# await on save
await book.save(**request.json)
return json({'method': request.method,
'status': 200,
'results': BookSerializer.serialize(book),
})
async def delete(self, request, book_id):
# await on database consults
book = await self.get_object(request, book_id)
# await on its deletion
await book.delete()
return json({'method': request.method,
'status': 200,
'results': None
})
app.add_route(BooksView.as_view(), '/books/')
app.add_route(BookView.as_view(), '/books/<book_id:int>/')
if __name__ == '__main__':
app.run()

View File

View File

@@ -0,0 +1,21 @@
from asyncorm.model import Model
from asyncorm.fields import CharField, IntegerField, DateField
BOOK_CHOICES = (
('hard cover', 'hard cover book'),
('paperback', 'paperback book')
)
# This is a simple model definition
class Book(Model):
name = CharField(max_length=50)
synopsis = CharField(max_length=255)
book_type = CharField(max_length=15, null=True, choices=BOOK_CHOICES)
pages = IntegerField(null=True)
date_created = DateField(auto_now=True)
class Meta():
ordering = ['name', ]
unique_together = ['name', 'synopsis']

View File

@@ -0,0 +1,15 @@
from asyncorm.model import ModelSerializer, SerializerMethod
from library.models import Book
class BookSerializer(ModelSerializer):
book_type = SerializerMethod()
def get_book_type(self, instance):
return instance.book_type_display()
class Meta():
model = Book
fields = [
'id', 'name', 'synopsis', 'book_type', 'pages', 'date_created'
]

View File

@@ -0,0 +1,2 @@
asyncorm==0.0.7
sanic==0.4.1

View File

@@ -6,6 +6,7 @@ from sanic.response import json, text
app = Sanic(__name__)
blueprint = Blueprint('name', url_prefix='/my_blueprint')
blueprint2 = Blueprint('name', url_prefix='/my_blueprint2')
blueprint3 = Blueprint('name', url_prefix='/my_blueprint3')
@blueprint.route('/foo')
@@ -17,8 +18,17 @@ async def foo(request):
async def foo2(request):
return json({'msg': 'hi from blueprint2'})
@blueprint3.websocket('/foo')
async def foo3(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
app.register_blueprint(blueprint)
app.register_blueprint(blueprint2)
app.blueprint(blueprint)
app.blueprint(blueprint2)
app.blueprint(blueprint3)
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -0,0 +1,136 @@
# This demo requires aioredis and environmental variables established in ENV_VARS
import json
import logging
import os
from datetime import datetime
import aioredis
import sanic
from sanic import Sanic
ENV_VARS = ["REDIS_HOST", "REDIS_PORT",
"REDIS_MINPOOL", "REDIS_MAXPOOL",
"REDIS_PASS", "APP_LOGFILE"]
app = Sanic(name=__name__)
logger = None
@app.middleware("request")
async def log_uri(request):
# Simple middleware to log the URI endpoint that was called
logger.info("URI called: {0}".format(request.url))
@app.listener('before_server_start')
async def before_server_start(app, loop):
logger.info("Starting redis pool")
app.redis_pool = await aioredis.create_pool(
(app.config.REDIS_HOST, int(app.config.REDIS_PORT)),
minsize=int(app.config.REDIS_MINPOOL),
maxsize=int(app.config.REDIS_MAXPOOL),
password=app.config.REDIS_PASS)
@app.listener('after_server_stop')
async def after_server_stop(app, loop):
logger.info("Closing redis pool")
app.redis_pool.close()
await app.redis_pool.wait_closed()
@app.middleware("request")
async def attach_db_connectors(request):
# Just put the db objects in the request for easier access
logger.info("Passing redis pool to request object")
request["redis"] = request.app.redis_pool
@app.route("/state/<user_id>", methods=["GET"])
async def access_state(request, user_id):
try:
# Check to see if the value is in cache, if so lets return that
with await request["redis"] as redis_conn:
state = await redis_conn.get(user_id, encoding="utf-8")
if state:
return sanic.response.json({"msg": "Success",
"status": 200,
"success": True,
"data": json.loads(state),
"finished_at": datetime.now().isoformat()})
# Then state object is not in redis
logger.critical("Unable to find user_data in cache.")
return sanic.response.HTTPResponse({"msg": "User state not found",
"success": False,
"status": 404,
"finished_at": datetime.now().isoformat()}, status=404)
except aioredis.ProtocolError:
logger.critical("Unable to connect to state cache")
return sanic.response.HTTPResponse({"msg": "Internal Server Error",
"status": 500,
"success": False,
"finished_at": datetime.now().isoformat()}, status=500)
@app.route("/state/<user_id>/push", methods=["POST"])
async def set_state(request, user_id):
try:
# Pull a connection from the pool
with await request["redis"] as redis_conn:
# Set the value in cache to your new value
await redis_conn.set(user_id, json.dumps(request.json), expire=1800)
logger.info("Successfully pushed state to cache")
return sanic.response.HTTPResponse({"msg": "Successfully pushed state to cache",
"success": True,
"status": 200,
"finished_at": datetime.now().isoformat()})
except aioredis.ProtocolError:
logger.critical("Unable to connect to state cache")
return sanic.response.HTTPResponse({"msg": "Internal Server Error",
"status": 500,
"success": False,
"finished_at": datetime.now().isoformat()}, status=500)
def configure():
# Setup environment variables
env_vars = [os.environ.get(v, None) for v in ENV_VARS]
if not all(env_vars):
# Send back environment variables that were not set
return False, ", ".join([ENV_VARS[i] for i, flag in env_vars if not flag])
else:
# Add all the env vars to our app config
app.config.update({k: v for k, v in zip(ENV_VARS, env_vars)})
setup_logging()
return True, None
def setup_logging():
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
logging_format += "%(message)s"
logging.basicConfig(
filename=app.config.APP_LOGFILE,
format=logging_format,
level=logging.DEBUG)
def main(result, missing):
if result:
try:
app.run(host="0.0.0.0", port=8080, debug=True)
except:
logging.critical("User killed server. Closing")
else:
logging.critical("Unable to start. Missing environment variables [{0}]".format(missing))
if __name__ == "__main__":
result, missing = configure()
logger = logging.getLogger()
main(result, missing)

View File

@@ -9,14 +9,15 @@ and pass in an instance of it when we create our Sanic instance. Inside this
class' default handler, we can do anything including sending exceptions to
an external service.
"""
from sanic.exceptions import Handler, SanicException
from sanic.handlers import ErrorHandler
from sanic.exceptions import SanicException
"""
Imports and code relevant for our CustomHandler class
(Ordinarily this would be in a separate file)
"""
class CustomHandler(Handler):
class CustomHandler(ErrorHandler):
def default(self, request, exception):
# Here, we have access to the exception object
@@ -42,7 +43,7 @@ from sanic.response import json
app = Sanic(__name__)
handler = CustomHandler(sanic=app)
handler = CustomHandler()
app.error_handler = handler

View File

@@ -0,0 +1,17 @@
from sanic import Sanic
from sanic import response
app = Sanic(__name__)
@app.route('/')
def handle_request(request):
return response.redirect('/redirect')
@app.route('/redirect')
async def test(request):
return response.json({"Redirected": True})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,5 +1,5 @@
from sanic import Sanic
from sanic.response import json
from sanic import response
from multiprocessing import Event
from signal import signal, SIGINT
import asyncio
@@ -9,10 +9,10 @@ app = Sanic(__name__)
@app.route("/")
async def test(request):
return json({"answer": "42"})
return response.json({"answer": "42"})
asyncio.set_event_loop(uvloop.new_event_loop())
server = app.create_server(host="0.0.0.0", port=8001)
server = app.create_server(host="0.0.0.0", port=8000)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
signal(SIGINT, lambda s, f: loop.stop())

View File

@@ -0,0 +1,62 @@
# encoding: utf-8
"""
You need the aiomysql
"""
import os
import aiomysql
from sanic import Sanic
from sanic.response import json
database_name = os.environ['DATABASE_NAME']
database_host = os.environ['DATABASE_HOST']
database_user = os.environ['DATABASE_USER']
database_password = os.environ['DATABASE_PASSWORD']
app = Sanic()
@app.listener("before_server_start")
async def get_pool(app, loop):
"""
the first param is the global instance ,
so we can store our connection pool in it .
and it can be used by different request
:param args:
:param kwargs:
:return:
"""
app.pool = {
"aiomysql": await aiomysql.create_pool(host=database_host, user=database_user, password=database_password,
db=database_name,
maxsize=5)}
async with app.pool['aiomysql'].acquire() as conn:
async with conn.cursor() as cur:
await cur.execute('DROP TABLE IF EXISTS sanic_polls')
await cur.execute("""CREATE TABLE sanic_polls (
id serial primary key,
question varchar(50),
pub_date timestamp
);""")
for i in range(0, 100):
await cur.execute("""INSERT INTO sanic_polls
(id, question, pub_date) VALUES ({}, {}, now())
""".format(i, i))
@app.route("/")
async def test():
result = []
data = {}
async with app.pool['aiomysql'].acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT question, pub_date FROM sanic_polls")
async for row in cur:
result.append({"question": row[0], "pub_date": row[1]})
if result or len(result) > 0:
data['data'] = res
return json(data)
if __name__ == '__main__':
app.run(host="127.0.0.1", workers=4, port=12000)

View File

@@ -0,0 +1,34 @@
""" To run this example you need additional aioredis package
"""
from sanic import Sanic, response
import aioredis
app = Sanic(__name__)
@app.route("/")
async def handle(request):
async with request.app.redis_pool.get() as redis:
await redis.set('test-my-key', 'value')
val = await redis.get('test-my-key')
return response.text(val.decode('utf-8'))
@app.listener('before_server_start')
async def before_server_start(app, loop):
app.redis_pool = await aioredis.create_pool(
('localhost', 6379),
minsize=5,
maxsize=10,
loop=loop
)
@app.listener('after_server_stop')
async def after_server_stop(app, loop):
app.redis_pool.close()
await app.redis_pool.wait_closed()
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,59 +1,51 @@
""" To run this example you need additional asyncpg package
"""
import os
import asyncio
import uvloop
from asyncpg import create_pool
from asyncpg import connect, create_pool
from sanic import Sanic
from sanic.response import json
DB_CONFIG = {
'host': '<host>',
'user': '<username>',
'user': '<user>',
'password': '<password>',
'port': '<port>',
'database': '<database>'
}
def jsonify(records):
"""
Parse asyncpg record response into JSON format
"""
return [{key: value for key, value in
zip(r.keys(), r.values())} for r in records]
return [dict(r.items()) for r in records]
app = Sanic(__name__)
@app.listener('before_server_start')
async def create_db(app, loop):
"""
Create some table and add some data
"""
async with create_pool(**DB_CONFIG) as pool:
async with pool.acquire() as connection:
async with connection.transaction():
await connection.execute('DROP TABLE IF EXISTS sanic_post')
await connection.execute("""CREATE TABLE sanic_post (
id serial primary key,
content varchar(50),
post_date timestamp
);""")
for i in range(0, 100):
await connection.execute(f"""INSERT INTO sanic_post
(id, content, post_date) VALUES ({i}, {i}, now())""")
async def register_db(app, loop):
app.pool = await create_pool(**DB_CONFIG, loop=loop, max_size=100)
async with app.pool.acquire() as connection:
await connection.execute('DROP TABLE IF EXISTS sanic_post')
await connection.execute("""CREATE TABLE sanic_post (
id serial primary key,
content varchar(50),
post_date timestamp
);""")
for i in range(0, 1000):
await connection.execute(f"""INSERT INTO sanic_post
(id, content, post_date) VALUES ({i}, {i}, now())""")
@app.route("/")
async def handler(request):
async with create_pool(**DB_CONFIG) as pool:
async with pool.acquire() as connection:
async with connection.transaction():
results = await connection.fetch('SELECT * FROM sanic_post')
return json({'posts': jsonify(results)})
@app.get('/')
async def root_get(request):
async with app.pool.acquire() as connection:
results = await connection.fetch('SELECT * FROM sanic_post')
return json({'posts': jsonify(results)})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
app.run(host='127.0.0.1', port=8080)

View File

@@ -1,3 +1,4 @@
## You need the following additional packages for this example
# aiopg
# peewee_async
@@ -10,8 +11,9 @@ from sanic.response import json
## peewee_async related imports
import peewee
from peewee_async import Manager, PostgresqlDatabase
from peewee import Model, BaseModel
from peewee_async import Manager, PostgresqlDatabase, execute
from functools import partial
# we instantiate a custom loop so we can pass it to our db manager
## from peewee_async docs:
@@ -19,42 +21,77 @@ from peewee_async import Manager, PostgresqlDatabase
# with manager! Its all automatic. But you can run Manager.connect() or
# Manager.close() when you need it.
class AsyncManager(Manager):
"""Inherit the peewee_async manager with our own object
configuration
# let's create a simple key value store:
class KeyValue(peewee.Model):
key = peewee.CharField(max_length=40, unique=True)
text = peewee.TextField(default='')
database.allow_sync = False
"""
class Meta:
database = database
def __init__(self, _model_class, *args, **kwargs):
super(AsyncManager, self).__init__(*args, **kwargs)
self._model_class = _model_class
self.database.allow_sync = False
# create table synchronously
KeyValue.create_table(True)
def _do_fill(self, method, *args, **kwargs):
_class_method = getattr(super(AsyncManager, self), method)
pf = partial(_class_method, self._model_class)
return pf(*args, **kwargs)
# OPTIONAL: close synchronous connection
database.close()
def new(self, *args, **kwargs):
return self._do_fill('create', *args, **kwargs)
# OPTIONAL: disable any future syncronous calls
objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
def get(self, *args, **kwargs):
return self._do_fill('get', *args, **kwargs)
def execute(self, query):
return execute(query)
app = Sanic('peewee_example')
def _get_meta_db_class(db):
"""creating a declartive class model for db"""
class _BlockedMeta(BaseModel):
def __new__(cls, name, bases, attrs):
_instance = super(_BlockedMeta, cls).__new__(cls, name, bases, attrs)
_instance.objects = AsyncManager(_instance, db)
return _instance
@app.listener('before_server_start')
def setup(app, loop):
database = PostgresqlDatabase(database='test',
class _Base(Model, metaclass=_BlockedMeta):
def to_dict(self):
return self._data
class Meta:
database=db
return _Base
def declarative_base(*args, **kwargs):
"""Returns a new Modeled Class after inheriting meta and Model classes"""
db = PostgresqlDatabase(*args, **kwargs)
return _get_meta_db_class(db)
AsyncBaseModel = declarative_base(database='test',
host='127.0.0.1',
user='postgres',
password='mysecretpassword')
objects = Manager(database, loop=loop)
# let's create a simple key value store:
class KeyValue(AsyncBaseModel):
key = peewee.CharField(max_length=40, unique=True)
text = peewee.TextField(default='')
app = Sanic('peewee_example')
@app.route('/post/<key>/<value>')
async def post(request, key, value):
"""
Save get parameters to database
"""
obj = await objects.create(KeyValue, key=key, text=value)
obj = await KeyValue.objects.new(key=key, text=value)
return json({'object_id': obj.id})
@@ -63,7 +100,7 @@ async def get(request):
"""
Load all objects from database
"""
all_objects = await objects.execute(KeyValue.select())
all_objects = await KeyValue.objects.execute(KeyValue.select())
serialized_obj = []
for obj in all_objects:
serialized_obj.append({

View File

@@ -9,4 +9,5 @@ async def test(request):
return json({"test": True})
app.run(host="0.0.0.0", port=8000)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -70,6 +70,11 @@ def query_string(request):
# Run Server
# ----------------------------------------------- #
@app.listener('before_server_start')
def before_start(app, loop):
log.info("SERVER STARTING")
@app.listener('after_server_start')
def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH")
@@ -77,7 +82,13 @@ def after_start(app, loop):
@app.listener('before_server_stop')
def before_stop(app, loop):
log.info("SERVER STOPPING")
@app.listener('after_server_stop')
def after_stop(app, loop):
log.info("TRIED EVERYTHING")
app.run(host="0.0.0.0", port=8000, debug=True)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)

29
examples/websocket.html Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>WebSocket demo</title>
</head>
<body>
<script>
var ws = new WebSocket('ws://' + document.domain + ':' + location.port + '/feed'),
messages = document.createElement('ul');
ws.onmessage = function (event) {
var messages = document.getElementsByTagName('ul')[0],
message = document.createElement('li'),
content = document.createTextNode('Received: ' + event.data);
message.appendChild(content);
messages.appendChild(message);
};
document.body.appendChild(messages);
window.setInterval(function() {
data = 'bye!'
ws.send(data);
var messages = document.getElementsByTagName('ul')[0],
message = document.createElement('li'),
content = document.createTextNode('Sent: ' + data);
message.appendChild(content);
messages.appendChild(message);
}, 1000);
</script>
</body>
</html>

23
examples/websocket.py Normal file
View File

@@ -0,0 +1,23 @@
from sanic import Sanic
from sanic.response import file
app = Sanic(__name__)
@app.route('/')
async def index(request):
return await file('websocket.html')
@app.websocket('/feed')
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
if __name__ == '__main__':
app.run()

View File

@@ -1,18 +1,11 @@
aiocache
aiofiles
aiohttp
aiohttp==1.3.5
chardet<=2.3.0
beautifulsoup4
bottle
coverage
falcon
gunicorn
httptools
kyoukai
flake8
pytest
recommonmark
sphinx
sphinx_rtd_theme
tornado
tox
ujson
uvloop

View File

@@ -2,3 +2,4 @@ aiofiles
httptools
ujson
uvloop
websockets

View File

@@ -1,6 +1,6 @@
from sanic.app import Sanic
from sanic.blueprints import Blueprint
__version__ = '0.4.1'
__version__ = '0.5.0'
__all__ = ['Sanic', 'Blueprint']

View File

@@ -8,6 +8,10 @@ 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')
@@ -24,9 +28,13 @@ if __name__ == "__main__":
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}
else:
ssl = None
app.run(host=args.host, port=args.port,
workers=args.workers, debug=args.debug)
workers=args.workers, debug=args.debug, ssl=ssl)
except ImportError:
log.error("No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"

View File

@@ -1,29 +1,32 @@
import logging
import re
import warnings
from asyncio import get_event_loop
from asyncio import get_event_loop, ensure_future, CancelledError
from collections import deque, defaultdict
from functools import partial
from inspect import isawaitable, stack, getmodulename
from traceback import format_exc
from urllib.parse import urlencode, urlunparse
from ssl import create_default_context, Purpose
from sanic.config import Config
from sanic.constants import HTTP_METHODS
from sanic.exceptions import ServerError, URLBuildError, SanicException
from sanic.handlers import ErrorHandler
from sanic.log import log
from sanic.response import HTTPResponse
from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.router import Router
from sanic.server import serve, serve_multiple, HttpProtocol
from sanic.static import register as static_register
from sanic.testing import TestClient
from sanic.testing import SanicTestClient
from sanic.views import CompositionView
from sanic.websocket import WebSocketProtocol, ConnectionClosed
class Sanic:
def __init__(self, name=None, router=None, error_handler=None):
def __init__(self, name=None, router=None, error_handler=None,
load_env=True, request_class=None):
# Only set up a default log handler if the
# end-user application didn't set anything up.
if not logging.root.handlers and log.level == logging.NOTSET:
@@ -41,8 +44,9 @@ class Sanic:
self.name = name
self.router = router or Router()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config()
self.config = Config(load_env=load_env)
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
@@ -51,6 +55,8 @@ class Sanic:
self.sock = None
self.listeners = defaultdict(list)
self.is_running = False
self.websocket_enabled = False
self.websocket_tasks = []
# Register alternative method names
self.go_fast = self.run
@@ -98,7 +104,8 @@ class Sanic:
return decorator
# Decorator
def route(self, uri, methods=frozenset({'GET'}), host=None):
def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False):
"""Decorate a function to be registered as a route
:param uri: path of the URL
@@ -114,34 +121,42 @@ class Sanic:
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler,
host=host)
host=host, strict_slashes=strict_slashes)
return handler
return response
# Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=frozenset({"GET"}), host=host)
def get(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"GET"}), host=host,
strict_slashes=strict_slashes)
def post(self, uri, host=None):
return self.route(uri, methods=frozenset({"POST"}), host=host)
def post(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"POST"}), host=host,
strict_slashes=strict_slashes)
def put(self, uri, host=None):
return self.route(uri, methods=frozenset({"PUT"}), host=host)
def put(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"PUT"}), host=host,
strict_slashes=strict_slashes)
def head(self, uri, host=None):
return self.route(uri, methods=frozenset({"HEAD"}), host=host)
def head(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
strict_slashes=strict_slashes)
def options(self, uri, host=None):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
def options(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
strict_slashes=strict_slashes)
def patch(self, uri, host=None):
return self.route(uri, methods=frozenset({"PATCH"}), host=host)
def patch(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
strict_slashes=strict_slashes)
def delete(self, uri, host=None):
return self.route(uri, methods=frozenset({"DELETE"}), host=host)
def delete(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"DELETE"}), host=host,
strict_slashes=strict_slashes)
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False):
"""A helper method to register class instance or
functions as a handler to the application url
routes.
@@ -165,9 +180,71 @@ class Sanic:
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler)
self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes)(handler)
return handler
# Decorator
def websocket(self, uri, host=None, strict_slashes=False):
"""Decorate a function to be registered as a websocket route
:param uri: path of the URL
:param host:
:return: decorated function
"""
self.enable_websocket()
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith('/'):
uri = '/' + uri
def response(handler):
async def websocket_handler(request, *args, **kwargs):
request.app = self
protocol = request.transport.get_protocol()
ws = await protocol.websocket_handshake(request)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.append(fut)
try:
await fut
except (CancelledError, ConnectionClosed):
pass
self.websocket_tasks.remove(fut)
await ws.close()
self.router.add(uri=uri, handler=websocket_handler,
methods=frozenset({'GET'}), host=host,
strict_slashes=strict_slashes)
return handler
return response
def add_websocket_route(self, handler, uri, host=None,
strict_slashes=False):
"""A helper method to register a function as a websocket route."""
return self.websocket(uri, host=host,
strict_slashes=strict_slashes)(handler)
def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket.
Websocket is enabled automatically if websocket routes are
added to the application.
"""
if not self.websocket_enabled:
# if the server is stopped, we want to cancel any ongoing
# websocket tasks, to allow the server to exit promptly
@self.listener('before_server_stop')
def cancel_websocket_tasks(app, loop):
for task in self.websocket_tasks:
task.cancel()
self.websocket_enabled = enable
def remove_route(self, uri, clean_cache=True, host=None):
self.router.remove(uri, clean_cache, host)
@@ -181,7 +258,11 @@ class Sanic:
def response(handler):
for exception in exceptions:
self.error_handler.add(exception, handler)
if isinstance(exception, (tuple, list)):
for e in exception:
self.error_handler.add(e, handler)
else:
self.error_handler.add(exception, handler)
return handler
return response
@@ -254,7 +335,7 @@ class Sanic:
the output URL's query string.
:param view_name: string referencing the view name
:param **kwargs: keys and values that are used to build request
:param \*\*kwargs: keys and values that are used to build request
parameters and query string arguments.
:return: the built URL
@@ -345,14 +426,17 @@ class Sanic:
def converted_response_type(self, response):
pass
async def handle_request(self, request, response_callback):
async def handle_request(self, request, write_callback, stream_callback):
"""Take a request from the HTTP Server and return a response object
to be sent back The HTTP Server only expects a response object, so
exception handling must be done here
:param request: HTTP Request object
:param response_callback: Response function to be called with the
response as the only argument
:param write_callback: Synchronous response function to be
called with the response as the only argument
:param stream_callback: Coroutine that handles streaming a
StreamingHTTPResponse if produced by the handler.
:return: Nothing
"""
try:
@@ -361,17 +445,7 @@ class Sanic:
# -------------------------------------------- #
request.app = self
response = False
# The if improves speed. I don't know why
if self.request_middleware:
for middleware in self.request_middleware:
response = middleware(request)
if isawaitable(response):
response = await response
if response:
break
response = await self._run_request_middleware(request)
# No middleware results
if not response:
# -------------------------------------------- #
@@ -389,20 +463,6 @@ class Sanic:
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
except Exception as e:
# -------------------------------------------- #
# Response Generation Failed
@@ -420,8 +480,23 @@ class Sanic:
else:
response = HTTPResponse(
"An error occurred while handling an error")
finally:
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
try:
response = await self._run_response_middleware(request,
response)
except:
log.exception(
'Exception occured in one of response middleware handlers'
)
response_callback(response)
# pass the response to the correct callback
if isinstance(response, StreamingHTTPResponse):
await stream_callback(response)
else:
write_callback(response)
# -------------------------------------------------------------------- #
# Testing
@@ -429,7 +504,7 @@ class Sanic:
@property
def test_client(self):
return TestClient(self)
return SanicTestClient(self)
# -------------------------------------------------------------------- #
# Execution
@@ -437,7 +512,7 @@ class Sanic:
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=HttpProtocol,
sock=None, workers=1, loop=None, protocol=None,
backlog=100, stop_event=None, register_sys_signals=True):
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
@@ -446,17 +521,18 @@ class Sanic:
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param before_start: Functions to be executed before the server starts
accepting connections
accepting connections
:param after_start: Functions to be executed after the server starts
accepting connections
:param before_stop: Functions to be executed when a stop signal is
received before it is respected
:param after_stop: Functions to be executed when all requests are
complete
:param ssl: SSLContext for SSL encryption of worker(s)
complete
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
received before it is respected
:param loop:
:param backlog:
:param stop_event:
@@ -464,19 +540,27 @@ class Sanic:
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol)
if stop_event is not None:
if debug:
warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning)
server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
loop=loop, protocol=protocol, backlog=backlog,
stop_event=stop_event, register_sys_signals=register_sys_signals)
register_sys_signals=register_sys_signals)
try:
self.is_running = True
if workers == 1:
serve(**server_settings)
else:
serve_multiple(server_settings, workers, stop_event)
serve_multiple(server_settings, workers)
except:
log.exception(
'Experienced exception while trying to serve')
@@ -488,26 +572,59 @@ class Sanic:
"""This kills the Sanic"""
get_event_loop().stop()
def __call__(self):
"""gunicorn compatibility"""
return self
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, after_start=None,
before_stop=None, after_stop=None, ssl=None,
sock=None, loop=None, protocol=HttpProtocol,
sock=None, loop=None, protocol=None,
backlog=100, stop_event=None):
"""Asynchronous version of `run`.
NOTE: This does not support multiprocessing and is not the preferred
way to run a Sanic application.
"""
if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol)
if stop_event is not None:
if debug:
warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning)
server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock,
loop=loop or get_event_loop(), protocol=protocol,
backlog=backlog, stop_event=stop_event,
run_async=True)
backlog=backlog, run_async=True)
return await serve(**server_settings)
async def _run_request_middleware(self, request):
# The if improves speed. I don't know why
if self.request_middleware:
for middleware in self.request_middleware:
response = middleware(request)
if isawaitable(response):
response = await response
if response:
return response
return None
async def _run_response_middleware(self, request, response):
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
return response
def _helper(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, after_start=None, before_stop=None,
after_stop=None, ssl=None, sock=None, workers=1, loop=None,
@@ -515,6 +632,20 @@ class Sanic:
register_sys_signals=True, run_async=False):
"""Helper function used by `run` and `create_server`."""
if isinstance(ssl, dict):
# try common aliaseses
cert = ssl.get('cert') or ssl.get('certificate')
key = ssl.get('key') or ssl.get('keyfile')
if cert is None or key is None:
raise ValueError("SSLContext or certificate and key required.")
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
context.load_cert_chain(cert, keyfile=key)
ssl = context
if stop_event is not None:
if debug:
warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning)
if loop is not None:
if debug:
warnings.simplefilter('default')
@@ -538,6 +669,7 @@ class Sanic:
server_settings = {
'protocol': protocol,
'request_class': self.request_class,
'host': host,
'port': port,
'sock': sock,
@@ -583,9 +715,10 @@ class Sanic:
server_settings['run_async'] = True
# Serve
proto = "http"
if ssl is not None:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
if host and port:
proto = "http"
if ssl is not None:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
return server_settings

View File

@@ -3,7 +3,9 @@ from collections import defaultdict, namedtuple
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
FutureRoute = namedtuple('Route',
['handler', 'uri', 'methods',
'host', 'strict_slashes'])
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
@@ -23,6 +25,7 @@ class Blueprint:
self.host = host
self.routes = []
self.websocket_routes = []
self.exceptions = []
self.listeners = defaultdict(list)
self.middlewares = []
@@ -43,7 +46,20 @@ class Blueprint:
app.route(
uri=uri[1:] if uri.startswith('//') else uri,
methods=future.methods,
host=future.host or self.host
host=future.host or self.host,
strict_slashes=future.strict_slashes
)(future.handler)
for future in self.websocket_routes:
# attach the blueprint name to the handler so that it can be
# prefixed properly in the router
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
)(future.handler)
# Middleware
@@ -70,19 +86,21 @@ class Blueprint:
for listener in listeners:
app.listener(event)(listener)
def route(self, uri, methods=frozenset({'GET'}), host=None):
def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False):
"""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.
"""
def decorator(handler):
route = FutureRoute(handler, uri, methods, host)
route = FutureRoute(handler, uri, methods, host, strict_slashes)
self.routes.append(route)
return handler
return decorator
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False):
"""Create a blueprint route from a function.
:param handler: function for handling uri requests. Accepts function,
@@ -103,7 +121,30 @@ class Blueprint:
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler)
self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes)(handler)
return handler
def websocket(self, uri, host=None, strict_slashes=False):
"""Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible.
"""
def decorator(handler):
route = FutureRoute(handler, uri, [], host, strict_slashes)
self.websocket_routes.append(route)
return handler
return decorator
def add_websocket_route(self, handler, uri, host=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.
:return: function or class instance
"""
self.websocket(uri=uri, host=host)(handler)
return handler
def listener(self, event):
@@ -149,23 +190,30 @@ class Blueprint:
self.statics.append(static)
# Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=["GET"], host=host)
def get(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["GET"], host=host,
strict_slashes=strict_slashes)
def post(self, uri, host=None):
return self.route(uri, methods=["POST"], host=host)
def post(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["POST"], host=host,
strict_slashes=strict_slashes)
def put(self, uri, host=None):
return self.route(uri, methods=["PUT"], host=host)
def put(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["PUT"], host=host,
strict_slashes=strict_slashes)
def head(self, uri, host=None):
return self.route(uri, methods=["HEAD"], host=host)
def head(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["HEAD"], host=host,
strict_slashes=strict_slashes)
def options(self, uri, host=None):
return self.route(uri, methods=["OPTIONS"], host=host)
def options(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["OPTIONS"], host=host,
strict_slashes=strict_slashes)
def patch(self, uri, host=None):
return self.route(uri, methods=["PATCH"], host=host)
def patch(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["PATCH"], host=host,
strict_slashes=strict_slashes)
def delete(self, uri, host=None):
return self.route(uri, methods=["DELETE"], host=host)
def delete(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["DELETE"], host=host,
strict_slashes=strict_slashes)

View File

@@ -1,9 +1,12 @@
import os
import types
SANIC_PREFIX = 'SANIC_'
class Config(dict):
def __init__(self, defaults=None):
def __init__(self, defaults=None, load_env=True):
super().__init__(defaults or {})
self.LOGO = """
▄▄▄▄▄
@@ -29,6 +32,9 @@ class Config(dict):
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
self.REQUEST_TIMEOUT = 60 # 60 seconds
if load_env:
self.load_environment_vars()
def __getattr__(self, attr):
try:
return self[attr]
@@ -90,3 +96,13 @@ class Config(dict):
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
def load_environment_vars(self):
for k, v in os.environ.items():
"""
Looks for any SANIC_ prefixed environment variables and applies
them to the configuration if present.
"""
if k.startswith(SANIC_PREFIX):
_, config_key = k.split(SANIC_PREFIX, 1)
self[config_key] = v

View File

@@ -19,7 +19,7 @@ _Translator.update({
def _quote(str):
r"""Quote a string for use in a cookie header.
"""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.

View File

@@ -70,7 +70,7 @@ TRACEBACK_WRAPPER_HTML = '''
{frame_html}
<p class="summary">
<b>{exc_name}: {exc_value}</b>
while handling uri <code>{uri}</code>
while handling path <code>{path}</code>
</p>
</div>
</body>

View File

@@ -16,9 +16,12 @@ from sanic.response import text, html
class ErrorHandler:
handlers = None
cached_handlers = None
_missing = object()
def __init__(self):
self.handlers = {}
self.handlers = []
self.cached_handlers = {}
self.debug = False
def _render_traceback_html(self, exception, request):
@@ -34,10 +37,21 @@ class ErrorHandler:
exc_name=exc_type.__name__,
exc_value=exc_value,
frame_html=''.join(frame_html),
uri=request.url)
path=request.path)
def add(self, exception, handler):
self.handlers[exception] = handler
self.handlers.append((exception, handler))
def lookup(self, exception):
handler = self.cached_handlers.get(exception, self._missing)
if handler is self._missing:
for exception_class, handler in self.handlers:
if isinstance(exception, exception_class):
self.cached_handlers[type(exception)] = handler
return handler
self.cached_handlers[type(exception)] = None
handler = None
return handler
def response(self, request, exception):
"""Fetches and executes an exception handler and returns a response
@@ -47,9 +61,13 @@ class ErrorHandler:
:param exception: Exception to handle
:return: Response object
"""
handler = self.handlers.get(type(exception), self.default)
handler = self.lookup(exception)
response = None
try:
response = handler(request=request, exception=exception)
if handler:
response = handler(request=request, exception=exception)
if response is None:
response = self.default(request=request, exception=exception)
except Exception:
self.log(format_exc())
if self.debug:

View File

@@ -2,7 +2,7 @@ from cgi import parse_header
from collections import namedtuple
from http.cookies import SimpleCookie
from httptools import parse_url
from urllib.parse import parse_qs
from urllib.parse import parse_qs, urlunparse
try:
from ujson import loads as json_loads
@@ -36,24 +36,20 @@ class RequestParameters(dict):
class Request(dict):
"""Properties of an HTTP request such as URL, headers, etc."""
__slots__ = (
'app', 'url', 'headers', 'version', 'method', '_cookies', 'transport',
'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip',
'app', 'headers', 'version', 'method', '_cookies', 'transport',
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip', '_parsed_url',
)
def __init__(self, url_bytes, headers, version, method, transport):
# TODO: Content-Encoding detection
url_parsed = parse_url(url_bytes)
self._parsed_url = parse_url(url_bytes)
self.app = None
self.url = url_parsed.path.decode('utf-8')
self.headers = headers
self.version = version
self.method = method
self.transport = transport
self.query_string = None
if url_parsed.query:
self.query_string = url_parsed.query.decode('utf-8')
# Init but do not inhale
self.body = []
@@ -69,6 +65,8 @@ class Request(dict):
try:
self.parsed_json = json_loads(self.body)
except Exception:
if not self.body:
return None
raise InvalidUsage("Failed when parsing body as json")
return self.parsed_json
@@ -123,6 +121,10 @@ class Request(dict):
self.parsed_args = RequestParameters()
return self.parsed_args
@property
def raw_args(self):
return {k: v[0] for k, v in self.args.items()}
@property
def cookies(self):
if self._cookies is None:
@@ -142,6 +144,46 @@ class Request(dict):
self._ip = self.transport.get_extra_info('peername')
return self._ip
@property
def scheme(self):
if self.app.websocket_enabled \
and self.headers.get('upgrade') == 'websocket':
scheme = 'ws'
else:
scheme = 'http'
if self.transport.get_extra_info('sslcontext'):
scheme += 's'
return scheme
@property
def host(self):
# it appears that httptools doesn't return the host
# so pull it from the headers
return self.headers.get('Host', '')
@property
def path(self):
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')
else:
return ''
@property
def url(self):
return urlunparse((
self.scheme,
self.host,
self.path,
None,
self.query_string,
None))
File = namedtuple('File', ['type', 'body', 'name'])

View File

@@ -1,6 +1,10 @@
from mimetypes import guess_type
from os import path
from ujson import dumps as json_dumps
try:
from ujson import dumps as json_dumps
except:
from json import dumps as json_dumps
from aiofiles import open as open_async
@@ -73,37 +77,16 @@ ALL_STATUS_CODES = {
}
class HTTPResponse:
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
class BaseHTTPResponse:
def _encode_body(self, data):
try:
# Try to encode it regularly
return data.encode()
except AttributeError:
# Convert it to a str if you can't
return str(data).encode()
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:
try:
# Try to encode it regularly
self.body = body.encode()
except AttributeError:
# Convert it to a str if you can't
self.body = str(body).encode()
else:
self.body = body_bytes
self.status = status
self.headers = headers or {}
self._cookies = 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''
if keep_alive and keep_alive_timeout is not None:
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
self.headers['Content-Length'] = self.headers.get(
'Content-Length', len(self.body))
self.headers['Content-Type'] = self.headers.get(
'Content-Type', self.content_type)
def _parse_headers(self):
headers = b''
for name, value in self.headers.items():
try:
@@ -115,6 +98,114 @@ class HTTPResponse:
b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
return headers
@property
def cookies(self):
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = (
'transport', 'streaming_fn',
'status', 'content_type', 'headers', '_cookies')
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
self.headers = headers or {}
self._cookies = None
def write(self, data):
"""Writes a chunk of data to the streaming response.
:param data: bytes-ish data to be written.
"""
if type(data) != bytes:
data = self._encode_body(data)
self.transport.write(
b"%x\r\n%b\r\n" % (len(data), data))
async def stream(
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)
self.transport.write(headers)
await self.streaming_fn(self)
self.transport.write(b'0\r\n\r\n')
def get_headers(
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''
if keep_alive and keep_alive_timeout is not None:
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
self.headers['Transfer-Encoding'] = 'chunked'
self.headers.pop('Content-Length', None)
self.headers['Content-Type'] = self.headers.get(
'Content-Type', self.content_type)
headers = self._parse_headers()
# Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all
status = COMMON_STATUS_CODES.get(self.status)
if not status:
status = ALL_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
)
class HTTPResponse(BaseHTTPResponse):
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
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:
self.body = self._encode_body(body)
else:
self.body = body_bytes
self.status = status
self.headers = headers or {}
self._cookies = 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''
if keep_alive and keep_alive_timeout is not None:
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
self.headers['Content-Length'] = self.headers.get(
'Content-Length', len(self.body))
self.headers['Content-Type'] = self.headers.get(
'Content-Type', self.content_type)
headers = self._parse_headers()
# Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all
status = COMMON_STATUS_CODES.get(self.status)
@@ -126,14 +217,14 @@ class HTTPResponse:
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,
self.body
)
version.encode(),
self.status,
status,
b'keep-alive' if keep_alive else b'close',
timeout_header,
headers,
self.body
)
@property
def cookies(self):
@@ -161,11 +252,11 @@ def text(body, status=200, headers=None,
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
:param content_type:
the content type (string) of the response
:param content_type: the content type (string) of the response
"""
return HTTPResponse(body, status=status, headers=headers,
content_type=content_type)
return HTTPResponse(
body, status=status, headers=headers,
content_type=content_type)
def raw(body, status=200, headers=None,
@@ -175,8 +266,7 @@ def raw(body, status=200, headers=None,
:param body: Response data.
:param status: Response code.
:param headers: Custom Headers.
:param content_type:
the content type (string) of the response
:param content_type: the content type (string) of the response.
"""
return HTTPResponse(body_bytes=body, status=status, headers=headers,
content_type=content_type)
@@ -220,6 +310,35 @@ async def file(location, mime_type=None, headers=None, _range=None):
body_bytes=out_stream)
def stream(
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`.
Example usage::
@app.route("/")
async def index(request):
async def streaming_fn(response):
await response.write('foo')
await response.write('bar')
return stream(streaming_fn, content_type='text/plain')
:param streaming_fn: A coroutine accepts a response and
writes content to that response.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
"""
return StreamingHTTPResponse(
streaming_fn,
headers=headers,
content_type=content_type,
status=status
)
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
"""Abort execution and cause a 302 redirect (by default).

View File

@@ -75,9 +75,10 @@ class Router:
"""Parse a parameter string into its constituent name, type, and
pattern
For example:
`parse_parameter_string('<param_one:[A-z]>')` ->
('param_one', str, '[A-z]')
For example::
parse_parameter_string('<param_one:[A-z]>')` ->
('param_one', str, '[A-z]')
:param parameter_string: String to parse
:return: tuple containing
@@ -95,9 +96,15 @@ class Router:
return name, _type, pattern
def add(self, uri, methods, handler, host=None):
def add(self, uri, methods, handler, host=None, strict_slashes=False):
# add regular version
self._add(uri, methods, handler, host)
if strict_slashes:
return
# Add versions with and without trailing /
slash_is_missing = (
not uri[-1] == '/'
and not self.routes_all.get(uri + '/', False)
@@ -137,9 +144,6 @@ class Router:
for host_ in host:
self.add(uri, methods, handler, host_)
return
else:
# default host
self.hosts.add('*')
# Dict for faster lookups of if method allowed
if methods:
@@ -281,14 +285,14 @@ class Router:
"""
# No virtual hosts specified; default behavior
if not self.hosts:
return self._get(request.url, 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.url, request.method,
return self._get(request.path, request.method,
request.headers.get("Host", ''))
# try default hosts
except NotFound:
return self._get(request.url, request.method, '')
return self._get(request.path, request.method, '')
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method, host):

View File

@@ -4,11 +4,17 @@ import traceback
import warnings
from functools import partial
from inspect import isawaitable
from multiprocessing import Process, Event
from os import set_inheritable
from signal import SIGTERM, SIGINT
from signal import signal as signal_func
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from multiprocessing import Process
from signal import (
SIGTERM, SIGINT,
signal as signal_func,
Signals
)
from socket import (
socket,
SOL_SOCKET,
SO_REUSEADDR,
)
from time import time
from httptools import HttpRequestParser
@@ -58,12 +64,13 @@ class HttpProtocol(asyncio.Protocol):
'parser', 'request', 'url', 'headers',
# request config
'request_handler', 'request_timeout', 'request_max_size',
'request_class',
# connection management
'_total_request_size', '_timeout_handler', '_last_communication_time')
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections=set(), request_timeout=60,
request_max_size=None):
request_max_size=None, request_class=None):
self.loop = loop
self.transport = None
self.request = None
@@ -76,6 +83,7 @@ class HttpProtocol(asyncio.Protocol):
self.error_handler = error_handler
self.request_timeout = request_timeout
self.request_max_size = request_max_size
self.request_class = request_class or Request
self._total_request_size = 0
self._timeout_handler = None
self._last_request_time = None
@@ -145,7 +153,7 @@ class HttpProtocol(asyncio.Protocol):
self.headers.append((name.decode().casefold(), value.decode()))
def on_headers_complete(self):
self.request = Request(
self.request = self.request_class(
url_bytes=self.url,
headers=CIDict(self.headers),
version=self.parser.get_http_version(),
@@ -159,20 +167,63 @@ class HttpProtocol(asyncio.Protocol):
def on_message_complete(self):
if self.request.body:
self.request.body = b''.join(self.request.body)
self._request_handler_task = self.loop.create_task(
self.request_handler(self.request, self.write_response))
self.request_handler(
self.request,
self.write_response,
self.stream_response))
# -------------------------------------------- #
# Responding
# -------------------------------------------- #
def write_response(self, response):
keep_alive = (
self.parser.should_keep_alive() and not self.signal.stopped)
"""
Writes response content synchronously to the transport.
"""
try:
keep_alive = (
self.parser.should_keep_alive() and not self.signal.stopped)
self.transport.write(
response.output(
self.request.version, keep_alive, self.request_timeout))
self.request.version, keep_alive,
self.request_timeout))
except AttributeError:
log.error(
('Invalid response object for url {}, '
'Expected Type: HTTPResponse, Actual Type: {}').format(
self.url, type(response)))
self.write_error(ServerError('Invalid response type'))
except RuntimeError:
log.error(
'Connection lost before response written @ {}'.format(
self.request.ip))
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(
repr(e)))
finally:
if not keep_alive:
self.transport.close()
else:
self._last_request_time = current_time
self.cleanup()
async def stream_response(self, response):
"""
Streams a response to the client asynchronously. Attaches
the transport to the response so the response consumer can
write to the response as needed.
"""
try:
keep_alive = (
self.parser.should_keep_alive() and not self.signal.stopped)
response.transport = self.transport
await response.stream(
self.request.version, keep_alive, self.request_timeout)
except AttributeError:
log.error(
('Invalid response object for url {}, '
@@ -191,7 +242,6 @@ class HttpProtocol(asyncio.Protocol):
if not keep_alive:
self.transport.close()
else:
# Record that we received data
self._last_request_time = current_time
self.cleanup()
@@ -212,7 +262,7 @@ class HttpProtocol(asyncio.Protocol):
self.transport.close()
def bail_out(self, message, from_error=False):
if from_error and self.transport.is_closing():
if from_error or self.transport.is_closing():
log.error(
("Transport closed @ {} and exception "
"experienced during error handling").format(
@@ -271,7 +321,8 @@ 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, ssl=None, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False):
register_sys_signals=True, run_async=False, connections=None,
signal=Signal(), request_class=None):
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
@@ -287,7 +338,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
`app` instance and `loop`
:param after_stop: function to be executed when a stop signal is
received after it is respected. Takes arguments
`app` instance and `loop`
`app` instance and `loop`
:param debug: enables debug output (slows server)
:param request_timeout: time in seconds
:param ssl: SSLContext
@@ -296,6 +347,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
:param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop
:param protocol: subclass of asyncio protocol class
:param request_class: Request class to use
:return: Nothing
"""
if not run_async:
@@ -307,8 +359,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(before_start, loop)
connections = set()
signal = Signal()
connections = connections if connections is not None else set()
server = partial(
protocol,
loop=loop,
@@ -318,6 +369,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
error_handler=error_handler,
request_timeout=request_timeout,
request_max_size=request_max_size,
request_class=request_class,
)
server_coroutine = loop.create_server(
@@ -379,7 +431,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
loop.close()
def serve_multiple(server_settings, workers, stop_event=None):
def serve_multiple(server_settings, workers):
"""Start multiple server processes simultaneously. Stop on interrupt
and terminate signals, and drain connections when complete.
@@ -396,19 +448,22 @@ def serve_multiple(server_settings, workers, stop_event=None):
" has more information.", DeprecationWarning)
server_settings['reuse_port'] = True
sock = socket()
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind((server_settings['host'], server_settings['port']))
set_inheritable(sock.fileno(), True)
server_settings['sock'] = sock
server_settings['host'] = None
server_settings['port'] = None
# Handling when custom socket is not provided.
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.set_inheritable(True)
server_settings['sock'] = sock
server_settings['host'] = None
server_settings['port'] = None
if stop_event is None:
stop_event = Event()
def sig_handler(signal, frame):
log.info("Received signal {}. Shutting down.".format(
Signals(signal).name))
signal_func(SIGINT, lambda s, f: stop_event.set())
signal_func(SIGTERM, lambda s, f: stop_event.set())
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
processes = []
for _ in range(workers):
@@ -423,6 +478,6 @@ def serve_multiple(server_settings, workers, stop_event=None):
# the above processes will block this until they're stopped
for process in processes:
process.terminate()
sock.close()
server_settings.get('sock').close()
asyncio.get_event_loop().stop()

View File

@@ -1,10 +1,12 @@
import traceback
from sanic.log import log
HOST = '127.0.0.1'
PORT = 42101
class TestClient:
class SanicTestClient:
def __init__(self, app):
self.app = app
@@ -17,10 +19,15 @@ class TestClient:
host=HOST, port=PORT, uri=uri)
log.info(url)
async with aiohttp.ClientSession(cookies=cookies) as session:
conn = aiohttp.TCPConnector(verify_ssl=False)
async with aiohttp.ClientSession(
cookies=cookies, connector=conn) as session:
async with getattr(
session, method.lower())(url, *args, **kwargs) as response:
response.text = await response.text()
try:
response.text = await response.text()
except UnicodeDecodeError as e:
response.text = None
response.body = await response.read()
return response
@@ -45,6 +52,8 @@ class TestClient:
**request_kwargs)
results[-1] = response
except Exception as e:
log.error(
'Exception:\n{}'.format(traceback.format_exc()))
exceptions.append(e)
self.app.stop()

View File

@@ -1,6 +1,6 @@
import warnings
from sanic.testing import TestClient
from sanic.testing import SanicTestClient
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
@@ -11,7 +11,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
"the next major version after 0.4.0. Please use the `test_client` "
"available on the app object.", DeprecationWarning)
test_client = TestClient(app)
test_client = SanicTestClient(app)
return test_client._sanic_endpoint_test(
method, uri, gather_request, debug, server_kwargs,
*request_args, **request_kwargs)

67
sanic/websocket.py Normal file
View File

@@ -0,0 +1,67 @@
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, **kwargs):
super().__init__(*args, **kwargs)
self.websocket = None
def connection_timeout(self):
# timeouts make no sense for websocket routes
if self.websocket is None:
super().connection_timeout()
def connection_lost(self, exc):
if self.websocket is not None:
self.websocket.connection_lost(exc)
super().connection_lost(exc)
def data_received(self, data):
if self.websocket is not None:
# pass the data to the websocket protocol
self.websocket.data_received(data)
else:
try:
super().data_received(data)
except HttpParserUpgrade:
# this is okay, it just indicates we've got an upgrade request
pass
def write_response(self, response):
if self.websocket is not None:
# websocket requests do not write a response
self.transport.close()
else:
super().write_response(response)
async def websocket_handshake(self, request):
# 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))
try:
key = handshake.check_request(get_header)
handshake.build_response(set_header, key)
except InvalidHandshake:
raise InvalidUsage('Invalid websocket request')
# 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'
request.transport.write(rv)
# hook up the websocket protocol
self.websocket = WebSocketCommonProtocol()
self.websocket.connection_made(request.transport)
return self.websocket

166
sanic/worker.py Normal file
View File

@@ -0,0 +1,166 @@
import os
import sys
import signal
import asyncio
import logging
try:
import ssl
except ImportError:
ssl = None
import uvloop
import gunicorn.workers.base as base
from sanic.server import trigger_events, serve, HttpProtocol, Signal
from sanic.websocket import WebSocketProtocol
class GunicornWorker(base.Worker):
def __init__(self, *args, **kw): # pragma: no cover
super().__init__(*args, **kw)
cfg = self.cfg
if cfg.is_ssl:
self.ssl_context = self._create_ssl_context(cfg)
else:
self.ssl_context = None
self.servers = []
self.connections = set()
self.exit_code = 0
self.signal = Signal()
def init_process(self):
# create new event_loop after fork
asyncio.get_event_loop().close()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
super().init_process()
def run(self):
is_debug = self.log.loglevel == logging.DEBUG
protocol = (WebSocketProtocol if self.app.callable.websocket_enabled
else HttpProtocol)
self._server_settings = self.app.callable._helper(
host=None,
port=None,
loop=self.loop,
debug=is_debug,
protocol=protocol,
ssl=self.ssl_context,
run_async=True
)
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)
self.loop.run_until_complete(self._check_alive())
trigger_events(self._server_settings.get('before_stop', []),
self.loop)
self.loop.run_until_complete(self.close())
finally:
trigger_events(self._server_settings.get('after_stop', []),
self.loop)
self.loop.close()
sys.exit(self.exit_code)
async def close(self):
if self.servers:
# stop accepting 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()
self.servers.clear()
# prepare connections for closing
self.signal.stopped = True
for conn in self.connections:
conn.close_if_idle()
while self.connections:
await asyncio.sleep(0.1)
async def _run(self):
for sock in self.sockets:
self.servers.append(await serve(
sock=sock,
connections=self.connections,
signal=self.signal,
**self._server_settings
))
async def _check_alive(self):
# If our parent changed then we shut down.
pid = os.getpid()
try:
while self.alive:
self.notify()
if pid == os.getpid() and self.ppid != os.getppid():
self.alive = False
self.log.info("Parent changed, shutting down: %s", self)
else:
await asyncio.sleep(1.0, loop=self.loop)
except (Exception, BaseException, GeneratorExit, KeyboardInterrupt):
pass
@staticmethod
def _create_ssl_context(cfg):
""" Creates SSLContext instance for usage in asyncio.create_server.
See ssl.SSLSocket.__init__ for more details.
"""
ctx = ssl.SSLContext(cfg.ssl_version)
ctx.load_cert_chain(cfg.certfile, cfg.keyfile)
ctx.verify_mode = cfg.cert_reqs
if cfg.ca_certs:
ctx.load_verify_locations(cfg.ca_certs)
if cfg.ciphers:
ctx.set_ciphers(cfg.ciphers)
return ctx
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.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.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.SIGABRT, self.handle_abort,
signal.SIGABRT, None)
# Don't let SIGTERM and SIGUSR1 disturb active requests
# by interrupting system calls
signal.siginterrupt(signal.SIGTERM, False)
signal.siginterrupt(signal.SIGUSR1, False)
def handle_quit(self, sig, frame):
self.alive = False
self.cfg.worker_int(self)
def handle_abort(self, sig, frame):
self.alive = False
self.exit_code = 1
self.cfg.worker_abort(self)

View File

@@ -4,8 +4,10 @@ Sanic
import codecs
import os
import re
from setuptools import setup
from distutils.errors import DistutilsPlatformError
from distutils.util import strtobool
from setuptools import setup
with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
__file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp:
@@ -15,32 +17,54 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
except IndexError:
raise RuntimeError('Unable to determine version.')
install_requires = [
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
]
if os.name != 'nt':
install_requires.append('uvloop>=0.5.3')
setup(
name='sanic',
version=version,
url='http://github.com/channelcat/sanic/',
license='MIT',
author='Channel Cat',
author_email='channelcat@gmail.com',
description=(
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'),
packages=['sanic'],
platforms='any',
install_requires=install_requires,
classifiers=[
'packages': ['sanic'],
'platforms': 'any',
'classifiers': [
'Development Status :: 2 - Pre-Alpha',
'Environment :: Web Environment',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
],
)
}
ujson = 'ujson>=1.35'
uvloop = 'uvloop>=0.5.3'
requirements = [
'httptools>=0.0.9',
uvloop,
ujson,
'aiofiles>=0.3.0',
'websockets>=3.2',
]
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
print("Installing without uJSON")
requirements.remove(ujson)
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
print("Installing without uvLoop")
requirements.remove(uvloop)
try:
setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs)
except DistutilsPlatformError as exception:
requirements.remove(ujson)
requirements.remove(uvloop)
print("Installing without uJSON or uvLoop")
setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs)
# Installation was successful
print(u"\n\n\U0001F680 "
"Sanic version {} installation suceeded.\n".format(version))

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDtTCCAp2gAwIBAgIJAO6wb0FSc/rNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTcwMzAzMTUyODAzWhcNMTkxMTI4MTUyODAzWjBF
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7mdB6Bflh
V5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLwdpaoTGqE
vYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49IAouBHq3
h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ8gjaC8/1
w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJPXtNrTwf
qEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABo4GnMIGkMB0GA1UdDgQWBBRa46Ix
9s9tmMqu+Zz1mocHghm4NTB1BgNVHSMEbjBsgBRa46Ix9s9tmMqu+Zz1mocHghm4
NaFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAO6wb0FSc/rNMAwGA1UdEwQF
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBACdrnM8zb7abxAJsU5WLn1IR0f2+EFA7
ezBEJBM4bn0IZrXuP5ThZ2wieJlshG0C16XN9+zifavHci+AtQwWsB0f/ppHdvWQ
7wt7JN88w+j0DNIYEadRCjWxR3gRAXPgKu3sdyScKFq8MvB49A2EdXRmQSTIM6Fj
teRbE+poxewFT0mhurf3xrtGiSALmv7uAzhRDqpYUzcUlbOGgkyFLYAOOdvZvei+
mfXDi4HKYxgyv53JxBARMdajnCHXM7zQ6Tjc8j1HRtmDQ3XapUB559KfxfODGQq5
zmeoZWU4duxcNXJM0Eiz1CJ39JoWwi8sqaGi/oskuyAh7YKyVTn8xa8=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7
mdB6BflhV5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLw
dpaoTGqEvYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49
IAouBHq3h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ
8gjaC8/1w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJ
PXtNrTwfqEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABAoIBAFgVasxTf3aaXbNo
7JzXMWb7W4iAG2GRNmZZzHA7hTSKFvS7jc3SX3n6WvDtEvlOi8ay2RyRNgEjBDP6
VZ/w2jUJjS5k7dN0Qb9nhPr5B9fS/0CAppcVfsx5/KEVFzniWOPyzQYyW7FJKu8h
4G5hrp/Ie4UH5tKtB6YUZB/wliyyQUkAZdBcoy1hfkOZLAXb1oofArKsiQUHIRA5
th1yyS4cZP8Upngd1EE+d95dFHM2F6iI2lj6DHuu+JxUZ+wKXoNimdG7JniRtIf4
56GoDov83Ey+XbIS6FSQc9nY0ijBDcubl/yP3roCQpE+MZ9BNEo5uj7YmCtAMYLW
TXTNBGUCgYEA4wdkH1NLdub2NcpqwmSA0AtbRvDkt0XTDWWwmuMr/+xPVa4sUKHs
80THQEX/WAZroP6IPbMP6BJhzb53vECukgC65qPxu6M9D1lBGtglxgen4AMu1bKK
gnM8onwARGIo/2ay6qRRZZCxg0TvBky3hbTcIM2zVrnKU6VVyGKHSV8CgYEAygxs
WQYrACv3XN6ZEzyxy08JgjbcnkPWK/m3VPcyHgdEkDu8+nDdUVdbF/js2JWMMx5g
vrPhZ7jVLOXGcLr5mVU4dG5tW5lU0bMy+YYxpEQDiBKlpXgfOsQnakHj7cCZ6bay
mKjJck2oEAQS9bqOJN/Ts5vhOmc8rmhkO7hnAh8CgYEArhVDy9Vl/1WYo6SD+m1w
bJbYtewPpQzwicxZAFuDqKk+KDf3GRkhBWTO2FUUOB4sN3YVaCI+5zf5MPeE/qAm
fCP9LM+3k6bXMkbBamEljdTfACHQruJJ3T+Z1gn5dnZCc5z/QncfRx8NTtfz5MO8
0dTeGnVAuBacs0kLHy2WCUcCgYALNBkl7pOf1NBIlAdE686oCV/rmoMtO3G6yoQB
8BsVUy3YGZfnAy8ifYeNkr3/XHuDsiGHMY5EJBmd/be9NID2oaUZv63MsHnljtw6
vdgu1Z6kgvQwcrK4nXvaBoFPA6kFLp5EnMde0TOKf89VVNzg6pBgmzon9OWGfj9g
mF8N3QKBgQCeoLwxUxpzEA0CPHm7DWF0LefVGllgZ23Eqncdy0QRku5zwwibszbL
sWaR3uDCc3oYcbSGCDVx3cSkvMAJNalc5ZHPfoV9W0+v392/rrExo5iwD8CSoCb2
gFWkeR7PBrD3NzFzFAWyiudzhBKHfRsB0MpCXbJV/WLqTlGIbEypjg==
-----END RSA PRIVATE KEY-----

BIN
tests/static/python.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,3 +1,4 @@
import asyncio
import inspect
from sanic import Sanic
@@ -23,6 +24,33 @@ def test_bp():
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')
@bp.post('/post/', strict_slashes=True)
def 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.status == 404
request, response = app.test_client.post('/post/')
assert response.text == 'OK'
request, response = app.test_client.post('/post')
assert response.status == 404
def test_bp_with_url_prefix():
app = Sanic('test_text')
bp = Blueprint('test_text', url_prefix='/test1')
@@ -236,6 +264,7 @@ def test_bp_static():
def test_bp_shorthand():
app = Sanic('test_shorhand_routes')
blueprint = Blueprint('test_shorhand_routes')
ev = asyncio.Event()
@blueprint.get('/get')
def handler(request):
@@ -265,6 +294,10 @@ def test_bp_shorthand():
def handler(request):
return text('OK')
@blueprint.websocket('/ws')
async def handler(request, ws):
ev.set()
app.blueprint(blueprint)
request, response = app.test_client.get('/get')
@@ -308,3 +341,11 @@ def test_bp_shorthand():
request, response = app.test_client.get('/delete')
assert response.status == 405
request, response = app.test_client.get('/ws', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'})
assert response.status == 101
assert ev.is_set()

View File

@@ -16,6 +16,17 @@ def test_load_from_object():
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"
app = Sanic()
assert app.config.TEST_ANSWER == "42"
del environ["SANIC_TEST_ANSWER"]
def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(load_env=False)
assert getattr(app.config, 'TEST_ANSWER', None) == None
del environ["SANIC_TEST_ANSWER"]
def test_load_from_file():
app = Sanic('test_load_from_file')

View File

@@ -44,6 +44,21 @@ def exception_app():
return app
def test_catch_exception_list():
app = Sanic('exception_list')
@app.exception([SanicExceptionTestException, NotFound])
def exception_list(request, exception):
return text("ok")
@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('/')
assert response.text == 'ok'
def test_no_exception(exception_app):
"""Test that a route works without an exception"""

View File

@@ -1,6 +1,7 @@
from sanic import Sanic
from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.handlers import ErrorHandler
from bs4 import BeautifulSoup
exception_handler_app = Sanic('test_exception_handler')
@@ -30,7 +31,7 @@ def handler_4(request):
@exception_handler_app.route('/5')
def handler_5(request):
class CustomServerError(ServerError):
status_code=200
pass
raise CustomServerError('Custom server error')
@@ -75,9 +76,50 @@ def test_html_traceback_output_in_debug_mode():
summary_text = " ".join(soup.select('.summary')[0].text.split())
assert (
"NameError: name 'bar' "
"is not defined while handling uri /4") == summary_text
"is not defined while handling path /4") == summary_text
def test_inherited_exception_handler():
request, response = exception_handler_app.test_client.get('/5')
assert response.status == 200
def test_exception_handler_lookup():
class CustomError(Exception):
pass
class CustomServerError(ServerError):
pass
def custom_error_handler():
pass
def server_error_handler():
pass
def import_error_handler():
pass
try:
ModuleNotFoundError
except:
class ModuleNotFoundError(ImportError):
pass
handler = ErrorHandler()
handler.add(ImportError, import_error_handler)
handler.add(CustomError, custom_error_handler)
handler.add(ServerError, server_error_handler)
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
# 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

View File

@@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic
from sanic.request import Request
from sanic.response import json, text, HTTPResponse
from sanic.exceptions import NotFound
# ------------------------------------------------------------ #
@@ -53,6 +54,27 @@ def test_middleware_response():
assert isinstance(results[2], HTTPResponse)
def test_middleware_response_exception():
app = Sanic('test_middleware_response_exception')
result = {'status_code': None}
@app.middleware('response')
async def process_response(reqest, response):
result['status_code'] = response.status
return response
@app.exception(NotFound)
async def error_handler(request, exception):
return text('OK', exception.status_code)
@app.route('/')
async def handler(request):
return text('FAIL')
request, response = app.test_client.get('/page_not_found')
assert response.text == 'OK'
assert result['status_code'] == 404
def test_middleware_override_request():
app = Sanic('test_middleware_override_request')

View File

@@ -1,49 +1,47 @@
from sanic import Sanic
from sanic.response import text
from sanic.exceptions import PayloadTooLarge
data_received_app = Sanic('data_received')
data_received_app.config.REQUEST_MAX_SIZE = 1
data_received_default_app = Sanic('data_received_default')
data_received_default_app.config.REQUEST_MAX_SIZE = 1
on_header_default_app = Sanic('on_header')
on_header_default_app.config.REQUEST_MAX_SIZE = 500
@data_received_app.route('/1')
async def handler1(request):
return text('OK')
@data_received_app.exception(PayloadTooLarge)
def handler_exception(request, exception):
return text('Payload Too Large from error_handler.', 413)
from sanic.response import text
def test_payload_too_large_from_error_handler():
data_received_app = Sanic('data_received')
data_received_app.config.REQUEST_MAX_SIZE = 1
@data_received_app.route('/1')
async def handler1(request):
return text('OK')
@data_received_app.exception(PayloadTooLarge)
def handler_exception(request, exception):
return text('Payload Too Large from error_handler.', 413)
response = data_received_app.test_client.get('/1', gather_request=False)
assert response.status == 413
assert response.text == 'Payload Too Large from error_handler.'
@data_received_default_app.route('/1')
async def handler2(request):
return text('OK')
def test_payload_too_large_at_data_received_default():
data_received_default_app = Sanic('data_received_default')
data_received_default_app.config.REQUEST_MAX_SIZE = 1
@data_received_default_app.route('/1')
async def handler2(request):
return text('OK')
response = data_received_default_app.test_client.get(
'/1', gather_request=False)
assert response.status == 413
assert response.text == 'Error: Payload Too Large'
@on_header_default_app.route('/1')
async def handler3(request):
return text('OK')
def test_payload_too_large_at_on_header_default():
on_header_default_app = Sanic('on_header')
on_header_default_app.config.REQUEST_MAX_SIZE = 500
@on_header_default_app.post('/1')
async def handler3(request):
return text('OK')
data = 'a' * 1000
response = on_header_default_app.test_client.post(
'/1', gather_request=False, data=data)

View File

@@ -88,4 +88,7 @@ def test_chained_redirect(redirect_app):
assert request.url.endswith('/1')
assert response.status == 200
assert response.text == 'OK'
assert response.url.endswith('/3')
try:
assert response.url.endswith('/3')
except AttributeError:
assert response.url.path.endswith('/3')

View File

@@ -1,10 +1,15 @@
from json import loads as json_loads, dumps as json_dumps
from urllib.parse import urlparse
import os
import ssl
import pytest
from sanic import Sanic
from sanic.exceptions import ServerError
from sanic.response import json, text, redirect
from sanic.response import json, text
from sanic.testing import HOST, PORT
# ------------------------------------------------------------ #
@@ -85,20 +90,28 @@ def test_json():
request, response = app.test_client.get('/')
try:
results = json_loads(response.text)
except:
raise ValueError("Expected JSON response but got '{}'".format(response))
results = json_loads(response.text)
assert results.get('test') == True
def test_empty_json():
app = Sanic('test_json')
@app.route('/')
async def handler(request):
assert request.json == None
return json(request.json)
request, response = app.test_client.get('/')
assert response.status == 200
assert response.text == 'null'
def test_invalid_json():
app = Sanic('test_json')
@app.route('/')
async def handler(request):
return json(request.json())
return json(request.json)
data = "I am not json"
request, response = app.test_client.get('/', data=data)
@@ -192,3 +205,61 @@ def test_post_form_multipart_form_data():
request, response = app.test_client.post(data=payload, headers=headers)
assert request.form.get('test') == 'OK'
@pytest.mark.parametrize(
'path,query,expected_url', [
('/foo', '', 'http://{}:{}/foo'),
('/bar/baz', '', 'http://{}:{}/bar/baz'),
('/moo/boo', 'arg1=val1', 'http://{}:{}/moo/boo?arg1=val1')
])
def test_url_attributes_no_ssl(path, query, expected_url):
app = Sanic('test_url_attrs_no_ssl')
async def handler(request):
return text('OK')
app.add_route(handler, path)
request, response = app.test_client.get(path + '?{}'.format(query))
assert request.url == expected_url.format(HOST, PORT)
parsed = urlparse(request.url)
assert parsed.scheme == request.scheme
assert parsed.path == request.path
assert parsed.query == request.query_string
assert parsed.netloc == request.host
@pytest.mark.parametrize(
'path,query,expected_url', [
('/foo', '', 'https://{}:{}/foo'),
('/bar/baz', '', 'https://{}:{}/bar/baz'),
('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1')
])
def test_url_attributes_with_ssl(path, query, expected_url):
app = Sanic('test_url_attrs_with_ssl')
current_dir = os.path.dirname(os.path.realpath(__file__))
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(
os.path.join(current_dir, 'certs/selfsigned.cert'),
keyfile=os.path.join(current_dir, 'certs/selfsigned.key'))
async def handler(request):
return text('OK')
app.add_route(handler, path)
request, response = app.test_client.get(
'https://{}:{}'.format(HOST, PORT) + path + '?{}'.format(query),
server_kwargs={'ssl': context})
assert request.url == expected_url.format(HOST, PORT)
parsed = urlparse(request.url)
assert parsed.scheme == request.scheme
assert parsed.path == request.path
assert parsed.query == request.query_string
assert parsed.netloc == request.host

View File

@@ -1,8 +1,12 @@
import asyncio
import pytest
from random import choice
from sanic import Sanic
from sanic.response import HTTPResponse
from sanic.response import HTTPResponse, stream, StreamingHTTPResponse
from sanic.testing import HOST, PORT
from unittest.mock import MagicMock
def test_response_body_not_a_string():
"""Test when a response body sent from the application is not a string"""
@@ -15,3 +19,78 @@ def test_response_body_not_a_string():
request, response = app.test_client.get('/hello')
assert response.text == str(random_num)
async def sample_streaming_fn(response):
response.write('foo,')
await asyncio.sleep(.001)
response.write('bar')
@pytest.fixture
def streaming_app():
app = Sanic('streaming')
@app.route("/")
async def test(request):
return stream(sample_streaming_fn, content_type='text/csv')
return app
def test_streaming_adds_correct_headers(streaming_app):
request, response = streaming_app.test_client.get('/')
assert response.headers['Transfer-Encoding'] == 'chunked'
assert response.headers['Content-Type'] == 'text/csv'
def test_streaming_returns_correct_content(streaming_app):
request, response = streaming_app.test_client.get('/')
assert response.text == 'foo,bar'
@pytest.mark.parametrize('status', [200, 201, 400, 401])
def test_stream_response_status_returns_correct_headers(status):
response = StreamingHTTPResponse(sample_streaming_fn, status=status)
headers = response.get_headers()
assert b"HTTP/1.1 %s" % str(status).encode() in headers
@pytest.mark.parametrize('keep_alive_timeout', [10, 20, 30])
def test_stream_response_keep_alive_returns_correct_headers(
keep_alive_timeout):
response = StreamingHTTPResponse(sample_streaming_fn)
headers = response.get_headers(
keep_alive=True, keep_alive_timeout=keep_alive_timeout)
assert b"Keep-Alive: %s\r\n" % str(keep_alive_timeout).encode() in headers
def test_stream_response_includes_chunked_header():
response = StreamingHTTPResponse(sample_streaming_fn)
headers = response.get_headers()
assert b"Transfer-Encoding: chunked\r\n" in headers
def test_stream_response_writes_correct_content_to_transport(streaming_app):
response = StreamingHTTPResponse(sample_streaming_fn)
response.transport = MagicMock(asyncio.Transport)
@streaming_app.listener('after_server_start')
async def run_stream(app, loop):
await response.stream()
assert response.transport.write.call_args_list[1][0][0] == (
b'4\r\nfoo,\r\n'
)
assert response.transport.write.call_args_list[2][0][0] == (
b'3\r\nbar\r\n'
)
assert response.transport.write.call_args_list[3][0][0] == (
b'0\r\n\r\n'
)
app.stop()
streaming_app.run(host=HOST, port=PORT)

View File

@@ -1,3 +1,4 @@
import asyncio
import pytest
from sanic import Sanic
@@ -22,6 +23,29 @@ def test_shorthand_routes_get():
request, response = app.test_client.post('/get')
assert response.status == 405
def test_route_strict_slash():
app = Sanic('test_route_strict_slash')
@app.get('/get', strict_slashes=True)
def handler(request):
return text('OK')
@app.post('/post/', strict_slashes=True)
def handler(request):
return text('OK')
request, response = app.test_client.get('/get')
assert response.text == 'OK'
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.status == 404
def test_route_optional_slash():
app = Sanic('test_route_optional_slash')
@@ -234,6 +258,23 @@ def test_dynamic_route_unhashable():
assert response.status == 404
def test_websocket_route():
app = Sanic('test_websocket_route')
ev = asyncio.Event()
@app.websocket('/ws')
async def handler(request, ws):
ev.set()
request, response = app.test_client.get('/ws', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'})
assert response.status == 101
assert ev.is_set()
def test_route_duplicate():
app = Sanic('test_route_duplicate')

View File

@@ -25,7 +25,7 @@ def get_file_content(static_file_directory, file_name):
return file.read()
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file(static_file_directory, file_name):
app = Sanic('test_static')
app.static(

View File

@@ -15,13 +15,13 @@ def test_methods(method):
class DummyView(HTTPMethodView):
def get(self, request):
async def get(self, request):
return text('', headers={'method': 'GET'})
def post(self, request):
return text('', headers={'method': 'POST'})
def put(self, request):
async def put(self, request):
return text('', headers={'method': 'PUT'})
def head(self, request):
@@ -30,7 +30,7 @@ def test_methods(method):
def options(self, request):
return text('', headers={'method': 'OPTIONS'})
def patch(self, request):
async def patch(self, request):
return text('', headers={'method': 'PATCH'})
def delete(self, request):

View File

@@ -10,11 +10,7 @@ python =
[testenv]
deps =
aiofiles
aiohttp
pytest
beautifulsoup4
coverage
-rrequirements-dev.txt
commands =
pytest tests {posargs}