commit
410299f5a1
22
CHANGELOG.md
Normal file
22
CHANGELOG.md
Normal 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
|
14
README.md
14
README.md
|
@ -4,23 +4,26 @@
|
||||||
[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/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/)
|
[![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 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/.
|
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.
|
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
|
## 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 |
|
| 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 |
|
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
|
||||||
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
|
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
|
||||||
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
|
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
|
||||||
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
|
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
|
||||||
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
|
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
|
||||||
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
|
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
|
||||||
|
| Tornado | Python 3.5 | 2,138 | 46.66ms |
|
||||||
|
|
||||||
## Hello World
|
## Hello World
|
||||||
|
|
||||||
|
@ -47,6 +50,9 @@ app.run(host="0.0.0.0", port=8000)
|
||||||
* [Middleware](docs/middleware.md)
|
* [Middleware](docs/middleware.md)
|
||||||
* [Exceptions](docs/exceptions.md)
|
* [Exceptions](docs/exceptions.md)
|
||||||
* [Blueprints](docs/blueprints.md)
|
* [Blueprints](docs/blueprints.md)
|
||||||
|
* [Cookies](docs/cookies.md)
|
||||||
|
* [Static Files](docs/static_files.md)
|
||||||
|
* [Deploying](docs/deploying.md)
|
||||||
* [Contributing](docs/contributing.md)
|
* [Contributing](docs/contributing.md)
|
||||||
* [License](LICENSE)
|
* [License](LICENSE)
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ from sanic import Blueprint
|
||||||
bp = Blueprint('my_blueprint')
|
bp = Blueprint('my_blueprint')
|
||||||
|
|
||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
async def bp_root():
|
async def bp_root(request):
|
||||||
return json({'my': 'blueprint'})
|
return json({'my': 'blueprint'})
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -42,7 +42,7 @@ from sanic import Sanic
|
||||||
from my_blueprint import bp
|
from my_blueprint import bp
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
app.register_blueprint(bp)
|
app.blueprint(bp)
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||||
```
|
```
|
||||||
|
@ -79,4 +79,33 @@ Exceptions can also be applied exclusively to blueprints globally.
|
||||||
@bp.exception(NotFound)
|
@bp.exception(NotFound)
|
||||||
def ignore_404s(request, exception):
|
def ignore_404s(request, exception):
|
||||||
return text("Yep, I totally found the page: {}".format(request.url))
|
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
50
docs/cookies.md
Normal 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
35
docs/deploying.md
Normal 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)
|
||||||
|
```
|
|
@ -8,6 +8,7 @@ The following request variables are accessible as properties:
|
||||||
`request.json` (any) - JSON body
|
`request.json` (any) - JSON body
|
||||||
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
|
`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.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
|
See request.py for more information
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ See request.py for more information
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json, text
|
||||||
|
|
||||||
@app.route("/json")
|
@app.route("/json")
|
||||||
def post_json(request):
|
def post_json(request):
|
||||||
|
@ -40,4 +41,9 @@ def post_json(request):
|
||||||
@app.route("/query_string")
|
@app.route("/query_string")
|
||||||
def query_string(request):
|
def query_string(request):
|
||||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
```
|
```
|
||||||
|
|
|
@ -10,16 +10,16 @@ from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@app.route('/tag/<tag>')
|
@app.route('/tag/<tag>')
|
||||||
async def person_handler(request, tag):
|
async def tag_handler(request, tag):
|
||||||
return text('Tag - {}'.format(tag))
|
return text('Tag - {}'.format(tag))
|
||||||
|
|
||||||
@app.route('/number/<integer_arg:int>')
|
@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))
|
return text('Integer - {}'.format(integer_arg))
|
||||||
|
|
||||||
@app.route('/number/<number_arg:number>')
|
@app.route('/number/<number_arg:number>')
|
||||||
async def person_handler(request, number_arg):
|
async def number_handler(request, number_arg):
|
||||||
return text('Number - {}'.format(number))
|
return text('Number - {}'.format(number_arg))
|
||||||
|
|
||||||
@app.route('/person/<name:[A-z]>')
|
@app.route('/person/<name:[A-z]>')
|
||||||
async def person_handler(request, name):
|
async def person_handler(request, name):
|
||||||
|
|
18
docs/static_files.md
Normal file
18
docs/static_files.md
Normal 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)
|
||||||
|
```
|
33
examples/aiohttp_example.py
Normal file
33
examples/aiohttp_example.py
Normal 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)
|
||||||
|
|
80
examples/sanic_peewee.py
Normal file
80
examples/sanic_peewee.py
Normal 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 there’s no need to connect and re-connect before executing async queries
|
||||||
|
# with manager! It’s 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)
|
||||||
|
|
|
@ -8,3 +8,5 @@ tox
|
||||||
gunicorn
|
gunicorn
|
||||||
bottle
|
bottle
|
||||||
kyoukai
|
kyoukai
|
||||||
|
falcon
|
||||||
|
tornado
|
|
@ -1,4 +1,6 @@
|
||||||
from .sanic import Sanic
|
from .sanic import Sanic
|
||||||
from .blueprints import Blueprint
|
from .blueprints import Blueprint
|
||||||
|
|
||||||
|
__version__ = '0.1.7'
|
||||||
|
|
||||||
__all__ = ['Sanic', 'Blueprint']
|
__all__ = ['Sanic', 'Blueprint']
|
||||||
|
|
36
sanic/__main__.py
Normal file
36
sanic/__main__.py
Normal 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))
|
|
@ -1,3 +1,6 @@
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
class BlueprintSetup:
|
class BlueprintSetup:
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
@ -22,7 +25,7 @@ class BlueprintSetup:
|
||||||
if self.url_prefix:
|
if self.url_prefix:
|
||||||
uri = self.url_prefix + uri
|
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):
|
def add_exception(self, handler, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -30,6 +33,15 @@ class BlueprintSetup:
|
||||||
"""
|
"""
|
||||||
self.app.exception(*args, **kwargs)(handler)
|
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):
|
def add_middleware(self, middleware, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Registers middleware to sanic
|
Registers middleware to sanic
|
||||||
|
@ -42,9 +54,15 @@ class BlueprintSetup:
|
||||||
|
|
||||||
class Blueprint:
|
class Blueprint:
|
||||||
def __init__(self, name, url_prefix=None):
|
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.name = name
|
||||||
self.url_prefix = url_prefix
|
self.url_prefix = url_prefix
|
||||||
self.deferred_functions = []
|
self.deferred_functions = []
|
||||||
|
self.listeners = defaultdict(list)
|
||||||
|
|
||||||
def record(self, func):
|
def record(self, func):
|
||||||
"""
|
"""
|
||||||
|
@ -73,6 +91,14 @@ class Blueprint:
|
||||||
return handler
|
return handler
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
def listener(self, event):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
def decorator(listener):
|
||||||
|
self.listeners[event].append(listener)
|
||||||
|
return listener
|
||||||
|
return decorator
|
||||||
|
|
||||||
def middleware(self, *args, **kwargs):
|
def middleware(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
@ -95,3 +121,9 @@ class Blueprint:
|
||||||
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
|
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
|
||||||
return handler
|
return handler
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
self.record(
|
||||||
|
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))
|
||||||
|
|
|
@ -22,3 +22,4 @@ class Config:
|
||||||
"""
|
"""
|
||||||
REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
||||||
REQUEST_TIMEOUT = 60 # 60 seconds
|
REQUEST_TIMEOUT = 60 # 60 seconds
|
||||||
|
ROUTER_CACHE_SIZE = 1024
|
||||||
|
|
129
sanic/cookies.py
Normal file
129
sanic/cookies.py
Normal 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()
|
|
@ -21,6 +21,15 @@ class ServerError(SanicException):
|
||||||
status_code = 500
|
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:
|
class Handler:
|
||||||
handlers = None
|
handlers = None
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from cgi import parse_header
|
from cgi import parse_header
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
from httptools import parse_url
|
from httptools import parse_url
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
from ujson import loads as json_loads
|
from ujson import loads as json_loads
|
||||||
|
@ -30,7 +31,7 @@ class Request:
|
||||||
Properties of an HTTP request such as URL, headers, etc.
|
Properties of an HTTP request such as URL, headers, etc.
|
||||||
"""
|
"""
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'url', 'headers', 'version', 'method',
|
'url', 'headers', 'version', 'method', '_cookies',
|
||||||
'query_string', 'body',
|
'query_string', 'body',
|
||||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||||
)
|
)
|
||||||
|
@ -52,6 +53,7 @@ class Request:
|
||||||
self.parsed_form = None
|
self.parsed_form = None
|
||||||
self.parsed_files = None
|
self.parsed_files = None
|
||||||
self.parsed_args = None
|
self.parsed_args = None
|
||||||
|
self._cookies = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
|
@ -105,6 +107,18 @@ class Request:
|
||||||
|
|
||||||
return 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'])
|
File = namedtuple('File', ['type', 'body', 'name'])
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,78 @@
|
||||||
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',
|
200: b'OK',
|
||||||
400: b'Bad Request',
|
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',
|
401: b'Unauthorized',
|
||||||
402: b'Payment Required',
|
402: b'Payment Required',
|
||||||
403: b'Forbidden',
|
403: b'Forbidden',
|
||||||
404: b'Not Found',
|
404: b'Not Found',
|
||||||
405: b'Method Not Allowed',
|
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',
|
500: b'Internal Server Error',
|
||||||
501: b'Not Implemented',
|
501: b'Not Implemented',
|
||||||
502: b'Bad Gateway',
|
502: b'Bad Gateway',
|
||||||
503: b'Service Unavailable',
|
503: b'Service Unavailable',
|
||||||
504: b'Gateway Timeout',
|
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:
|
class HTTPResponse:
|
||||||
__slots__ = ('body', 'status', 'content_type', 'headers')
|
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||||
|
|
||||||
def __init__(self, body=None, status=200, headers=None,
|
def __init__(self, body=None, status=200, headers=None,
|
||||||
content_type='text/plain', body_bytes=b''):
|
content_type='text/plain', body_bytes=b''):
|
||||||
|
@ -30,6 +85,7 @@ class HTTPResponse:
|
||||||
|
|
||||||
self.status = status
|
self.status = status
|
||||||
self.headers = headers or {}
|
self.headers = headers or {}
|
||||||
|
self._cookies = None
|
||||||
|
|
||||||
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||||
# This is all returned in a kind-of funky way
|
# This is all returned in a kind-of funky way
|
||||||
|
@ -44,6 +100,13 @@ class HTTPResponse:
|
||||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
||||||
for name, value in self.headers.items()
|
for name, value in self.headers.items()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Try 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'
|
return (b'HTTP/%b %d %b\r\n'
|
||||||
b'Content-Type: %b\r\n'
|
b'Content-Type: %b\r\n'
|
||||||
b'Content-Length: %d\r\n'
|
b'Content-Length: %d\r\n'
|
||||||
|
@ -52,7 +115,7 @@ class HTTPResponse:
|
||||||
b'%b') % (
|
b'%b') % (
|
||||||
version.encode(),
|
version.encode(),
|
||||||
self.status,
|
self.status,
|
||||||
STATUS_CODES.get(self.status, b'FAIL'),
|
status,
|
||||||
self.content_type.encode(),
|
self.content_type.encode(),
|
||||||
len(self.body),
|
len(self.body),
|
||||||
b'keep-alive' if keep_alive else b'close',
|
b'keep-alive' if keep_alive else b'close',
|
||||||
|
@ -61,10 +124,16 @@ class HTTPResponse:
|
||||||
self.body
|
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):
|
def json(body, status=200, headers=None):
|
||||||
return HTTPResponse(ujson.dumps(body), headers=headers, status=status,
|
return HTTPResponse(json_dumps(body), headers=headers, status=status,
|
||||||
content_type="application/json; charset=utf-8")
|
content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
def text(body, status=200, headers=None):
|
def text(body, status=200, headers=None):
|
||||||
|
@ -75,3 +144,17 @@ def text(body, status=200, headers=None):
|
||||||
def html(body, status=200, headers=None):
|
def html(body, status=200, headers=None):
|
||||||
return HTTPResponse(body, status=status, headers=headers,
|
return HTTPResponse(body, status=status, headers=headers,
|
||||||
content_type="text/html; charset=utf-8")
|
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)
|
||||||
|
|
177
sanic/router.py
177
sanic/router.py
|
@ -1,9 +1,26 @@
|
||||||
import re
|
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
|
from .exceptions import NotFound, InvalidUsage
|
||||||
|
|
||||||
Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters'])
|
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
|
||||||
Parameter = namedtuple("Parameter", ['name', 'cast'])
|
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:
|
class Router:
|
||||||
|
@ -18,22 +35,16 @@ class Router:
|
||||||
function provided Parameters can also have a type by appending :type to
|
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
|
the <parameter>. If no type is provided, a string is expected. A regular
|
||||||
expression can also be passed in as the type
|
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
|
|
||||||
"""
|
"""
|
||||||
routes = None
|
routes_static = None
|
||||||
regex_types = {
|
routes_dynamic = None
|
||||||
"string": (None, "[^/]+"),
|
routes_always_check = None
|
||||||
"int": (int, "\d+"),
|
|
||||||
"number": (float, "[0-9\\.]+"),
|
|
||||||
"alpha": (None, "[A-Za-z]+"),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
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):
|
def add(self, uri, methods, handler):
|
||||||
"""
|
"""
|
||||||
|
@ -45,42 +56,52 @@ class Router:
|
||||||
When executed, it should provide a response object.
|
When executed, it should provide a response object.
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
if uri in self.routes_all:
|
||||||
|
raise RouteExists("Route already registered: {}".format(uri))
|
||||||
|
|
||||||
# Dict for faster lookups of if method allowed
|
# Dict for faster lookups of if method allowed
|
||||||
methods_dict = None
|
|
||||||
if methods:
|
if methods:
|
||||||
methods_dict = {method: True for method in methods}
|
methods = frozenset(methods)
|
||||||
|
|
||||||
parameters = []
|
parameters = []
|
||||||
|
properties = {"unhashable": None}
|
||||||
|
|
||||||
def add_parameter(match):
|
def add_parameter(match):
|
||||||
# We could receive NAME or NAME:PATTERN
|
# We could receive NAME or NAME:PATTERN
|
||||||
parts = match.group(1).split(':')
|
name = match.group(1)
|
||||||
if len(parts) == 2:
|
pattern = 'string'
|
||||||
parameter_name, parameter_pattern = parts
|
if ':' in name:
|
||||||
else:
|
name, pattern = name.split(':', 1)
|
||||||
parameter_name = parts[0]
|
|
||||||
parameter_pattern = 'string'
|
|
||||||
|
|
||||||
|
default = (str, pattern)
|
||||||
# Pull from pre-configured types
|
# Pull from pre-configured types
|
||||||
parameter_regex = self.regex_types.get(parameter_pattern)
|
_type, pattern = REGEX_TYPES.get(pattern, default)
|
||||||
if parameter_regex:
|
parameter = Parameter(name=name, cast=_type)
|
||||||
parameter_type, parameter_pattern = parameter_regex
|
|
||||||
else:
|
|
||||||
parameter_type = None
|
|
||||||
|
|
||||||
parameter = Parameter(name=parameter_name, cast=parameter_type)
|
|
||||||
parameters.append(parameter)
|
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)
|
return '({})'.format(pattern)
|
||||||
pattern = re.compile("^{}$".format(pattern_string))
|
|
||||||
|
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
|
||||||
|
pattern = re.compile(r'^{}$'.format(pattern_string))
|
||||||
|
|
||||||
route = Route(
|
route = Route(
|
||||||
handler=handler, methods=methods_dict, pattern=pattern,
|
handler=handler, methods=methods, pattern=pattern,
|
||||||
parameters=parameters)
|
parameters=parameters)
|
||||||
self.routes.append(route)
|
|
||||||
|
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):
|
def get(self, request):
|
||||||
"""
|
"""
|
||||||
|
@ -89,58 +110,42 @@ class Router:
|
||||||
:param request: Request object
|
:param request: Request object
|
||||||
:return: handler, arguments, keyword arguments
|
:return: handler, arguments, keyword arguments
|
||||||
"""
|
"""
|
||||||
|
return self._get(request.url, request.method)
|
||||||
|
|
||||||
route = None
|
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
|
||||||
args = []
|
def _get(self, url, method):
|
||||||
kwargs = {}
|
"""
|
||||||
for _route in self.routes:
|
Gets a request handler based on the URL of the request, or raises an
|
||||||
match = _route.pattern.match(request.url)
|
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:
|
||||||
|
match = route.pattern.match(url)
|
||||||
|
else:
|
||||||
|
# Move on to testing all regex routes
|
||||||
|
for route in self.routes_dynamic[url_hash(url)]:
|
||||||
|
match = route.pattern.match(url)
|
||||||
if match:
|
if match:
|
||||||
for index, parameter in enumerate(_route.parameters, start=1):
|
|
||||||
value = match.group(index)
|
|
||||||
if parameter.cast:
|
|
||||||
kwargs[parameter.name] = parameter.cast(value)
|
|
||||||
else:
|
|
||||||
kwargs[parameter.name] = value
|
|
||||||
route = _route
|
|
||||||
break
|
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
|
|
||||||
else:
|
else:
|
||||||
raise NotFound("Requested URL {} not found".format(request.url))
|
# Lastly, check against all regex routes that cannot be hashed
|
||||||
|
for route in self.routes_always_check:
|
||||||
|
match = route.pattern.match(url)
|
||||||
class SimpleRouter:
|
if match:
|
||||||
"""
|
break
|
||||||
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 = None
|
|
||||||
if methods:
|
|
||||||
methods_dict = {method: True for method in methods}
|
|
||||||
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:
|
else:
|
||||||
raise NotFound("Requested URL {} not found".format(request.url))
|
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
|
||||||
|
|
176
sanic/sanic.py
176
sanic/sanic.py
|
@ -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 inspect import isawaitable
|
||||||
|
from multiprocessing import Process, Event
|
||||||
|
from signal import signal, SIGTERM, SIGINT
|
||||||
|
from time import sleep
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
@ -8,6 +13,7 @@ from .log import log, logging
|
||||||
from .response import HTTPResponse
|
from .response import HTTPResponse
|
||||||
from .router import Router
|
from .router import Router
|
||||||
from .server import serve
|
from .server import serve
|
||||||
|
from .static import register as static_register
|
||||||
from .exceptions import ServerError
|
from .exceptions import ServerError
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,10 +23,15 @@ class Sanic:
|
||||||
self.router = router or Router()
|
self.router = router or Router()
|
||||||
self.error_handler = error_handler or Handler(self)
|
self.error_handler = error_handler or Handler(self)
|
||||||
self.config = Config()
|
self.config = Config()
|
||||||
self.request_middleware = []
|
self.request_middleware = deque()
|
||||||
self.response_middleware = []
|
self.response_middleware = deque()
|
||||||
self.blueprints = {}
|
self.blueprints = {}
|
||||||
self._blueprint_order = []
|
self._blueprint_order = []
|
||||||
|
self.loop = None
|
||||||
|
self.debug = None
|
||||||
|
|
||||||
|
# Register alternative method names
|
||||||
|
self.go_fast = self.run
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Registration
|
# Registration
|
||||||
|
@ -35,6 +46,11 @@ class Sanic:
|
||||||
:return: decorated function
|
: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):
|
def response(handler):
|
||||||
self.router.add(uri=uri, methods=methods, handler=handler)
|
self.router.add(uri=uri, methods=methods, handler=handler)
|
||||||
return handler
|
return handler
|
||||||
|
@ -44,9 +60,8 @@ class Sanic:
|
||||||
# Decorator
|
# Decorator
|
||||||
def exception(self, *exceptions):
|
def exception(self, *exceptions):
|
||||||
"""
|
"""
|
||||||
Decorates a function to be registered as a route
|
Decorates a function to be registered as a handler for exceptions
|
||||||
:param uri: path of the URL
|
:param *exceptions: exceptions
|
||||||
:param methods: list or tuple of methods allowed
|
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -69,7 +84,7 @@ class Sanic:
|
||||||
if attach_to == 'request':
|
if attach_to == 'request':
|
||||||
self.request_middleware.append(middleware)
|
self.request_middleware.append(middleware)
|
||||||
if attach_to == 'response':
|
if attach_to == 'response':
|
||||||
self.response_middleware.append(middleware)
|
self.response_middleware.appendleft(middleware)
|
||||||
return middleware
|
return middleware
|
||||||
|
|
||||||
# Detect which way this was called, @middleware or @middleware('AT')
|
# Detect which way this was called, @middleware or @middleware('AT')
|
||||||
|
@ -79,7 +94,17 @@ class Sanic:
|
||||||
attach_to = args[0]
|
attach_to = args[0]
|
||||||
return register_middleware
|
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.
|
Registers a blueprint on the application.
|
||||||
:param blueprint: Blueprint object
|
:param blueprint: Blueprint object
|
||||||
|
@ -96,10 +121,19 @@ class Sanic:
|
||||||
self._blueprint_order.append(blueprint)
|
self._blueprint_order.append(blueprint)
|
||||||
blueprint.register(self, options)
|
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
|
# Request Handling
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def converted_response_type(self, response):
|
||||||
|
pass
|
||||||
|
|
||||||
async def handle_request(self, request, response_callback):
|
async def handle_request(self, request, response_callback):
|
||||||
"""
|
"""
|
||||||
Takes a request from the HTTP Server and returns a response object to
|
Takes a request from the HTTP Server and returns a response object to
|
||||||
|
@ -111,7 +145,10 @@ class Sanic:
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Middleware process_request
|
# -------------------------------------------- #
|
||||||
|
# Request Middleware
|
||||||
|
# -------------------------------------------- #
|
||||||
|
|
||||||
response = False
|
response = False
|
||||||
# The if improves speed. I don't know why
|
# The if improves speed. I don't know why
|
||||||
if self.request_middleware:
|
if self.request_middleware:
|
||||||
|
@ -124,6 +161,10 @@ class Sanic:
|
||||||
|
|
||||||
# No middleware results
|
# No middleware results
|
||||||
if not response:
|
if not response:
|
||||||
|
# -------------------------------------------- #
|
||||||
|
# Execute Handler
|
||||||
|
# -------------------------------------------- #
|
||||||
|
|
||||||
# Fetch handler from router
|
# Fetch handler from router
|
||||||
handler, args, kwargs = self.router.get(request)
|
handler, args, kwargs = self.router.get(request)
|
||||||
if handler is None:
|
if handler is None:
|
||||||
|
@ -136,7 +177,10 @@ class Sanic:
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
|
|
||||||
# Middleware process_response
|
# -------------------------------------------- #
|
||||||
|
# Response Middleware
|
||||||
|
# -------------------------------------------- #
|
||||||
|
|
||||||
if self.response_middleware:
|
if self.response_middleware:
|
||||||
for middleware in self.response_middleware:
|
for middleware in self.response_middleware:
|
||||||
_response = middleware(request, response)
|
_response = middleware(request, response)
|
||||||
|
@ -147,6 +191,10 @@ class Sanic:
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# -------------------------------------------- #
|
||||||
|
# Response Generation Failed
|
||||||
|
# -------------------------------------------- #
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.error_handler.response(request, e)
|
response = self.error_handler.response(request, e)
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
|
@ -166,22 +214,66 @@ class Sanic:
|
||||||
# Execution
|
# Execution
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None,
|
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
||||||
before_stop=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
|
Runs the HTTP Server and listens until keyboard interrupt or term
|
||||||
signal. On termination, drains connections before closing.
|
signal. On termination, drains connections before closing.
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
:param port: Port to host on
|
:param port: Port to host on
|
||||||
:param debug: Enables debug output (slows server)
|
:param debug: Enables debug output (slows server)
|
||||||
|
:param before_start: Function to be executed before the server starts
|
||||||
|
accepting connections
|
||||||
:param after_start: Function to be executed after the server starts
|
:param after_start: Function to be executed after the server starts
|
||||||
listening
|
accepting connections
|
||||||
:param before_stop: Function to be executed when a stop signal is
|
:param before_stop: Function to be executed when a stop signal is
|
||||||
received before it is respected
|
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
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
self.error_handler.debug = True
|
self.error_handler.debug = True
|
||||||
self.debug = debug
|
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:
|
if debug:
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
|
@ -191,23 +283,59 @@ class Sanic:
|
||||||
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
|
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
serve(
|
if workers == 1:
|
||||||
host=host,
|
serve(**server_settings)
|
||||||
port=port,
|
else:
|
||||||
debug=debug,
|
log.info('Spinning up {} workers...'.format(workers))
|
||||||
after_start=after_start,
|
|
||||||
before_stop=before_stop,
|
self.serve_multiple(server_settings, workers)
|
||||||
request_handler=self.handle_request,
|
|
||||||
request_timeout=self.config.REQUEST_TIMEOUT,
|
|
||||||
request_max_size=self.config.REQUEST_MAX_SIZE,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(
|
log.exception(
|
||||||
'Experienced exception while trying to serve: {}'.format(e))
|
'Experienced exception while trying to serve: {}'.format(e))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
log.info("Server Stopped")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""
|
"""
|
||||||
This kills the Sanic
|
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()
|
||||||
|
|
|
@ -110,6 +110,9 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_body(self, body):
|
def on_body(self, body):
|
||||||
|
if self.request.body:
|
||||||
|
self.request.body += body
|
||||||
|
else:
|
||||||
self.request.body = body
|
self.request.body = body
|
||||||
|
|
||||||
def on_message_complete(self):
|
def on_message_complete(self):
|
||||||
|
@ -122,8 +125,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
|
|
||||||
def write_response(self, response):
|
def write_response(self, response):
|
||||||
try:
|
try:
|
||||||
keep_alive = all(
|
keep_alive = self.parser.should_keep_alive() \
|
||||||
[self.parser.should_keep_alive(), self.signal.stopped])
|
and not self.signal.stopped
|
||||||
self.transport.write(
|
self.transport.write(
|
||||||
response.output(
|
response.output(
|
||||||
self.request.version, keep_alive, self.request_timeout))
|
self.request.version, keep_alive, self.request_timeout))
|
||||||
|
@ -157,15 +160,48 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def serve(host, port, request_handler, after_start=None, before_stop=None,
|
def trigger_events(events, loop):
|
||||||
debug=False, request_timeout=60,
|
"""
|
||||||
request_max_size=None):
|
:param events: one or more sync or async functions to execute
|
||||||
# Create Event Loop
|
:param loop: event loop
|
||||||
loop = async_loop.new_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)
|
asyncio.set_event_loop(loop)
|
||||||
# I don't think we take advantage of this
|
|
||||||
# And it slows everything waaayyy down
|
if debug:
|
||||||
# loop.set_debug(debug)
|
loop.set_debug(debug)
|
||||||
|
|
||||||
|
trigger_events(before_start, loop)
|
||||||
|
|
||||||
connections = {}
|
connections = {}
|
||||||
signal = Signal()
|
signal = Signal()
|
||||||
|
@ -176,18 +212,15 @@ def serve(host, port, request_handler, after_start=None, before_stop=None,
|
||||||
request_handler=request_handler,
|
request_handler=request_handler,
|
||||||
request_timeout=request_timeout,
|
request_timeout=request_timeout,
|
||||||
request_max_size=request_max_size,
|
request_max_size=request_max_size,
|
||||||
), host, port)
|
), host, port, reuse_port=reuse_port, sock=sock)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
http_server = loop.run_until_complete(server_coroutine)
|
http_server = loop.run_until_complete(server_coroutine)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.error("Unable to start server: {}".format(e))
|
log.exception("Unable to start server")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Run the on_start function if provided
|
trigger_events(after_start, loop)
|
||||||
if after_start:
|
|
||||||
result = after_start(loop)
|
|
||||||
if isawaitable(result):
|
|
||||||
loop.run_until_complete(result)
|
|
||||||
|
|
||||||
# Register signals for graceful termination
|
# Register signals for graceful termination
|
||||||
for _signal in (SIGINT, SIGTERM):
|
for _signal in (SIGINT, SIGTERM):
|
||||||
|
@ -199,10 +232,7 @@ def serve(host, port, request_handler, after_start=None, before_stop=None,
|
||||||
log.info("Stop requested, draining connections...")
|
log.info("Stop requested, draining connections...")
|
||||||
|
|
||||||
# Run the on_stop function if provided
|
# Run the on_stop function if provided
|
||||||
if before_stop:
|
trigger_events(before_stop, loop)
|
||||||
result = before_stop(loop)
|
|
||||||
if isawaitable(result):
|
|
||||||
loop.run_until_complete(result)
|
|
||||||
|
|
||||||
# Wait for event loop to finish and all connections to drain
|
# Wait for event loop to finish and all connections to drain
|
||||||
http_server.close()
|
http_server.close()
|
||||||
|
@ -216,5 +246,6 @@ def serve(host, port, request_handler, after_start=None, before_stop=None,
|
||||||
while connections:
|
while connections:
|
||||||
loop.run_until_complete(asyncio.sleep(0.1))
|
loop.run_until_complete(asyncio.sleep(0.1))
|
||||||
|
|
||||||
|
trigger_events(after_stop, loop)
|
||||||
|
|
||||||
loop.close()
|
loop.close()
|
||||||
log.info("Server Stopped")
|
|
||||||
|
|
59
sanic/static.py
Normal file
59
sanic/static.py
Normal 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)
|
|
@ -5,12 +5,13 @@ HOST = '127.0.0.1'
|
||||||
PORT = 42101
|
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)
|
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
|
||||||
log.info(url)
|
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:
|
async with getattr(session, method)(url, *args, **kwargs) as response:
|
||||||
response.text = await response.text()
|
response.text = await response.text()
|
||||||
|
response.body = await response.read()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
def _collect_request(request):
|
def _collect_request(request):
|
||||||
results.append(request)
|
results.append(request)
|
||||||
|
|
||||||
async def _collect_response(loop):
|
async def _collect_response(sanic, loop):
|
||||||
try:
|
try:
|
||||||
response = await local_request(method, uri, *request_args,
|
response = await local_request(method, uri, *request_args,
|
||||||
**request_kwargs)
|
**request_kwargs)
|
||||||
|
|
15
setup.py
15
setup.py
|
@ -1,11 +1,23 @@
|
||||||
"""
|
"""
|
||||||
Sanic
|
Sanic
|
||||||
"""
|
"""
|
||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
import re
|
||||||
from setuptools import setup
|
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(
|
setup(
|
||||||
name='Sanic',
|
name='Sanic',
|
||||||
version="0.1.3",
|
version=version,
|
||||||
url='http://github.com/channelcat/sanic/',
|
url='http://github.com/channelcat/sanic/',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Channel Cat',
|
author='Channel Cat',
|
||||||
|
@ -17,6 +29,7 @@ setup(
|
||||||
'uvloop>=0.5.3',
|
'uvloop>=0.5.3',
|
||||||
'httptools>=0.0.9',
|
'httptools>=0.0.9',
|
||||||
'ujson>=1.35',
|
'ujson>=1.35',
|
||||||
|
'aiofiles>=0.3.0',
|
||||||
],
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 2 - Pre-Alpha',
|
'Development Status :: 2 - Pre-Alpha',
|
||||||
|
|
11
tests/performance/falcon/simple_server.py
Normal file
11
tests/performance/falcon/simple_server.py
Normal 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())
|
|
@ -15,5 +15,5 @@ app = Sanic("test")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return json({"test": True})
|
return json({"test": True})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=sys.argv[1])
|
app.run(host="0.0.0.0", port=sys.argv[1])
|
||||||
|
|
19
tests/performance/tornado/simple_server.py
Normal file
19
tests/performance/tornado/simple_server.py
Normal 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()
|
|
@ -1,3 +1,5 @@
|
||||||
|
import inspect
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
|
@ -17,7 +19,7 @@ def test_bp():
|
||||||
def handler(request):
|
def handler(request):
|
||||||
return text('Hello')
|
return text('Hello')
|
||||||
|
|
||||||
app.register_blueprint(bp)
|
app.blueprint(bp)
|
||||||
request, response = sanic_endpoint_test(app)
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
assert response.text == 'Hello'
|
assert response.text == 'Hello'
|
||||||
|
@ -30,7 +32,7 @@ def test_bp_with_url_prefix():
|
||||||
def handler(request):
|
def handler(request):
|
||||||
return text('Hello')
|
return text('Hello')
|
||||||
|
|
||||||
app.register_blueprint(bp)
|
app.blueprint(bp)
|
||||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||||
|
|
||||||
assert response.text == 'Hello'
|
assert response.text == 'Hello'
|
||||||
|
@ -49,8 +51,8 @@ def test_several_bp_with_url_prefix():
|
||||||
def handler2(request):
|
def handler2(request):
|
||||||
return text('Hello2')
|
return text('Hello2')
|
||||||
|
|
||||||
app.register_blueprint(bp)
|
app.blueprint(bp)
|
||||||
app.register_blueprint(bp2)
|
app.blueprint(bp2)
|
||||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||||
assert response.text == 'Hello'
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
@ -70,7 +72,7 @@ def test_bp_middleware():
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
return text('FAIL')
|
return text('FAIL')
|
||||||
|
|
||||||
app.register_blueprint(blueprint)
|
app.blueprint(blueprint)
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app)
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
@ -97,7 +99,7 @@ def test_bp_exception_handler():
|
||||||
def handler_exception(request, exception):
|
def handler_exception(request, exception):
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
app.register_blueprint(blueprint)
|
app.blueprint(blueprint)
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, uri='/1')
|
request, response = sanic_endpoint_test(app, uri='/1')
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
@ -109,3 +111,55 @@ def test_bp_exception_handler():
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, uri='/3')
|
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
44
tests/test_cookies.py
Normal 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
|
|
@ -86,3 +86,43 @@ def test_middleware_override_response():
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == 'OK'
|
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]
|
||||||
|
|
53
tests/test_multiprocessing.py
Normal file
53
tests/test_multiprocessing.py
Normal 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
|
|
@ -80,3 +80,38 @@ def test_post_json():
|
||||||
|
|
||||||
assert request.json.get('test') == 'OK'
|
assert request.json.get('test') == 'OK'
|
||||||
assert response.text == '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'
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from json import loads as json_loads, dumps as json_dumps
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
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
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,6 +10,24 @@ from sanic.utils import sanic_endpoint_test
|
||||||
# UTF-8
|
# 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():
|
def test_dynamic_route():
|
||||||
app = Sanic('test_dynamic_route')
|
app = Sanic('test_dynamic_route')
|
||||||
|
|
||||||
|
@ -102,3 +122,59 @@ def test_dynamic_route_regex():
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, uri='/folder/')
|
request, response = sanic_endpoint_test(app, uri='/folder/')
|
||||||
assert response.status == 200
|
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
30
tests/test_static.py
Normal 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
|
Loading…
Reference in New Issue
Block a user