Compare commits

...

328 Commits
0.1.2 ... 0.2.0

Author SHA1 Message Date
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
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
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
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
Raphael Deem
be9eca2d63 use try/except 2016-12-24 23:05:03 -08: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
Eli Uriegas
98b08676e2 Merge pull request #177 from seemethere/increment_version
Increment version to 0.1.8
2016-11-29 15:59:48 -06:00
Eli Uriegas
39f3a63ced Increment version to 0.1.8 2016-11-29 15:59:03 -06:00
Eli Uriegas
89e2084489 Merge pull request #172 from 38elements/timeout
Change request timeout process
2016-11-29 15:56:07 -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
Eli Uriegas
cce47a633a Merge pull request #167 from AntonDnepr/class-based-views
Class based views
2016-11-27 21:33:46 -06:00
Eli Uriegas
ec2330c42b Merge pull request #169 from jpiasetz/simplify_imports
Use explicit import for httptools
2016-11-27 20:02:57 -06: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
38elements
ee89b6ad03 before process 2016-11-26 16:47:16 +09:00
38elements
a5e6d6d2e8 Use default error process 2016-11-26 16:02:44 +09:00
Anton Zhyrney
1eea1f5485 rename&remove redundant code 2016-11-26 08:45:08 +02:00
Anton Zhyrney
da4567eea5 changes in doc 2016-11-26 08:44:46 +02:00
38elements
9010a6573f Add status code 2016-11-26 15:21:57 +09:00
38elements
d8e480ab48 Change sleep time 2016-11-26 14:47:42 +09:00
38elements
0bd61f6a57 Use write_response 2016-11-26 14:14:30 +09:00
38elements
c01cbb3a8c Change Request timeout process
This add a request timeout exception.
It cancels task, when request is timeout.
2016-11-26 13:55:45 +09:00
John Piasetzki
0ca5c4eeff Use explicit import for httptools
Explicit importing the parser and the exception to save a name lookup.
2016-11-25 15:14:44 -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
Anton Zhyrney
c3c7964e2e pep8 fixes 2016-11-25 09:29:25 +02:00
Anton Zhyrney
fca0221d91 update readme 2016-11-25 09:14:37 +02:00
Anton Zhyrney
9f2d73e2f1 class based views implementation for sanic 2016-11-25 09:10:25 +02:00
Eli Uriegas
fc19f2ea34 Merge pull request #163 from channelcat/request-data-vars
Access Request like a dictionary

Closes #129 #132
2016-11-23 13:51:17 -06:00
Eli Uriegas
aa0f15fbb2 Adding a new line 2016-11-23 11:03:00 -06:00
Eli Uriegas
93f50b8ef7 Merge pull request #160 from jiajunhuang/log
fix the way using logging.exception
2016-11-21 10:37:10 -06:00
Eli Uriegas
7b85843363 Merge pull request #165 from abhishek7/master
Revised error message in server.py (Issue #157)
2016-11-21 10:17:02 -06:00
abhishek7
f7f578ed44 Fixed Exception error log on line 157 of server.py 2016-11-20 21:37:01 -05:00
Abhishek
de92603ccf Merge pull request #3 from channelcat/master
Updating base fork
2016-11-20 21:28:33 -05:00
Channel Cat
d02fffb6b8 Fixing import of CIMultiDict 2016-11-19 18:41:40 -08:00
Channel Cat
922c96e3c1 Updated test terminology 2016-11-19 18:26:03 -08:00
Channel Cat
993627ec44 Merged with master 2016-11-19 18:21:44 -08:00
Channel Cat
01681599ff Fixed new test error with aiohttp 2016-11-19 18:13:02 -08:00
Channel Cat
3ce6434532 Fix flake8 2016-11-19 18:04:35 -08:00
Channel Cat
a97e554f8f Added shared request data 2016-11-19 17:48:28 -08:00
Eli Uriegas
fd5a79a685 Merge pull request #162 from r0fls/ciheaders
Update headers to use CIMultiDict instead of dict.
2016-11-19 18:32:26 -06:00
Raphael Deem
635921adc7 Update headers to use CIMultiDict instead of dict 2016-11-19 16:09:38 -08:00
jiajunhuang
9eb4cecbc1 fix the way using logging.exception 2016-11-19 15:19:38 +08:00
Eli Uriegas
879b9a4a15 Merge pull request #159 from r0fls/namedefault
provide default app name
2016-11-18 20:47:50 -06:00
Raphael Deem
8be4dc8fb5 update readme example to use default 2016-11-18 17:22:24 -08:00
Raphael Deem
f16ea20de5 provide default app name 2016-11-18 17:16:48 -08:00
Eli Uriegas
c51b14856e Merge pull request #154 from jackfischer/master
Example for using error_handler
2016-11-16 13:03:20 -06:00
Eli Uriegas
88ee71c425 Merge pull request #155 from seemethere/fix_flake8_errors
Fix the flake8 error caused by new flake8 version
2016-11-16 12:58:27 -06:00
Eli Uriegas
edb12da154 Fix the flake8 error caused by new flake8 version 2016-11-16 12:55:13 -06:00
Jack Fischer
d9f6846c76 improved default handling 2016-11-16 07:55:54 -05:00
Jack Fischer
9e0747db15 Example for using error_handler 2016-11-15 19:37:40 -05:00
Eli Uriegas
ae3d33ad58 Merge pull request #149 from asvetlov/patch-1
aiohttp is slightly faster actually
2016-11-14 10:24:15 -06:00
Manuel Miranda
edb25f799d Caching example (#150)
* Caching example using aiocache

* Caching example using aiocache

* Added aiocache to requirements

* Fixed example with newest aiocache
2016-11-13 17:11:31 -06:00
Andrew Svetlov
0822674f70 aiohttp is slightly faster actually
Disabling access log increases RPS a lot
2016-11-11 22:36:49 +02:00
Eli Uriegas
49d004736a Merge pull request #148 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2016-11-10 22:12:09 -06:00
The Gitter Badger
695f8733bb Add Gitter badge 2016-11-11 04:11:07 +00:00
Eli Uriegas
b51af7f4bf Merge pull request #147 from webtic/master
Add the client address to the request header
2016-11-10 15:30:04 -06:00
Eli Uriegas
28ce2447ef Update variable name
Give `ra` a more explicit name
2016-11-10 15:28:16 -06:00
Eli Uriegas
42e3a50274 Merge pull request #145 from pahaz/fix-request-parse-multipart-form
Fix request parse multipart form
2016-11-10 09:00:06 -06:00
Paul Jongsma
8ebc92c236 pass flake8 tests 2016-11-10 13:09:37 +01:00
Paul Jongsma
b92e46df40 fix whitespace 2016-11-10 13:06:27 +01:00
Paul Jongsma
be5588d5d8 Add the client address to the request header 2016-11-10 12:53:00 +01:00
Pahaz Blinov
0d9fb2f927 docs(request): return value docstring 2016-11-09 18:04:15 +05:00
Pahaz Blinov
0e9819fba1 fix(request): parse_multipart_form should return RequestParameters
I have this code:

```
form = FileForm(request.files)
```

and it raise error because the `request.files` is `dict` but `RequestParameters` is expected =/
2016-11-09 00:36:37 +05:00
Pahaz Blinov
aaee40aabd Merge pull request #6 from channelcat/master
fix(request.py): problem in case of request without content-type head…
2016-11-09 00:30:39 +05:00
Pahaz Blinov
5efe51b661 fix(request.py): problem in case of request without content-type header (#142)
* fix(request.py): exception if access request.form on GET request

* fix(request): just make a unification (parsed_form and parsed_files) + RFC fixes

parsed_form and parsed_files must be a RequestParameters type in all cases!
2016-11-07 15:27:50 -06:00
Pahaz Blinov
50f63142db Merge pull request #4 from channelcat/master
pull from master
2016-11-06 21:35:20 +05:00
Pahaz Blinov
1b65b2e0c6 fix(blueprints): @middleware IndexError (#139) 2016-11-06 10:08:55 -06:00
Manuel Miranda
ce8742c605 Caching example using aiocache (#140)
* Keep-alive requests stay open if communicating

* time.time faster than loop.time?

* Fix flake8

* Add aiofiles to requirements.txt

* Caching example using aiocache

* Caching example using aiocache

* Added aiocache to requirements
2016-11-06 09:26:15 -06:00
Eli Uriegas
01a013b48a Merge pull request #141 from seemethere/fix_query_tests
Fix value error for query string test
2016-11-05 12:16:13 -06:00
Eli Uriegas
3a2eeb9709 Fix value error for query string test 2016-11-05 13:12:55 -05:00
Eli Uriegas
1271c7d958 Merge pull request #128 from channelcat/keep-alive-timeout-fix
Keep alive timeout fix
2016-11-05 12:11:41 -06:00
Eli Uriegas
0032f525ce Merge pull request #138 from seemethere/add_aiofiles_requirements
Add aiofiles to requirements.txt
2016-11-05 12:11:08 -06:00
Eli Uriegas
df2f91b82f Add aiofiles to requirements.txt 2016-11-03 09:35:14 -05:00
Eli Uriegas
3a1ef6bef2 Merge pull request #125 from clenimar/fix-comments
Fix comments over-indentation
2016-11-03 08:27:39 -06:00
Eli Uriegas
28488075b9 Merge pull request #137 from imbolc/upload-without-content-type
Fix upload without content-type
2016-11-03 08:26:05 -06:00
imbolc
3cd3b2d9b7 Fix upload without content-type 2016-11-03 12:34:55 +07:00
Channel Cat
3d88818841 Merge pull request #135 from Sharpek/master
Add loop kwargs to sanic_endpoint_test
2016-11-02 21:37:52 -07:00
Channel Cat
b74cf65eca Merge pull request #134 from RyanKung/patch-1
Update README.md
2016-11-02 21:28:36 -07:00
Marcin Baran
80fcacaf8b Add loop kwargs to sanic_endpoint_test 2016-11-02 12:27:58 +01:00
Ryan Kung
96fcd8443f Update README.md
via flake8
2016-11-01 14:35:06 +08:00
Channel Cat
707c55fbe7 Fix flake8 2016-10-28 03:35:30 -07:00
Channel Cat
c44b5551bc time.time faster than loop.time? 2016-10-28 03:13:03 -07:00
Channel Cat
bd28da0abc Keep-alive requests stay open if communicating 2016-10-28 02:56:32 -07:00
Abhishek
410299f5a1 Merge pull request #2 from channelcat/master
Updates from Oct 17th
2016-10-27 20:06:56 -04:00
Clenimar Filemon
f3fc958a0c Fix comments over-indentation 2016-10-27 11:09:36 -03:00
Channel Cat
47b417db28 Merge pull request #119 from jackfischer/master
Add example with async http requests (Issue #96)
2016-10-26 22:43:01 -07:00
Jack Fischer
5171cdd305 add example with async http requests 2016-10-26 16:53:34 -04:00
Channel Cat
65950250d9 Merge pull request #111 from channelcat/reverse-static
Reverse static arguments
2016-10-25 02:52:21 -07:00
Channel Cat
74ae0007d3 Reverse static arguments 2016-10-25 02:45:28 -07:00
Channel Cat
977081f4af Merge pull request #110 from channelcat/cookies-lazy-creation
Lazy cookie creation
2016-10-25 01:50:33 -07:00
Channel Cat
ee70f1e55e Upped to version 0.1.6 2016-10-25 01:49:43 -07:00
Channel Cat
9c16f6dbea Fix flake8 issues 2016-10-25 01:36:12 -07:00
Channel Cat
c50aa34dd9 Lazy cookie creation 2016-10-25 01:27:54 -07:00
Channel Cat
0e479d53da Merge pull request #104 from channelcat/pr/101
Static file support
2016-10-24 22:42:01 -07:00
Channel Cat
984c086296 Merge pull request #105 from channelcat/blueprint-ordering
Added blueprint order test and used deques to add blueprints
2016-10-24 02:45:31 -07:00
Channel Cat
53e00b2b4c Added blueprint order test and used deques to add blueprints 2016-10-24 02:09:07 -07:00
Channel Cat
bb1cb29edd Merge pull request #103 from pcdinh/master
Made Pylint happy: clean up some unused variables
2016-10-24 01:22:06 -07:00
Channel Cat
bf6879e46f Made static file serving part of Sanic
Added sanic.static, blueprint.static, documentation, and testing
2016-10-24 01:21:06 -07:00
Channel Cat
12e900e8f9 Merge pull request #100 from chhsiao90/test-router
Add test for method not allow situation
2016-10-23 21:15:31 -07:00
imbolc
d7fff12b71 Static middleware 2016-10-24 02:17:03 +07:00
chhsiao90
9051e985a0 Add test for method not allow situation 2016-10-23 21:58:57 +08:00
pcdinh
5361c6f243 e is an unused variable. Safe to remove 2016-10-23 19:38:28 +07:00
pcdinh
963aef19e0 w is unused variable to it is safe to suppress Pylint warning using _
(underscore)
2016-10-23 19:36:08 +07:00
Channel Cat
201e232a0d Releasing 0.1.5 2016-10-23 03:43:01 -07:00
Channel Cat
6a71ea50bd Merge pull request #99 from channelcat/fix-incomplete-body
Fix incomplete request body being read
2016-10-23 03:35:23 -07:00
Channel Cat
47ec026536 Fix incomplete request body being read 2016-10-23 03:30:13 -07:00
Channel Cat
e70263d012 Merge pull request #87 from channelcat/blueprint-extras
Blueprint start/stop listeners + ordering
2016-10-23 02:04:55 -07:00
Channel Cat
658ced9188 Merge pull request #98 from channelcat/cookies
Adding cookie capabilities for issue #74
2016-10-23 02:04:30 -07:00
Channel Cat
23290b8627 Merge pull request #95 from narzeja/example_peewee_async
Provide example of using peewee_async with Sanic
2016-10-23 02:04:02 -07:00
Channel Cat
41ea40fc35 increased server event handler type flexibility 2016-10-23 01:51:46 -07:00
Channel Cat
3802141007 Adding cookie capabilities for issue #74 2016-10-23 01:32:16 -07:00
Channel Cat
50ae2048cc Merge pull request #93 from rogererens/patch-1
Fix typos
2016-10-22 03:07:14 -07:00
Channel Cat
b21ab3db12 Merge pull request #91 from pcdinh/master
Document `request.body` as a way to get raw POST body
2016-10-22 03:04:34 -07:00
Channel Cat
c80abb8cad Merge pull request #94 from narzeja/bugfix_missing_req_bp_doc
Simple blueprint was missing the 'request' parameter
2016-10-22 02:54:21 -07:00
pcdinh
a3bd1eaeab Merge branch 'master' of https://github.com/pcdinh/sanic 2016-10-22 14:29:20 +07:00
narzeja
be0739614d better get example 2016-10-22 08:52:37 +02:00
narzeja
b048f1bad3 better POST example 2016-10-22 08:50:56 +02:00
narzeja
c3628407eb post method doc 2016-10-22 08:48:19 +02:00
narzeja
96c13fe23c post method requires 'GET' 2016-10-22 08:47:51 +02:00
narzeja
ac9770dd89 a bit more informative return value when posting 2016-10-22 08:46:26 +02:00
narzeja
0e2c092ce3 fix method naming conflict 2016-10-22 08:40:24 +02:00
narzeja
22876b31b1 Provide example of using peewee_async with Sanic 2016-10-22 08:36:46 +02:00
narzeja
113047d450 Simple blueprint was missing the 'request' parameter 2016-10-22 07:13:14 +02:00
Roger Erens
268a87e3b4 Fix typos
I guess renaming was forgotten in a copy-n-paste frenzy?!
2016-10-21 23:47:13 +02:00
pcdinh
452764a8eb Document request.body as a way to get raw POST body 2016-10-22 01:35:38 +07:00
Channel Cat
f540f1e7c4 reverting reverted change 2016-10-21 04:32:05 -07:00
Channel Cat
9b561e83e3 Revert "."
This reverts commit 77c69e3810.
2016-10-21 04:14:50 -07:00
Channel Cat
77c69e3810 . 2016-10-21 04:11:40 -07:00
Channel Cat
a5614f6880 Added server start/stop listeners and reverse ordering on response middleware to blueprints 2016-10-21 04:11:18 -07:00
Channel Cat
b74d312c57 Merge pull request #84 from channelcat/update-changelog
Moved changelog and posted new benchmarks in readme
2016-10-21 04:01:27 -07:00
pcdinh
2312a176fe Document request.body as a way to get raw POST body 2016-10-21 17:55:30 +07:00
Channel Cat
e060dbfec8 Moved changelog and posted new benchmarks in readme 2016-10-21 00:11:52 -07:00
Channel Cat
8f6e5a1263 Merge pull request #82 from htkm/81
Content Type of JSON response should not have a charset
2016-10-20 21:45:43 -07:00
Hyungtae Kim
c256825de6 Content Type of JSON response should not have a charset 2016-10-20 13:38:03 -07:00
Channel Cat
cab43503d0 Merge branch 'jpiasetz-fast_router' 2016-10-20 11:34:28 +00:00
Channel Cat
d4e2d94816 Added support for routes with / in custom regexes and updated lru to use url and method 2016-10-20 11:33:28 +00:00
John Piasetzki
f510550888 Fix flake8 2016-10-20 01:37:12 -04:00
John Piasetzki
fc4c192237 Add simple uri hash to lookup 2016-10-20 01:29:22 -04:00
John Piasetzki
f4b45deb7f Convert dict to set 2016-10-20 00:28:05 -04:00
John Piasetzki
d1beabfc8f Add lru_cache to get 2016-10-20 00:28:05 -04:00
John Piasetzki
baf1ce95b1 Refactor get 2016-10-20 00:28:05 -04:00
John Piasetzki
e25e1c0e4b Convert string formats 2016-10-20 00:28:05 -04:00
John Piasetzki
04a6cc9416 Refactor add parameter 2016-10-20 00:28:05 -04:00
John Piasetzki
50e4dd167e Extract constant 2016-10-19 23:43:31 -04:00
John Piasetzki
f2cc404d7f Remove simple router 2016-10-19 23:41:22 -04:00
Channel Cat
f6a8dbf486 Merge pull request #79 from Eyepea/master
Enable after_start and before_stop callbacks for multiprocess
2016-10-19 17:17:04 -07:00
Ludovic Gasc (GMLudo)
7dcdc6208d Enable after_start and before_stop callbacks for multiprocess 2016-10-20 01:01:51 +02:00
Channel Cat
f5569f1723 Merge pull request #71 from channelcat/tornado-results
Added tornado benchmarks
2016-10-19 01:47:25 -07:00
Channel Cat
0327e6efba Added tornado benchmarks 2016-10-19 01:47:12 -07:00
Ubuntu
138b947b95 Merge branch 'mikoim-feature/statuscode' 2016-10-19 08:37:56 +00:00
Ubuntu
3d00ca09b9 Added fast lookup dict for common response codes 2016-10-19 08:37:35 +00:00
Ubuntu
69345272cd Merge branch 'feature/statuscode' of https://github.com/mikoim/sanic into mikoim-feature/statuscode 2016-10-19 08:26:11 +00:00
Channel Cat
b6a06afdc0 Merge pull request #63 from blakev/feature/performance-tornado
Adds `tornado` test server for speed comparison (#13)
2016-10-19 01:21:56 -07:00
Channel Cat
2903e7ee7c Merge pull request #65 from blakev/feature/expose-loop
Exposes `loop`in sanic `serve` and `run` functions (#64)
2016-10-19 01:21:15 -07:00
Channel Cat
d5e4355a1c Merge pull request #70 from mikoim/feature/tests
Added tests for Request.form
2016-10-19 01:20:35 -07:00
Eshin Kunishima
6d2d9d3afc Added tests for Request.form 2016-10-19 16:29:40 +09:00
Channel Cat
71a783e7e1 Merge pull request #59 from yishibashi/comment-fix
comment fixed
2016-10-18 21:14:10 -07:00
Channel Cat
a6fa496c30 Merge pull request #60 from kylefrost/master
Fix routing doc typo
2016-10-18 21:13:26 -07:00
Channel Cat
f34fa40ed2 Merge pull request #68 from channelcat/server-start-exception
Changed start failure to print exception
2016-10-18 16:51:58 -07:00
Channel Cat
c58741fe7a Changed start failure to print exception 2016-10-18 16:50:14 -07:00
Eshin Kunishima
7b0f524fb3 Added HTTP status codes
Based on http.HTTPStatus
2016-10-19 01:53:11 +09:00
Blake VandeMerwe
5e459cb69d Exposes loopin sanic serve and run functions (#64) 2016-10-18 10:05:29 -06:00
Blake VandeMerwe
cbb1f99ccb Adds tornado test server for speed comparison (#13) 2016-10-18 09:41:45 -06:00
Kyle Frost
3c05382e07 Fix routing doc typo 2016-10-18 08:13:37 -04:00
yishibashi
7c3faea0dd comment fixed 2016-10-18 19:32:47 +09:00
Channel Cat
452438dc07 Delete test.py, not needed 2016-10-18 02:52:35 -07:00
Channel Cat
d961e9d1a2 Merge pull request #58 from channelcat/release-0.1.4
release 0.1.4 - multiprocessing
2016-10-18 02:11:11 -07:00
Channel Cat
8142121c90 Update setup.py 2016-10-18 01:51:17 -07:00
Channel Cat
a904a57fa2 Merge pull request #57 from channelcat/multiprocessing
Added multiprocessing
2016-10-18 01:48:56 -07:00
Channel Cat
4ecb4d2cce Added newline to fix flake8 error 2016-10-18 01:38:50 -07:00
Channel Cat
0a26408c9d Merge pull request #48 from GenericError/patch-1
Improved grammar
2016-10-18 01:32:17 -07:00
Channel Cat
c539933e38 Fixed unused import, added change log 2016-10-18 01:31:09 -07:00
Channel Cat
6f105a647e Added multiprocessing 2016-10-18 01:22:49 -07:00
Channel Cat
18aa937f29 Fix slowdown 2016-10-17 23:34:07 -07:00
Abhishek
f95fe4192b Merge pull request #1 from channelcat/master
Updating the fork
2016-10-17 19:36:13 -04:00
Generic Error
625af9a21d Updated capitalisation 2016-10-18 07:04:24 +11:00
Generic Error
0e0d4dd3bc Improved grammar
Improved the grammar and the capitalisation consistency
2016-10-17 20:30:42 +11:00
Channel Cat
0c28cdbaf4 Correcting blueprint documentation
issue #37
2016-10-16 14:28:42 -07:00
Channel Cat
73ef816d89 Merge pull request #45 from mindflayer/master
Fix for string tokens
2016-10-16 14:23:29 -07:00
Channel Cat
031a95e4d9 Merge pull request #39 from seemethere/fix_flake8_errors
Fix flake8 errors
2016-10-16 13:54:36 -07:00
Giorgio Salluzzo
2ee4c0fc6a Merge branch 'master' of github.com:mindflayer/sanic 2016-10-16 22:42:06 +02:00
Giorgio Salluzzo
3e8b8fb46f Fix for issue #44. 2016-10-16 22:41:56 +02:00
Channel Cat
40d8602270 Merge pull request #43 from abhishek7/master
Minor updates to blueprints.md, middleware.md, blueprints.py, and request.py
2016-10-16 13:33:01 -07:00
abhishek7
3c7a8a5f45 Added some documentation to request.py, removed extra line in blueprints.py, and minor grammar enhancements to blueprints.md and middleware.md 2016-10-16 11:35:45 -04:00
Eli Uriegas
bfee7afd0c Remove the 120 line length, reset to default 2016-10-16 08:02:22 -05:00
Eli Uriegas
ea0a037248 Fix flake8 errors 2016-10-16 08:01:59 -05:00
Channel Cat
0148d65dd2 Merge pull request #38 from channelcat/readme-status-images
Adding more sweet readme status images
2016-10-16 04:36:57 -07:00
Channel Cat
8449527ecd Adding more sweet readme status images 2016-10-16 04:36:36 -07:00
Channel Cat
7ceba1ae9d Changed install instructions to use pypi 2016-10-16 02:58:17 -07:00
Channel Cat
4f102a9cf2 Fixed setup.py bad classifier and upped version 2016-10-16 02:44:16 -07:00
Channel Cat
d008c25eff Changed travis image to link to project, derp 2016-10-16 02:37:02 -07:00
71 changed files with 3573 additions and 357 deletions

4
.gitignore vendored
View File

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

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 --max-line-length=120 sanic
script: py.test -v tests
- '3.5'
- '3.6'
install: pip install tox-travis
script: tox
deploy:
provider: pypi
user: channelcat

22
CHANGELOG.md Normal file
View File

@@ -0,0 +1,22 @@
Version 0.1
-----------
- 0.1.7
- Reversed static url and directory arguments to meet spec
- 0.1.6
- Static files
- Lazy Cookie Loading
- 0.1.5
- Cookies
- Blueprint listeners and ordering
- Faster Router
- Fix: Incomplete file reads on medium+ sized post requests
- Breaking: after_start and before_stop now pass sanic as their first argument
- 0.1.4
- Multiprocessing
- 0.1.3
- Blueprint support
- Faster Response processing
- 0.1.1 - 0.1.2
- Struggling to update pypi via CI
- 0.1.0
- Released to public

View File

@@ -1,24 +1,31 @@
![build verification](https://travis-ci.org/channelcat/sanic.svg?branch=master)
# Sanic
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based off the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/.
[![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)
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.
[![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 a 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.
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 | 30,601 | 3.23ms |
| 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
@@ -26,17 +33,21 @@ All tests were run on a AWS medium instance running ubuntu, using 1 process. Ea
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
app = Sanic()
@app.route("/")
async def test(request):
return json({ "hello": "world" })
return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
app.run(host="0.0.0.0", port=8000)
```
## Installation
* `python -m pip install git+https://github.com/channelcat/sanic/`
* `python -m pip install sanic`
## Documentation
* [Getting started](docs/getting_started.md)
@@ -45,6 +56,12 @@ app.run(host="0.0.0.0", port=8000)
* [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)
* [Custom Protocol](docs/custom_protocol.md)
* [Testing](docs/testing.md)
* [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md)
* [License](LICENSE)
@@ -62,7 +79,7 @@ app.run(host="0.0.0.0", port=8000)
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄

View File

@@ -3,7 +3,7 @@
Blueprints are objects that can be used for sub-routing within an application.
Instead of adding routes to the application object, blueprints define similar
methods for adding routes, which are then registered with the application in a
flexible and plugable manner.
flexible and pluggable manner.
## Why?
@@ -29,7 +29,7 @@ from sanic import Blueprint
bp = Blueprint('my_blueprint')
@bp.route('/')
async def bp_root():
async def bp_root(request):
return json({'my': 'blueprint'})
```
@@ -42,7 +42,7 @@ from sanic import Sanic
from my_blueprint import bp
app = Sanic(__name__)
app.register_blueprint(bp)
app.blueprint(bp)
app.run(host='0.0.0.0', port=8000, debug=True)
```
@@ -56,9 +56,7 @@ In this example, the registered routes in the `app.router` will look like:
```
## Middleware
Using blueprints allows you to also register middleware exclusively for that
blueprint, without interfering with other blueprints or routes registered
directly on the application object.
Using blueprints allows you to also register middleware globally.
```python
@bp.middleware
@@ -75,11 +73,39 @@ async def halt_response(request, response):
```
## Exceptions
Exceptions can also be applied exclusively to blueprints without interfering
with other blueprints or routes registered on the application object.
Exceptions can also 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.
```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
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
```python
bp = Blueprint('my_blueprint')
@bp.listen('before_server_start')
async def setup_connection():
global database
database = mysql.connect(host='127.0.0.1'...)
@bp.listen('after_server_stop')
async def close_connection():
await database.close()
```

58
docs/class_based_views.md Normal file
View File

@@ -0,0 +1,58 @@
# 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.
## 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):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(SimpleView.as_view(), '/')
```
If you need any url params just mention them in method definition:
```python
class NameView(HTTPMethodView):
def get(self, request, name):
return text('Hello {}'.format(name))
app.add_route(NameView.as_view(), '/<name>')
```
If you want to add decorator for class, you could set decorators variable
```
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')
```

50
docs/cookies.md Normal file
View File

@@ -0,0 +1,50 @@
# Cookies
## Request
Request cookies can be accessed via the request.cookie dictionary
### Example
```python
from sanic import Sanic
from sanic.response import text
@app.route("/cookie")
async def test(request):
test_cookie = request.cookies.get('test')
return text("Test cookie set to: {}".format(test_cookie))
```
## Response
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
```python
from sanic import Sanic
from sanic.response import text
@app.route("/cookie")
async def test(request):
response = text("There's a cookie up in this response")
response.cookies['test'] = 'It worked!'
response.cookies['test']['domain'] = '.gotta-go-fast.com'
response.cookies['test']['httponly'] = True
return response
```

70
docs/custom_protocol.md Normal file
View File

@@ -0,0 +1,70 @@
# Custom Protocol
You can change the behavior of protocol by using custom protocol.
If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic.
* loop
`loop` is an asyncio compatible event loop.
* connections
`connections` is a `set object` to store protocol objects.
When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections.
* signal
`signal` is a `sanic.server.Signal object` with `stopped attribute`.
When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`.
* request_handler
`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments.
* error_handler
`error_handler` is a `sanic.exceptions.Handler` object.
* request_timeout
`request_timeout` is seconds for timeout.
* request_max_size
`request_max_size` is bytes of max request size.
## Example
By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.
In this example, By rewriting `write_response()`, if the handler returns `str`, 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)
```

35
docs/deploying.md Normal file
View File

@@ -0,0 +1,35 @@
# Deploying
When it comes to deploying Sanic, there's not much to it, but there are
a few things to take note of.
## 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:
```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.
## 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:
`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:
```python
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
```

View File

@@ -4,7 +4,7 @@ Make sure you have pip and python 3.5 before starting
## Benchmarks
* Install Sanic
* `python3 -m pip install git+https://github.com/channelcat/sanic/`
* `python3 -m pip install sanic`
* Edit main.py to include:
```python
from sanic import Sanic
@@ -20,6 +20,6 @@ app.run(host="0.0.0.0", port=8000, debug=True)
```
* Run `python3 main.py`
You now have a working sanic server! To continue on, check out:
You now have a working Sanic server! To continue on, check out:
* [Request Data](request_data.md)
* [Routing](routing.md)

View File

@@ -1,6 +1,6 @@
# Middleware
Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware return a response object, the request will stop processing and a response will be returned.
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 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.
@@ -27,3 +27,23 @@ async def handler(request):
app.run(host="0.0.0.0", port=8000)
```
## Middleware chain
If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware:
```python
app = Sanic(__name__)
@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 middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks.

View File

@@ -8,6 +8,7 @@ The following request variables are accessible as properties:
`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
See request.py for more information
@@ -15,7 +16,7 @@ See request.py for more information
```python
from sanic import Sanic
from sanic.response import json
from sanic.response import json, text
@app.route("/json")
def post_json(request):
@@ -40,4 +41,9 @@ def post_json(request):
@app.route("/query_string")
def query_string(request):
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
@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)
```

View File

@@ -10,16 +10,16 @@ from sanic import Sanic
from sanic.response import text
@app.route('/tag/<tag>')
async def person_handler(request, tag):
async def tag_handler(request, tag):
return text('Tag - {}'.format(tag))
@app.route('/number/<integer_arg:int>')
async def person_handler(request, integer_arg):
async def integer_handler(request, integer_arg):
return text('Integer - {}'.format(integer_arg))
@app.route('/number/<number_arg:number>')
async def person_handler(request, number_arg):
return text('Number - {}'.format(number))
async def number_handler(request, number_arg):
return text('Number - {}'.format(number_arg))
@app.route('/person/<name:[A-z]>')
async def person_handler(request, name):
@@ -29,4 +29,16 @@ 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 handler2(request, name):
return text('Folder - {}'.format(name))
app.add_route(handler2, '/folder/<name>')
async def person_handler2(request, name):
return text('Person - {}'.format(name))
app.add_route(person_handler2, '/person/<name:[A-z]>')
```

18
docs/static_files.md Normal file
View File

@@ -0,0 +1,18 @@
# Static Files
Both directories and files can be served by registering with static
## Example
```python
app = Sanic(__name__)
# Serves files from the static folder to the URL /static
app.static('/static', './static')
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
# is requested
app.static('/the_best.png', '/home/ubuntu/test.png')
app.run(host="0.0.0.0", port=8000)
```

51
docs/testing.md Normal file
View File

@@ -0,0 +1,51 @@
# 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']
```

View File

@@ -0,0 +1,33 @@
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):
"""
Use session object to perform 'get' request on url
"""
async with 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(loop=loop) as session:
response = await fetch(session, url)
return json(response)
app.run(host="0.0.0.0", port=8000, loop=loop)

41
examples/cache_example.py Normal file
View File

@@ -0,0 +1,41 @@
"""
Example of caching using aiocache package. To run it you will need a Redis
instance running in localhost:6379.
Running this example you will see that the first call lasts 3 seconds and
the rest are instant because the value is retrieved from the Redis.
If you want more info about the package check
https://github.com/argaen/aiocache
"""
import asyncio
import aiocache
from sanic import Sanic
from sanic.response import json
from sanic.log import log
from aiocache import cached
from aiocache.serializers import JsonSerializer
app = Sanic(__name__)
aiocache.settings.set_defaults(
cache="aiocache.RedisCache"
)
@cached(key="my_custom_key", serializer=JsonSerializer())
async def expensive_call():
log.info("Expensive has been called")
await asyncio.sleep(3)
return {"test": True}
@app.route("/")
async def test(request):
log.info("Received GET /")
return json(await expensive_call())
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop())

View File

@@ -0,0 +1,60 @@
"""
Example intercepting uncaught exceptions using Sanic's error handler framework.
This may be useful for developers wishing to use Sentry, Airbrake, etc.
or a custom system to log and monitor unexpected errors in production.
First we create our own class inheriting from Handler in sanic.exceptions,
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.
"""
"""
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):
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)
"""
This is an ordinary Sanic server, with the exception that we set the
server's error_handler to an instance of our CustomHandler
"""
from sanic import Sanic
from sanic.response import json
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
return json({"test": True})
app.run(host="0.0.0.0", port=8000, debug=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,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(logger=True)
@sanic.route("/")
def test(request):
log.info("received request; responding with 'hey'")
return text("hey")
sanic.run(host="0.0.0.0", port=8000)

View File

@@ -0,0 +1,21 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.config import Config
from sanic.exceptions import RequestTimeout
Config.REQUEST_TIMEOUT = 1
app = Sanic(__name__)
@app.route('/')
async def test(request):
await asyncio.sleep(3)
return text('Hello, world!')
@app.exception(RequestTimeout)
def timeout(request, exception):
return text('RequestTimeout from error_handler.', 408)
app.run(host='0.0.0.0', port=8000)

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
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
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)
loop = asyncio.get_event_loop()
async def get_pool():
return await aiopg.create_pool(connection)
app = Sanic(name=__name__)
pool = loop.run_until_complete(get_pool())
async def prepare_db():
""" Let's create some table and add some data
"""
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):
async with pool.acquire() as conn:
async with conn.cursor() as cur:
result = []
await cur.execute("SELECT question, pub_date FROM sanic_polls")
async for row in cur:
result.append({"question": row[0], "pub_date": row[1]})
return json({"polls": result})
if __name__ == '__main__':
loop.run_until_complete(prepare_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

View File

@@ -0,0 +1,73 @@
""" 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
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
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)
loop = asyncio.get_event_loop()
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))
async def get_engine():
return await create_engine(connection)
app = Sanic(name=__name__)
engine = loop.run_until_complete(get_engine())
async def prepare_db():
""" Let's add some data
"""
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 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__':
loop.run_until_complete(prepare_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

View File

@@ -0,0 +1,65 @@
""" 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
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
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]
loop = asyncio.get_event_loop()
async def make_pool():
return await create_pool(**DB_CONFIG)
app = Sanic(__name__)
pool = loop.run_until_complete(make_pool())
async def create_db():
""" Create some table and add some data
"""
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 pool.acquire() as connection:
async with connection.transaction():
results = await connection.fetch('SELECT * FROM sanic_post')
return json({'posts': jsonify(results)})
if __name__ == '__main__':
loop.run_until_complete(create_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

80
examples/sanic_peewee.py Normal file
View File

@@ -0,0 +1,80 @@
## You need the following additional packages for this example
# aiopg
# peewee_async
# peewee
## sanic imports
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')
objects = Manager(database, loop=loop)
## from peewee_async docs:
# Also theres no need to connect and re-connect before executing async queries
# with manager! Its all automatic. But you can run Manager.connect() or
# Manager.close() when you need it.
# let's create a simple key value store:
class KeyValue(peewee.Model):
key = peewee.CharField(max_length=40, unique=True)
text = peewee.TextField(default='')
class Meta:
database = database
# create table synchronously
KeyValue.create_table(True)
# OPTIONAL: close synchronous connection
database.close()
# OPTIONAL: disable any future syncronous calls
objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
app = Sanic('peewee_example')
@app.route('/post/<key>/<value>')
async def post(request, key, value):
"""
Save get parameters to database
"""
obj = await objects.create(KeyValue, key=key, text=value)
return json({'object_id': obj.id})
@app.route('/get')
async def get(request):
"""
Load all objects from database
"""
all_objects = await objects.execute(KeyValue.select())
serialized_obj = []
for obj in all_objects:
serialized_obj.append({
'id': obj.id,
'key': obj.key,
'value': obj.text}
)
return json({'objects': serialized_obj})
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000, loop=loop)

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

32
examples/vhosts.py Normal file
View File

@@ -0,0 +1,32 @@
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")
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

@@ -2,9 +2,13 @@ httptools
ujson
uvloop
aiohttp
aiocache
pytest
coverage
tox
gunicorn
bottle
kyoukai
falcon
tornado
aiofiles

View File

@@ -1,3 +1,5 @@
httptools
ujson
uvloop
uvloop
aiofiles
multidict

View File

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

36
sanic/__main__.py Normal file
View File

@@ -0,0 +1,36 @@
from argparse import ArgumentParser
from importlib import import_module
from .log import log
from .sanic import Sanic
if __name__ == "__main__":
parser = ArgumentParser(prog='sanic')
parser.add_argument('--host', dest='host', type=str, default='127.0.0.1')
parser.add_argument('--port', dest='port', type=int, default=8000)
parser.add_argument('--workers', dest='workers', type=int, default=1, )
parser.add_argument('--debug', dest='debug', action="store_true")
parser.add_argument('module')
args = parser.parse_args()
try:
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
module = import_module(module_name)
app = getattr(module, app_name, None)
if not isinstance(app, Sanic):
raise ValueError("Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?"
.format(type(app).__name__, args.module))
app.run(host=args.host, port=args.port,
workers=args.workers, debug=args.debug)
except ImportError:
log.error("No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
.format(module_name))
except ValueError as e:
log.error("{}".format(e))

View File

@@ -1,3 +1,6 @@
from collections import defaultdict
class BlueprintSetup:
"""
"""
@@ -15,14 +18,17 @@ 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.router.add(uri, 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):
"""
@@ -30,6 +36,15 @@ class BlueprintSetup:
"""
self.app.exception(*args, **kwargs)(handler)
def add_static(self, uri, file_or_directory, *args, **kwargs):
"""
Registers static files to sanic
"""
if self.url_prefix:
uri = self.url_prefix + uri
self.app.static(uri, file_or_directory, *args, **kwargs)
def add_middleware(self, middleware, *args, **kwargs):
"""
Registers middleware to sanic
@@ -41,10 +56,17 @@ 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
:param url_prefix: URL to be prefixed before all route URLs
"""
self.name = name
self.url_prefix = url_prefix
self.deferred_functions = []
self.listeners = defaultdict(list)
self.host = host
def record(self, func):
"""
@@ -65,26 +87,41 @@ class Blueprint:
for deferred in self.deferred_functions:
deferred(state)
def route(self, uri, methods=None):
def route(self, uri, methods=None, host=None):
"""
"""
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, host=None):
"""
"""
self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler
def listener(self, event):
"""
"""
def decorator(listener):
self.listeners[event].append(listener)
return listener
return decorator
def middleware(self, *args, **kwargs):
"""
"""
def register_middleware(middleware):
self.record(lambda s: s.add_middleware(middleware, *args, **kwargs))
self.record(
lambda s: s.add_middleware(middleware, *args, **kwargs))
return middleware
# Detect which way this was called, @middleware or @middleware('AT')
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
middleware = args[0]
args = []
return register_middleware(args[0])
return register_middleware(middleware)
else:
return register_middleware
@@ -95,3 +132,9 @@ class Blueprint:
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
return handler
return decorator
def static(self, uri, file_or_directory, *args, **kwargs):
"""
"""
self.record(
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))

View File

@@ -22,3 +22,4 @@ class Config:
"""
REQUEST_MAX_SIZE = 100000000 # 100 megababies
REQUEST_TIMEOUT = 60 # 60 seconds
ROUTER_CACHE_SIZE = 1024

130
sanic/cookies.py Normal file
View File

@@ -0,0 +1,130 @@
from datetime import datetime
import re
import string
# ------------------------------------------------------------ #
# SimpleCookie
# ------------------------------------------------------------ #
# Straight up copied this section of dark magic from SimpleCookie
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
_Translator = {n: '\\%03o' % n
for n in set(range(256)) - set(map(ord, _UnescapedChars))}
_Translator.update({
ord('"'): '\\"',
ord('\\'): '\\\\',
})
def _quote(str):
r"""Quote a string for use in a cookie header.
If the string does not need to be double-quoted, then just return the
string. Otherwise, surround the string in doublequotes and quote
(with a \) special characters.
"""
if str is None or _is_legal_key(str):
return str
else:
return '"' + str.translate(_Translator) + '"'
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
# ------------------------------------------------------------ #
# Custom SimpleCookie
# ------------------------------------------------------------ #
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
"""
def __init__(self, headers):
super().__init__()
self.headers = headers
self.cookie_headers = {}
def __setitem__(self, key, value):
# If this cookie doesn't exist, add it to the header keys
cookie_header = self.cookie_headers.get(key)
if not cookie_header:
cookie = Cookie(key, value)
cookie_header = MultiHeader("Set-Cookie")
self.cookie_headers[key] = cookie_header
self.headers[cookie_header] = cookie
return super().__setitem__(key, cookie)
else:
self[key].value = value
def __delitem__(self, key):
del self.cookie_headers[key]
return super().__delitem__(key)
class Cookie(dict):
"""
This is a stripped down version of Morsel from SimpleCookie #gottagofast
"""
_keys = {
"expires": "expires",
"path": "Path",
"comment": "Comment",
"domain": "Domain",
"max-age": "Max-Age",
"secure": "Secure",
"httponly": "HttpOnly",
"version": "Version",
}
_flags = {'secure', 'httponly'}
def __init__(self, key, value):
if key in self._keys:
raise KeyError("Cookie name is a reserved word")
if not _is_legal_key(key):
raise KeyError("Cookie key contains illegal characters")
self.key = key
self.value = value
super().__init__()
def __setitem__(self, key, value):
if key not in self._keys:
raise KeyError("Unknown cookie property")
return super().__setitem__(key, value)
def encode(self, encoding):
output = ['%s=%s' % (self.key, _quote(self.value))]
for key, value in self.items():
if key == 'max-age' and isinstance(value, int):
output.append('%s=%d' % (self._keys[key], value))
elif key == 'expires' and isinstance(value, datetime):
output.append('%s=%s' % (
self._keys[key],
value.strftime("%a, %d-%b-%Y %T GMT")
))
elif key in self._flags:
output.append(self._keys[key])
else:
output.append('%s=%s' % (self._keys[key], value))
return "; ".join(output).encode(encoding)
# ------------------------------------------------------------ #
# Header Trickery
# ------------------------------------------------------------ #
class MultiHeader:
"""
Allows us to set a header within response that has a unique key,
but may contain duplicate header names
"""
def __init__(self, name):
self.name = name
def encode(self):
return self.name.encode()

View File

@@ -1,4 +1,5 @@
from .response import text
from .log import log
from traceback import format_exc
@@ -21,6 +22,23 @@ class ServerError(SanicException):
status_code = 500
class FileNotFound(NotFound):
status_code = 404
def __init__(self, message, path, relative_url):
super().__init__(message)
self.path = path
self.relative_url = relative_url
class RequestTimeout(SanicException):
status_code = 408
class PayloadTooLarge(SanicException):
status_code = 413
class Handler:
handlers = None
@@ -39,13 +57,31 @@ class Handler:
: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:
if self.sanic.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):
return text("Error: {}".format(exception), status=getattr(exception, 'status_code', 500))
return text(
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500))
elif self.sanic.debug:
return text("Error: {}\nException: {}".format(exception, format_exc()), status=500)
response_message = (
'Exception occurred while handling uri: "{}"\n{}'.format(
request.url, format_exc()))
log.error(response_message)
return text(response_message, status=500)
else:
return text("An error occurred while generating the request", status=500)
return text(
'An error occurred while generating the response', status=500)

View File

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

View File

@@ -1,12 +1,20 @@
from cgi import parse_header
from collections import namedtuple
from http.cookies import SimpleCookie
from httptools import parse_url
from urllib.parse import parse_qs
from ujson import loads as json_loads
from sanic.exceptions import InvalidUsage
from .log import log
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
# > If the media type remains unknown, the recipient SHOULD treat it
# > as type "application/octet-stream"
class RequestParameters(dict):
"""
Hosts a dict with lists as values where get returns the first
@@ -17,6 +25,9 @@ class RequestParameters(dict):
self.super = super()
self.super.__init__(*args, **kwargs)
def __getitem__(self, name):
return self.get(name)
def get(self, name, default=None):
values = self.super.get(name)
return values[0] if values else default
@@ -25,9 +36,12 @@ class RequestParameters(dict):
return self.super.get(name, default)
class Request:
class Request(dict):
"""
Properties of an HTTP request such as URL, headers, etc.
"""
__slots__ = (
'url', 'headers', 'version', 'method',
'url', 'headers', 'version', 'method', '_cookies',
'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
)
@@ -39,7 +53,9 @@ class Request:
self.headers = headers
self.version = version
self.method = method
self.query_string = url_parsed.query.decode('utf-8') if url_parsed.query else None
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
@@ -47,33 +63,48 @@ class Request:
self.parsed_form = None
self.parsed_files = None
self.parsed_args = None
self._cookies = None
@property
def json(self):
if not self.parsed_json:
if self.parsed_json is None:
try:
self.parsed_json = json_loads(self.body)
except:
pass
except Exception:
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:
self.parsed_form = {}
self.parsed_files = {}
content_type, parameters = parse_header(self.headers.get('Content-Type'))
self.parsed_form = RequestParameters()
self.parsed_files = RequestParameters()
content_type = self.headers.get(
'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
content_type, parameters = parse_header(content_type)
try:
if content_type is None or content_type == 'application/x-www-form-urlencoded':
self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8')))
if content_type == 'application/x-www-form-urlencoded':
self.parsed_form = RequestParameters(
parse_qs(self.body.decode('utf-8')))
elif content_type == 'multipart/form-data':
# TODO: Stream this instead of reading to/from memory
boundary = parameters['boundary'].encode('utf-8')
self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary)
except Exception as e:
log.exception(e)
pass
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
except Exception:
log.exception("Failed when parsing form")
return self.parsed_form
@@ -88,12 +119,26 @@ class Request:
def args(self):
if self.parsed_args is None:
if self.query_string:
self.parsed_args = RequestParameters(parse_qs(self.query_string))
self.parsed_args = RequestParameters(
parse_qs(self.query_string))
else:
self.parsed_args = {}
return self.parsed_args
@property
def cookies(self):
if self._cookies is None:
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
if cookie is not None:
cookies = SimpleCookie()
cookies.load(cookie)
self._cookies = {name: cookie.value
for name, cookie in cookies.items()}
else:
self._cookies = {}
return self._cookies
File = namedtuple('File', ['type', 'body', 'name'])
@@ -103,10 +148,10 @@ 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 (dict), files (dict)
:return: fields (RequestParameters), files (RequestParameters)
"""
files = {}
fields = {}
files = RequestParameters()
fields = RequestParameters()
form_parts = body.split(boundary)
for form_part in form_parts[1:-1]:
@@ -125,7 +170,8 @@ def parse_multipart_form(body, boundary):
colon_index = form_line.index(':')
form_header_field = form_line[0:colon_index]
form_header_value, form_parameters = parse_header(form_line[colon_index + 2:])
form_header_value, form_parameters = parse_header(
form_line[colon_index + 2:])
if form_header_field == 'Content-Disposition':
if 'filename' in form_parameters:
@@ -136,8 +182,16 @@ def parse_multipart_form(body, boundary):
post_data = form_part[line_index:-4]
if file_name or file_type:
files[field_name] = File(type=file_type, name=file_name, body=post_data)
file = File(type=file_type, name=file_name, body=post_data)
if field_name in files:
files[field_name].append(file)
else:
files[field_name] = [file]
else:
fields[field_name] = post_data.decode('utf-8')
value = post_data.decode('utf-8')
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
return fields, files

View File

@@ -1,34 +1,98 @@
import ujson
from aiofiles import open as open_async
from mimetypes import guess_type
from os import path
STATUS_CODES = {
from ujson import dumps as json_dumps
from .cookies import CookieJar
COMMON_STATUS_CODES = {
200: b'OK',
400: b'Bad Request',
404: b'Not Found',
500: b'Internal Server Error',
}
ALL_STATUS_CODES = {
100: b'Continue',
101: b'Switching Protocols',
102: b'Processing',
200: b'OK',
201: b'Created',
202: b'Accepted',
203: b'Non-Authoritative Information',
204: b'No Content',
205: b'Reset Content',
206: b'Partial Content',
207: b'Multi-Status',
208: b'Already Reported',
226: b'IM Used',
300: b'Multiple Choices',
301: b'Moved Permanently',
302: b'Found',
303: b'See Other',
304: b'Not Modified',
305: b'Use Proxy',
307: b'Temporary Redirect',
308: b'Permanent Redirect',
400: b'Bad Request',
401: b'Unauthorized',
402: b'Payment Required',
403: b'Forbidden',
404: b'Not Found',
405: b'Method Not Allowed',
406: b'Not Acceptable',
407: b'Proxy Authentication Required',
408: b'Request Timeout',
409: b'Conflict',
410: b'Gone',
411: b'Length Required',
412: b'Precondition Failed',
413: b'Request Entity Too Large',
414: b'Request-URI Too Long',
415: b'Unsupported Media Type',
416: b'Requested Range Not Satisfiable',
417: b'Expectation Failed',
422: b'Unprocessable Entity',
423: b'Locked',
424: b'Failed Dependency',
426: b'Upgrade Required',
428: b'Precondition Required',
429: b'Too Many Requests',
431: b'Request Header Fields Too Large',
500: b'Internal Server Error',
501: b'Not Implemented',
502: b'Bad Gateway',
503: b'Service Unavailable',
504: b'Gateway Timeout',
505: b'HTTP Version Not Supported',
506: b'Variant Also Negotiates',
507: b'Insufficient Storage',
508: b'Loop Detected',
510: b'Not Extended',
511: b'Network Authentication Required'
}
class HTTPResponse:
__slots__ = ('body', 'status', 'content_type', 'headers')
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
def __init__(self, body=None, status=200, headers=None, content_type='text/plain', body_bytes=b''):
def __init__(self, body=None, status=200, headers=None,
content_type='text/plain', body_bytes=b''):
self.content_type = content_type
if body is not None:
self.body = body.encode('utf-8')
try:
# Try to encode it regularly
self.body = body.encode('utf-8')
except AttributeError:
# Convert it to a str if you can't
self.body = str(body).encode('utf-8')
else:
self.body = body_bytes
self.status = status
self.headers = headers or {}
self._cookies = None
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
# This is all returned in a kind-of funky way
@@ -39,14 +103,30 @@ 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()
)
return b'HTTP/%b %d %b\r\nContent-Type: %b\r\nContent-Length: %d\r\nConnection: %b\r\n%b%b\r\n%b' % (
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
status = COMMON_STATUS_CODES.get(self.status)
if not status:
status = ALL_STATUS_CODES.get(self.status)
return (b'HTTP/%b %d %b\r\n'
b'Content-Type: %b\r\n'
b'Content-Length: %d\r\n'
b'Connection: %b\r\n'
b'%b%b\r\n'
b'%b') % (
version.encode(),
self.status,
STATUS_CODES.get(self.status, b'FAIL'),
status,
self.content_type.encode(),
len(self.body),
b'keep-alive' if keep_alive else b'close',
@@ -55,15 +135,37 @@ class HTTPResponse:
self.body
)
@property
def cookies(self):
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
def json(body, status=200, headers=None):
return HTTPResponse(ujson.dumps(body), headers=headers, status=status,
content_type="application/json; charset=utf-8")
return HTTPResponse(json_dumps(body), headers=headers, status=status,
content_type="application/json")
def text(body, status=200, headers=None):
return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8")
return HTTPResponse(body, status=status, headers=headers,
content_type="text/plain; charset=utf-8")
def html(body, status=200, headers=None):
return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8")
return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None):
filename = path.split(location)[-1]
async with open_async(location, mode='rb') as _file:
out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
return HTTPResponse(status=200,
headers=headers,
content_type=mime_type,
body_bytes=out_stream)

View File

@@ -1,128 +1,197 @@
import re
from collections import namedtuple
from collections import defaultdict, namedtuple
from functools import lru_cache
from .config import Config
from .exceptions import NotFound, InvalidUsage
Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters'])
Parameter = namedtuple("Parameter", ['name', 'cast'])
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
REGEX_TYPES = {
'string': (str, r'[^/]+'),
'int': (int, r'\d+'),
'number': (float, r'[0-9\\.]+'),
'alpha': (str, r'[A-Za-z]+'),
}
def url_hash(url):
return url.count('/')
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):
@app.route('/my_url/<my_param>', methods=['GET', 'POST', ...])
def my_route(request, my_param):
do stuff...
or
@app.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
TODO:
This probably needs optimization for larger sets of routes,
since it checks every route until it finds a match which is bad and I should feel bad
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>. 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 = None
regex_types = {
"string": (None, "\w+"),
"int": (int, "\d+"),
"number": (float, "[0-9\\.]+"),
"alpha": (None, "[A-Za-z]+"),
}
routes_static = None
routes_dynamic = None
routes_always_check = None
def __init__(self):
self.routes = []
self.routes_all = {}
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
:param handler: Request handler function. When executed, it should provide a response object.
:param methods: Array of accepted method names.
If none are provided, any method is allowed
:param handler: Request handler function.
When executed, it should provide a response object.
:return: Nothing
"""
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:
self.hosts.add(host)
uri = host + uri
if uri in self.routes_all:
raise RouteExists("Route already registered: {}".format(uri))
# Dict for faster lookups of if method allowed
methods_dict = {method: True for method in methods} if methods else None
if methods:
methods = frozenset(methods)
parameters = []
properties = {"unhashable": None}
def add_parameter(match):
# We could receive NAME or NAME:PATTERN
parts = match.group(1).split(':')
if len(parts) == 2:
parameter_name, parameter_pattern = parts
else:
parameter_name = parts[0]
parameter_pattern = 'string'
name = match.group(1)
pattern = 'string'
if ':' in name:
name, pattern = name.split(':', 1)
default = (str, pattern)
# Pull from pre-configured types
parameter_regex = self.regex_types.get(parameter_pattern)
if parameter_regex:
parameter_type, parameter_pattern = parameter_regex
else:
parameter_type = None
parameter = Parameter(name=parameter_name, cast=parameter_type)
_type, pattern = REGEX_TYPES.get(pattern, default)
parameter = Parameter(name=name, cast=_type)
parameters.append(parameter)
return "({})".format(parameter_pattern)
# Mark the whole route as unhashable if it has the hash key in it
if re.search('(^|[^^]){1}/', pattern):
properties['unhashable'] = True
# Mark the route as unhashable if it matches the hash key
elif re.search(pattern, '/'):
properties['unhashable'] = True
pattern_string = re.sub("<(.+?)>", add_parameter, uri)
pattern = re.compile("^{}$".format(pattern_string))
return '({})'.format(pattern)
route = Route(handler=handler, methods=methods_dict, pattern=pattern, parameters=parameters)
self.routes.append(route)
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)
self.routes_all[uri] = route
if properties['unhashable']:
self.routes_always_check.append(route)
elif parameters:
self.routes_dynamic[url_hash(uri)].append(route)
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
Gets a request handler based on the URL of the request, or raises an
error
:param request: Request object
:return: handler, arguments, keyword arguments
"""
route = None
args = []
kwargs = {}
for _route in self.routes:
match = _route.pattern.match(request.url)
if match:
for index, parameter in enumerate(_route.parameters, start=1):
value = match.group(index)
kwargs[parameter.name] = parameter.cast(value) if parameter.cast is not None else value
route = _route
break
if route:
if route.methods and request.method not in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
status_code=405)
return route.handler, args, kwargs
if self.hosts is None:
return self._get(request.url, request.method, '')
else:
raise NotFound("Requested URL {} not found".format(request.url))
return self._get(request.url, request.method,
request.headers.get("Host", ''))
class SimpleRouter:
"""
Simple router records and reads all routes from a dictionary
It does not support parameters in routes, but is very fast
"""
routes = None
def __init__(self):
self.routes = {}
def add(self, uri, methods, handler):
# Dict for faster lookups of method allowed
methods_dict = {method: True for method in methods} if methods else None
self.routes[uri] = Route(handler=handler, methods=methods_dict, pattern=uri, parameters=None)
def get(self, request):
route = self.routes.get(request.url)
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
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.
:param url: Request URL
: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:
if route.methods and request.method not in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
status_code=405)
return route.handler, [], {}
match = route.pattern.match(url)
else:
raise NotFound("Requested URL {} not found".format(request.url))
# Move on to testing all regex routes
for route in self.routes_dynamic[url_hash(url)]:
match = route.pattern.match(url)
if match:
break
else:
# Lastly, check against all regex routes that cannot be hashed
for route in self.routes_always_check:
match = route.pattern.match(url)
if match:
break
else:
raise NotFound('Requested URL {} not found'.format(url))
if route.methods and method not in route.methods:
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
method, url), status_code=405)
kwargs = {p.name: p.cast(value)
for value, p
in zip(match.groups(1), route.parameters)}
return route.handler, [], kwargs

View File

@@ -1,33 +1,57 @@
import asyncio
from inspect import isawaitable
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 traceback import format_exc
import logging
from .config import Config
from .exceptions import Handler
from .log import log, logging
from .log import log
from .response import HTTPResponse
from .router import Router
from .server import serve
from .server import serve, HttpProtocol
from .static import register as static_register
from .exceptions import ServerError
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from os import set_inheritable
class Sanic:
def __init__(self, name, router=None, error_handler=None):
def __init__(self, name=None, router=None,
error_handler=None, logger=None):
if logger is None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s: %(levelname)s: %(message)s"
)
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.config = Config()
self.request_middleware = []
self.response_middleware = []
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
# -------------------------------------------------------------------- #
# Registration
# -------------------------------------------------------------------- #
# Decorator
def route(self, uri, methods=None):
def route(self, uri, methods=None, host=None):
"""
Decorates a function to be registered as a route
:param uri: path of the URL
@@ -35,18 +59,39 @@ class Sanic:
:return: decorated function
"""
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith('/'):
uri = '/' + uri
def response(handler):
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, 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, 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 route
:param uri: path of the URL
:param methods: list or tuple of methods allowed
Decorates a function to be registered as a handler for exceptions
:param *exceptions: exceptions
:return: decorated function
"""
@@ -69,7 +114,7 @@ class Sanic:
if attach_to == 'request':
self.request_middleware.append(middleware)
if attach_to == 'response':
self.response_middleware.append(middleware)
self.response_middleware.appendleft(middleware)
return middleware
# Detect which way this was called, @middleware or @middleware('AT')
@@ -79,7 +124,17 @@ class Sanic:
attach_to = args[0]
return register_middleware
def register_blueprint(self, blueprint, **options):
# Static Files
def static(self, uri, file_or_directory, pattern='.+',
use_modified_since=True):
"""
Registers a root to serve files from. The input can either be a file
or a directory. See
"""
static_register(self, uri, file_or_directory, pattern,
use_modified_since)
def blueprint(self, blueprint, **options):
"""
Registers a blueprint on the application.
:param blueprint: Blueprint object
@@ -96,20 +151,34 @@ class Sanic:
self._blueprint_order.append(blueprint)
blueprint.register(self, options)
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")
return self.blueprint(*args, **kwargs)
# -------------------------------------------------------------------- #
# Request Handling
# -------------------------------------------------------------------- #
def converted_response_type(self, response):
pass
async def handle_request(self, request, response_callback):
"""
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
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
:param response_callback: Response function to be called with the
response as the only argument
:return: Nothing
"""
try:
# Middleware process_request
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
response = False
# The if improves speed. I don't know why
if self.request_middleware:
@@ -122,36 +191,52 @@ class Sanic:
# No middleware results
if not response:
# -------------------------------------------- #
# Execute Handler
# -------------------------------------------- #
# Fetch handler from router
handler, args, kwargs = self.router.get(request)
if handler is None:
raise ServerError("'None' was returned while requesting a handler from the router")
raise ServerError(
("'None' was returned while requesting a "
"handler from the router"))
# Run response handler
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
# Middleware process_response
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
except Exception as e:
# -------------------------------------------- #
# Response Generation Failed
# -------------------------------------------- #
try:
response = self.error_handler.response(request, e)
if isawaitable(response):
response = await response
except Exception as e:
if self.debug:
response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc()))
response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format(
e, format_exc()))
else:
response = HTTPResponse("An error occured while handling an error")
response = HTTPResponse(
"An error occured while handling an error")
response_callback(response)
@@ -159,19 +244,71 @@ class Sanic:
# Execution
# -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, before_stop=None):
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, protocol=HttpProtocol, backlog=100,
stop_event=None):
"""
Runs the HTTP Server and listens until keyboard interrupt or term signal.
On termination, drains connections before closing.
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 after_start: Function to be executed after the server starts listening
:param before_stop: Function to be executed when a stop signal is received before it is respected
: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 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
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
self.error_handler.debug = True
self.debug = debug
self.loop = loop
server_settings = {
'protocol': protocol,
'host': host,
'port': port,
'sock': sock,
'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
}
# -------------------------------------------- #
# Register start/stop events
# -------------------------------------------- #
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
if debug:
log.setLevel(logging.DEBUG)
@@ -181,21 +318,63 @@ class Sanic:
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
try:
serve(
host=host,
port=port,
debug=debug,
after_start=after_start,
before_stop=before_stop,
request_handler=self.handle_request,
request_timeout=self.config.REQUEST_TIMEOUT,
request_max_size=self.config.REQUEST_MAX_SIZE,
)
except:
pass
if workers == 1:
serve(**server_settings)
else:
log.info('Spinning up {} workers...'.format(workers))
self.serve_multiple(server_settings, workers, stop_event)
except Exception as e:
log.exception(
'Experienced exception while trying to serve')
log.info("Server Stopped")
def stop(self):
"""
This kills the Sanic
"""
asyncio.get_event_loop().stop()
if self.processes is not None:
for process in self.processes:
process.terminate()
self.sock.close()
get_event_loop().stop()
def serve_multiple(self, 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:
"""
server_settings['reuse_port'] = True
# Create a stop event to be triggered by a signal
if stop_event is None:
stop_event = Event()
signal(SIGINT, lambda s, f: stop_event.set())
signal(SIGTERM, lambda s, f: stop_event.set())
self.sock = socket()
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.sock.bind((server_settings['host'], server_settings['port']))
set_inheritable(self.sock.fileno(), True)
server_settings['sock'] = self.sock
server_settings['host'] = None
server_settings['port'] = None
self.processes = []
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start()
self.processes.append(process)
for process in self.processes:
process.join()
# the above processes will block this until they're stopped
self.stop()

View File

@@ -1,29 +1,43 @@
import asyncio
from functools import partial
from inspect import isawaitable
from multidict import CIMultiDict
from signal import SIGINT, SIGTERM
import httptools
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:
except ImportError:
async_loop = asyncio
from .log import log
from .request import Request
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
class Signal:
stopped = False
class HttpProtocol(asyncio.Protocol):
__slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection
'parser', 'request', 'url', 'headers', # request params
'request_handler', 'request_timeout', 'request_max_size', # request config
'_total_request_size', '_timeout_handler') # connection management
current_time = None
def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60,
class HttpProtocol(asyncio.Protocol):
__slots__ = (
# event loop, connection
'loop', 'transport', 'connections', 'signal',
# request params
'parser', 'request', 'url', 'headers',
# request config
'request_handler', 'request_timeout', 'request_max_size',
# connection management
'_total_request_size', '_timeout_handler', '_last_communication_time')
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections={}, request_timeout=60,
request_max_size=None):
self.loop = loop
self.transport = None
@@ -34,74 +48,99 @@ class HttpProtocol(asyncio.Protocol):
self.signal = signal
self.connections = connections
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
self.request_max_size = request_max_size
self._total_request_size = 0
self._timeout_handler = None
self._last_request_time = None
self._request_handler_task = None
# -------------------------------------------- #
# -------------------------------------------- #
# Connection
# -------------------------------------------- #
def connection_made(self, transport):
self.connections[self] = True
self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout)
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):
self.bail_out("Request timed out, connection closed")
# -------------------------------------------- #
# 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)
else:
if self._request_handler_task:
self._request_handler_task.cancel()
exception = RequestTimeout('Request Timeout')
self.write_error(exception)
# -------------------------------------------- #
# Parsing
# -------------------------------------------- #
def data_received(self, data):
# Check for the request itself getting too large and exceeding memory limits
# Check for the request itself getting too large and exceeding
# 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:
assert self.request is None
self.headers = []
self.parser = httptools.HttpRequestParser(self)
self.parser = HttpRequestParser(self)
# Parse request chunk or close connection
try:
self.parser.feed_data(data)
except httptools.parser.errors.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')))
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=dict(self.headers),
headers=CIMultiDict(self.headers),
version=self.parser.get_http_version(),
method=self.parser.get_method().decode()
)
def on_body(self, body):
self.request.body = body
if self.request.body:
self.request.body += body
else:
self.request.body = body
def on_message_complete(self):
self.loop.create_task(self.request_handler(self.request, self.write_response))
self._request_handler_task = self.loop.create_task(
self.request_handler(self.request, self.write_response))
# -------------------------------------------- #
# Responding
@@ -109,24 +148,42 @@ class HttpProtocol(asyncio.Protocol):
def write_response(self, response):
try:
keep_alive = self.parser.should_keep_alive() and not self.signal.stopped
self.transport.write(response.output(self.request.version, keep_alive, self.request_timeout))
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))
if not keep_alive:
self.transport.close()
else:
# Record that we received data
self._last_request_time = current_time
self.cleanup()
except Exception as e:
self.bail_out("Writing request failed, connection closed {}".format(e))
self.bail_out(
"Writing response failed, connection closed {}".format(e))
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))
self.transport.close()
except Exception as e:
self.bail_out(
"Writing error failed, connection closed {}".format(e))
def bail_out(self, message):
exception = ServerError(message)
self.write_error(exception)
log.error(message)
self.transport.close()
def cleanup(self):
self.parser = None
self.request = None
self.url = None
self.headers = None
self._request_handler_task = None
self._total_request_size = 0
def close_if_idle(self):
@@ -140,39 +197,100 @@ class HttpProtocol(asyncio.Protocol):
return False
def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60,
request_max_size=None):
# Create Event Loop
loop = async_loop.new_event_loop()
asyncio.set_event_loop(loop)
# I don't think we take advantage of this
# And it slows everything waaayyy down
# loop.set_debug(debug)
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:
"""
global current_time
current_time = time()
loop.call_later(1, partial(update_current_time, loop))
connections = {}
def trigger_events(events, loop):
"""
:param events: one or more sync or async functions to execute
:param loop: event loop
"""
if events:
if not isinstance(events, list):
events = [events]
for event in events:
result = event(loop)
if isawaitable(result):
loop.run_until_complete(result)
def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, debug=False,
request_timeout=60, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100):
"""
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 single argument `loop`
:param after_start: Function to be executed after the server starts
listening. Takes single argument `loop`
:param before_stop: Function to be executed when a stop signal is
received before it is respected. Takes single argumenet `loop`
:param after_stop: Function to be executed when a stop signal is
received after it is respected. Takes single argumenet `loop`
:param debug: Enables debug output (slows server)
:param request_timeout: time in seconds
: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()
asyncio.set_event_loop(loop)
if debug:
loop.set_debug(debug)
trigger_events(before_start, loop)
connections = set()
signal = Signal()
server_coroutine = loop.create_server(lambda: HttpProtocol(
server = partial(
protocol,
loop=loop,
connections=connections,
signal=signal,
request_handler=request_handler,
error_handler=error_handler,
request_timeout=request_timeout,
request_max_size=request_max_size,
), host, port)
)
server_coroutine = loop.create_server(
server,
host,
port,
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))
try:
http_server = loop.run_until_complete(server_coroutine)
except OSError as e:
log.error("Unable to start server: {}".format(e))
return
except:
except Exception:
log.exception("Unable to start server")
return
# Run the on_start function if provided
if after_start:
result = after_start(loop)
if isawaitable(result):
loop.run_until_complete(result)
trigger_events(after_start, loop)
# Register signals for graceful termination
for _signal in (SIGINT, SIGTERM):
@@ -184,10 +302,7 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug
log.info("Stop requested, draining connections...")
# Run the on_stop function if provided
if before_stop:
result = before_stop(loop)
if isawaitable(result):
loop.run_until_complete(result)
trigger_events(before_stop, loop)
# Wait for event loop to finish and all connections to drain
http_server.close()
@@ -195,11 +310,12 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug
# Complete all tasks on the loop
signal.stopped = True
for connection in connections.keys():
for connection in connections:
connection.close_if_idle()
while connections:
loop.run_until_complete(asyncio.sleep(0.1))
trigger_events(after_stop, loop)
loop.close()
log.info("Server Stopped")

65
sanic/static.py Normal file
View File

@@ -0,0 +1,65 @@
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
def register(app, uri, file_or_directory, pattern, use_modified_since):
# TODO: Though sanic is not a file server, I feel like we should atleast
# make a good effort here. Modified-since is nice, but we could
# also look into etags, expires, and caching
"""
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
"""
# If we're not trying to match a file directly,
# serve from the folder
if not path.isfile(file_or_directory):
uri += '<file_uri:' + pattern + '>'
async def _handler(request, file_uri=None):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if file_uri and '../' in file_uri:
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 = 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
# and it has not been modified since
if use_modified_since:
stats = await stat(file_path)
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT',
gmtime(stats.st_mtime))
if request.headers.get('If-Modified-Since') == modified_since:
return HTTPResponse(status=304)
headers['Last-Modified'] = modified_since
return await file(file_path, headers=headers)
except:
raise FileNotFound('File not found',
path=file_or_directory,
relative_url=file_uri)
app.route(uri, methods=['GET'])(_handler)

View File

@@ -5,26 +5,28 @@ HOST = '127.0.0.1'
PORT = 42101
async def local_request(method, uri, *args, **kwargs):
async def local_request(method, uri, cookies=None, *args, **kwargs):
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
log.info(url)
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(cookies=cookies) as session:
async with getattr(session, method)(url, *args, **kwargs) as response:
response.text = await response.text()
response.body = await response.read()
return response
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
loop=None, 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(loop):
async def _collect_response(sanic, loop):
try:
response = await local_request(method, uri, *request_args,
**request_kwargs)
@@ -33,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
exceptions.append(e)
app.stop()
app.run(host=HOST, port=42101, after_start=_collect_response)
app.run(host=HOST, debug=debug, port=PORT,
after_start=_collect_response, loop=loop, **server_kwargs)
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
@@ -44,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))

63
sanic/views.py Normal file
View File

@@ -0,0 +1,63 @@
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
to every HTTP method you want to support.
For example:
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 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:
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.as_view(), '/')
2) app.route('/')(DummyView.as_view())
To add any decorator you could set it into decorators variable
"""
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

View File

@@ -1,11 +1,23 @@
"""
Sanic
"""
import codecs
import os
import re
from setuptools import setup
with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
__file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp:
try:
version = re.findall(r"^__version__ = '([^']+)'\r?$",
fp.read(), re.M)[0]
except IndexError:
raise RuntimeError('Unable to determine version.')
setup(
name='Sanic',
version="0.1.2",
version=version,
url='http://github.com/channelcat/sanic/',
license='MIT',
author='Channel Cat',
@@ -17,9 +29,11 @@ setup(
'uvloop>=0.5.3',
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
'multidict>=2.0',
],
classifiers=[
'Development Status :: 1 - Alpha',
'Development Status :: 2 - Pre-Alpha',
'Environment :: Web Environment',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.5',

View File

@@ -15,4 +15,4 @@ async def handle(request):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', handle)
web.run_app(app, port=sys.argv[1])
web.run_app(app, port=sys.argv[1], access_log=None)

View File

@@ -0,0 +1,11 @@
# Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker falc:app
import falcon
import ujson as json
class TestResource:
def on_get(self, req, resp):
resp.body = json.dumps({"test": True})
app = falcon.API()
app.add_route('/', TestResource())

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

@@ -15,5 +15,5 @@ app = Sanic("test")
async def test(request):
return json({"test": True})
app.run(host="0.0.0.0", port=sys.argv[1])
if __name__ == '__main__':
app.run(host="0.0.0.0", port=sys.argv[1])

View File

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

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

@@ -1,3 +1,5 @@
import inspect
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json, text
@@ -17,7 +19,7 @@ def test_bp():
def handler(request):
return text('Hello')
app.register_blueprint(bp)
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
assert response.text == 'Hello'
@@ -30,7 +32,7 @@ def test_bp_with_url_prefix():
def handler(request):
return text('Hello')
app.register_blueprint(bp)
app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'Hello'
@@ -49,14 +51,79 @@ def test_several_bp_with_url_prefix():
def handler2(request):
return text('Hello2')
app.register_blueprint(bp)
app.register_blueprint(bp2)
app.blueprint(bp)
app.blueprint(bp2)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'Hello'
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')
@@ -70,7 +137,7 @@ def test_bp_middleware():
async def handler(request):
return text('FAIL')
app.register_blueprint(blueprint)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app)
@@ -97,7 +164,7 @@ def test_bp_exception_handler():
def handler_exception(request, exception):
return text("OK")
app.register_blueprint(blueprint)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/1')
assert response.status == 400
@@ -108,4 +175,56 @@ def test_bp_exception_handler():
assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/3')
assert response.status == 200
assert response.status == 200
def test_bp_listeners():
app = Sanic('test_middleware')
blueprint = Blueprint('test_middleware')
order = []
@blueprint.listener('before_server_start')
def handler_1(sanic, loop):
order.append(1)
@blueprint.listener('after_server_start')
def handler_2(sanic, loop):
order.append(2)
@blueprint.listener('after_server_start')
def handler_3(sanic, loop):
order.append(3)
@blueprint.listener('before_server_stop')
def handler_4(sanic, loop):
order.append(5)
@blueprint.listener('before_server_stop')
def handler_5(sanic, loop):
order.append(4)
@blueprint.listener('after_server_stop')
def handler_6(sanic, loop):
order.append(6)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/')
assert order == [1,2,3,4,5,6]
def test_bp_static():
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
blueprint = Blueprint('test_static')
blueprint.static('/testing.file', current_file)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents

57
tests/test_cookies.py Normal file
View File

@@ -0,0 +1,57 @@
from datetime import datetime, timedelta
from http.cookies import SimpleCookie
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
def test_cookies():
app = Sanic('test_text')
@app.route('/')
def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test']))
response.cookies['right_back'] = 'at you'
return response
request, response = sanic_endpoint_test(app, cookies={"test": "working!"})
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you'
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')
@app.route('/')
def handler(request):
response = text("OK")
response.cookies['test'] = 'at you'
response.cookies['test']['httponly'] = True
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
return response
request, response = sanic_endpoint_test(app)
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
assert response_cookies['test'].value == 'at you'
assert response_cookies['test']['httponly'] == True

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,86 @@
import pytest
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
assert response.body == b'An error occurred while generating the response'
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'

33
tests/test_logging.py Normal file
View File

@@ -0,0 +1,33 @@
import asyncio
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', logger=True)
@app.route('/')
def handler(request):
log.info('hello world')
return text('hello')
request, response = sanic_endpoint_test(app)
log_text = log_stream.getvalue().strip().split('\n')[-3]
assert log_text == "module: test_logging; function: handler(); message: hello world"
if __name__ =="__main__":
test_log()

View File

@@ -86,3 +86,43 @@ def test_middleware_override_response():
assert response.status == 200
assert response.text == 'OK'
def test_middleware_order():
app = Sanic('test_middleware_order')
order = []
@app.middleware('request')
async def request1(request):
order.append(1)
@app.middleware('request')
async def request2(request):
order.append(2)
@app.middleware('request')
async def request3(request):
order.append(3)
@app.middleware('response')
async def response1(request, response):
order.append(6)
@app.middleware('response')
async def response2(request, response):
order.append(5)
@app.middleware('response')
async def response3(request, response):
order.append(4)
@app.route('/')
async def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app)
assert response.status == 200
assert order == [1,2,3,4,5,6]

View File

@@ -0,0 +1,81 @@
from multiprocessing import Array, Event, Process
from time import sleep, time
from ujson import loads as json_loads
import pytest
from sanic import Sanic
from sanic.response import json
from sanic.utils import local_request, HOST, PORT
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
# TODO: Figure out why this freezes on pytest but not when
# executed via interpreter
@pytest.mark.skip(
reason="Freezes with pytest not on interpreter")
def test_multiprocessing():
app = Sanic('test_json')
response = Array('c', 50)
@app.route('/')
async def handler(request):
return json({"test": True})
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
@pytest.mark.skip(
reason="Freezes with pytest not on interpreter")
def test_drain_connections():
app = Sanic('test_json')
@app.route('/')
async def handler(request):
return json({"test": True})
stop_event = Event()
async def after_start(*args, **kwargs):
http_response = await local_request('get', '/')
stop_event.set()
start = time()
app.serve_multiple({
'host': HOST,
'port': PORT,
'after_start': after_start,
'request_handler': app.handle_request,
}, workers=2, stop_event=stop_event)
end = time()
assert end - start < 0.05

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

@@ -0,0 +1,24 @@
from sanic import Sanic
from sanic.response import json
from sanic.utils import sanic_endpoint_test
from ujson import loads
def test_storage():
app = Sanic('test_text')
@app.middleware('request')
def store(request):
request['user'] = 'sanic'
request['sidekick'] = 'tails'
del request['sidekick']
@app.route('/')
def handler(request):
return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') })
request, response = sanic_endpoint_test(app)
response_json = loads(response.text)
assert response_json['user'] == 'sanic'
assert response_json.get('sidekick') is None

View File

@@ -0,0 +1,40 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.exceptions import RequestTimeout
from sanic.utils import sanic_endpoint_test
from sanic.config import Config
Config.REQUEST_TIMEOUT = 1
request_timeout_app = Sanic('test_request_timeout')
request_timeout_default_app = Sanic('test_request_timeout_default')
@request_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(2)
return text('OK')
@request_timeout_app.exception(RequestTimeout)
def handler_exception(request, exception):
return text('Request Timeout from error_handler.', 408)
def test_server_error_request_timeout():
request, response = sanic_endpoint_test(request_timeout_app, uri='/1')
assert response.status == 408
assert response.text == 'Request Timeout from error_handler.'
@request_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(2)
return text('OK')
def test_default_server_error_request_timeout():
request, response = sanic_endpoint_test(
request_timeout_default_app, uri='/1')
assert response.status == 408
assert response.text == 'Error: Request Timeout'

View File

@@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
from sanic.exceptions import ServerError
# ------------------------------------------------------------ #
@@ -32,6 +33,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 +91,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')
@@ -56,12 +111,30 @@ def test_query_string():
async def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")])
request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")])
assert request.args.get('test1') == '1'
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
# ------------------------------------------------------------ #
@@ -80,3 +153,38 @@ def test_post_json():
assert request.json.get('test') == 'OK'
assert response.text == 'OK'
def test_post_form_urlencoded():
app = Sanic('test_post_form_urlencoded')
@app.route('/')
async def handler(request):
return text('OK')
payload = 'test=OK'
headers = {'content-type': 'application/x-www-form-urlencoded'}
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
assert request.form.get('test') == 'OK'
def test_post_form_multipart_form_data():
app = Sanic('test_post_form_multipart_form_data')
@app.route('/')
async def handler(request):
return text('OK')
payload = '------sanic\r\n' \
'Content-Disposition: form-data; name="test"\r\n' \
'\r\n' \
'OK\r\n' \
'------sanic--\r\n'
headers = {'content-type': 'multipart/form-data; boundary=----sanic'}
request, response = sanic_endpoint_test(app, data=payload, headers=headers)
assert request.form.get('test') == '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

@@ -1,6 +1,8 @@
from json import loads as json_loads, dumps as json_dumps
import pytest
from sanic import Sanic
from sanic.response import json, text
from sanic.response import text
from sanic.router import RouteExists, RouteDoesNotExist
from sanic.utils import sanic_endpoint_test
@@ -8,6 +10,24 @@ from sanic.utils import sanic_endpoint_test
# UTF-8
# ------------------------------------------------------------ #
def test_static_routes():
app = Sanic('test_dynamic_route')
@app.route('/test')
async def handler1(request):
return text('OK1')
@app.route('/pizazz')
async def handler2(request):
return text('OK2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, uri='/pizazz')
assert response.text == 'OK2'
def test_dynamic_route():
app = Sanic('test_dynamic_route')
@@ -39,6 +59,11 @@ def test_dynamic_route_string():
assert response.text == 'OK'
assert results[0] == 'test123'
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
assert response.text == 'OK'
assert results[1] == 'favicon.ico'
def test_dynamic_route_int():
app = Sanic('test_dynamic_route_int')
@@ -59,7 +84,7 @@ def test_dynamic_route_int():
def test_dynamic_route_number():
app = Sanic('test_dynamic_route_int')
app = Sanic('test_dynamic_route_number')
results = []
@@ -80,7 +105,7 @@ def test_dynamic_route_number():
def test_dynamic_route_regex():
app = Sanic('test_dynamic_route_int')
app = Sanic('test_dynamic_route_regex')
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
async def handler(request, folder_id):
@@ -97,3 +122,344 @@ def test_dynamic_route_regex():
request, response = sanic_endpoint_test(app, uri='/folder/')
assert response.status == 200
def test_dynamic_route_unhashable():
app = Sanic('test_dynamic_route_unhashable')
@app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
async def handler(request, unhashable):
return text('OK')
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
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
assert response.status == 404
def test_route_duplicate():
app = Sanic('test_route_duplicate')
with pytest.raises(RouteExists):
@app.route('/test')
async def handler1(request):
pass
@app.route('/test')
async def handler2(request):
pass
with pytest.raises(RouteExists):
@app.route('/test/<dynamic>/')
async def handler1(request, dynamic):
pass
@app.route('/test/<dynamic>/')
async def handler2(request, dynamic):
pass
def test_method_not_allowed():
app = Sanic('test_method_not_allowed')
@app.route('/test', methods=['GET'])
async def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, method='post', uri='/test')
assert response.status == 405
def test_static_add_route():
app = Sanic('test_static_add_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.text == 'OK1'
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.text == 'OK2'
def test_dynamic_add_route():
app = Sanic('test_dynamic_add_route')
results = []
async def handler(request, name):
results.append(name)
return text('OK')
app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.text == 'OK'
assert results[0] == 'test123'
def test_dynamic_add_route_string():
app = Sanic('test_dynamic_add_route_string')
results = []
async def handler(request, name):
results.append(name)
return text('OK')
app.add_route(handler, '/folder/<name:string>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.text == 'OK'
assert results[0] == 'test123'
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
assert response.text == 'OK'
assert results[1] == 'favicon.ico'
def test_dynamic_add_route_int():
app = Sanic('test_dynamic_add_route_int')
results = []
async def handler(request, folder_id):
results.append(folder_id)
return text('OK')
app.add_route(handler, '/folder/<folder_id:int>')
request, response = sanic_endpoint_test(app, uri='/folder/12345')
assert response.text == 'OK'
assert type(results[0]) is int
request, response = sanic_endpoint_test(app, uri='/folder/asdf')
assert response.status == 404
def test_dynamic_add_route_number():
app = Sanic('test_dynamic_add_route_number')
results = []
async def handler(request, weight):
results.append(weight)
return text('OK')
app.add_route(handler, '/weight/<weight:number>')
request, response = sanic_endpoint_test(app, uri='/weight/12345')
assert response.text == 'OK'
assert type(results[0]) is float
request, response = sanic_endpoint_test(app, uri='/weight/1234.56')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
assert response.status == 404
def test_dynamic_add_route_regex():
app = Sanic('test_dynamic_route_int')
async def handler(request, folder_id):
return text('OK')
app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>')
request, response = sanic_endpoint_test(app, uri='/folder/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test1')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/')
assert response.status == 200
def test_dynamic_add_route_unhashable():
app = Sanic('test_dynamic_add_route_unhashable')
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
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
assert response.status == 404
def test_add_route_duplicate():
app = Sanic('test_add_route_duplicate')
with pytest.raises(RouteExists):
async def handler1(request):
pass
async def handler2(request):
pass
app.add_route(handler1, '/test')
app.add_route(handler2, '/test')
with pytest.raises(RouteExists):
async def handler1(request, dynamic):
pass
async def handler2(request, dynamic):
pass
app.add_route(handler1, '/test/<dynamic>/')
app.add_route(handler2, '/test/<dynamic>/')
def test_add_route_method_not_allowed():
app = Sanic('test_add_route_method_not_allowed')
async def handler(request):
return text('OK')
app.add_route(handler, '/test', methods=['GET'])
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
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

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

62
tests/test_static.py Normal file
View File

@@ -0,0 +1,62 @@
import inspect
import os
import pytest
from sanic import Sanic
from sanic.utils import sanic_endpoint_test
@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', static_file_path)
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == static_file_content
def test_static_directory(
static_file_directory, static_file_path, static_file_content):
app = Sanic('test_static')
app.static('/dir', static_file_directory)
request, response = sanic_endpoint_test(app, uri='/dir/test.file')
assert response.status == 200
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

23
tests/test_vhosts.py Normal file
View File

@@ -0,0 +1,23 @@
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
def test_vhosts():
app = Sanic('test_text')
@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!"

196
tests/test_views.py Normal file
View File

@@ -0,0 +1,196 @@
from sanic import Sanic
from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.utils import sanic_endpoint_test
def test_methods():
app = Sanic('test_methods')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
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")
assert response.text == 'I am post method'
request, response = sanic_endpoint_test(app, method="put")
assert response.text == 'I am put method'
request, response = sanic_endpoint_test(app, method="patch")
assert response.text == 'I am patch method'
request, response = sanic_endpoint_test(app, method="delete")
assert response.text == 'I am delete method'
def test_unexisting_methods():
app = Sanic('test_unexisting_methods')
class DummyView(HTTPMethodView):
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'
request, response = sanic_endpoint_test(app, method="post")
assert response.text == 'Error: Method POST not allowed for URL /'
def test_argument_methods():
app = Sanic('test_argument_methods')
class DummyView(HTTPMethodView):
def get(self, request, my_param_here):
return text('I am get method with %s' % my_param_here)
app.add_route(DummyView.as_view(), '/<my_param_here>')
request, response = sanic_endpoint_test(app, uri='/test123')
assert response.text == 'I am get method with test123'
def test_with_bp():
app = Sanic('test_with_bp')
bp = Blueprint('test_text')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
def test_with_bp_with_url_prefix():
app = Sanic('test_with_bp_with_url_prefix')
bp = Blueprint('test_text', url_prefix='/test1')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'I am get method'
def test_with_middleware():
app = Sanic('test_with_middleware')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
app.add_route(DummyView.as_view(), '/')
results = []
@app.middleware
async def handler(request):
results.append(request)
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
assert type(results[0]) is Request
def test_with_middleware_response():
app = Sanic('test_with_middleware_response')
results = []
@app.middleware('request')
async def process_response(request):
results.append(request)
@app.middleware('response')
async def process_response(request, response):
results.append(request)
results.append(response)
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
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)
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

33
tox.ini
View File

@@ -1,34 +1,25 @@
[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
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