Compare commits

..

264 Commits
0.1.8 ... 0.3.0

Author SHA1 Message Date
Eli Uriegas
3d790a71a0 Merge pull request #358 from seemethere/incrment_version_030
Increment version to 0.3.0
2017-01-27 19:37:02 -06:00
Eli Uriegas
cfc32d940a Increment version to 0.3.0 2017-01-27 19:36:03 -06:00
Eli Uriegas
59242df7d6 Move serve_multiple, fix tests (#357)
* Move serve_multiple, remove stop_events, fix tests

Moves serve_multiple out of the app, removes stop_event (adds a
deprecation warning, but it also wasn't doing anything) fixes
multiprocessing tests so that they don't freeze pytest's runner.

Other notes:

Also moves around some imports so that they are better optimized as
well.

* Re-add in stop_event, maybe it wasn't so bad!

* Get rid of unused warnings import
2017-01-27 19:34:21 -06:00
Eli Uriegas
fad9fbca6f Merge pull request #335 from r0fls/remove-loop
remove loop as argument and update examples
2017-01-27 19:25:09 -06:00
Eli Uriegas
c81c92967c Merge pull request #354 from JordanP/use_isinstance
Use ``isinstance(`` instead of ``issubclass(type(``
2017-01-27 15:05:12 -06:00
Jordan Pittier
fa36dcbe09 Use `isinstance( instead of issubclass(type(`
When we already have an `instance` it's less typing and faster to
use `isinstance`.
2017-01-27 11:11:29 +01:00
Raphael Deem
c72bcc136c add semaphore concurrency limit example 2017-01-26 17:43:54 -08:00
Raphael Deem
d52f5f0b09 remove loop as argument and update examples 2017-01-26 17:38:46 -08:00
Eli Uriegas
4f6650fce2 Merge pull request #349 from kdelwat/compositionview-docs
Add documentation for the CompositionView class
2017-01-26 19:38:32 -06:00
Raphael Deem
2774414f9e Merge pull request #350 from r0fls/deprecate-loop
add deprecate loop message
2017-01-26 15:24:43 -08:00
Eli Uriegas
c1f1dca22c Merge pull request #351 from r0fls/revert-log-changes
use string formatting in start message
2017-01-26 17:17:49 -06:00
Raphael Deem
965fbc917d use string formatting in start message 2017-01-26 15:11:53 -08:00
Raphael Deem
2a1b63c93c add deprecate loop message 2017-01-26 15:07:42 -08:00
Cadel Watson
011be51232 Add documentation for the CompositionView class 2017-01-27 09:52:41 +11:00
Eli Uriegas
d1c4f3172f Merge pull request #348 from r0fls/275
add async run
2017-01-26 10:37:52 -06:00
Raphael Deem
14697c7ea9 review changes 2017-01-26 08:36:00 -08:00
Raphael Deem
54ca8c787b add async run 2017-01-25 22:47:18 -08:00
Channel Cat
88c3bffe20 Merge pull request #342 from channelcat/fix-slow-upload
Fix slow upload
2017-01-25 22:04:48 -08:00
Channel Cat
a4f85cd02e Merge pull request #343 from channelcat/cookie-usability
Cookie deletion and default path
2017-01-25 21:59:43 -08:00
Channel Cat
fd118a41fd Only use request.body 2017-01-25 21:56:49 -08:00
Channel Cat
b0f71c5304 Fixed default path in documentation 2017-01-25 21:48:39 -08:00
Raphael Deem
a162f2ce34 Merge branch 'master' into cookie-usability 2017-01-25 21:24:30 -08:00
Eli Uriegas
0f64702d72 Merge pull request #347 from r0fls/345
false cookie attributes should not be set
2017-01-25 18:51:02 -06:00
Raphael Deem
3c355f19eb false cookie attributes should not be set 2017-01-25 16:47:14 -08:00
Raphael Deem
fb9f2171a9 Merge pull request #346 from r0fls/event-docstring
fix before/after event docstrings
2017-01-25 16:29:10 -08:00
Raphael Deem
4efcb6d5ad fix before/after event docstrings 2017-01-25 16:25:16 -08:00
Raphael Deem
5ddc166b12 Merge pull request #344 from atbentley/docs
Update blueprints docs with correct listener example
2017-01-25 10:48:17 -08:00
Andrew Bentley
ecd7e5bdd3 Update blueprints docs with correct listener example 2017-01-25 23:01:57 +11:00
Channel Cat
d0a121ad06 Added del cookie and default path 2017-01-25 01:53:39 -08:00
Channel Cat
85a28d3c30 Fix slow upload 2017-01-25 01:19:21 -08:00
Eli Uriegas
0ad31a471b Merge pull request #341 from r0fls/340
set error handler debug from run debug arg
2017-01-24 21:07:15 -06:00
Raphael Deem
28f7abd1f8 set error handler debug from run debug arg 2017-01-24 17:24:06 -08:00
Raphael Deem
049a5a7493 Merge pull request #339 from lixxu/master
add sanic-jinja2 extension
2017-01-23 23:46:26 -08:00
Lix Xu
a9ee01c7ec add sanic-jinja2 extension 2017-01-24 15:34:32 +08:00
Manuel Miranda
1f89b15792 Caching example (#334)
* Caching example using aiocache

* Caching example using aiocache

* Added aiocache to requirements

* Fixed example with newest aiocache

* Fixed bug in cache example
2017-01-23 10:19:42 -06:00
Raphael Deem
afe390d407 Merge pull request #253 from dutradda/add_register_sys_signals_flag
add a flag to skip SIGINT and SIGTERM signals registration
2017-01-21 23:09:12 -08:00
Eli Uriegas
6f4f2758d6 Merge pull request #331 from ctlaltdefeat/json-response-args
allowed passing arguments to json response encoder
2017-01-21 15:31:57 -06:00
ctlaltdefeat
592ee5f839 fixed line length to satisfy travis 2017-01-21 23:02:02 +02:00
ctlaltdefeat
a811c84e99 allowed passing arguments to json response encoder 2017-01-21 22:40:34 +02:00
Eli Uriegas
214162adf0 Merge branch 'master' into add_register_sys_signals_flag 2017-01-21 10:25:57 -06:00
Raphael Deem
28396c8620 Merge pull request #327 from awiddersheim/logging-override
Make it easier to override logging
2017-01-20 16:23:03 -08:00
Andrew Widdersheim
72fba62e09 Make it easier to override logging
Take influence from how Werkzeug configures logging by only configuring
a handler if no root handlers were previously configured by the end
user.
2017-01-20 18:26:55 -05:00
Raphael Deem
5669cafc59 Merge pull request #328 from awiddersheim/remove-multidict
Remove multidict requirement
2017-01-20 15:14:39 -08:00
Andrew Widdersheim
0c7275da1a Remove multidict requirement
This is no longer necessary after #302.
2017-01-20 18:12:54 -05:00
Eli Uriegas
e5cbf25cbd Merge pull request #325 from seemethere/add_key_error
Simplify RequestParameters
2017-01-20 15:27:00 -06:00
Eli Uriegas
c3cf618e84 Merge pull request #326 from seemethere/fix_exception_monitoring_documentation
Fix exception_monitoring example to actually work
2017-01-20 14:39:26 -06:00
Eli Uriegas
7780a8c187 Merge pull request #320 from r0fls/method-decorators
add method shorthands
2017-01-20 14:38:34 -06:00
Eli Uriegas
40f1e14bb7 Fix exception_monitoring example to actually work
Closes #324

Super was used incorrectly in the example, also fixed some formatting
here and there.
2017-01-20 14:36:15 -06:00
Eli Uriegas
a7cd4ccd09 Simplify RequestParameters
Simplifies request parameters, it defined a bit more than it had too,
added some docstrings and made the code simpler as well. Should now
raise a KeyError on __getitem__ as @amsb had noted on commit 9dd954b
2017-01-20 14:31:24 -06:00
Raphael Deem
6fd69b6284 separate tests 2017-01-20 10:19:14 -08:00
Raphael Deem
96424b6b0a add method shorthands 2017-01-20 00:07:22 -08:00
Eli Uriegas
bef34d66f5 Merge pull request #314 from seemethere/make_closed_transport_handling_more_robust
Add exception handling for closed transports
2017-01-19 21:31:30 -06:00
Eli Uriegas
c6049be688 Merge pull request #316 from youknowone/route-get
For function decorators, ['GET'] is the default methods
2017-01-19 21:31:06 -06:00
Eli Uriegas
5ea653244e Merge pull request #319 from seemethere/fix_docs_links
Fixes doc link extensions from .html to .md
2017-01-19 21:30:40 -06:00
Eli Uriegas
596bb54ee3 Oops on 2 of the non-relative links 2017-01-19 21:26:37 -06:00
Eli Uriegas
7554e87374 Fixes doc link extensions from .html to .md
Whoops! Totally missed that all the links pointed to `.html` files
instead of `.md` files
2017-01-19 21:24:08 -06:00
Cadel Watson
6176964bdf Clarify, reformat, and add to documentation guides (#318)
* Reorder and clarify the 'Request Data' guide, adding a section on RequestParameters

* Clarify routing guide, adding introduction and HTTP types sections

* Clarify the use-cases of middleware

* Clean up formatting in the exceptions guide and add some common exceptions.

* Fix formatting of blueprints and add use-case example.

* Clarify the class-based views guide

* Clarify and fix formatting of cookies guide

* Clarify static files guide

* Clarify the custom protocols guide.

* Add more information to the deploying guide

* Fix broken list in the community extensions list.

* Add introduction and improve warning to contributing guide

* Expand getting started guide

* Reorder guides and add links between them

* Standardise heading capitalisation
2017-01-19 21:18:52 -06:00
Eli Uriegas
f77bb81def Merge pull request #229 from kdelwat/sphinx-docs
Use Sphinx for documentation
2017-01-19 16:54:02 -06:00
Eli Uriegas
ed4752bbc0 Move transport close to finally statment 2017-01-19 16:35:48 -06:00
Cadel Watson
30862c0a3e Update documentation generation instructions. 2017-01-20 09:32:08 +11:00
Cadel Watson
7c4ffa8866 Merge branch 'master' into sphinx-docs 2017-01-20 09:30:42 +11:00
Eli Uriegas
a79f073077 Merge pull request #228 from seanpar203/issue_41
Adding more docstrings
2017-01-19 16:18:28 -06:00
Raphael Deem
4682d7b4b8 Merge pull request #296 from mdaue/ssl
Add SSL context to server
2017-01-19 11:29:07 -08:00
Raphael Deem
d2217b5c5f Merge branch 'master' into ssl 2017-01-19 11:23:21 -08:00
Jeong YunWon
0a160c4a0b For function decorators, ['GET'] is the default methods 2017-01-19 23:56:51 +09:00
Eli Uriegas
aed8255a8b Merge pull request #315 from h4ck3rm1k3/patch-1
untie
2017-01-19 08:28:18 -06:00
James Michael DuPont
ba60659894 untie 2017-01-19 04:04:16 -05:00
Raphael Deem
1ad7b95437 Merge pull request #306 from zkanda/always-log-error-exception
Always log if there's an exception occurred.
2017-01-19 00:22:46 -08:00
zkanda
cc43ee3b3d Always log of there's an exception occured. 2017-01-19 16:03:12 +08:00
Eli Uriegas
5a7b70c054 Merge pull request #313 from r0fls/logging-var
remove logger from run
2017-01-18 23:50:07 -06:00
Eli Uriegas
e9bfa30c1d Add exception handling for closed transports
Adds handling for closed transports in the server for `write_response`
as well as `write_error`, letting it all flow to `bail_out` seemed to be
a little light handed in terms of telling the logs where the
error actually occured.

Handling to fix the infinite `write_error` loop is still there and those
exceptions will get reported on in the debug logs.
2017-01-18 23:46:22 -06:00
Raphael Deem
bb83a25a52 remove logger from run 2017-01-18 21:45:30 -08:00
Eli Uriegas
99c8d779dc Merge pull request #312 from r0fls/allow-vhost-lists
allow using a list of hosts on a route
2017-01-18 23:42:59 -06:00
Raphael Deem
2c1ff5bf5d allow using a list of hosts on a route 2017-01-18 19:41:32 -08:00
Eli Uriegas
e218a59911 Merge pull request #311 from youknowone/route-overload
Feature: Routing overload
2017-01-18 16:32:46 -06:00
Jeong YunWon
11f3c79a77 Feature: Routing overload
When user specifies HTTP methods to function handlers, it automatically
will be overloaded unless they duplicate.

Example:

    # This is a new route. It works as before.
    @app.route('/overload', methods=['GET'])
    async def handler1(request):
        return text('OK1')

    # This is the exiting route but a new method. They are merged and
    # work as combined. The route will serve all of GET, POST and PUT.
    @app.route('/overload', methods=['POST', 'PUT'])
    async def handler2(request):
        return text('OK2')

    # This is the existing route and PUT method is the duplicated method.
    # It raises RouteExists.
    @app.route('/overload', methods=['PUT', 'DELETE'])
    async def handler3(request):
	return text('Duplicated')
2017-01-19 07:12:45 +09:00
Cadel Watson
5cfd8b9aa8 Fix formatting of 'Final Word' in README 2017-01-19 08:58:13 +11:00
Cadel Watson
ebce7b01c7 Add new guides to documentation index 2017-01-19 08:54:20 +11:00
Cadel Watson
9d4b104d2d Merge branch 'master' into sphinx-docs 2017-01-19 08:48:54 +11:00
Raphael Deem
5a6fb679c9 Merge pull request #309 from seemethere/fix_write_error_loop
Fixes write_error loop from bail_out function
2017-01-17 18:11:22 -08:00
Raphael Deem
9102a9cd6e Merge branch 'master' into ssl 2017-01-17 18:09:33 -08:00
Eli Uriegas
573d1da0ef Fixes write_error loop from bail_out function
Fixes stack-overflow found in #307
2017-01-17 18:28:22 -06:00
Raphael Deem
2848dce968 Merge pull request #308 from r0fls/287
update logging placement
2017-01-17 15:51:38 -08:00
Raphael Deem
ba1e006585 update logging placement 2017-01-17 15:49:17 -08:00
Eli Uriegas
9108a4c69f Merge pull request #291 from subyraman/master
Add rich HTML traceback in debug mode, add HTML 500 page in prod
2017-01-17 15:47:37 -06:00
Eli Uriegas
f42ceb1baa Merge pull request #304 from gpip/missing_ip_slots
cannot use the new .ip without updating __slots__
2017-01-17 10:54:21 -06:00
Guilherme Polo
5903dd2939 cannot use the new .ip without updating __slots__ 2017-01-17 02:58:45 -02:00
Eli Uriegas
55b39a3f15 Merge pull request #301 from r0fls/cache-remote-ip
cache the remote IP property
2017-01-16 20:07:53 -06:00
Raphael Deem
9bc69f7de9 use hasattr 2017-01-16 17:35:08 -08:00
Channel Cat
2aa380c5a3 Merge pull request #302 from channelcat/request-headers-ci
Trimmed down features of CIMultiDict
2017-01-16 17:08:12 -08:00
Channel Cat
638fbcb619 Encoding needs a default 2017-01-16 17:03:55 -08:00
Channel Cat
ccbbce0036 Fix header capitalization on input
also removed redundant utf-8 in encodes/decodes
2017-01-16 16:55:55 -08:00
Channel Cat
41918eaf0a Trimmed down features of CIMultiDict 2017-01-16 16:12:42 -08:00
Eli Uriegas
5c344f7efa Remove redundant else 2017-01-16 17:51:56 -06:00
Raphael Deem
213580ea78 cache the remote IP property 2017-01-16 15:48:55 -08:00
Eli Uriegas
d5fd269fda Merge pull request #300 from channelcat/move-request-ip-retry
Moved Remote-Addr header to request.ip
2017-01-16 17:35:04 -06:00
Ubuntu
2cf4baddfb Moved Remote-Addr header to request.ip so it can be pulled on-demand 2017-01-16 23:27:50 +00:00
Eli Uriegas
48d496936a Merge pull request #294 from subyraman/redirect
Add redirect method from @pcdinh
2017-01-16 13:53:02 -06:00
Eli Uriegas
e410c06a68 Merge pull request #293 from r0fls/extensions-docs
Extensions docs
2017-01-15 19:37:30 -06:00
Eli Uriegas
f6c2d0bcaf Merge pull request #297 from seemethere/increment_020
Increment version to 0.2.0
2017-01-14 11:25:10 -06:00
Eli Uriegas
e2a16f96a8 Increment version to 0.2.0 2017-01-14 11:24:31 -06:00
Matt Daue
49fdc6563f Add SSL to server
Add ssl variable passthrough to following:
-- sanic.run
-- server.serve
Add ssl variable to loop.create_server to enable built-in async context socket wrapper
Update documentation
Tested with worker = 1, and worker = 2.

Signed-off-by: Matt Daue <mattdaue@gmail.com>
2017-01-14 07:16:59 -05:00
Suby Raman
b5bbef09c5 add redirect method 2017-01-14 00:47:28 -05:00
Suby Raman
7a1e089725 add headers none test 2017-01-14 00:45:04 -05:00
Suby Raman
7de3f7aa78 rename test app 2017-01-14 00:43:30 -05:00
Suby Raman
02b9a0a297 add redirect code from @pcdinh 2017-01-14 00:41:54 -05:00
Raphael Deem
a86cc32dff Update extensions.md
Since there have been a few extensions discussed in open issues, it makes sense to start keeping track of them.
2017-01-12 19:30:22 -08:00
Raphael Deem
974c857a97 add a list of extensions 2017-01-12 19:25:08 -08:00
Suby Raman
6e53fa03f5 add beautifulsoup4 to tox 2017-01-12 20:03:35 -05:00
Suby Raman
8c5e214131 html and tests pass 2017-01-12 19:54:34 -05:00
Eli Uriegas
cf60ebd988 Merge pull request #289 from seemethere/update_request_form_getitem
Update request.form to work with __getitem__
2017-01-11 17:02:45 -06:00
Eli Uriegas
9dd954bccd Update request.form to work with __getitem__ 2017-01-11 16:55:34 -06:00
Eli Uriegas
a02eb8e7cb Merge pull request #284 from r0fls/remove-defualt-type
remove default from host in _get method
2017-01-11 10:01:11 -06:00
Eli Uriegas
a93ca9b8f1 Merge pull request #285 from r0fls/blueprint-domains
add vhosts to blueprints
2017-01-11 10:00:35 -06:00
Raphael Deem
15e4ec7ffb add ability to override default host in blueprint 2017-01-10 22:08:15 -08:00
Raphael Deem
62df50e22b add vhosts to blueprints 2017-01-10 21:35:07 -08:00
Raphael Deem
055430d4b8 remove default from host in _get method 2017-01-10 16:01:21 -08:00
Eli Uriegas
57f27c41e0 Merge pull request #278 from r0fls/vhosts
add support for virtual hosts
2017-01-10 15:18:36 -06:00
Cadel Watson
385328ffd5 Generate API documentation in the _api folder 2017-01-10 09:41:00 +11:00
Cadel Watson
34d1e5e67e Convert README to RestructuredText and include it in the documentation index. 2017-01-10 09:28:33 +11:00
Raphael Deem
4f832ac9af add support for virtual hosts 2017-01-08 18:46:29 -08:00
Eli Uriegas
b0bf989056 Merge pull request #261 from yoloseem/parsed_json
Cache request.json even when it's empty.
2017-01-08 11:57:33 -06:00
Eli Uriegas
fd0e8624c4 Merge pull request #276 from r0fls/workers
Fix multiple worker problem
2017-01-08 11:57:10 -06:00
Eli Uriegas
5566668a5f Change the skips to actual pytest skips
By using the builtin pytest skips we can identify that the tests are still there but are being currently skipped.

Will update later to remove the skips once we figure out why they freeze with pytest (I experienced this same issue with multiprocessing when testing start/stop events).
2017-01-08 11:55:08 -06:00
Raphael Deem
f8e6becb9e skip multiprocessing tests 2017-01-07 18:58:02 -08:00
Raphael Deem
dd28d70680 fix stop event 2017-01-07 18:46:38 -08:00
Raphael Deem
ed8e3f237c this branch is broken 2017-01-07 15:28:21 -08:00
Raphael Deem
77c04c4cf9 fix multiple worker problem 2017-01-07 12:57:14 -08:00
Eli Uriegas
ebc8d7168a Merge pull request #274 from AntonDnepr/cbw_fixes
Class based views fixes
2017-01-07 10:07:14 -06:00
Anton Zhyrney
434fa74e67 removed debug from test 2017-01-07 07:14:27 +02:00
Anton Zhyrney
47a4f34cdf tests&small update 2017-01-07 07:13:49 +02:00
Anton Zhyrney
1317b1799c add docstrings&updated docs 2017-01-07 06:57:07 +02:00
Anton Zhyrney
fcae4a9f0a added as_view 2017-01-07 06:30:23 +02:00
Eli Uriegas
d733c5bb7c Merge pull request #271 from easydaniel/master
Asyncpg example
2017-01-06 09:11:42 -06:00
Eli Uriegas
baf8254907 Change Ellipsis to None for consistency 2017-01-05 15:29:57 -06:00
Eli Uriegas
802e7d4654 Merge pull request #269 from r0fls/263
add configurable backlog
2017-01-04 20:37:19 -06:00
Raphael Deem
616e20d467 move backlog to run() 2017-01-04 09:31:06 -08:00
easydaniel
5c7c2cf85e Update sanic_asyncpg_example.py
Remove unused library
2017-01-04 23:35:06 +08:00
DanielChien
1942644434 modify config to varbles 2017-01-04 23:30:29 +08:00
DanielChien
b67482de9b add example for asyncpg 2017-01-04 23:29:09 +08:00
Eli Uriegas
35bb71f952 Merge pull request #270 from seemethere/add_server_event_tests
Add server event tests
2017-01-04 00:32:13 -06:00
Eli Uriegas
9c91b09ab1 Fix this to actually reflect current behavior 2017-01-04 00:23:59 -06:00
Eli Uriegas
06911a8d2e Add tests for server start/stop event functions 2017-01-04 00:23:35 -06:00
Raphael Deem
e7922c1b54 add configurable backlog #263 2017-01-03 18:36:31 -08:00
Eli Uriegas
16959caa34 Merge pull request #259 from yoloseem/route
Correct Router documentation
2017-01-03 18:47:54 -06:00
Eli Uriegas
0675f388b9 Merge pull request #255 from seemethere/add_more_verbose_debug_error_handling
Add more verbose debug error handling
2017-01-03 15:43:26 -06:00
Eli Uriegas
b4ad9459da Merge pull request #260 from yoloseem/gitign
Update .gitignore
2017-01-03 14:42:00 -06:00
Eli Uriegas
8a9b1fee14 Merge pull request #265 from youknowone/test-middleware-schedule
Prevent flooding of meaningless traceback in `sanic_endpoint_test`
2017-01-03 14:41:23 -06:00
Jeong YunWon
e6eb697bb2 Use constant PORT rather than literal in test code (#266) 2017-01-03 14:40:13 -06:00
Eli Uriegas
4ccc782e29 Merge pull request #209 from 38elements/protocol
Customizable protocol
2017-01-03 11:52:54 -06:00
Hyunjun Kim
cfdd9f66d1 Correct sanic.router.Router documentation 2017-01-02 14:32:22 +09:00
Hyunjun Kim
035cbf84ae Cache request.json even when it's null or empty
In case of request body is set to `{}`, `[]` or `null`, even it's
already processed, parsed_json won't be used due to its boolean
evaluation.
2017-01-02 14:20:20 +09:00
Hyunjun Kim
31e92a8b4f Update .gitignore
* .python-version is generated by `pyenv local` command
* .eggs/ directory contains *.egg files
2017-01-02 13:33:24 +09:00
Jeong YunWon
87c24e5a7c Prevent flooding of meaningless traceback in sanic_endpoint_test
When Sanic has an exception in a request middleware, it fails to
save request object in `results`. In `sanic_endpoint_test`, because
it always requires `results` to have both `request` and `response` objects,
it prints traceback like attached example. It is not a user code and
it doesn't give any information to users, it is better to suppress
to print this kind of error.

To fix it, this patch insert collect hook as first request middleware
to guarantee to successfully run it always.

```
app = <sanic.sanic.Sanic object at 0x1102b5358>, method = 'get', uri = '/ping/', gather_request = True, loop = None
debug = True, request_args = (), request_kwargs = {}
_collect_request = <function sanic_endpoint_test.<locals>._collect_request at 0x11286c158>
_collect_response = <function sanic_endpoint_test.<locals>._collect_response at 0x11286c378>

    def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
                            loop=None, debug=False, *request_args,
                            **request_kwargs):
        results = []
        exceptions = []

        if gather_request:
            @app.middleware
            def _collect_request(request):
                results.append(request)

        async def _collect_response(sanic, loop):
            try:
                response = await local_request(method, uri, *request_args,
                                               **request_kwargs)
                results.append(response)
            except Exception as e:
                exceptions.append(e)
            app.stop()

        app.run(host=HOST, debug=debug, port=42101,
                after_start=_collect_response, loop=loop)

        if exceptions:
            raise ValueError("Exception during request: {}".format(exceptions))

        if gather_request:
            try:
>               request, response = results
E               ValueError: not enough values to unpack (expected 2, got 1)

../sanic/sanic/utils.py:46: ValueError
```
2017-01-02 13:18:22 +09:00
Eli Uriegas
552ff9d736 Merge pull request #235 from yoloseem/patch-1
Allow Sanic-inherited application
2016-12-31 13:21:44 -06:00
Eli Uriegas
738396c2e2 Merge pull request #236 from seanpar203/token_property
Add token property to request
2016-12-31 13:21:12 -06:00
Eli Uriegas
9f18b5a096 Merge pull request #248 from youknowone/debuggable-typecheck
Handle hooks parameters in more debuggable way
2016-12-31 13:17:06 -06:00
Eli Uriegas
15c965c08c Make exception tests test unhandled exceptions
* Adds tests for unhandled exceptions
* Adds tests for unhandled exceptions in exception handlers
* Rewrites tests to utilize pytest fixtures (No need to create the app
on import)
2016-12-30 13:50:12 -06:00
Eli Uriegas
7a8fd6b0df Add more verbose error handling
* Adds logging to error messages in debug mode as pointed out in PR #249,
while also improving the debug message.
2016-12-30 13:48:17 -06:00
Eli Uriegas
3ada6f358c Merge pull request #197 from r0fls/178
convert header values to strings
2016-12-30 12:17:29 -06:00
Eli Uriegas
f1c2854358 Merge branch 'master' into 178 2016-12-30 12:15:08 -06:00
Eli Uriegas
87559a34f8 Include more explicit loop for headers conversion
Also merges master changes into this PR for this branch
2016-12-30 12:13:16 -06:00
Eli Uriegas
9586351f37 Merge pull request #243 from dutradda/add_remove_route_method
created methods to remove a route from api/router
2016-12-30 09:39:57 -06:00
Diogo
0f6ed642da created methods to remove a route from api/router 2016-12-30 07:36:57 -02:00
Diogo Dutra
6d1d4ade19 add a flag to skip SIGINT and SIGTERM signals registration 2016-12-29 19:35:41 -02:00
Eli Uriegas
73a57e1105 Merge pull request #252 from AntonDnepr/misprints
Small docs improvements
2016-12-29 11:49:55 -06:00
Anton Zhyrney
e7314d1775 fix misprints&renaming 2016-12-29 19:22:11 +02:00
38elements
ee8f8c2930 Merge branch 'master' into protocol 2016-12-29 16:44:15 +09:00
38elements
64e0e2d19f Improve custom_protocol.md 2016-12-29 16:41:04 +09:00
38elements
6bb4dae5e0 Fix format in custom_protocol.md 2016-12-29 13:25:04 +09:00
38elements
83e9d08853 Add document for custom protocol 2016-12-29 13:11:27 +09:00
Eli Uriegas
7dfa62516c Merge pull request #238 from r0fls/73
stop multiple worker server without sleep loop; issue #73
2016-12-28 18:05:06 -06:00
Jeong YunWon
15e7d8ab2e Handle hooks parameters in more debuggable way
1. not list() -> callable()
The args of hooking parameters of Sanic have to be callables.
For wrong parameters, errors will be generated from:
```
    listeners += args
```

By checking just list type, the raised error will be associated
with `[args]` instead of `args`, which is not given by users.
With this patch, the raised error will be associated with `args`.
Then users can notice their argument was neither callable nor list
in the easier way.

2. Function -> Functions in document
Regarding the parameter as a list is harmless to the user code.
But unawareness of its type can be list can limit the potent of
the user code.
2016-12-28 18:14:57 +09:00
Raphael Deem
a4f77984b7 stop multiple worker server without sleep loop; issue #73 2016-12-26 14:51:16 -08:00
38elements
39b279f0f2 Improve examples/custom_protocol.py 2016-12-26 23:54:59 +09:00
38elements
ac44900fc4 Add test and example for custom protocol 2016-12-26 23:41:10 +09:00
Sean Parsons
548458c3e0 Added test for new token property on request object. 2016-12-26 06:48:53 -05:00
Sean Parsons
986b0aa106 Added token property to request object. 2016-12-26 06:41:41 -05:00
Hyunjun Kim
01b42fb399 Allow Sanic-inherited application 2016-12-26 20:37:16 +09:00
Eli Uriegas
f74d44152a Merge pull request #234 from seemethere/update_tests_to_use_tox
Change travis job to use tox
2016-12-25 19:10:36 -08:00
Eli Uriegas
1557854755 Update to make flake8 actually work 2016-12-25 19:05:11 -08:00
Eli Uriegas
56d6c2a929 Change travis job to use tox 2016-12-25 18:55:25 -08:00
Eli Uriegas
5402e6d3a6 Merge pull request #212 from r0fls/176
allow overriding logging basicConfig settings
2016-12-25 09:13:51 -08:00
Cadel Watson
52c59e7133 Fix all docstring errors.
When generating documentation with sphinx-apidoc, there are currently
many docstring errors, mostly minor. This commit fixes all errors.
2016-12-25 20:43:45 +11:00
Cadel Watson
7654c2f902 Use Sphinx for documentation.
This commit creates configuration files and an index page for
documentation using Sphinx. The recommonmark package is used to enable
Markdown support for Sphinx, using the Common Mark specification. This
means that the current documentation doesn't need to be rewritten.
2016-12-25 20:24:34 +11:00
Raphael Deem
be9eca2d63 use try/except 2016-12-24 23:05:03 -08:00
Sean Parsons
d5ad5e46da Update response docstrings to be explicit on whats returned. 2016-12-25 01:24:17 -05:00
Sean Parsons
a486fb99a9 Updated json function docstrings to be more consistent. 2016-12-25 01:06:40 -05:00
Sean Parsons
2b10860c32 Added docstrings to sanic.response.py for issue 41 2016-12-25 01:05:26 -05:00
Sean Parsons
a03f216f42 Added additional docstrings to blueprints.py 2016-12-25 00:47:51 -05:00
Raphael Deem
7d7cbaacf1 header format function 2016-12-24 20:56:13 -08:00
Raphael Deem
00b5a496dd type -> isinstance 2016-12-24 20:56:13 -08:00
Raphael Deem
7e6c92dc52 convert header values to strings 2016-12-24 20:56:13 -08:00
Eli Uriegas
6cf3754051 Merge pull request #226 from seemethere/increment_019
Increment version to 0.1.9
2016-12-24 18:54:07 -08:00
Eli Uriegas
cf7616ebe5 Increment version to 0.1.9 2016-12-24 18:51:16 -08:00
Eli Uriegas
5b7964f8b6 Merge pull request #225 from seemethere/add_response_body_not_a_string_test
Add test for PR: #215
2016-12-24 18:50:11 -08:00
Eli Uriegas
f1f38c24da Add test for PR: #215 2016-12-24 18:47:15 -08:00
Eli Uriegas
67a50becb0 Merge pull request #215 from cr0hn/patch-1
Improvement: avoid to encoding in each HTTP Response
2016-12-24 18:41:01 -08:00
Eli Uriegas
d7e94473f3 Use a try/except, it's a bit faster
Also reorder some imports and add some comments
2016-12-24 18:37:55 -08:00
Eli Uriegas
184c896f41 Merge pull request #224 from seemethere/rewrite_static_files_tests
Rewrite static files tests
2016-12-24 18:23:06 -08:00
Eli Uriegas
8be849cc40 Rewrite static files tests
Relates to PR #188

Changes include:
- Rewriting to work with pytest fixtures and an actual static directory
- Addition of a test that covers file paths that must be
  unquoted as a uri
2016-12-24 18:18:56 -08:00
Eli Uriegas
275851a755 Merge pull request #188 from webtic/master
Find URL encoded filenames on the fs by decoding them first
2016-12-24 18:14:45 -08:00
Eli Uriegas
16182472fa Remove trailing whitespace 2016-12-24 18:11:46 -08:00
Eli Uriegas
29f3c22fed Rework conditionals to not be inline 2016-12-24 18:11:12 -08:00
Eli Uriegas
a116666d55 Merge pull request #223 from r0fls/115
Raise error if response is malformed.
2016-12-24 17:12:17 -08:00
Raphael Deem
c2622511ce Raise error if response is malformed. Issue #115 2016-12-24 17:09:41 -08:00
Eli Uriegas
50243037eb Merge pull request #222 from seemethere/add_py36_testing
Adds python36 to tox.ini and .travis.yml
2016-12-24 14:11:55 -08:00
Eli Uriegas
74f305cfb7 Adds python36 to tox.ini and .travis.yml 2016-12-24 14:06:53 -08:00
Eli Uriegas
75990fbaf4 Merge pull request #220 from kgantsov/master
Make golang performance test return JSON instead of string
2016-12-24 13:40:24 -08:00
cr0hn
cc982c5a61 Update response.py
Type check by isinstance
2016-12-24 15:24:25 +01:00
38elements
2d05243c4a Refactor arguments of run function 2016-12-24 22:49:48 +09:00
Konstantin Hantsov
2f0a582aa7 Make golang performance test return JSON instead of string 2016-12-24 10:28:34 +01:00
Eli Uriegas
665881471d Merge pull request #217 from cr0hn/patch-3
Upgraded Middlewares doc: Explain how to chain two (or more) middlewares
2016-12-23 22:31:03 -08:00
38elements
39211f8fbd Refactor arguments of serve function 2016-12-24 11:40:07 +09:00
Raphael Deem
32ea45d403 allow overriding logging.basicConfig 2016-12-23 16:17:34 -08:00
Eli Uriegas
cd17a42234 Fix some verbage 2016-12-23 09:59:28 -08:00
Eli Uriegas
8e19b5938c Merge pull request #216 from cr0hn/patch-2
Apply response Middleware always
2016-12-23 09:57:38 -08:00
Eli Uriegas
9e208ab744 Merge pull request #210 from kdelwat/testing-documentation
Create documentation for testing server endpoints.
2016-12-23 09:57:22 -08:00
Eli Uriegas
94bd9702e5 Merge pull request #211 from rmno/master
Fixed import error in docs
2016-12-23 09:57:08 -08:00
cr0hn
3add40625d Explain how to chain two (or more) middlewares
A funny and useful examples about how to chain middlewares.
2016-12-23 16:07:59 +01:00
cr0hn
5afae986a0 Apply response Middleware always
Response middleware are useful to apply some post-process information, just before send to the user. For example: Add some HTTP headers (security headers, for example), remove "Server" banner (for security reasons) or cookie management. 

The change is very very simple: although an "request" middleware has produced any response, we'll even apply the response middlewares.
2016-12-23 15:59:04 +01:00
cr0hn
f091d82bad Improvement
improvement: support fo binary data as a input. This do that the response process has more performance because not encoding needed.
2016-12-23 13:12:59 +01:00
Romano Bodha
5c1ef2c1cf Fixed import error 2016-12-23 01:42:05 +01:00
Cadel Watson
8411255700 Create documentation for testing server endpoints.
Currently the sanic.utils functionality is undocumented. This provides
information on the interface as well as a complete example of testing
a server endpoint.
2016-12-23 11:08:04 +11:00
38elements
c657c531b4 Customizable protocol 2016-12-23 00:13:38 +09:00
Eli Uriegas
ef9d8710f5 Merge pull request #200 from 38elements/invalid-usage
Change HttpParserError process
2016-12-17 22:50:00 -06:00
38elements
75fc9f91b9 Change HttpParserError process 2016-12-18 09:25:39 +09:00
Eli Uriegas
545d9eb59b Merge pull request #198 from sagnew/patch-2
Fix quotes in sample code for consistency
2016-12-14 11:39:22 -06:00
Sam Agnew
a9b67c3028 Fix quotes in sample code for consistency 2016-12-14 12:36:33 -05:00
Eli Uriegas
35e79f3985 Merge pull request #171 from jpiasetz/convert_dict_to_set
Convert connections dict to set
2016-12-14 11:30:45 -06:00
Eli Uriegas
435d5585e9 Fix leftover blank line
flake8 build failed here: https://travis-ci.org/channelcat/sanic/builds/183991976
2016-12-14 11:29:09 -06:00
Eli Uriegas
ddfb7f2861 Merge branch 'master' into convert_dict_to_set 2016-12-14 11:26:31 -06:00
Eli Uriegas
6c806549ae Merge pull request #194 from sagnew/patch-1
Fix PEP8 in Hello World example
2016-12-13 11:44:53 -06:00
Sam Agnew
8957e4ec25 Fix PEP8 in Hello World example 2016-12-13 12:35:46 -05:00
Paul Jongsma
2003eceba1 remove trailing space 2016-12-13 10:41:39 +01:00
Eli Uriegas
8fc1462d11 Merge pull request #193 from r0fls/jinja-example
add jinja example
2016-12-13 00:30:07 -06:00
Raphael Deem
93b45e9598 add jinja example 2016-12-12 22:25:24 -08:00
Eli Uriegas
a3a14cdab2 Merge pull request #170 from jpiasetz/convert_lambda_to_partial
Convert server lambda to partial
2016-12-12 20:40:29 -06:00
Paul Jongsma
9ba2f99ea2 added a comment on why to decode the file_path 2016-12-13 01:10:24 +01:00
Eli Uriegas
f9db796a6e Merge pull request #189 from AntonDnepr/aiopg_examples
Aiopg examples
2016-12-12 13:22:32 -06:00
Eli Uriegas
94c7aaf7f8 Merge pull request #190 from kamyarg/master
url params docs typo fix
2016-12-12 13:21:46 -06:00
kamyar
6ef6d9a905 url params docs typo fix
add missing '>' in url params docs example
2016-12-11 16:34:22 +02:00
Anton Zhyrney
b44e9baaec aiopg with sqlalchemy example 2016-12-11 14:21:02 +02:00
Anton Zhyrney
f9176bfdea pep8&improvements 2016-12-11 14:14:03 +02:00
Anton Zhyrney
721044b378 improvements for aiopg example 2016-12-11 14:04:24 +02:00
Anton Zhyrney
154f8570f0 add sanic aiopg example with raw sql 2016-12-11 13:43:31 +02:00
Paul Jongsma
0464d31a9c Find URL encoded filenames on the fs by decoding them first 2016-12-10 12:16:37 +01:00
Eli Uriegas
e3453553e1 Merge pull request #183 from 38elements/payload-too-large
Change Payload Too Large process
2016-12-08 10:27:55 -06:00
Eli Uriegas
6abaa78f9e Merge pull request #186 from r0fls/master
return 400 on invalid json post data
2016-12-08 10:27:29 -06:00
Raphael Deem
457507d8dc return 400 on invalid json post data 2016-12-07 20:40:31 -08:00
Eli Uriegas
3ea1a80496 Merge pull request #185 from 1a23456789/master
Fix test_request_timeout.py
2016-12-06 16:04:15 -06:00
1a23456789
fac4bca4f4 Fix test_request_timeout.py
This increases sleep time, Because sometimes timeout error does not occur.
2016-12-06 10:44:08 +09:00
38elements
662e0c9965 Change Payload Too Large process
When Payload Too Large occurs, it uses error handler.
2016-12-04 10:50:32 +09:00
Eli Uriegas
80af9e6d76 Merge pull request #173 from jackfischer/master
fix for cookie header capitalization bug
2016-12-03 17:28:38 -06:00
Jack Fischer
9b466db5c9 test for http2 lowercase header cookies 2016-12-03 15:19:24 -05:00
Jack Fischer
c34427690a Merge branch 'master' of git://github.com/channelcat/sanic 2016-12-03 15:08:07 -05:00
Eli Uriegas
d8a974bb4f Merge pull request #175 from Derrreks/master
Improving comments
2016-12-02 20:07:28 -06:00
Derek Schuster
70c56b7db3 fixing line length 2016-11-28 14:22:07 -05:00
Derek Schuster
209b763302 fix typo 2016-11-28 14:05:47 -05:00
Derek Schuster
190b7a6076 improving comments and examples 2016-11-28 14:00:39 -05:00
Jack Fischer
0c215685f2 refactoring cookies 2016-11-27 08:30:46 -05:00
Jack Fischer
d86ac5e3e0 fix for cookie header capitalization bug 2016-11-26 11:20:29 -05:00
John Piasetzki
47927608b2 Convert connections dict to set
Connections don't need to be a dict since the value is never used
2016-11-25 15:14:19 -05:00
John Piasetzki
13808bf282 Convert server lambda to partial
Partials are faster then lambdas for repeated calls.
2016-11-25 15:13:58 -05:00
73 changed files with 3274 additions and 651 deletions

6
.gitignore vendored
View File

@@ -1,11 +1,15 @@
*~
*.egg-info
*.egg
*.eggs
*.pyc
.coverage
.coverage.*
coverage
.tox
settings.py
*.pyc
.idea/*
.cache/*
.python-version
docs/_build/
docs/_api/

View File

@@ -1,14 +1,10 @@
sudo: false
language: python
python:
- '3.5'
install:
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- python setup.py install
- pip install flake8
- pip install pytest
before_script: flake8 sanic
script: py.test -v tests
- '3.6'
install: pip install tox-travis
script: tox
deploy:
provider: pypi
user: channelcat

View File

@@ -1,91 +0,0 @@
# Sanic
[![Join the chat at https://gitter.im/sanic-python/Lobby](https://badges.gitter.im/sanic-python/Lobby.svg)](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/)
[![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/)
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/.
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
## Benchmarks
All tests were run on an AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for Falcon and Flask but did not speed up requests.
| Server | Implementation | Requests/sec | Avg Latency |
| ------- | ------------------- | ------------:| -----------:|
| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms |
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
| Tornado | Python 3.5 | 2,138 | 46.66ms |
## Hello World
```python
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
app.run(host="0.0.0.0", port=8000)
```
## Installation
* `python -m pip install sanic`
## Documentation
* [Getting started](docs/getting_started.md)
* [Request Data](docs/request_data.md)
* [Routing](docs/routing.md)
* [Middleware](docs/middleware.md)
* [Exceptions](docs/exceptions.md)
* [Blueprints](docs/blueprints.md)
* [Class Based Views](docs/class_based_views.md)
* [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md)
* [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md)
* [License](LICENSE)
## TODO:
* Streamed file processing
* File output
* Examples of integrations with 3rd-party modules
* RESTful router
## Limitations:
* No wheels for uvloop and httptools on Windows :(
## Final Thoughts:
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀

127
README.rst Normal file
View File

@@ -0,0 +1,127 @@
Sanic
=================================
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
Benchmarks
----------
All tests were run on an AWS medium instance running ubuntu, using 1
process. Each script delivered a small JSON response and was tested with
wrk using 100 connections. Pypy was tested for Falcon and Flask but did
not speed up requests.
+-----------+-----------------------+----------------+---------------+
| Server | Implementation | Requests/sec | Avg Latency |
+===========+=======================+================+===============+
| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms |
+-----------+-----------------------+----------------+---------------+
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
+-----------+-----------------------+----------------+---------------+
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
+-----------+-----------------------+----------------+---------------+
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
+-----------+-----------------------+----------------+---------------+
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
+-----------+-----------------------+----------------+---------------+
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
+-----------+-----------------------+----------------+---------------+
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
+-----------+-----------------------+----------------+---------------+
| Tornado | Python 3.5 | 2,138 | 46.66ms |
+-----------+-----------------------+----------------+---------------+
Hello World Example
-------------------
.. code:: python
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
SSL Example
-----------
Optionally pass in an SSLContext:
.. code:: python
import ssl
certificate = "/path/to/certificate"
keyfile = "/path/to/keyfile"
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certificate, keyfile=keyfile)
app.run(host="0.0.0.0", port=8443, ssl=context)
Installation
------------
- ``python -m pip install sanic``
Documentation
-------------
Documentation can be found in the ``docs`` directory.
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master
:target: https://travis-ci.org/channelcat/sanic
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
TODO
----
* Streamed file processing
* File output
* Examples of integrations with 3rd-party modules
* RESTful router
Limitations
-----------
* No wheels for uvloop and httptools on Windows :(
Final Thoughts
--------------
::
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀

View File

@@ -1,25 +1,20 @@
# Blueprints
Blueprints are objects that can be used for sub-routing within an application.
Instead of adding routes to the application object, blueprints define similar
Instead of adding routes to the application instance, blueprints define similar
methods for adding routes, which are then registered with the application in a
flexible and pluggable manner.
## Why?
Blueprints are especially useful for larger applications, where your application
logic can be broken down into several groups or areas of responsibility.
It is also useful for API versioning, where one blueprint may point at
`/v1/<routes>`, and another pointing at `/v2/<routes>`.
Blueprints are especially useful for larger applications, where your
application logic can be broken down into several groups or areas of
responsibility.
## My First Blueprint
The following shows a very simple blueprint that registers a handler-function at
the root `/` of your application.
Suppose you save this file as `my_blueprint.py`, this can be imported in your
Suppose you save this file as `my_blueprint.py`, which can be imported into your
main application later.
```python
@@ -34,7 +29,8 @@ async def bp_root(request):
```
## Registering Blueprints
## Registering blueprints
Blueprints must be registered with the application.
```python
@@ -48,14 +44,19 @@ app.run(host='0.0.0.0', port=8000, debug=True)
```
This will add the blueprint to the application and register any routes defined
by that blueprint.
In this example, the registered routes in the `app.router` will look like:
by that blueprint. In this example, the registered routes in the `app.router`
will look like:
```python
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
```
## Middleware
## Using blueprints
Blueprints have much the same functionality as an application instance.
### Middleware
Using blueprints allows you to also register middleware globally.
```python
@@ -72,40 +73,92 @@ async def halt_response(request, response):
return text('I halted the response')
```
## Exceptions
Exceptions can also be applied exclusively to blueprints globally.
### Exceptions
Exceptions can be applied exclusively to blueprints globally.
```python
@bp.exception(NotFound)
def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
```
## Static files
Static files can also be served globally, under the blueprint prefix.
### Static files
Static files can be served globally, under the blueprint prefix.
```python
bp.static('/folder/to/serve', '/web/path')
```
## Start and Stop
Blueprints and run functions during the start and stop process of the server.
If running in multiprocessor mode (more than 1 worker), these are triggered after the workers fork
## Start and stop
Blueprints can run functions during the start and stop process of the server.
If running in multiprocessor mode (more than 1 worker), these are triggered
after the workers fork.
Available events are:
* before_server_start - Executed before the server begins to accept connections
* after_server_start - Executed after the server begins to accept connections
* before_server_stop - Executed before the server stops accepting connections
* after_server_stop - Executed after the server is stopped and all requests are complete
- `before_server_start`: Executed before the server begins to accept connections
- `after_server_start`: Executed after the server begins to accept connections
- `before_server_stop`: Executed before the server stops accepting connections
- `after_server_stop`: Executed after the server is stopped and all requests are complete
```python
bp = Blueprint('my_blueprint')
@bp.listen('before_server_start')
async def setup_connection():
@bp.listener('before_server_start')
async def setup_connection(app, loop):
global database
database = mysql.connect(host='127.0.0.1'...)
@bp.listen('after_server_stop')
async def close_connection():
@bp.listener('after_server_stop')
async def close_connection(app, loop):
await database.close()
```
## Use-case: API versioning
Blueprints can be very useful for API versioning, where one blueprint may point
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
When a blueprint is initialised, it can take an optional `url_prefix` argument,
which will be prepended to all routes defined on the blueprint. This feature
can be used to implement our API versioning scheme.
```python
# blueprints.py
from sanic.response import text
from sanic import Blueprint
blueprint_v1 = Blueprint('v1')
blueprint_v2 = Blueprint('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')
```
When we register our blueprints on the app, the routes `/v1` and `/v2` will now
point to the individual blueprints, which allows the creation of *sub-sites*
for each API version.
```python
# main.py
from sanic import Sanic
from blueprints import blueprint_v1, blueprint_v2
app = Sanic(__name__)
app.blueprint(blueprint_v1)
app.blueprint(blueprint_v2)
app.run(host='0.0.0.0', port=8000, debug=True)
```
**Previous:** [Exceptions](exceptions.md)
**Next:** [Class-based views](class_based_views.md)

View File

@@ -1,15 +1,32 @@
# Class based views
# Class-Based Views
Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response.
Class-based views are simply classes which implement response behaviour to
requests. They provide a way to compartmentalise handling of different HTTP
request types at the same endpoint. Rather than defining and decorating three
different handler functions, one for each of an endpoint's supported request
type, the endpoint can be assigned a class-based view.
## Defining views
A class-based view should subclass `HTTPMethodView`. You can then implement
class methods for every HTTP request type you want to support. If a request is
received that has no defined method, a `405: Method not allowed` response will
be generated.
To register a class-based view on an endpoint, the `app.add_route` method is
used. The first argument should be the defined class with the method `as_view`
invoked, and the second should be the URL endpoint.
The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class
using all these methods would look like the following.
## Examples
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
class SimpleView(HTTPMethodView):
def get(self, request):
@@ -27,11 +44,14 @@ class SimpleView(HTTPMethodView):
def delete(self, request):
return text('I am delete method')
app.add_route(SimpleView(), '/')
app.add_route(SimpleView.as_view(), '/')
```
If you need any url params just mention them in method definition:
## URL parameters
If you need any URL parameters, as discussed in the routing guide, include them
in the method definition.
```python
class NameView(HTTPMethodView):
@@ -39,6 +59,54 @@ class NameView(HTTPMethodView):
def get(self, request, name):
return text('Hello {}'.format(name))
app.add_route(NameView(), '/<name')
app.add_route(NameView.as_view(), '/<name>')
```
## Decorators
If you want to add any decorators to the class, you can set the `decorators`
class variable. These will be applied to the class when `as_view` is called.
```
class ViewWithDecorator(HTTPMethodView):
decorators = [some_decorator_here]
def get(self, request, name):
return text('Hello I have a decorator')
app.add_route(ViewWithDecorator.as_view(), '/url')
```
## Using CompositionView
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
move handler functions outside of the view class.
Handler functions for each supported HTTP method are defined elsewhere in the
source, and then added to the view using the `CompositionView.add` method. The
first parameter is a list of HTTP methods to handle (e.g. `['GET', 'POST']`),
and the second is the handler function. The following example shows
`CompositionView` usage with both an external handler function and an inline
lambda:
```python
from sanic import Sanic
from sanic.views import CompositionView
from sanic.response import text
app = Sanic(__name__)
def get_handler(request):
return text('I am a get method')
view = CompositionView()
view.add(['GET'], get_handler)
view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
# Use the new view to handle requests to the base URL
app.add_route(view, '/')
```
**Previous:** [Blueprints](blueprints.md)
**Next:** [Cookies](cookies.md)

155
docs/conf.py Normal file
View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Sanic documentation build configuration file, created by
# sphinx-quickstart on Sun Dec 25 18:07:21 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
import os
import sys
# Add support for Markdown documentation using Recommonmark
from recommonmark.parser import CommonMarkParser
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
# autogenerate documentation from docstrings
root_directory = os.path.dirname(os.getcwd())
sys.path.insert(0, root_directory)
import sanic
# -- General configuration ------------------------------------------------
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages']
templates_path = ['_templates']
# Enable support for both Restructured Text and Markdown
source_parsers = {'.md': CommonMarkParser}
source_suffix = ['.rst', '.md']
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'Sanic'
copyright = '2016, Sanic contributors'
author = 'Sanic contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = sanic.__version__
# The full version, including alpha/beta/rc tags.
release = sanic.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
#
# modules.rst is generated by sphinx-apidoc but is unused. This suppresses
# a warning about it.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'modules.rst']
# The name of the Pygments (syntax highlighting) style to use.
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
# a list of builtin themes.
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# 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 = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# 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'),
]
# -- 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)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (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'),
]
# -- Options for Epub output ----------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']

View File

@@ -1,10 +1,35 @@
# How to contribute to Sanic
# Contributing
Thank you for your interest!
Thank you for your interest! Sanic is always looking for contributors. If you
don't feel comfortable contributing code, adding docstrings to the source files
is very appreciated.
## Running tests
* `python -m pip install pytest`
* `python -m pytest tests`
## Documentation
Sanic's documentation is built
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
Markdown and can be found in the `docs` folder, while the module reference is
automatically generated using `sphinx-apidoc`.
To generate the documentation from scratch:
```bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
```
The HTML documentation will be created in the `docs/_build` folder.
## Warning
One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged.
One of the main goals of Sanic is speed. Code that lowers the performance of
Sanic without significant gains in usability, security, or features may not be
merged. Please don't let this intimidate you! If you have any concerns about an
idea, open an issue for discussion and help.
**Previous:** [Sanic extensions](extensions.md)

View File

@@ -1,13 +1,13 @@
# Cookies
## Request
Cookies are pieces of data which persist inside a user's browser. Sanic can
both read and write cookies, which are stored as key-value pairs.
Request cookies can be accessed via the request.cookie dictionary
## Reading cookies
### Example
A user's cookies can be accessed `Request` object's `cookie` dictionary.
```python
from sanic import Sanic
from sanic.response import text
@app.route("/cookie")
@@ -16,28 +16,11 @@ async def test(request):
return text("Test cookie set to: {}".format(test_cookie))
```
## Response
## Writing cookies
Response cookies can be set like dictionary values and
have the following parameters available:
* expires - datetime - Time for cookie to expire on the client's browser
* path - string - The Path attribute specifies the subset of URLs to
which this cookie applies
* comment - string - Cookie comment (metadata)
* domain - string - Specifies the domain for which the
cookie is valid. An explicitly specified domain must always
start with a dot.
* max-age - number - Number of seconds the cookie should live for
* secure - boolean - Specifies whether the cookie will only be sent via
HTTPS
* httponly - boolean - Specifies whether the cookie cannot be read
by javascript
### Example
When returning a response, cookies can be set on the `Response` object.
```python
from sanic import Sanic
from sanic.response import text
@app.route("/cookie")
@@ -47,4 +30,50 @@ async def test(request):
response.cookies['test']['domain'] = '.gotta-go-fast.com'
response.cookies['test']['httponly'] = True
return response
```
```
## Deleting cookies
Cookies can be removed semantically or explicitly.
```python
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
```
Response cookies can be set like dictionary values and have the following
parameters available:
- `expires` (datetime): The time for the cookie to expire on the
client's browser.
- `path` (string): The subset of URLs to which this cookie applies. Defaults to /.
- `comment` (string): A comment (metadata).
- `domain` (string): Specifies the domain for which the cookie is valid. An
explicitly specified domain must always start with a dot.
- `max-age` (number): Number of seconds the cookie should live for.
- `secure` (boolean): Specifies whether the cookie will only be sent via
HTTPS.
- `httponly` (boolean): Specifies whether the cookie cannot be read by
Javascript.
**Previous:** [Class-based views](class_based_views.md)
**Next:** [Custom protocols](custom_protocol.md)

76
docs/custom_protocol.md Normal file
View File

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

View File

@@ -1,35 +1,59 @@
# Deploying
When it comes to deploying Sanic, there's not much to it, but there are
a few things to take note of.
Deploying Sanic is made simple by the inbuilt webserver. After defining an
instance of `sanic.Sanic`, we can call the `run` method with the following
keyword arguments:
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
- `port` *(default `8000`)*: Port to host the server on.
- `debug` *(default `False`)*: Enables debug output (slows server).
- `before_start` *(default `None`)*: Function or list of functions to be executed
before the server starts accepting connections.
- `after_start` *(default `None`)*: Function or list of functions to be executed
after the server starts accepting connections.
- `before_stop` *(default `None`)*: Function or list of functions to be
executed when a stop signal is received before it is
respected.
- `after_stop` *(default `None`)*: Function or list of functions to be executed
when all requests are complete.
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
- `sock` *(default `None`)*: Socket for the server to accept connections from.
- `workers` *(default `1`)*: Number of worker processes to spawn.
- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is
specified, Sanic creates its own event loop.
- `protocol` *(default `HttpProtocol`)*: Subclass
of
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
## Workers
By default, Sanic listens in the main process using only 1 CPU core.
To crank up the juice, just specify the number of workers in the run
arguments like so:
By default, Sanic listens in the main process using only one CPU core. To crank
up the juice, just specify the number of workers in the `run` arguments.
```python
app.run(host='0.0.0.0', port=1337, workers=4)
```
Sanic will automatically spin up multiple processes and route
traffic between them. We recommend as many workers as you have
available cores.
Sanic will automatically spin up multiple processes and route traffic between
them. We recommend as many workers as you have available cores.
## Running via Command
## Running via command
If you like using command line arguments, you can launch a sanic server
by executing the module. For example, if you initialized sanic as
app in a file named server.py, you could run the server like so:
If you like using command line arguments, you can launch a Sanic server by
executing the module. For example, if you initialized Sanic as `app` in a file
named `server.py`, you could run the server like so:
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
With this way of running sanic, it is not necessary to run app.run in
your python file. If you do, just make sure you wrap it in name == main
like so:
With this way of running sanic, it is not necessary to invoke `app.run` in your
Python file. If you do, make sure you wrap it so that it only executes when
directly run by the interpreter.
```python
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
```
```
**Previous:** [Request Data](request_data.md)
**Next:** [Static Files](static_files.md)

View File

@@ -1,28 +1,49 @@
# Exceptions
Exceptions can be thrown from within request handlers and will automatically be handled by Sanic. Exceptions take a message as their first argument, and can also take a status_code to be passed back in the HTTP response. Check sanic.exceptions for the full list of exceptions to throw.
Exceptions can be thrown from within request handlers and will automatically be
handled by Sanic. Exceptions take a message as their first argument, and can
also take a status code to be passed back in the HTTP response.
## Throwing an exception
To throw an exception, simply `raise` the relevant exception from the
`sanic.exceptions` module.
```python
from sanic import Sanic
from sanic.exceptions import ServerError
@app.route('/killme')
def i_am_ready_to_die(request):
raise ServerError("Something bad happened")
raise ServerError("Something bad happened", status_code=500)
```
## Handling Exceptions
## Handling exceptions
Just use the @exception decorator. The decorator expects a list of exceptions to handle as arguments. You can pass SanicException to catch them all! The exception handler must expect a request and exception object as arguments.
To override Sanic's default handling of an exception, the `@app.exception`
decorator is used. The decorator expects a list of exceptions to handle as
arguments. You can pass `SanicException` to catch them all! The decorated
exception handler function must take a `Request` and `Exception` object as
arguments.
```python
from sanic import Sanic
from sanic.response import text
from sanic.exceptions import NotFound
@app.exception(NotFound)
def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
```
```
## Useful exceptions
Some of the most useful exceptions are presented below:
- `NotFound`: called when a suitable route for the request isn't found.
- `ServerError`: called when something goes wrong inside the server. This
usually occurs if there is an exception raised in user code.
See the `sanic.exceptions` module for the full list of exceptions to throw.
**Previous:** [Middleware](middleware.md)
**Next:** [Blueprints](blueprints.md)

12
docs/extensions.md Normal file
View File

@@ -0,0 +1,12 @@
# Sanic Extensions
A list of Sanic extensions created by the community.
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
Allows using redis, memcache or an in memory store.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
**Previous:** [Testing](testing.md)
**Next:** [Contributing](contributing.md)

View File

@@ -1,25 +1,29 @@
# Getting Started
Make sure you have pip and python 3.5 before starting
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
syntax, so earlier versions of python won't work.
## Benchmarks
* Install Sanic
* `python3 -m pip install sanic`
* Edit main.py to include:
```python
from sanic import Sanic
from sanic.response import json
1. Install Sanic: `python3 -m pip install sanic`
2. Create a file called `main.py` with the following code:
app = Sanic(__name__)
```python
from sanic import Sanic
from sanic.response import text
@app.route("/")
async def test(request):
return json({ "hello": "world" })
app = Sanic(__name__)
app.run(host="0.0.0.0", port=8000, debug=True)
```
* Run `python3 main.py`
@app.route("/")
async def test(request):
return text('Hello world!')
You now have a working Sanic server! To continue on, check out:
* [Request Data](request_data.md)
* [Routing](routing.md)
app.run(host="0.0.0.0", port=8000, debug=True)
```
3. Run the server: `python3 main.py`
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
the message *Hello world!*.
You now have a working Sanic server!
**Next:** [Routing](routing.md)

33
docs/index.rst Normal file
View File

@@ -0,0 +1,33 @@
.. include:: ../README.rst
Guides
======
.. toctree::
:maxdepth: 2
getting_started
routing
request_data
deploying
static_files
middleware
exceptions
blueprints
class_based_views
cookies
custom_protocol
testing
extensions
contributing
Module Documentation
====================
.. toctree::
Module Reference <_api/sanic>
* :ref:`genindex`
* :ref:`search`

View File

@@ -1,18 +1,61 @@
# Middleware
Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware returns a response object, the request will stop processing and a response will be returned.
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.
Middleware is registered via the middleware decorator, and can either be added as 'request' or 'response' middleware, based on the argument provided in the decorator. Response middleware receives both the request and the response as arguments.
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
receives both the request and the response as arguments.
## Examples
The simplest middleware doesn't modify the request or response at all:
```python
@app.middleware('request')
async def print_on_request(request):
print("I print when a request is received by the server")
@app.middleware('response')
async def print_on_response(request, response):
print("I print when a response is returned by the server")
```
## Modifying the request or response
Middleware can modify the request or response parameter it is given, *as long
as it does not return it*. The following example shows a practical use-case for
this.
```python
app = Sanic(__name__)
@app.middleware
async def halt_request(request):
print("I am a spy")
@app.middleware('response')
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
app.run(host="0.0.0.0", port=8000)
```
The above code will apply the two middleware in order. First, the middleware
**custom_banner** will change the HTTP response header *Server* to
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
are invoked *after* a user function returns a response.
## Responding early
If middleware returns a `HTTPResponse` object, the request will stop processing
and the response will be returned. If this occurs to a request before the
relevant user route handler is reached, the handler will never be called.
Returning a response will also prevent any further middleware from running.
```python
@app.middleware('request')
async def halt_request(request):
return text('I halted the request')
@@ -20,10 +63,8 @@ async def halt_request(request):
@app.middleware('response')
async def halt_response(request, response):
return text('I halted the response')
@app.route('/')
async def handler(request):
return text('I would like to speak now please')
app.run(host="0.0.0.0", port=8000)
```
**Previous:** [Static Files](static_files.md)
**Next:** [Exceptions](exceptions.md)

View File

@@ -1,49 +1,95 @@
# Request Data
## Properties
When an endpoint receives a HTTP request, the route function is passed a
`Request` object.
The following request variables are accessible as properties:
The following variables are accessible as properties on `Request` objects:
`request.files` (dictionary of File objects) - List of files that have a name, body, and type
`request.json` (any) - JSON body
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
- `json` (any) - JSON body
See request.py for more information
```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 request's `query_string` variable holds the unparsed string value.
## Examples
```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 })
```
- `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')
file_parameters = {
'body': test_file.body,
'name': test_file.name,
'type': test_file.type,
}
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
```
- `form` (dict) - Posted form variables.
```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') })
```
- `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.
## Accessing values using `get` and `getlist`
The request properties which return a dictionary actually return a subclass of
`dict` called `RequestParameters`. The key difference when using this object is
the distinction between the `get` and `getlist` methods.
- `get(key, default=None)` operates as normal, except that when the value of
the given key is a list, *only the first item is returned*.
- `getlist(key, default=None)` operates as normal, *returning the entire list*.
```python
from sanic import Sanic
from sanic.response import json, text
from sanic.request import RequestParameters
@app.route("/json")
def post_json(request):
return json({ "received": True, "message": request.json })
args = RequestParameters()
args['titles'] = ['Post 1', 'Post 2']
@app.route("/form")
def post_json(request):
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
args.get('titles') # => 'Post 1'
@app.route("/files")
def post_json(request):
test_file = request.files.get('test')
file_parameters = {
'body': test_file.body,
'name': test_file.name,
'type': test_file.type,
}
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
@app.route("/query_string")
def query_string(request):
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
@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)
args.getlist('titles') # => ['Post 1', 'Post 2']
```
**Previous:** [Routing](routing.md)
**Next:** [Deploying](deploying.md)

View File

@@ -1,17 +1,48 @@
# Routing
Sanic comes with a basic router that supports request parameters. To specify a parameter, surround it with carrots like so: `<PARAM>`. Request parameters will be passed to the request handler functions as keyword arguments. To specify a type, add a :type after the parameter name, in the carrots. If the parameter does not match the type supplied, Sanic will throw a NotFound exception, resulting in a 404 page not found error.
Routing allows the user to specify handler functions for different URL endpoints.
## Examples
A basic route looks like the following, where `app` is an instance of the
`Sanic` class:
```python
from sanic.response import json
@app.route("/")
async def test(request):
return json({ "hello": "world" })
```
When the url `http://server.url/` is accessed (the base url of the server), the
final `/` is matched by the router to the handler function, `test`, which then
returns a JSON object.
Sanic handler functions must be defined using the `async def` syntax, as they
are asynchronous functions.
## Request parameters
Sanic comes with a basic router that supports request parameters.
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
Request parameters will be passed to the route handler functions as keyword
arguments.
```python
from sanic import Sanic
from sanic.response import text
@app.route('/tag/<tag>')
async def tag_handler(request, tag):
return text('Tag - {}'.format(tag))
```
To specify a type for the parameter, add a `:type` after the parameter name,
inside the quotes. If the parameter does not match the specified type, Sanic
will throw a `NotFound` exception, resulting in a `404: Page not found` error
on the URL.
```python
from sanic.response import text
@app.route('/number/<integer_arg:int>')
async def integer_handler(request, integer_arg):
@@ -29,16 +60,52 @@ async def person_handler(request, name):
async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id))
async def handler1(request):
return text('OK')
app.add_route(handler1, '/test')
```
async def handler(request, name):
return text('Folder - {}'.format(name))
app.add_route(handler, '/folder/<name>')
## HTTP request types
async def person_handler(request, name):
return text('Person - {}'.format(name))
app.add_route(handler, '/person/<name:[A-z]>')
By default, a route defined on a URL will be used for all requests to that URL.
However, the `@app.route` decorator accepts an optional parameter, `methods`,
which restricts the handler function to the HTTP methods in the given list.
```python
from sanic.response import text
@app.route('/post')
async def post_handler(request, methods=['POST']):
return text('POST request - {}'.format(request.json))
@app.route('/get')
async def GET_handler(request, methods=['GET']):
return text('GET request - {}'.format(request.args))
```
## The `add_route` method
As we have seen, routes are often specified using the `@app.route` decorator.
However, this decorator is really just a wrapper for the `app.add_route`
method, which is used as follows:
```python
from sanic.response import text
# Define the handler functions
async def handler1(request):
return text('OK')
async def handler2(request, name):
return text('Folder - {}'.format(name))
async def person_handler2(request, name):
return text('Person - {}'.format(name))
# Add each handler function as a route
app.add_route(handler1, '/test')
app.add_route(handler2, '/folder/<name>')
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
```
**Previous:** [Getting Started](getting_started.md)
**Next:** [Request Data](request_data.md)

View File

@@ -1,10 +1,11 @@
# Static Files
Both directories and files can be served by registering with static
## Example
Static files and directories, such as an image file, are served by Sanic when
registered with the `app.static` method. The method takes an endpoint URL and a
filename. The file specified will then be accessible via the given endpoint.
```python
from sanic import Sanic
app = Sanic(__name__)
# Serves files from the static folder to the URL /static
@@ -16,3 +17,7 @@ app.static('/the_best.png', '/home/ubuntu/test.png')
app.run(host="0.0.0.0", port=8000)
```
**Previous:** [Deploying](deploying.md)
**Next:** [Middleware](middleware.md)

55
docs/testing.md Normal file
View File

@@ -0,0 +1,55 @@
# Testing
Sanic endpoints can be tested locally using the `sanic.utils` module, which
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
library. The `sanic_endpoint_test` function runs a local server, issues a
configurable request to an endpoint, and returns the result. It takes the
following arguments:
- `app` An instance of a Sanic app.
- `method` *(default `'get'`)* A string representing the HTTP method to use.
- `uri` *(default `'/'`)* A string representing the endpoint to test.
- `gather_request` *(default `True`)* A boolean which determines whether the
original request will be returned by the function. If set to `True`, the
return value is a tuple of `(request, response)`, if `False` only the
response is returned.
- `loop` *(default `None`)* The event loop to use.
- `debug` *(default `False`)* A boolean which determines whether to run the
server in debug mode.
The function further takes the `*request_args` and `**request_kwargs`, which
are passed directly to the aiohttp ClientSession request. For example, to
supply data with a GET request, `method` would be `get` and the keyword
argument `params={'value', 'key'}` would be supplied. More information about
the available arguments to aiohttp can be found
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
Below is a complete example of an endpoint test,
using [pytest](http://doc.pytest.org/en/latest/). The test checks that the
`/challenge` endpoint responds to a GET request with a supplied challenge
string.
```python
import pytest
import aiohttp
from sanic.utils import sanic_endpoint_test
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
def test_endpoint_challenge():
# Create the challenge data
request_data = {'challenge': 'dummy_challenge'}
# Send the request to the endpoint, using the default `get` method
request, response = sanic_endpoint_test(app,
uri='/challenge',
params=request_data)
# Assert that the server responds with the challenge string
assert response.text == request_data['challenge']
```
**Previous:** [Custom protocols](custom_protocol.md)
**Next:** [Sanic extensions](extensions.md)

View File

@@ -1,12 +1,8 @@
from sanic import Sanic
from sanic.response import json
import uvloop
import aiohttp
#Create an event loop manually so that we can use it for both sanic & aiohttp
loop = uvloop.new_event_loop()
app = Sanic(__name__)
async def fetch(session, url):
@@ -24,10 +20,9 @@ async def test(request):
"""
url = "https://api.github.com/repos/channelcat/sanic"
async with aiohttp.ClientSession(loop=loop) as session:
async with aiohttp.ClientSession() as session:
response = await fetch(session, url)
return json(response)
app.run(host="0.0.0.0", port=8000, loop=loop)
app.run(host="0.0.0.0", port=8000, workers=2)

View File

@@ -21,7 +21,7 @@ from aiocache.serializers import JsonSerializer
app = Sanic(__name__)
aiocache.settings.set_defaults(
cache="aiocache.RedisCache"
class_="aiocache.RedisCache"
)

View File

@@ -9,31 +9,27 @@ 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
"""
Imports and code relevant for our CustomHandler class
(Ordinarily this would be in a separate file)
"""
from sanic.response import text
from sanic.exceptions import Handler, SanicException
class CustomHandler(Handler):
def default(self, request, exception):
# Here, we have access to the exception object
# and can do anything with it (log, send to external service, etc)
# Some exceptions are trivial and built into Sanic (404s, etc)
if not issubclass(type(exception), SanicException):
if not isinstance(exception, SanicException):
print(exception)
# Then, we must finish handling the exception by returning
# our response to the client
# For this we can just call the super class' default handler
return super.default(self, request, exception)
return super().default(request, exception)
"""
@@ -49,11 +45,12 @@ app = Sanic(__name__)
handler = CustomHandler(sanic=app)
app.error_handler = handler
@app.route("/")
async def test(request):
# Here, something occurs which causes an unexpected exception
# This exception will flow to our custom handler.
x = 1 / 0
1 / 0
return json({"test": True})

18
examples/jinja_example.py Normal file
View File

@@ -0,0 +1,18 @@
## To use this example:
# curl -d '{"name": "John Doe"}' localhost:8000
from sanic import Sanic
from sanic.response import html
from jinja2 import Template
template = Template('Hello {{ name }}!')
app = Sanic(__name__)
@app.route('/')
async def test(request):
data = request.json
return html(template.render(**data))
app.run(host="0.0.0.0", port=8000)

View File

@@ -0,0 +1,36 @@
from sanic import Sanic
from sanic.response import json
import asyncio
import aiohttp
app = Sanic(__name__)
sem = None
def init(sanic, loop):
global sem
CONCURRENCY_PER_WORKER = 4
sem = asyncio.Semaphore(CONCURRENCY_PER_WORKER, loop=loop)
async def bounded_fetch(session, url):
"""
Use session object to perform 'get' request on url
"""
async with sem, session.get(url) as response:
return await response.json()
@app.route("/")
async def test(request):
"""
Download and serve example JSON
"""
url = "https://api.github.com/repos/channelcat/sanic"
async with aiohttp.ClientSession() as session:
response = await bounded_fetch(session, url)
return json(response)
app.run(host="0.0.0.0", port=8000, workers=2, before_start=init)

View File

@@ -0,0 +1,23 @@
from sanic import Sanic
from sanic.response import text
import json
import logging
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
logging_format += "%(message)s"
logging.basicConfig(
format=logging_format,
level=logging.DEBUG
)
log = logging.getLogger()
# Set logger to override default basicConfig
sanic = Sanic()
@sanic.route("/")
def test(request):
log.info("received request; responding with 'hey'")
return text("hey")
sanic.run(host="0.0.0.0", port=8000)

20
examples/run_async.py Normal file
View File

@@ -0,0 +1,20 @@
from sanic import Sanic
from sanic.response import json
from multiprocessing import Event
from signal import signal, SIGINT
import asyncio
app = Sanic(__name__)
@app.route("/")
async def test(request):
return json({"answer": "42"})
server = app.create_server(host="0.0.0.0", port=8001)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
signal(SIGINT, lambda s, f: loop.close())
try:
loop.run_forever()
except:
loop.stop()

View File

@@ -0,0 +1,65 @@
""" To run this example you need additional aiopg package
"""
import os
import asyncio
import uvloop
import aiopg
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']
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
database_password,
database_host,
database_name)
async def get_pool():
return await aiopg.create_pool(connection)
app = Sanic(name=__name__)
async def prepare_db(app, loop):
"""
Let's create some table and add some data
"""
async with aiopg.create_pool(connection) as pool:
async with pool.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 handle(request):
result = []
async def test_select():
async with aiopg.create_pool(connection) as pool:
async with pool.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]})
res = await test_select()
return json({'polls': result})
if __name__ == '__main__':
app.run(host='0.0.0.0',
port=8000,
debug=True,
before_start=prepare_db)

View File

@@ -0,0 +1,66 @@
""" To run this example you need additional aiopg package
"""
import os
import asyncio
import datetime
import uvloop
from aiopg.sa import create_engine
import sqlalchemy as sa
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']
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
database_password,
database_host,
database_name)
metadata = sa.MetaData()
polls = sa.Table('sanic_polls', metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('question', sa.String(50)),
sa.Column("pub_date", sa.DateTime))
app = Sanic(name=__name__)
async def prepare_db(app, loop):
""" Let's add some data
"""
async with create_engine(connection) as engine:
async with engine.acquire() as conn:
await conn.execute('DROP TABLE IF EXISTS sanic_polls')
await conn.execute("""CREATE TABLE sanic_polls (
id serial primary key,
question varchar(50),
pub_date timestamp
);""")
for i in range(0, 100):
await conn.execute(
polls.insert().values(question=i,
pub_date=datetime.datetime.now())
)
@app.route("/")
async def handle(request):
async with create_engine(connection) as engine:
async with engine.acquire() as conn:
result = []
async for row in conn.execute(polls.select()):
result.append({"question": row.question, "pub_date": row.pub_date})
return json({"polls": result})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, before_start=prepare_db)

View File

@@ -0,0 +1,58 @@
""" To run this example you need additional asyncpg package
"""
import os
import asyncio
import uvloop
from asyncpg import create_pool
from sanic import Sanic
from sanic.response import json
DB_CONFIG = {
'host': '<host>',
'user': '<username>',
'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]
app = Sanic(__name__)
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())""")
@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)})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, before_start=create_db)

View File

@@ -9,19 +9,18 @@ from sanic import Sanic
from sanic.response import json
## peewee_async related imports
import uvloop
import peewee
from peewee_async import Manager, PostgresqlDatabase
# we instantiate a custom loop so we can pass it to our db manager
loop = uvloop.new_event_loop()
database = PostgresqlDatabase(database='test',
host='127.0.0.1',
user='postgres',
password='mysecretpassword')
def setup(app, loop):
database = PostgresqlDatabase(database='test',
host='127.0.0.1',
user='postgres',
password='mysecretpassword')
objects = Manager(database, loop=loop)
objects = Manager(database, loop=loop)
## from peewee_async docs:
# Also theres no need to connect and re-connect before executing async queries
@@ -76,5 +75,4 @@ async def get(request):
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000, loop=loop)
app.run(host='0.0.0.0', port=8000, before_start=setup)

View File

@@ -64,11 +64,11 @@ def query_string(request):
# Run Server
# ----------------------------------------------- #
def after_start(loop):
def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH")
def before_stop(loop):
def before_stop(app, loop):
log.info("TRIED EVERYTHING")

39
examples/vhosts.py Normal file
View File

@@ -0,0 +1,39 @@
from sanic.response import text
from sanic import Sanic
from sanic.blueprints import Blueprint
# Usage
# curl -H "Host: example.com" localhost:8000
# curl -H "Host: sub.example.com" localhost:8000
# curl -H "Host: bp.example.com" localhost:8000/question
# curl -H "Host: bp.example.com" localhost:8000/answer
app = Sanic()
bp = Blueprint("bp", host="bp.example.com")
@app.route('/', host=["example.com",
"somethingelse.com",
"therestofyourdomains.com"])
async def hello(request):
return text("Some defaults")
@app.route('/', host="example.com")
async def hello(request):
return text("Answer")
@app.route('/', host="sub.example.com")
async def hello(request):
return text("42")
@bp.route("/question")
async def hello(request):
return text("What is the meaning of life?")
@bp.route("/answer")
async def hello(request):
return text("42")
app.register_blueprint(bp)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -12,3 +12,6 @@ kyoukai
falcon
tornado
aiofiles
sphinx
recommonmark
beautifulsoup4

View File

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

View File

@@ -1,6 +1,6 @@
from .sanic import Sanic
from .blueprints import Blueprint
__version__ = '0.1.8'
__version__ = '0.3.0'
__all__ = ['Sanic', 'Blueprint']

View File

@@ -20,7 +20,7 @@ if __name__ == "__main__":
module = import_module(module_name)
app = getattr(module, app_name, None)
if type(app) is not Sanic:
if not isinstance(app, Sanic):
raise ValueError("Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?"
.format(type(app).__name__, args.module))

View File

@@ -3,6 +3,7 @@ from collections import defaultdict
class BlueprintSetup:
"""
Creates a blueprint state like object.
"""
def __init__(self, blueprint, app, options):
@@ -18,24 +19,27 @@ class BlueprintSetup:
#: blueprint.
self.url_prefix = url_prefix
def add_route(self, handler, uri, methods):
def add_route(self, handler, uri, methods, host=None):
"""
A helper method to register a handler to the application url routes.
"""
if self.url_prefix:
uri = self.url_prefix + uri
self.app.route(uri=uri, methods=methods)(handler)
if host is None:
host = self.blueprint.host
self.app.route(uri=uri, methods=methods, host=host)(handler)
def add_exception(self, handler, *args, **kwargs):
"""
Registers exceptions to sanic
Registers exceptions to sanic.
"""
self.app.exception(*args, **kwargs)(handler)
def add_static(self, uri, file_or_directory, *args, **kwargs):
"""
Registers static files to sanic
Registers static files to sanic.
"""
if self.url_prefix:
uri = self.url_prefix + uri
@@ -44,7 +48,7 @@ class BlueprintSetup:
def add_middleware(self, middleware, *args, **kwargs):
"""
Registers middleware to sanic
Registers middleware to sanic.
"""
if args or kwargs:
self.app.middleware(*args, **kwargs)(middleware)
@@ -53,7 +57,7 @@ class BlueprintSetup:
class Blueprint:
def __init__(self, name, url_prefix=None):
def __init__(self, name, url_prefix=None, host=None):
"""
Creates a new blueprint
:param name: Unique name of the blueprint
@@ -63,6 +67,7 @@ class Blueprint:
self.url_prefix = url_prefix
self.deferred_functions = []
self.listeners = defaultdict(list)
self.host = host
def record(self, func):
"""
@@ -73,32 +78,43 @@ class Blueprint:
def make_setup_state(self, app, options):
"""
Returns a new BlueprintSetup object
"""
return BlueprintSetup(self, app, options)
def register(self, app, options):
"""
Registers the blueprint to the sanic app.
"""
state = self.make_setup_state(app, options)
for deferred in self.deferred_functions:
deferred(state)
def route(self, uri, methods=None):
def route(self, uri, methods=frozenset({'GET'}), host=None):
"""
Creates 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):
self.record(lambda s: s.add_route(handler, uri, methods))
self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler
return decorator
def add_route(self, handler, uri, methods=None):
def add_route(self, handler, uri, methods=None, host=None):
"""
Creates a blueprint route from a function.
:param handler: Function to handle uri request.
:param uri: Endpoint at which the route will be accessible.
:param methods: List of acceptable HTTP methods.
"""
self.record(lambda s: s.add_route(handler, uri, methods))
self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler
def listener(self, event):
"""
Create a listener from a decorated function.
:param event: Event to listen to.
"""
def decorator(listener):
self.listeners[event].append(listener)
@@ -107,6 +123,7 @@ class Blueprint:
def middleware(self, *args, **kwargs):
"""
Creates a blueprint middleware from a decorated function.
"""
def register_middleware(middleware):
self.record(
@@ -123,6 +140,7 @@ class Blueprint:
def exception(self, *args, **kwargs):
"""
Creates a blueprint exception from a decorated function.
"""
def decorator(handler):
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
@@ -131,6 +149,9 @@ class Blueprint:
def static(self, uri, file_or_directory, *args, **kwargs):
"""
Creates a blueprint static route from a decorated function.
:param uri: Endpoint at which the route will be accessible.
:param file_or_directory: Static asset.
"""
self.record(
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))

View File

@@ -42,8 +42,9 @@ class CookieJar(dict):
"""
CookieJar dynamically writes headers as cookies are added and removed
It gets around the limitation of one header per name by using the
MultiHeader class to provide a unique key that encodes to Set-Cookie
MultiHeader class to provide a unique key that encodes to Set-Cookie.
"""
def __init__(self, headers):
super().__init__()
self.headers = headers
@@ -54,6 +55,7 @@ class CookieJar(dict):
cookie_header = self.cookie_headers.get(key)
if not cookie_header:
cookie = Cookie(key, value)
cookie['path'] = '/'
cookie_header = MultiHeader("Set-Cookie")
self.cookie_headers[key] = cookie_header
self.headers[cookie_header] = cookie
@@ -62,8 +64,14 @@ class CookieJar(dict):
self[key].value = value
def __delitem__(self, key):
del self.cookie_headers[key]
return super().__delitem__(key)
if key not in self.cookie_headers:
self[key] = ''
self[key]['max-age'] = 0
else:
cookie_header = self.cookie_headers[key]
del self.headers[cookie_header]
del self.cookie_headers[key]
return super().__delitem__(key)
class Cookie(dict):
@@ -107,7 +115,8 @@ class Cookie(dict):
value.strftime("%a, %d-%b-%Y %T GMT")
))
elif key in self._flags:
output.append(self._keys[key])
if self[key]:
output.append(self._keys[key])
else:
output.append('%s=%s' % (self._keys[key], value))

View File

@@ -1,5 +1,104 @@
from .response import text
from traceback import format_exc
from .response import text, html
from .log import log
from traceback import format_exc, extract_tb
import sys
TRACEBACK_STYLE = '''
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
}
p {
margin: 0;
}
.summary {
padding: 10px;
}
h1 {
margin-bottom: 0;
}
h3 {
margin-top: 10px;
}
h3 code {
font-size: 24px;
}
.frame-line > * {
padding: 5px 10px;
}
.frame-line {
margin-bottom: 5px;
}
.frame-code {
font-size: 16px;
padding-left: 30px;
}
.tb-wrapper {
border: 1px solid #f3f3f3;
}
.tb-header {
background-color: #f3f3f3;
padding: 5px 10px;
}
.frame-descriptor {
background-color: #e2eafb;
}
.frame-descriptor {
font-size: 14px;
}
</style>
'''
TRACEBACK_WRAPPER_HTML = '''
<html>
<head>
{style}
</head>
<body>
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
<p class="summary">
<b>{exc_name}: {exc_value}</b>
while handling uri <code>{uri}</code>
</p>
</div>
</body>
</html>
'''
TRACEBACK_LINE_HTML = '''
<div class="frame-line">
<p class="frame-descriptor">
File {0.filename}, line <i>{0.lineno}</i>,
in <code><b>{0.name}</b></code>
</p>
<p class="frame-code"><code>{0.line}</code></p>
</div>
'''
INTERNAL_SERVER_ERROR_HTML = '''
<h1>Internal Server Error</h1>
<p>
The server encountered an internal error and cannot complete
your request.
</p>
'''
class SanicException(Exception):
@@ -34,12 +133,31 @@ class RequestTimeout(SanicException):
status_code = 408
class PayloadTooLarge(SanicException):
status_code = 413
class Handler:
handlers = None
def __init__(self, sanic):
def __init__(self):
self.handlers = {}
self.sanic = sanic
self.debug = False
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
frames = extract_tb(tb)
frame_html = []
for frame in frames:
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
return TRACEBACK_WRAPPER_HTML.format(
style=TRACEBACK_STYLE,
exc_name=exc_type.__name__,
exc_value=exc_value,
frame_html=''.join(frame_html),
uri=request.url)
def add(self, exception, handler):
self.handlers[exception] = handler
@@ -47,23 +165,40 @@ class Handler:
def response(self, request, exception):
"""
Fetches and executes an exception handler and returns a response object
:param request: Request
:param exception: Exception to handle
:return: Response object
"""
handler = self.handlers.get(type(exception), self.default)
response = handler(request=request, exception=exception)
try:
response = handler(request=request, exception=exception)
except:
log.error(format_exc())
if self.debug:
response_message = (
'Exception raised in exception handler "{}" '
'for uri: "{}"\n{}').format(
handler.__name__, request.url, format_exc())
log.error(response_message)
return text(response_message, 500)
else:
return text('An error occurred while handling an error', 500)
return response
def default(self, request, exception):
if issubclass(type(exception), SanicException):
log.error(format_exc())
if isinstance(exception, SanicException):
return text(
"Error: {}".format(exception),
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500))
elif self.sanic.debug:
return text(
"Error: {}\nException: {}".format(
exception, format_exc()), status=500)
elif self.debug:
html_output = self._render_traceback_html(exception, request)
response_message = (
'Exception occurred while handling uri: "{}"\n{}'.format(
request.url, format_exc()))
log.error(response_message)
return html(html_output, status=500)
else:
return text(
"An error occurred while generating the request", status=500)
return html(INTERNAL_SERVER_ERROR_HTML, status=500)

View File

@@ -1,5 +1,3 @@
import logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
log = logging.getLogger(__name__)
log = logging.getLogger('sanic')

View File

@@ -4,6 +4,7 @@ from http.cookies import SimpleCookie
from httptools import parse_url
from urllib.parse import parse_qs
from ujson import loads as json_loads
from sanic.exceptions import InvalidUsage
from .log import log
@@ -20,16 +21,13 @@ class RequestParameters(dict):
value of the list and getlist returns the whole shebang
"""
def __init__(self, *args, **kwargs):
self.super = super()
self.super.__init__(*args, **kwargs)
def get(self, name, default=None):
values = self.super.get(name)
return values[0] if values else default
"""Return the first value, either the default or actual"""
return super().get(name, [default])[0]
def getlist(self, name, default=None):
return self.super.get(name, default)
"""Return the entire list"""
return super().get(name, default)
class Request(dict):
@@ -37,24 +35,26 @@ class Request(dict):
Properties of an HTTP request such as URL, headers, etc.
"""
__slots__ = (
'url', 'headers', 'version', 'method', '_cookies',
'url', 'headers', 'version', 'method', '_cookies', 'transport',
'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip',
)
def __init__(self, url_bytes, headers, version, method):
def __init__(self, url_bytes, headers, version, method, transport):
# TODO: Content-Encoding detection
url_parsed = parse_url(url_bytes)
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 = None
self.body = []
self.parsed_json = None
self.parsed_form = None
self.parsed_files = None
@@ -63,14 +63,25 @@ class Request(dict):
@property
def json(self):
if not self.parsed_json:
if self.parsed_json is None:
try:
self.parsed_json = json_loads(self.body)
except Exception:
log.exception("failed when parsing body as json")
raise InvalidUsage("Failed when parsing body as json")
return self.parsed_json
@property
def token(self):
"""
Attempts to return the auth header token.
:return: token related to request
"""
auth_header = self.headers.get('Authorization')
if auth_header is not None:
return auth_header.split()[1]
return auth_header
@property
def form(self):
if self.parsed_form is None:
@@ -89,7 +100,7 @@ class Request(dict):
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
except Exception:
log.exception("failed when parsing form")
log.exception("Failed when parsing form")
return self.parsed_form
@@ -114,15 +125,22 @@ class Request(dict):
@property
def cookies(self):
if self._cookies is None:
if 'Cookie' in self.headers:
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
if cookie is not None:
cookies = SimpleCookie()
cookies.load(self.headers['Cookie'])
cookies.load(cookie)
self._cookies = {name: cookie.value
for name, cookie in cookies.items()}
else:
self._cookies = {}
return self._cookies
@property
def ip(self):
if not hasattr(self, '_ip'):
self._ip = self.transport.get_extra_info('peername')
return self._ip
File = namedtuple('File', ['type', 'body', 'name'])
@@ -130,6 +148,7 @@ File = namedtuple('File', ['type', 'body', 'name'])
def parse_multipart_form(body, boundary):
"""
Parses a request body and returns fields and files
:param body: Bytes request body
:param boundary: Bytes multipart boundary
:return: fields (RequestParameters), files (RequestParameters)

View File

@@ -1,9 +1,11 @@
from aiofiles import open as open_async
from .cookies import CookieJar
from mimetypes import guess_type
from os import path
from ujson import dumps as json_dumps
from .cookies import CookieJar
COMMON_STATUS_CODES = {
200: b'OK',
400: b'Bad Request',
@@ -79,7 +81,12 @@ class HTTPResponse:
self.content_type = content_type
if body is not None:
self.body = body.encode('utf-8')
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
@@ -96,10 +103,14 @@ class HTTPResponse:
headers = b''
if self.headers:
headers = b''.join(
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
for name, value in self.headers.items()
)
for name, value in self.headers.items():
try:
headers += (
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
except AttributeError:
headers += (
b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
# Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all
@@ -131,22 +142,47 @@ class HTTPResponse:
return self._cookies
def json(body, status=200, headers=None):
return HTTPResponse(json_dumps(body), headers=headers, status=status,
content_type="application/json")
def json(body, status=200, headers=None, **kwargs):
"""
Returns response object with body in json format.
:param body: Response data to be serialized.
:param status: Response code.
:param headers: Custom Headers.
:param \**kwargs: Remaining arguments that are passed to the json encoder.
"""
return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
status=status, content_type="application/json")
def text(body, status=200, headers=None):
"""
Returns response object with body in text format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers,
content_type="text/plain; charset=utf-8")
def html(body, status=200, headers=None):
"""
Returns response object with body in html format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None):
"""
Returns response object with file data.
:param location: Location of file on system.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
"""
filename = path.split(location)[-1]
async with open_async(location, mode='rb') as _file:
@@ -158,3 +194,26 @@ async def file(location, mime_type=None, headers=None):
headers=headers,
content_type=mime_type,
body_bytes=out_stream)
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
"""
Aborts execution and causes a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type:
the content type (string) of the response
:returns: the redirecting Response
"""
headers = headers or {}
# According to RFC 7231, a relative URI is now permitted.
headers['Location'] = to
return HTTPResponse(
status=status,
headers=headers,
content_type=content_type)

View File

@@ -3,6 +3,7 @@ from collections import defaultdict, namedtuple
from functools import lru_cache
from .config import Config
from .exceptions import NotFound, InvalidUsage
from .views import CompositionView
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
@@ -23,18 +24,36 @@ class RouteExists(Exception):
pass
class RouteDoesNotExist(Exception):
pass
class Router:
"""
Router supports basic routing with parameters and method checks
Usage:
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
def my_route(request, my_parameter):
.. code-block:: python
@sanic.route('/my/url/<my_param>', methods=['GET', 'POST', ...])
def my_route(request, my_param):
do stuff...
or
.. code-block:: python
@sanic.route('/my/url/<my_param:my_type>', methods['GET', 'POST', ...])
def my_route_with_type(request, my_param: my_type):
do stuff...
Parameters will be passed as keyword arguments to the request handling
function provided Parameters can also have a type by appending :type to
the <parameter>. If no type is provided, a string is expected. A regular
expression can also be passed in as the type
function. Provided parameters can also have a type by appending :type to
the <parameter>. Given parameter must be able to be type-casted to this.
If no type is provided, a string is expected. A regular expression can
also be passed in as the type. The argument given to the function will
always be a string, independent of the type.
"""
routes_static = None
routes_dynamic = None
@@ -45,19 +64,36 @@ class Router:
self.routes_static = {}
self.routes_dynamic = defaultdict(list)
self.routes_always_check = []
self.hosts = None
def add(self, uri, methods, handler):
def add(self, uri, methods, handler, host=None):
"""
Adds a handler to the route list
:param uri: Path to match
:param methods: Array of accepted method names.
If none are provided, any method is allowed
If none are provided, any method is allowed
:param handler: Request handler function.
When executed, it should provide a response object.
When executed, it should provide a response object.
:return: Nothing
"""
if uri in self.routes_all:
raise RouteExists("Route already registered: {}".format(uri))
if host is not None:
# we want to track if there are any
# vhosts on the Router instance so that we can
# default to the behavior without vhosts
if self.hosts is None:
self.hosts = set(host)
else:
if isinstance(host, list):
host = set(host)
self.hosts.add(host)
if isinstance(host, str):
uri = host + uri
else:
for h in host:
self.add(uri, methods, handler, h)
return
# Dict for faster lookups of if method allowed
if methods:
@@ -91,9 +127,35 @@ class Router:
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
pattern = re.compile(r'^{}$'.format(pattern_string))
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters)
def merge_route(route, methods, handler):
# merge to the existing route when possible.
if not route.methods or not methods:
# method-unspecified routes are not mergeable.
raise RouteExists(
"Route already registered: {}".format(uri))
elif route.methods.intersection(methods):
# already existing method is not overloadable.
duplicated = methods.intersection(route.methods)
raise RouteExists(
"Route already registered: {} [{}]".format(
uri, ','.join(list(duplicated))))
if isinstance(route.handler, CompositionView):
view = route.handler
else:
view = CompositionView()
view.add(route.methods, route.handler)
view.add(methods, handler)
route = route._replace(
handler=view, methods=methods.union(route.methods))
return route
route = self.routes_all.get(uri)
if route:
route = merge_route(route, methods, handler)
else:
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters)
self.routes_all[uri] = route
if properties['unhashable']:
@@ -103,17 +165,41 @@ class Router:
else:
self.routes_static[uri] = route
def remove(self, uri, clean_cache=True, host=None):
if host is not None:
uri = host + uri
try:
route = self.routes_all.pop(uri)
except KeyError:
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
if route in self.routes_always_check:
self.routes_always_check.remove(route)
elif url_hash(uri) in self.routes_dynamic \
and route in self.routes_dynamic[url_hash(uri)]:
self.routes_dynamic[url_hash(uri)].remove(route)
else:
self.routes_static.pop(uri)
if clean_cache:
self._get.cache_clear()
def get(self, request):
"""
Gets a request handler based on the URL of the request, or raises an
error
:param request: Request object
:return: handler, arguments, keyword arguments
"""
return self._get(request.url, request.method)
if self.hosts is None:
return self._get(request.url, request.method, '')
else:
return self._get(request.url, request.method,
request.headers.get("Host", ''))
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
def _get(self, url, method):
def _get(self, url, method, host):
"""
Gets a request handler based on the URL of the request, or raises an
error. Internal method for caching.
@@ -121,6 +207,7 @@ class Router:
:param method: Request method
:return: handler, arguments, keyword arguments
"""
url = host + url
# Check against known static routes
route = self.routes_static.get(url)
if route:

View File

@@ -1,37 +1,46 @@
import logging
from asyncio import get_event_loop
from collections import deque
from functools import partial
from inspect import isawaitable, stack, getmodulename
from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT
from time import sleep
from traceback import format_exc
from .config import Config
from .exceptions import Handler
from .log import log, logging
from .exceptions import ServerError
from .log import log
from .response import HTTPResponse
from .router import Router
from .server import serve
from .server import serve, serve_multiple, HttpProtocol
from .static import register as static_register
from .exceptions import ServerError
class Sanic:
def __init__(self, name=None, router=None, error_handler=None):
def __init__(self, name=None, router=None,
error_handler=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:
formatter = logging.Formatter(
"%(asctime)s: %(levelname)s: %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)
log.addHandler(handler)
log.setLevel(logging.INFO)
if name is None:
frame_records = stack()[1]
name = getmodulename(frame_records[1])
self.name = name
self.router = router or Router()
self.error_handler = error_handler or Handler(self)
self.error_handler = error_handler or Handler()
self.config = Config()
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
self._blueprint_order = []
self.loop = None
self.debug = None
self.sock = None
self.processes = None
# Register alternative method names
self.go_fast = self.run
@@ -41,9 +50,10 @@ class Sanic:
# -------------------------------------------------------------------- #
# Decorator
def route(self, uri, methods=None):
def route(self, uri, methods=frozenset({'GET'}), host=None):
"""
Decorates a function to be registered as a route
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: decorated function
@@ -55,29 +65,54 @@ class Sanic:
uri = '/' + uri
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler)
self.router.add(uri=uri, methods=methods, handler=handler,
host=host)
return handler
return response
def add_route(self, handler, uri, methods=None):
# Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=["GET"], host=host)
def post(self, uri, host=None):
return self.route(uri, methods=["POST"], host=host)
def put(self, uri, host=None):
return self.route(uri, methods=["PUT"], host=host)
def head(self, uri, host=None):
return self.route(uri, methods=["HEAD"], host=host)
def options(self, uri, host=None):
return self.route(uri, methods=["OPTIONS"], host=host)
def patch(self, uri, host=None):
return self.route(uri, methods=["PATCH"], host=host)
def add_route(self, handler, uri, methods=None, host=None):
"""
A helper method to register class instance or
functions as a handler to the application url
routes.
:param handler: function or class instance
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: function or class instance
"""
self.route(uri=uri, methods=methods)(handler)
self.route(uri=uri, methods=methods, host=host)(handler)
return handler
def remove_route(self, uri, clean_cache=True, host=None):
self.router.remove(uri, clean_cache, host)
# Decorator
def exception(self, *exceptions):
"""
Decorates a function to be registered as a handler for exceptions
:param *exceptions: exceptions
:param \*exceptions: exceptions
:return: decorated function
"""
@@ -123,6 +158,7 @@ class Sanic:
def blueprint(self, blueprint, **options):
"""
Registers a blueprint on the application.
:param blueprint: Blueprint object
:param options: option dictionary with blueprint defaults
:return: Nothing
@@ -140,7 +176,8 @@ class Sanic:
def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0
log.warning("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method instead")
"version 1.0. Please use the blueprint method instead",
DeprecationWarning)
return self.blueprint(*args, **kwargs)
# -------------------------------------------------------------------- #
@@ -155,9 +192,10 @@ class Sanic:
Takes a request from the HTTP Server and returns 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
response as the only argument
:return: Nothing
"""
try:
@@ -193,18 +231,18 @@ class Sanic:
if isawaitable(response):
response = await response
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
# -------------------------------------------- #
# 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
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:
# -------------------------------------------- #
@@ -222,7 +260,7 @@ class Sanic:
e, format_exc()))
else:
response = HTTPResponse(
"An error occured while handling an error")
"An error occurred while handling an error")
response_callback(response)
@@ -231,42 +269,52 @@ 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, sock=None,
workers=1, loop=None):
after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None, register_sys_signals=True):
"""
Runs the HTTP Server and listens until keyboard interrupt or term
signal. On termination, drains connections before closing.
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param before_start: Function to be executed before the server starts
accepting connections
:param after_start: Function to be executed after the server starts
accepting connections
:param before_stop: Function to be executed when a stop signal is
received before it is respected
:param after_stop: Function to be executed when all requests are
complete
:param before_start: Functions to be executed before the server starts
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)
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param loop: asyncio compatible event loop
received before it is respected
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
self.error_handler.debug = True
self.error_handler.debug = debug
self.debug = debug
if loop is not None:
log.warning("Passing a loop will be deprecated in version 0.4.0"
" https://github.com/channelcat/sanic/pull/335"
" has more information.", DeprecationWarning)
self.loop = loop
server_settings = {
'protocol': protocol,
'host': host,
'port': port,
'sock': sock,
'ssl': ssl,
'debug': debug,
'request_handler': self.handle_request,
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop
'register_sys_signals': register_sys_signals,
'backlog': backlog
}
# -------------------------------------------- #
@@ -283,7 +331,7 @@ class Sanic:
for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name]
if args:
if type(args) is not list:
if callable(args):
args = [args]
listeners += args
if reverse:
@@ -297,15 +345,16 @@ class Sanic:
log.debug(self.config.LOGO)
# Serve
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
proto = "http"
if ssl is not None:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
try:
if workers == 1:
serve(**server_settings)
else:
log.info('Spinning up {} workers...'.format(workers))
self.serve_multiple(server_settings, workers)
serve_multiple(server_settings, workers, stop_event)
except Exception as e:
log.exception(
@@ -314,44 +363,65 @@ class Sanic:
log.info("Server Stopped")
def stop(self):
"""
This kills the Sanic
"""
"""This kills the Sanic"""
get_event_loop().stop()
@staticmethod
def serve_multiple(server_settings, workers, stop_event=None):
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,
backlog=100, stop_event=None):
"""
Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete.
:param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch
:param stop_event: if provided, is used as a stop signal
:return:
Asynchronous version of `run`.
"""
server_settings['reuse_port'] = True
if loop is not None:
log.warning("Passing a loop will be deprecated in version 0.4.0"
" https://github.com/channelcat/sanic/pull/335"
" has more information.", DeprecationWarning)
loop = get_event_loop()
server_settings = {
'protocol': protocol,
'host': host,
'port': port,
'sock': sock,
'ssl': ssl,
'debug': debug,
'request_handler': self.handle_request,
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop,
'backlog': backlog
}
# Create a stop event to be triggered by a signal
if not stop_event:
stop_event = Event()
signal(SIGINT, lambda s, f: stop_event.set())
signal(SIGTERM, lambda s, f: stop_event.set())
# -------------------------------------------- #
# Register start/stop events
# -------------------------------------------- #
processes = []
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.start()
processes.append(process)
for event_name, settings_name, args, reverse in (
("before_server_start", "before_start", before_start, False),
("after_server_start", "after_start", after_start, False),
("before_server_stop", "before_stop", before_stop, True),
("after_server_stop", "after_stop", after_stop, True)):
listeners = []
for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name]
if args:
if callable(args):
args = [args]
listeners += args
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners
# Infinitely wait for the stop event
try:
while not stop_event.is_set():
sleep(0.3)
except:
pass
server_settings['run_async'] = True
log.info('Spinning down workers...')
for process in processes:
process.terminate()
for process in processes:
process.join()
# Serve
proto = "http"
if ssl is not None:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
return await serve(**server_settings)

View File

@@ -1,12 +1,20 @@
import asyncio
import os
import traceback
from functools import partial
from inspect import isawaitable
from multidict import CIMultiDict
from signal import SIGINT, SIGTERM
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 time import time
from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
from .exceptions import ServerError
try:
import uvloop as async_loop
except ImportError:
@@ -14,14 +22,33 @@ except ImportError:
from .log import log
from .request import Request
from .exceptions import RequestTimeout
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
current_time = None
class Signal:
stopped = False
current_time = None
class CIDict(dict):
"""
Case Insensitive dict where all keys are converted to lowercase
This does not maintain the inputted case when calling items() or keys()
in favor of speed, since headers are case insensitive
"""
def get(self, key, default=None):
return super().get(key.casefold(), default)
def __getitem__(self, key):
return super().__getitem__(key.casefold())
def __setitem__(self, key, value):
return super().__setitem__(key.casefold(), value)
def __contains__(self, key):
return super().__contains__(key.casefold())
class HttpProtocol(asyncio.Protocol):
@@ -36,7 +63,7 @@ class HttpProtocol(asyncio.Protocol):
'_total_request_size', '_timeout_handler', '_last_communication_time')
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections={}, request_timeout=60,
signal=Signal(), connections=set(), request_timeout=60,
request_max_size=None):
self.loop = loop
self.transport = None
@@ -60,30 +87,28 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- #
def connection_made(self, transport):
self.connections[self] = True
self.connections.add(self)
self._timeout_handler = self.loop.call_later(
self.request_timeout, self.connection_timeout)
self.transport = transport
self._last_request_time = current_time
def connection_lost(self, exc):
del self.connections[self]
self.connections.discard(self)
self._timeout_handler.cancel()
self.cleanup()
def connection_timeout(self):
# Check if
time_elapsed = current_time - self._last_request_time
if time_elapsed < self.request_timeout:
time_left = self.request_timeout - time_elapsed
self._timeout_handler = \
self.loop.call_later(time_left, self.connection_timeout)
self._timeout_handler = (
self.loop.call_later(time_left, self.connection_timeout))
else:
if self._request_handler_task:
self._request_handler_task.cancel()
response = self.error_handler.response(
self.request, RequestTimeout('Request Timeout'))
self.write_response(response)
exception = RequestTimeout('Request Timeout')
self.write_error(exception)
# -------------------------------------------- #
# Parsing
@@ -94,9 +119,8 @@ class HttpProtocol(asyncio.Protocol):
# memory limits
self._total_request_size += len(data)
if self._total_request_size > self.request_max_size:
return self.bail_out(
"Request too large ({}), connection closed".format(
self._total_request_size))
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
# Create parser if this is the first time we're receiving data
if self.parser is None:
@@ -107,39 +131,35 @@ class HttpProtocol(asyncio.Protocol):
# Parse request chunk or close connection
try:
self.parser.feed_data(data)
except HttpParserError as e:
self.bail_out(
"Invalid request data, connection closed ({})".format(e))
except HttpParserError:
exception = InvalidUsage('Bad Request')
self.write_error(exception)
def on_url(self, url):
self.url = url
def on_header(self, name, value):
if name == b'Content-Length' and int(value) > self.request_max_size:
return self.bail_out(
"Request body too large ({}), connection closed".format(value))
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
self.headers.append((name.decode(), value.decode('utf-8')))
self.headers.append((name.decode().casefold(), value.decode()))
def on_headers_complete(self):
remote_addr = self.transport.get_extra_info('peername')
if remote_addr:
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
self.request = Request(
url_bytes=self.url,
headers=CIMultiDict(self.headers),
headers=CIDict(self.headers),
version=self.parser.get_http_version(),
method=self.parser.get_method().decode()
method=self.parser.get_method().decode(),
transport=self.transport
)
def on_body(self, body):
if self.request.body:
self.request.body += body
else:
self.request.body = body
self.request.body.append(body)
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))
@@ -149,24 +169,54 @@ class HttpProtocol(asyncio.Protocol):
def write_response(self, response):
try:
keep_alive = self.parser.should_keep_alive() \
and not self.signal.stopped
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))
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(e))
finally:
if not keep_alive:
self.transport.close()
else:
# Record that we received data
self._last_request_time = current_time
self.cleanup()
def write_error(self, exception):
try:
response = self.error_handler.response(self.request, exception)
version = self.request.version if self.request else '1.1'
self.transport.write(response.output(version))
except RuntimeError:
log.error(
'Connection lost before error written @ {}'.format(
self.request.ip))
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(e))
"Writing error failed, connection closed {}".format(e),
from_error=True)
finally:
self.transport.close()
def bail_out(self, message):
log.debug(message)
self.transport.close()
def bail_out(self, message, from_error=False):
if from_error and self.transport.is_closing():
log.error(
("Transport closed @ {} and exception "
"experienced during error handling").format(
self.transport.get_extra_info('peername')))
log.debug(
'Exception:\n{}'.format(traceback.format_exc()))
else:
exception = ServerError(message)
self.write_error(exception)
log.error(message)
def cleanup(self):
self.parser = None
@@ -191,6 +241,7 @@ def update_current_time(loop):
"""
Caches the current time, since it is needed
at the end of every keep-alive request to update the request timeout time
:param loop:
:return:
"""
@@ -214,27 +265,38 @@ def trigger_events(events, loop):
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, sock=None,
request_max_size=None, reuse_port=False, loop=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):
"""
Starts asynchronous HTTP Server on an individual process.
:param host: Address to host on
:param port: Port to host on
:param request_handler: Sanic request handler with middleware
:param error_handler: Sanic error handler with middleware
:param before_start: Function to be executed before the server starts
listening. Takes arguments `app` instance and `loop`
:param after_start: Function to be executed after the server starts
listening. Takes single argument `loop`
listening. Takes arguments `app` instance and `loop`
:param before_stop: Function to be executed when a stop signal is
received before it is respected. Takes single argumenet `loop`
received before it is respected. Takes arguments
`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`
:param debug: Enables debug output (slows server)
:param request_timeout: time in seconds
:param ssl: SSLContext
:param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit
:param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
loop = loop or async_loop.new_event_loop()
loop = async_loop.new_event_loop()
asyncio.set_event_loop(loop)
if debug:
@@ -242,9 +304,10 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(before_start, loop)
connections = {}
connections = set()
signal = Signal()
server_coroutine = loop.create_server(lambda: HttpProtocol(
server = partial(
protocol,
loop=loop,
connections=connections,
signal=signal,
@@ -252,28 +315,43 @@ 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,
), host, port, reuse_port=reuse_port, sock=sock)
)
server_coroutine = loop.create_server(
server,
host,
port,
ssl=ssl,
reuse_port=reuse_port,
sock=sock,
backlog=backlog
)
# Instead of pulling time at the end of every request,
# pull it once per minute
loop.call_soon(partial(update_current_time, loop))
if run_async:
return server_coroutine
try:
http_server = loop.run_until_complete(server_coroutine)
except Exception:
except:
log.exception("Unable to start server")
return
trigger_events(after_start, loop)
# Register signals for graceful termination
for _signal in (SIGINT, SIGTERM):
loop.add_signal_handler(_signal, loop.stop)
if register_sys_signals:
for _signal in (SIGINT, SIGTERM):
loop.add_signal_handler(_signal, loop.stop)
pid = os.getpid()
try:
log.info('Starting worker [{}]'.format(pid))
loop.run_forever()
finally:
log.info("Stop requested, draining connections...")
log.info("Stopping worker [{}]".format(pid))
# Run the on_stop function if provided
trigger_events(before_stop, loop)
@@ -284,7 +362,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
# Complete all tasks on the loop
signal.stopped = True
for connection in connections.keys():
for connection in connections:
connection.close_if_idle()
while connections:
@@ -293,3 +371,51 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(after_stop, loop)
loop.close()
def serve_multiple(server_settings, workers, stop_event=None):
"""
Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete.
:param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch
:param stop_event: if provided, is used as a stop signal
:return:
"""
if server_settings.get('loop', None) is not None:
log.warning("Passing a loop will be deprecated in version 0.4.0"
" https://github.com/channelcat/sanic/pull/335"
" 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
if stop_event is None:
stop_event = Event()
signal_func(SIGINT, lambda s, f: stop_event.set())
signal_func(SIGTERM, lambda s, f: stop_event.set())
processes = []
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start()
processes.append(process)
for process in processes:
process.join()
# the above processes will block this until they're stopped
for process in processes:
process.terminate()
sock.close()
asyncio.get_event_loop().stop()

View File

@@ -2,6 +2,7 @@ from aiofiles.os import stat
from os import path
from re import sub
from time import strftime, gmtime
from urllib.parse import unquote
from .exceptions import FileNotFound, InvalidUsage
from .response import file, HTTPResponse
@@ -14,12 +15,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
"""
Registers a static directory handler with Sanic by adding a route to the
router and registering a handler.
:param app: Sanic
:param file_or_directory: File or directory path to serve from
:param uri: URL to serve from
:param pattern: regular expression used to match files in the URL
:param use_modified_since: If true, send file modified time, and return
not modified if the browser's matches the server's
not modified if the browser's matches the
server's
"""
# If we're not trying to match a file directly,
@@ -32,12 +35,17 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
# served. os.path.realpath seems to be very slow
if file_uri and '../' in file_uri:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \
if file_uri else file_or_directory
file_path = file_or_directory
if file_uri:
file_path = path.join(
file_or_directory, sub('^[/]*', '', file_uri))
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = unquote(file_path)
try:
headers = {}
# Check if the client has been sent this file before

View File

@@ -16,15 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
loop=None, debug=False, *request_args,
**request_kwargs):
debug=False, server_kwargs={},
*request_args, **request_kwargs):
results = []
exceptions = []
if gather_request:
@app.middleware
def _collect_request(request):
results.append(request)
app.request_middleware.appendleft(_collect_request)
async def _collect_response(sanic, loop):
try:
@@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
exceptions.append(e)
app.stop()
app.run(host=HOST, debug=debug, port=42101,
after_start=_collect_response, loop=loop)
app.run(host=HOST, debug=debug, port=PORT,
after_start=_collect_response, **server_kwargs)
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
@@ -47,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
return request, response
except:
raise ValueError(
"request and response object expected, got ({})".format(
"Request and response object expected, got ({})".format(
results))
else:
try:
return results[0]
except:
raise ValueError(
"request object expected, got ({})".format(results))
"Request object expected, got ({})".format(results))

View File

@@ -3,34 +3,100 @@ from .exceptions import InvalidUsage
class HTTPMethodView:
""" Simple class based implementation of view for the sanic.
You should implement methods(get, post, put, patch, delete) for the class
You should implement methods (get, post, put, patch, delete) for the class
to every HTTP method you want to support.
For example:
class DummyView(View):
For example:
.. code-block:: python
class DummyView(HTTPMethodView):
def get(self, request, *args, **kwargs):
return text('I am get method')
def put(self, request, *args, **kwargs):
return text('I am put method')
etc.
If someone try use not implemented method, there will be 405 response
If you need any url params just mention them in method definition like:
class DummyView(View):
If someone tries to use a non-implemented method, there will be a
405 response.
If you need any url params just mention them in method definition:
.. code-block:: python
class DummyView(HTTPMethodView):
def get(self, request, my_param_here, *args, **kwargs):
return text('I am get method with %s' % my_param_here)
To add the view into the routing you could use
1) app.add_route(DummyView(), '/')
2) app.route('/')(DummyView())
1) app.add_route(DummyView.as_view(), '/')
2) app.route('/')(DummyView.as_view())
To add any decorator you could set it into decorators variable
"""
def __call__(self, request, *args, **kwargs):
decorators = []
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
if handler:
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
@classmethod
def as_view(cls, *class_args, **class_kwargs):
""" Converts the class into an actual view function that can be used
with the routing system.
"""
def view(*args, **kwargs):
self = view.view_class(*class_args, **class_kwargs)
return self.dispatch_request(*args, **kwargs)
if cls.decorators:
view.__module__ = cls.__module__
for decorator in cls.decorators:
view = decorator(view)
view.view_class = cls
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
return view
class CompositionView:
""" Simple method-function mapped view for the sanic.
You can add handler functions to methods (get, post, put, patch, delete)
for every HTTP method you want to support.
For example:
view = CompositionView()
view.add(['GET'], lambda request: text('I am get method'))
view.add(['POST', 'PUT'], lambda request: text('I am post/put method'))
etc.
If someone tries to use a non-implemented method, there will be a
405 response.
"""
def __init__(self):
self.handlers = {}
def add(self, methods, handler):
for method in methods:
if method in self.handlers:
raise KeyError(
'Method {} already is registered.'.format(method))
self.handlers[method] = handler
def __call__(self, request, *args, **kwargs):
handler = self.handlers.get(request.method.upper(), None)
if handler is None:
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
return handler(request, *args, **kwargs)

View File

@@ -30,7 +30,6 @@ setup(
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
'multidict>=2.0',
],
classifiers=[
'Development Status :: 2 - Pre-Alpha',

View File

@@ -1,16 +1,30 @@
package main
import (
"fmt"
"os"
"net/http"
"encoding/json"
"net/http"
"os"
)
type TestJSONResponse struct {
Test bool
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
response := TestJSONResponse{true}
js, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":" + os.Args[1], nil)
http.HandleFunc("/", handler)
http.ListenAndServe(":"+os.Args[1], nil)
}

View File

@@ -0,0 +1 @@
I need to be decoded as a uri

1
tests/static/test.file Normal file
View File

@@ -0,0 +1 @@
I am just a regular static file

20
tests/test_bad_request.py Normal file
View File

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

View File

@@ -59,6 +59,71 @@ def test_several_bp_with_url_prefix():
request, response = sanic_endpoint_test(app, uri='/test2/')
assert response.text == 'Hello2'
def test_bp_with_host():
app = Sanic('test_bp_host')
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
@bp.route('/')
def handler(request):
return text('Hello')
@bp.route('/', host="sub.example.com")
def handler(request):
return text('Hello subdomain!')
app.blueprint(bp)
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/',
headers=headers)
assert response.text == 'Hello'
headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/',
headers=headers)
assert response.text == 'Hello subdomain!'
def test_several_bp_with_host():
app = Sanic('test_text')
bp = Blueprint('test_text',
url_prefix='/test',
host="example.com")
bp2 = Blueprint('test_text2',
url_prefix='/test',
host="sub.example.com")
@bp.route('/')
def handler(request):
return text('Hello')
@bp2.route('/')
def handler2(request):
return text('Hello2')
@bp2.route('/other/')
def handler2(request):
return text('Hello3')
app.blueprint(bp)
app.blueprint(bp2)
assert bp.host == "example.com"
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test/',
headers=headers)
assert response.text == 'Hello'
assert bp2.host == "sub.example.com"
headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test/',
headers=headers)
assert response.text == 'Hello2'
request, response = sanic_endpoint_test(app, uri='/test/other/',
headers=headers)
assert response.text == 'Hello3'
def test_bp_middleware():
app = Sanic('test_middleware')
@@ -162,4 +227,4 @@ def test_bp_static():
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents
assert response.body == current_file_contents

View File

@@ -3,6 +3,7 @@ from http.cookies import SimpleCookie
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
import pytest
# ------------------------------------------------------------ #
@@ -25,6 +26,39 @@ def test_cookies():
assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you'
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
])
def test_false_cookies(httponly, expected):
app = Sanic('test_text')
@app.route('/')
def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test']))
response.cookies['right_back'] = 'at you'
response.cookies['right_back']['httponly'] = httponly
return response
request, response = sanic_endpoint_test(app)
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
'HttpOnly' in response_cookies == expected
def test_http2_cookies():
app = Sanic('test_http2_cookies')
@app.route('/')
async def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test']))
return response
headers = {'cookie': 'test=working!'}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == 'Cookies are: working!'
def test_cookie_options():
app = Sanic('test_text')
@@ -41,4 +75,23 @@ def test_cookie_options():
response_cookies.load(response.headers.get('Set-Cookie', {}))
assert response_cookies['test'].value == 'at you'
assert response_cookies['test']['httponly'] == True
assert response_cookies['test']['httponly'] == True
def test_cookie_deletion():
app = Sanic('test_text')
@app.route('/')
def handler(request):
response = text("OK")
del response.cookies['i_want_to_die']
response.cookies['i_never_existed'] = 'testing'
del response.cookies['i_never_existed']
return response
request, response = sanic_endpoint_test(app)
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
assert int(response_cookies['i_want_to_die']['max-age']) == 0
with pytest.raises(KeyError):
hold_my_beer = response.cookies['i_never_existed']

View File

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

View File

@@ -1,51 +1,112 @@
import pytest
from bs4 import BeautifulSoup
from sanic import Sanic
from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.utils import sanic_endpoint_test
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
exception_app = Sanic('test_exceptions')
class SanicExceptionTestException(Exception):
pass
@exception_app.route('/')
def handler(request):
return text('OK')
@pytest.fixture(scope='module')
def exception_app():
app = Sanic('test_exceptions')
@app.route('/')
def handler(request):
return text('OK')
@app.route('/error')
def handler_error(request):
raise ServerError("OK")
@app.route('/404')
def handler_404(request):
raise NotFound("OK")
@app.route('/invalid')
def handler_invalid(request):
raise InvalidUsage("OK")
@app.route('/divide_by_zero')
def handle_unhandled_exception(request):
1 / 0
@app.route('/error_in_error_handler_handler')
def custom_error_handler(request):
raise SanicExceptionTestException('Dummy message!')
@app.exception(SanicExceptionTestException)
def error_in_error_handler_handler(request, exception):
1 / 0
return app
@exception_app.route('/error')
def handler_error(request):
raise ServerError("OK")
@exception_app.route('/404')
def handler_404(request):
raise NotFound("OK")
@exception_app.route('/invalid')
def handler_invalid(request):
raise InvalidUsage("OK")
def test_no_exception():
def test_no_exception(exception_app):
"""Test that a route works without an exception"""
request, response = sanic_endpoint_test(exception_app)
assert response.status == 200
assert response.text == 'OK'
def test_server_error_exception():
def test_server_error_exception(exception_app):
"""Test the built-in ServerError exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/error')
assert response.status == 500
def test_invalid_usage_exception():
def test_invalid_usage_exception(exception_app):
"""Test the built-in InvalidUsage exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/invalid')
assert response.status == 400
def test_not_found_exception():
def test_not_found_exception(exception_app):
"""Test the built-in NotFound exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/404')
assert response.status == 404
def test_handled_unhandled_exception(exception_app):
"""Test that an exception not built into sanic is handled"""
request, response = sanic_endpoint_test(
exception_app, uri='/divide_by_zero')
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
assert soup.h1.text == 'Internal Server Error'
message = " ".join(soup.p.text.split())
assert message == (
"The server encountered an internal error and "
"cannot complete your request.")
def test_exception_in_exception_handler(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = sanic_endpoint_test(
exception_app, uri='/error_in_error_handler_handler')
assert response.status == 500
assert response.body == b'An error occurred while handling an error'
def test_exception_in_exception_handler_debug_off(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = sanic_endpoint_test(
exception_app,
uri='/error_in_error_handler_handler',
debug=False)
assert response.status == 500
assert response.body == b'An error occurred while handling an error'
def test_exception_in_exception_handler_debug_off(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = sanic_endpoint_test(
exception_app,
uri='/error_in_error_handler_handler',
debug=True)
assert response.status == 500
assert response.body.startswith(b'Exception raised in exception ')

View File

@@ -2,6 +2,7 @@ from sanic import Sanic
from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.utils import sanic_endpoint_test
from bs4 import BeautifulSoup
exception_handler_app = Sanic('test_exception_handler')
@@ -21,6 +22,12 @@ def handler_3(request):
raise NotFound("OK")
@exception_handler_app.route('/4')
def handler_4(request):
foo = bar
return text(foo)
@exception_handler_app.exception(NotFound, ServerError)
def handler_exception(request, exception):
return text("OK")
@@ -47,3 +54,20 @@ def test_text_exception__handler():
exception_handler_app, uri='/random')
assert response.status == 200
assert response.text == 'OK'
def test_html_traceback_output_in_debug_mode():
request, response = sanic_endpoint_test(
exception_handler_app, uri='/4', debug=True)
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
html = str(soup)
assert 'response = handler(request, *args, **kwargs)' in html
assert 'handler_4' in html
assert 'foo = bar' in html
summary_text = " ".join(soup.select('.summary')[0].text.split())
assert (
"NameError: name 'bar' "
"is not defined while handling uri /4") == summary_text

37
tests/test_logging.py Normal file
View File

@@ -0,0 +1,37 @@
import asyncio
import uuid
from sanic.response import text
from sanic import Sanic
from io import StringIO
from sanic.utils import sanic_endpoint_test
import logging
logging_format = '''module: %(module)s; \
function: %(funcName)s(); \
message: %(message)s'''
def test_log():
log_stream = StringIO()
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
logging.basicConfig(
format=logging_format,
level=logging.DEBUG,
stream=log_stream
)
log = logging.getLogger()
app = Sanic('test_logging')
rand_string = str(uuid.uuid4())
@app.route('/')
def handler(request):
log.info(rand_string)
return text('hello')
request, response = sanic_endpoint_test(app)
log_text = log_stream.getvalue()
assert rand_string in log_text
if __name__ == "__main__":
test_log()

View File

@@ -51,7 +51,7 @@ def test_middleware_response():
assert response.text == 'OK'
assert type(results[0]) is Request
assert type(results[1]) is Request
assert issubclass(type(results[2]), HTTPResponse)
assert isinstance(results[2], HTTPResponse)
def test_middleware_override_request():

View File

@@ -1,53 +1,26 @@
from multiprocessing import Array, Event, Process
from time import sleep
from ujson import loads as json_loads
import multiprocessing
import random
import signal
from sanic import Sanic
from sanic.response import json
from sanic.utils import local_request, HOST, PORT
from sanic.utils import HOST, PORT
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
def test_multiprocessing():
"""Tests that the number of children we produce is correct"""
# Selects a number at random so we can spot check
num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1))
app = Sanic('test_multiprocessing')
process_list = set()
# TODO: Figure out why this freezes on pytest but not when
# executed via interpreter
def stop_on_alarm(*args):
for process in multiprocessing.active_children():
process_list.add(process.pid)
process.terminate()
def skip_test_multiprocessing():
app = Sanic('test_json')
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1)
app.run(HOST, PORT, workers=num_workers)
response = Array('c', 50)
@app.route('/')
async def handler(request):
return json({"test": True})
assert len(process_list) == num_workers
stop_event = Event()
async def after_start(*args, **kwargs):
http_response = await local_request('get', '/')
response.value = http_response.text.encode()
stop_event.set()
def rescue_crew():
sleep(5)
stop_event.set()
rescue_process = Process(target=rescue_crew)
rescue_process.start()
app.serve_multiple({
'host': HOST,
'port': PORT,
'after_start': after_start,
'request_handler': app.handle_request,
'request_max_size': 100000,
}, workers=2, stop_event=stop_event)
rescue_process.terminate()
try:
results = json_loads(response.value)
except:
raise ValueError("Expected JSON response but got '{}'".format(response))
assert results.get('test') == True

View File

@@ -0,0 +1,54 @@
from sanic import Sanic
from sanic.response import text
from sanic.exceptions import PayloadTooLarge
from sanic.utils import sanic_endpoint_test
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)
def test_payload_too_large_from_error_handler():
response = sanic_endpoint_test(
data_received_app, uri='/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():
response = sanic_endpoint_test(
data_received_default_app, uri='/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():
data = 'a' * 1000
response = sanic_endpoint_test(
on_header_default_app, method='post', uri='/1',
gather_request=False, data=data)
assert response.status == 413
assert response.text == 'Error: Payload Too Large'

View File

@@ -12,7 +12,7 @@ request_timeout_default_app = Sanic('test_request_timeout_default')
@request_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(1)
await asyncio.sleep(2)
return text('OK')
@@ -29,7 +29,7 @@ def test_server_error_request_timeout():
@request_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(1)
await asyncio.sleep(2)
return text('OK')

View File

@@ -1,8 +1,10 @@
from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic
from sanic.response import json, text
from sanic.response import json, text, redirect
from sanic.utils import sanic_endpoint_test
from sanic.exceptions import ServerError
import pytest
# ------------------------------------------------------------ #
# GET
@@ -32,6 +34,47 @@ def test_text():
assert response.text == 'Hello'
def test_headers():
app = Sanic('test_text')
@app.route('/')
async def handler(request):
headers = {"spam": "great"}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
assert response.headers.get('spam') == 'great'
def test_non_str_headers():
app = Sanic('test_text')
@app.route('/')
async def handler(request):
headers = {"answer": 42}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
assert response.headers.get('answer') == '42'
def test_invalid_response():
app = Sanic('test_invalid_response')
@app.exception(ServerError)
def handler_exception(request, exception):
return text('Internal Server Error.', 500)
@app.route('/')
async def handler(request):
return 'This should fail'
request, response = sanic_endpoint_test(app)
assert response.status == 500
assert response.text == "Internal Server Error."
def test_json():
app = Sanic('test_json')
@@ -49,6 +92,19 @@ def test_json():
assert results.get('test') == True
def test_invalid_json():
app = Sanic('test_json')
@app.route('/')
async def handler(request):
return json(request.json())
data = "I am not json"
request, response = sanic_endpoint_test(app, data=data)
assert response.status == 400
def test_query_string():
app = Sanic('test_query_string')
@@ -62,6 +118,24 @@ def test_query_string():
assert request.args.get('test2') == 'false'
def test_token():
app = Sanic('test_post_token')
@app.route('/')
async def handler(request):
return text('OK')
# uuid4 generated token.
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
headers = {
'content-type': 'application/json',
'Authorization': 'Token {}'.format(token)
}
request, response = sanic_endpoint_test(app, headers=headers)
assert request.token == token
# ------------------------------------------------------------ #
# POST
# ------------------------------------------------------------ #
@@ -115,3 +189,73 @@ def test_post_form_multipart_form_data():
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
assert request.form.get('test') == 'OK'
@pytest.fixture
def redirect_app():
app = Sanic('test_redirection')
@app.route('/redirect_init')
async def redirect_init(request):
return redirect("/redirect_target")
@app.route('/redirect_init_with_301')
async def redirect_init_with_301(request):
return redirect("/redirect_target", status=301)
@app.route('/redirect_target')
async def redirect_target(request):
return text('OK')
return app
def test_redirect_default_302(redirect_app):
"""
We expect a 302 default status code and the headers to be set.
"""
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init",
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
def test_redirect_headers_none(redirect_app):
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init",
headers=None,
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
def test_redirect_with_301(redirect_app):
"""
Test redirection with a different status code.
"""
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init_with_301",
allow_redirects=False)
assert response.status == 301
assert response.headers["Location"] == "/redirect_target"
def test_get_then_redirect_follow_redirect(redirect_app):
"""
With `allow_redirects` we expect a 200.
"""
response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init", gather_request=False,
allow_redirects=True)
assert response.status == 200
assert response.text == 'OK'

18
tests/test_response.py Normal file
View File

@@ -0,0 +1,18 @@
from random import choice
from sanic import Sanic
from sanic.response import HTTPResponse
from sanic.utils import sanic_endpoint_test
def test_response_body_not_a_string():
"""Test when a response body sent from the application is not a string"""
app = Sanic('response_body_not_a_string')
random_num = choice(range(1000))
@app.route('/hello')
async def hello_route(request):
return HTTPResponse(body=random_num)
request, response = sanic_endpoint_test(app, uri='/hello')
assert response.text == str(random_num)

View File

@@ -2,7 +2,7 @@ import pytest
from sanic import Sanic
from sanic.response import text
from sanic.router import RouteExists
from sanic.router import RouteExists, RouteDoesNotExist
from sanic.utils import sanic_endpoint_test
@@ -10,6 +10,84 @@ from sanic.utils import sanic_endpoint_test
# UTF-8
# ------------------------------------------------------------ #
def test_shorthand_routes_get():
app = Sanic('test_shorhand_routes_get')
@app.get('/get')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/get', method='get')
assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/get', method='post')
assert response.status == 405
def test_shorthand_routes_post():
app = Sanic('test_shorhand_routes_post')
@app.post('/post')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/post', method='post')
assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/post', method='get')
assert response.status == 405
def test_shorthand_routes_put():
app = Sanic('test_shorhand_routes_put')
@app.put('/put')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/put', method='put')
assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/put', method='get')
assert response.status == 405
def test_shorthand_routes_patch():
app = Sanic('test_shorhand_routes_patch')
@app.patch('/patch')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/patch', method='patch')
assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/patch', method='get')
assert response.status == 405
def test_shorthand_routes_head():
app = Sanic('test_shorhand_routes_head')
@app.head('/head')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/head', method='head')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/head', method='get')
assert response.status == 405
def test_shorthand_routes_options():
app = Sanic('test_shorhand_routes_options')
@app.options('/options')
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/options', method='options')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/options', method='get')
assert response.status == 405
def test_static_routes():
app = Sanic('test_dynamic_route')
@@ -356,3 +434,174 @@ def test_add_route_method_not_allowed():
request, response = sanic_endpoint_test(app, method='post', uri='/test')
assert response.status == 405
def test_remove_static_route():
app = Sanic('test_remove_static_route')
async def handler1(request):
return text('OK1')
async def handler2(request):
return text('OK2')
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.status == 200
app.remove_route('/test')
app.remove_route('/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.status == 404
def test_remove_dynamic_route():
app = Sanic('test_remove_dynamic_route')
async def handler(request, name):
return text('OK')
app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.status == 200
app.remove_route('/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.status == 404
def test_remove_inexistent_route():
app = Sanic('test_remove_inexistent_route')
with pytest.raises(RouteDoesNotExist):
app.remove_route('/test')
def test_remove_unhashable_route():
app = Sanic('test_remove_unhashable_route')
async def handler(request, unhashable):
return text('OK')
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 200
app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 404
def test_remove_route_without_clean_cache():
app = Sanic('test_remove_static_route')
async def handler(request):
return text('OK')
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
app.remove_route('/test', clean_cache=True)
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 404
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
app.remove_route('/test', clean_cache=False)
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
def test_overload_routes():
app = Sanic('test_dynamic_route')
@app.route('/overload', methods=['GET'])
async def handler1(request):
return text('OK1')
@app.route('/overload', methods=['POST', 'PUT'])
async def handler2(request):
return text('OK2')
request, response = sanic_endpoint_test(app, 'get', uri='/overload')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload')
assert response.text == 'OK2'
request, response = sanic_endpoint_test(app, 'put', uri='/overload')
assert response.text == 'OK2'
request, response = sanic_endpoint_test(app, 'delete', uri='/overload')
assert response.status == 405
with pytest.raises(RouteExists):
@app.route('/overload', methods=['PUT', 'DELETE'])
async def handler3(request):
return text('Duplicated')
def test_unmergeable_overload_routes():
app = Sanic('test_dynamic_route')
@app.route('/overload_whole', methods=None)
async def handler1(request):
return text('OK1')
with pytest.raises(RouteExists):
@app.route('/overload_whole', methods=['POST', 'PUT'])
async def handler2(request):
return text('Duplicated')
request, response = sanic_endpoint_test(app, 'get', uri='/overload_whole')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload_whole')
assert response.text == 'OK1'
@app.route('/overload_part', methods=['GET'])
async def handler1(request):
return text('OK1')
with pytest.raises(RouteExists):
@app.route('/overload_part')
async def handler2(request):
return text('Duplicated')
request, response = sanic_endpoint_test(app, 'get', uri='/overload_part')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload_part')
assert response.status == 405

View File

@@ -0,0 +1,59 @@
from io import StringIO
from random import choice
from string import ascii_letters
import signal
import pytest
from sanic import Sanic
AVAILABLE_LISTENERS = [
'before_start',
'after_start',
'before_stop',
'after_stop'
]
def create_listener(listener_name, in_list):
async def _listener(app, loop):
print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name))
in_list.insert(0, app.name + listener_name)
return _listener
def start_stop_app(random_name_app, **run_kwargs):
def stop_on_alarm(signum, frame):
raise KeyboardInterrupt('SIGINT for sanic to stop gracefully')
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1)
try:
random_name_app.run(**run_kwargs)
except KeyboardInterrupt:
pass
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
def test_single_listener(listener_name):
"""Test that listeners on their own work"""
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
start_stop_app(
random_name_app,
**{listener_name: create_listener(listener_name, output)})
assert random_name_app.name + listener_name == output.pop()
def test_all_listeners():
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
start_stop_app(
random_name_app,
**{listener_name: create_listener(listener_name, output)
for listener_name in AVAILABLE_LISTENERS})
for listener_name in AVAILABLE_LISTENERS:
assert random_name_app.name + listener_name == output.pop()

View File

@@ -0,0 +1,50 @@
from sanic import Sanic
from sanic.response import HTTPResponse
from sanic.utils import HOST, PORT
from unittest.mock import MagicMock
import pytest
import asyncio
from queue import Queue
async def stop(app, loop):
await asyncio.sleep(0.1)
app.stop()
calledq = Queue()
def set_loop(app, loop):
loop.add_signal_handler = MagicMock()
def after(app, loop):
calledq.put(loop.add_signal_handler.called)
def test_register_system_signals():
"""Test if sanic register system signals"""
app = Sanic('test_register_system_signals')
@app.route('/hello')
async def hello_route(request):
return HTTPResponse()
app.run(HOST, PORT,
before_start=set_loop,
after_start=stop,
after_stop=after)
assert calledq.get() == True
def test_dont_register_system_signals():
"""Test if sanic don't register system signals"""
app = Sanic('test_register_system_signals')
@app.route('/hello')
async def hello_route(request):
return HTTPResponse()
app.run(HOST, PORT,
before_start=set_loop,
after_start=stop,
after_stop=after,
register_sys_signals=False)
assert calledq.get() == False

View File

@@ -1,30 +1,62 @@
import inspect
import os
import pytest
from sanic import Sanic
from sanic.utils import sanic_endpoint_test
def test_static_file():
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
@pytest.fixture(scope='module')
def static_file_directory():
"""The static directory to serve"""
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, 'static')
return static_directory
@pytest.fixture(scope='module')
def static_file_path(static_file_directory):
"""The path to the static file that we want to serve"""
return os.path.join(static_file_directory, 'test.file')
@pytest.fixture(scope='module')
def static_file_content(static_file_path):
"""The content of the static file to check"""
with open(static_file_path, 'rb') as file:
return file.read()
def test_static_file(static_file_path, static_file_content):
app = Sanic('test_static')
app.static('/testing.file', current_file)
app.static('/testing.file', static_file_path)
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents
assert response.body == static_file_content
def test_static_directory():
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
with open(current_file, 'rb') as file:
current_file_contents = file.read()
def test_static_directory(
static_file_directory, static_file_path, static_file_content):
app = Sanic('test_static')
app.static('/dir', current_directory)
app.static('/dir', static_file_directory)
request, response = sanic_endpoint_test(app, uri='/dir/test_static.py')
request, response = sanic_endpoint_test(app, uri='/dir/test.file')
assert response.status == 200
assert response.body == current_file_contents
assert response.body == static_file_content
def test_static_url_decode_file(static_file_directory):
decode_me_path = os.path.join(static_file_directory, 'decode me.txt')
with open(decode_me_path, 'rb') as file:
decode_me_contents = file.read()
app = Sanic('test_static')
app.static('/dir', static_file_directory)
request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt')
assert response.status == 200
assert response.body == decode_me_contents

39
tests/test_vhosts.py Normal file
View File

@@ -0,0 +1,39 @@
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
def test_vhosts():
app = Sanic('test_vhosts')
@app.route('/', host="example.com")
async def handler(request):
return text("You're at example.com!")
@app.route('/', host="subdomain.example.com")
async def handler(request):
return text("You're at subdomain.example.com!")
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "You're at example.com!"
headers = {"Host": "subdomain.example.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "You're at subdomain.example.com!"
def test_vhosts_with_list():
app = Sanic('test_vhosts')
@app.route('/', host=["hello.com", "world.com"])
async def handler(request):
return text("Hello, world!")
headers = {"Host": "hello.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "Hello, world!"
headers = {"Host": "world.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "Hello, world!"

View File

@@ -26,7 +26,7 @@ def test_methods():
def delete(self, request):
return text('I am delete method')
app.add_route(DummyView(), '/')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
@@ -48,7 +48,7 @@ def test_unexisting_methods():
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post")
@@ -63,7 +63,7 @@ def test_argument_methods():
def get(self, request, my_param_here):
return text('I am get method with %s' % my_param_here)
app.add_route(DummyView(), '/<my_param_here>')
app.add_route(DummyView.as_view(), '/<my_param_here>')
request, response = sanic_endpoint_test(app, uri='/test123')
@@ -79,7 +79,7 @@ def test_with_bp():
def get(self, request):
return text('I am get method')
bp.add_route(DummyView(), '/')
bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
@@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix():
def get(self, request):
return text('I am get method')
bp.add_route(DummyView(), '/')
bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/')
@@ -112,7 +112,7 @@ def test_with_middleware():
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
app.add_route(DummyView.as_view(), '/')
results = []
@@ -145,11 +145,52 @@ def test_with_middleware_response():
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
assert type(results[0]) is Request
assert type(results[1]) is Request
assert issubclass(type(results[2]), HTTPResponse)
assert isinstance(results[2], HTTPResponse)
def test_with_custom_class_methods():
app = Sanic('test_with_custom_class_methods')
class DummyView(HTTPMethodView):
global_var = 0
def _iternal_method(self):
self.global_var += 10
def get(self, request):
self._iternal_method()
return text('I am get method and global var is {}'.format(self.global_var))
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method and global var is 10'
def test_with_decorator():
app = Sanic('test_with_decorator')
results = []
def stupid_decorator(view):
def decorator(*args, **kwargs):
results.append(1)
return view(*args, **kwargs)
return decorator
class DummyView(HTTPMethodView):
decorators = [stupid_decorator]
def get(self, request):
return text('I am get method')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
assert results[0] == 1

34
tox.ini
View File

@@ -1,34 +1,26 @@
[tox]
envlist = py35, report
envlist = py35, py36, flake8
[travis]
python =
3.5: py35, flake8
3.6: py36, flake8
[testenv]
deps =
aiohttp
pytest
# pytest-cov
coverage
beautifulsoup4
commands =
coverage run -m pytest tests {posargs}
mv .coverage .coverage.{envname}
pytest tests {posargs}
basepython:
py35: python3.5
whitelist_externals =
coverage
mv
echo
[testenv:report]
[testenv:flake8]
deps =
flake8
commands =
coverage combine
coverage report
coverage html
echo "Open file://{toxinidir}/coverage/index.html"
basepython =
python3.5
flake8 sanic