Compare commits

..

101 Commits
0.1.3 ... 0.1.7

Author SHA1 Message Date
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
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
38 changed files with 1454 additions and 225 deletions

View File

@@ -1,13 +1,13 @@
language: python
python:
- '3.5'
- '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
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- python setup.py install
- pip install flake8
- pip install pytest
before_script: flake8 sanic
script: py.test -v tests
deploy:
provider: pypi

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,29 @@
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
# 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/.
[![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/)
On top of being flask-like, sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
Sanic is 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
@@ -36,7 +41,7 @@ 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 +50,9 @@ app.run(host="0.0.0.0", port=8000)
* [Middleware](docs/middleware.md)
* [Exceptions](docs/exceptions.md)
* [Blueprints](docs/blueprints.md)
* [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md)
* [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md)
* [License](LICENSE)

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

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

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.

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

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

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

@@ -8,3 +8,5 @@ tox
gunicorn
bottle
kyoukai
falcon
tornado

View File

@@ -1,4 +1,6 @@
from .sanic import Sanic
from .blueprints import Blueprint
__version__ = '0.1.7'
__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 type(app) is not 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:
"""
"""
@@ -22,7 +25,7 @@ class BlueprintSetup:
if self.url_prefix:
uri = self.url_prefix + uri
self.app.router.add(uri, methods, handler)
self.app.route(uri=uri, methods=methods)(handler)
def add_exception(self, handler, *args, **kwargs):
"""
@@ -30,6 +33,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
@@ -42,9 +54,15 @@ class BlueprintSetup:
class Blueprint:
def __init__(self, name, url_prefix=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)
def record(self, func):
"""
@@ -73,12 +91,20 @@ class Blueprint:
return handler
return decorator
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')
@@ -95,3 +121,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

129
sanic/cookies.py Normal file
View File

@@ -0,0 +1,129 @@
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

@@ -21,6 +21,15 @@ 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 Handler:
handlers = None
@@ -44,8 +53,13 @@ class Handler:
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)
return text(
"Error: {}\nException: {}".format(
exception, format_exc()), status=500)
else:
return text("An error occurred while generating the request", status=500)
return text(
"An error occurred while generating the request", status=500)

View File

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

View File

@@ -1,5 +1,6 @@
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
@@ -26,8 +27,11 @@ class RequestParameters(dict):
class Request:
"""
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 +43,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,13 +53,14 @@ 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:
try:
self.parsed_json = json_loads(self.body)
except:
except Exception:
pass
return self.parsed_json
@@ -63,14 +70,19 @@ class Request:
if self.parsed_form is None:
self.parsed_form = {}
self.parsed_files = {}
content_type, parameters = parse_header(self.headers.get('Content-Type'))
content_type, parameters = parse_header(
self.headers.get('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')))
is_url_encoded = (
content_type == 'application/x-www-form-urlencoded')
if content_type is None or is_url_encoded:
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)
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
except Exception as e:
log.exception(e)
pass
@@ -88,12 +100,25 @@ 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:
if 'Cookie' in self.headers:
cookies = SimpleCookie()
cookies.load(self.headers['Cookie'])
self._cookies = {name: cookie.value
for name, cookie in cookies.items()}
else:
self._cookies = {}
return self._cookies
File = namedtuple('File', ['type', 'body', 'name'])
@@ -125,7 +150,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,7 +162,8 @@ 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)
files[field_name] = File(
type=file_type, name=file_name, body=post_data)
else:
fields[field_name] = post_data.decode('utf-8')

View File

@@ -1,25 +1,81 @@
import ujson
from aiofiles import open as open_async
from .cookies import CookieJar
from mimetypes import guess_type
from os import path
from ujson import dumps as json_dumps
STATUS_CODES = {
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:
@@ -29,6 +85,7 @@ class HTTPResponse:
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
@@ -43,10 +100,22 @@ class HTTPResponse:
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' % (
# 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 +124,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,9 +1,26 @@
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 Router:
@@ -14,115 +31,121 @@ class Router:
def my_route(request, my_parameter):
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>. If no type is provided, a string is expected. A regular
expression can also be passed in as 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 = []
def add(self, uri, methods, handler):
"""
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 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 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
"""
return self._get(request.url, request.method)
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
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
def _get(self, url, method):
"""
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
"""
# 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, args, kwargs
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)
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)
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, [], {}
else:
raise NotFound("Requested URL {} not found".format(request.url))
kwargs = {p.name: p.cast(value)
for value, p
in zip(match.groups(1), route.parameters)}
return route.handler, [], kwargs

View File

@@ -1,5 +1,10 @@
import asyncio
from asyncio import get_event_loop
from collections import deque
from functools import partial
from inspect import isawaitable
from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT
from time import sleep
from traceback import format_exc
from .config import Config
@@ -8,6 +13,7 @@ from .log import log, logging
from .response import HTTPResponse
from .router import Router
from .server import serve
from .static import register as static_register
from .exceptions import ServerError
@@ -17,10 +23,15 @@ class Sanic:
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
# Register alternative method names
self.go_fast = self.run
# -------------------------------------------------------------------- #
# Registration
@@ -35,6 +46,11 @@ 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)
return handler
@@ -44,9 +60,8 @@ class Sanic:
# 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 +84,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 +94,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 +121,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,17 +161,26 @@ 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
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
@@ -143,15 +191,22 @@ class Sanic:
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 +214,66 @@ 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):
"""
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: Function to be executed before the server starts
accepting connections
:param after_start: Function to be executed after the server starts
accepting connections
:param before_stop: Function to be executed when a stop signal is
received before it is respected
:param after_stop: Function to be executed when all requests are
complete
:param 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
:return: Nothing
"""
self.error_handler.debug = True
self.debug = debug
self.loop = loop
server_settings = {
'host': host,
'port': port,
'sock': sock,
'debug': debug,
'request_handler': self.handle_request,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop
}
# -------------------------------------------- #
# 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 type(args) is not list:
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 +283,59 @@ 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:
if workers == 1:
serve(**server_settings)
else:
log.info('Spinning up {} workers...'.format(workers))
self.serve_multiple(server_settings, workers)
except Exception as e:
log.exception(
'Experienced exception while trying to serve: {}'.format(e))
pass
log.info("Server Stopped")
def stop(self):
"""
This kills the Sanic
"""
asyncio.get_event_loop().stop()
get_event_loop().stop()
@staticmethod
def serve_multiple(server_settings, workers, stop_event=None):
"""
Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete.
:param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch
:param stop_event: if provided, is used as a stop signal
:return:
"""
server_settings['reuse_port'] = True
# Create a stop event to be triggered by a signal
if not stop_event:
stop_event = Event()
signal(SIGINT, lambda s, f: stop_event.set())
signal(SIGTERM, lambda s, f: stop_event.set())
processes = []
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.start()
processes.append(process)
# Infinitely wait for the stop event
try:
while not stop_event.is_set():
sleep(0.3)
except:
pass
log.info('Spinning down workers...')
for process in processes:
process.terminate()
for process in processes:
process.join()

View File

@@ -6,7 +6,7 @@ import httptools
try:
import uvloop as async_loop
except:
except ImportError:
async_loop = asyncio
from .log import log
@@ -18,12 +18,18 @@ class Signal:
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
__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')
def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60,
def __init__(self, *, loop, request_handler, signal=Signal(),
connections={}, request_timeout=60,
request_max_size=None):
self.loop = loop
self.transport = None
@@ -46,7 +52,8 @@ class HttpProtocol(asyncio.Protocol):
def connection_made(self, transport):
self.connections[self] = True
self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout)
self._timeout_handler = self.loop.call_later(
self.request_timeout, self.connection_timeout)
self.transport = transport
def connection_lost(self, exc):
@@ -63,10 +70,13 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- #
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))
return self.bail_out(
"Request too large ({}), connection closed".format(
self._total_request_size))
# Create parser if this is the first time we're receiving data
if self.parser is None:
@@ -78,14 +88,16 @@ class HttpProtocol(asyncio.Protocol):
try:
self.parser.feed_data(data)
except httptools.parser.errors.HttpParserError as e:
self.bail_out("Invalid request data, connection closed ({})".format(e))
self.bail_out(
"Invalid request data, connection closed ({})".format(e))
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))
return self.bail_out(
"Request body too large ({}), connection closed".format(value))
self.headers.append((name.decode(), value.decode('utf-8')))
@@ -98,10 +110,14 @@ class HttpProtocol(asyncio.Protocol):
)
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.loop.create_task(
self.request_handler(self.request, self.write_response))
# -------------------------------------------- #
# Responding
@@ -109,14 +125,18 @@ 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:
self.cleanup()
except Exception as e:
self.bail_out("Writing request failed, connection closed {}".format(e))
self.bail_out(
"Writing request failed, connection closed {}".format(e))
def bail_out(self, message):
log.error(message)
@@ -140,14 +160,48 @@ 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()
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, 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):
"""
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 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 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
:return: Nothing
"""
loop = loop or 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)
if debug:
loop.set_debug(debug)
trigger_events(before_start, loop)
connections = {}
signal = Signal()
@@ -158,21 +212,15 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug
request_handler=request_handler,
request_timeout=request_timeout,
request_max_size=request_max_size,
), host, port)
), host, port, reuse_port=reuse_port, sock=sock)
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 +232,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()
@@ -201,5 +246,6 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug
while connections:
loop.run_until_complete(asyncio.sleep(0.1))
trigger_events(after_stop, loop)
loop.close()
log.info("Server Stopped")

59
sanic/static.py Normal file
View File

@@ -0,0 +1,59 @@
from aiofiles.os import stat
from os import path
from re import sub
from time import strftime, gmtime
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 = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \
if file_uri else file_or_directory
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,12 +5,13 @@ 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
@@ -24,7 +25,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
def _collect_request(request):
results.append(request)
async def _collect_response(loop):
async def _collect_response(sanic, loop):
try:
response = await local_request(method, uri, *request_args,
**request_kwargs)

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.3",
version=version,
url='http://github.com/channelcat/sanic/',
license='MIT',
author='Channel Cat',
@@ -17,6 +29,7 @@ setup(
'uvloop>=0.5.3',
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
],
classifiers=[
'Development Status :: 2 - Pre-Alpha',

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

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

@@ -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,8 +51,8 @@ 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'
@@ -70,7 +72,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 +99,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 +110,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

44
tests/test_cookies.py Normal file
View File

@@ -0,0 +1,44 @@
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_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

@@ -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,53 @@
from multiprocessing import Array, Event, Process
from time import sleep
from ujson import loads as json_loads
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
def skip_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

View File

@@ -80,3 +80,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'

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
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')
@@ -97,3 +122,59 @@ 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_dynamic_route')
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

30
tests/test_static.py Normal file
View File

@@ -0,0 +1,30 @@
import inspect
import os
from sanic import Sanic
from sanic.utils import sanic_endpoint_test
def test_static_file():
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
app.static('/testing.file', current_file)
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents
def test_static_directory():
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
app.static('/dir', current_directory)
request, response = sanic_endpoint_test(app, uri='/dir/test_static.py')
assert response.status == 200
assert response.body == current_file_contents