Merge branch 'master' into master

This commit is contained in:
Raphael Deem 2017-04-24 00:47:01 -07:00 committed by GitHub
commit 74cc7be922
27 changed files with 404 additions and 69 deletions

62
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,62 @@
# Contributing
Thank you for your interest! Sanic is always looking for contributors. If you
don't feel comfortable contributing code, adding docstrings to the source files
is very appreciated.
## Installation
To develop on sanic (and mainly to just run the tests) it is highly recommend to
install from sources.
So assume you have already cloned the repo and are in the working directory with
a virtual environment already set up, then run:
```bash
python setup.py develop && pip install -r requirements-dev.txt
```
## Running tests
To run the tests for sanic it is recommended to use tox like so:
```bash
tox
```
See it's that simple!
## Pull requests!
So the pull request approval rules are pretty simple:
1. All pull requests must pass unit tests
2. All pull requests must be reviewed and approved by at least
one current collaborator on the project
3. All pull requests must pass flake8 checks
4. If you decide to remove/change anything from any common interface
a deprecation message should accompany it.
5. If you implement a new feature you should have at least one unit
test to accompany it.
## Documentation
Sanic's documentation is built
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
Markdown and can be found in the `docs` folder, while the module reference is
automatically generated using `sphinx-apidoc`.
To generate the documentation from scratch:
```bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
```
The HTML documentation will be created in the `docs/_build` folder.
## Warning
One of the main goals of Sanic is speed. Code that lowers the performance of
Sanic without significant gains in usability, security, or features may not be
merged. Please don't let this intimidate you! If you have any concerns about an
idea, open an issue for discussion and help.

View File

@ -83,3 +83,4 @@ Out of the box there are just a few predefined values which can be overwritten w
| ----------------- | --------- | --------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |

View File

@ -4,10 +4,39 @@ Thank you for your interest! Sanic is always looking for contributors. If you
don't feel comfortable contributing code, adding docstrings to the source files
is very appreciated.
## Installation
To develop on sanic (and mainly to just run the tests) it is highly recommend to
install from sources.
So assume you have already cloned the repo and are in the working directory with
a virtual environment already set up, then run:
```bash
python setup.py develop && pip install -r requirements-dev.txt
```
## Running tests
* `python -m pip install pytest`
* `python -m pytest tests`
To run the tests for sanic it is recommended to use tox like so:
```bash
tox
```
See it's that simple!
## Pull requests!
So the pull request approval rules are pretty simple:
1. All pull requests must pass unit tests
* All pull requests must be reviewed and approved by at least
one current collaborator on the project
* All pull requests must pass flake8 checks
* If you decide to remove/change anything from any common interface
a deprecation message should accompany it.
* If you implement a new feature you should have at least one unit
test to accompany it.
## Documentation

View File

@ -0,0 +1,41 @@
from sanic import Sanic
from sanic import response
from tornado.platform.asyncio import BaseAsyncIOLoop, to_asyncio_future
from distributed import LocalCluster, Client
app = Sanic(__name__)
def square(x):
return x**2
@app.listener('after_server_start')
async def setup(app, loop):
# configure tornado use asyncio's loop
ioloop = BaseAsyncIOLoop(loop)
# init distributed client
app.client = Client('tcp://localhost:8786', loop=ioloop, start=False)
await to_asyncio_future(app.client._start())
@app.listener('before_server_stop')
async def stop(app, loop):
await to_asyncio_future(app.client._shutdown())
@app.route('/<value:int>')
async def test(request, value):
future = app.client.submit(square, value)
result = await to_asyncio_future(future._result())
return response.text(f'The square of {value} is {result}')
if __name__ == '__main__':
# Distributed cluster should run somewhere else
with LocalCluster(scheduler_port=8786, nanny=False, n_workers=2,
threads_per_worker=1) as cluster:
app.run(host="0.0.0.0", port=8000)

View File

@ -1,9 +1,7 @@
"""
Example intercepting uncaught exceptions using Sanic's error handler framework.
This may be useful for developers wishing to use Sentry, Airbrake, etc.
or a custom system to log and monitor unexpected errors in production.
First we create our own class inheriting from Handler in sanic.exceptions,
and pass in an instance of it when we create our Sanic instance. Inside this
class' default handler, we can do anything including sending exceptions to
@ -39,7 +37,7 @@ server's error_handler to an instance of our CustomHandler
"""
from sanic import Sanic
from sanic.response import json
from sanic import response
app = Sanic(__name__)
@ -52,7 +50,7 @@ async def test(request):
# Here, something occurs which causes an unexpected exception
# This exception will flow to our custom handler.
1 / 0
return json({"test": True})
return response.json({"test": True})
app.run(host="0.0.0.0", port=8000, debug=True)
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,18 +1,27 @@
## To use this example:
# curl -d '{"name": "John Doe"}' localhost:8000
# Render templates in a Flask like way from a "template" directory in the project
from sanic import Sanic
from sanic.response import html
from jinja2 import Template
template = Template('Hello {{ name }}!')
from sanic import response
from jinja2 import Evironment, PackageLoader, select_autoescape
app = Sanic(__name__)
# Load the template environment with async support
template_env = Environment(
loader=jinja2.PackageLoader('yourapplication', 'templates'),
autoescape=jinja2.select_autoescape(['html', 'xml']),
enable_async=True
)
# Load the template from file
template = template_env.get_template("example_template.html")
@app.route('/')
async def test(request):
data = request.json
return html(template.render(**data))
rendered_template = await template.render_async(**data)
return response.html(rendered_template)
app.run(host="0.0.0.0", port=8000)
app.run(host="0.0.0.0", port=8080, debug=True)

View File

@ -0,0 +1,26 @@
"""
Modify header or status in response
"""
from sanic import Sanic
from sanic import response
app = Sanic(__name__)
@app.route('/')
def handle_request(request):
return response.json(
{'message': 'Hello world!'},
headers={'X-Served-By': 'sanic'},
status=200
)
@app.route('/unauthorized')
def handle_request(request):
return response.json(
{'message': 'You are not authorized'},
headers={'X-Served-By': 'sanic'},
status=404
)
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,6 +1,5 @@
from sanic import Sanic
from sanic.response import text
import json
from sanic import response
import logging
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
@ -18,6 +17,6 @@ sanic = Sanic()
@sanic.route("/")
def test(request):
log.info("received request; responding with 'hey'")
return text("hey")
return response.text("hey")
sanic.run(host="0.0.0.0", port=8000)

View File

@ -0,0 +1,85 @@
from sanic import Sanic
from sanic_session import InMemorySessionInterface
from sanic_jinja2 import SanicJinja2
import json
import plotly
import pandas as pd
import numpy as np
app = Sanic(__name__)
jinja = SanicJinja2(app)
session = InMemorySessionInterface(cookie_name=app.name, prefix=app.name)
@app.middleware('request')
async def print_on_request(request):
print(request.headers)
await session.open(request)
@app.middleware('response')
async def print_on_response(request, response):
await session.save(request, response)
@app.route('/')
async def index(request):
rng = pd.date_range('1/1/2011', periods=7500, freq='H')
ts = pd.Series(np.random.randn(len(rng)), index=rng)
graphs = [
dict(
data=[
dict(
x=[1, 2, 3],
y=[10, 20, 30],
type='scatter'
),
],
layout=dict(
title='first graph'
)
),
dict(
data=[
dict(
x=[1, 3, 5],
y=[10, 50, 30],
type='bar'
),
],
layout=dict(
title='second graph'
)
),
dict(
data=[
dict(
x=ts.index, # Can use the pandas data structures directly
y=ts
)
]
)
]
# Add "ids" to each of the graphs to pass up to the client
# for templating
ids = ['graph-{}'.format(i) for i, _ in enumerate(graphs)]
# Convert the figures to JSON
# PlotlyJSONEncoder appropriately converts pandas, datetime, etc
# objects to their JSON equivalents
graphJSON = json.dumps(graphs, cls=plotly.utils.PlotlyJSONEncoder)
return jinja.render('index.html', request,
ids=ids,
graphJSON=graphJSON)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=True)

View File

@ -0,0 +1,5 @@
pandas==0.19.2
plotly==2.0.7
sanic==0.5.0
sanic-jinja2==0.5.1
sanic-session==0.1.3

View File

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

View File

@ -5,7 +5,7 @@ motor==1.1
sanic==0.2.0
"""
from sanic import Sanic
from sanic.response import json
from sanic import response
app = Sanic('motor_mongodb')
@ -25,7 +25,7 @@ async def get(request):
for doc in docs:
doc['id'] = str(doc['_id'])
del doc['_id']
return json(docs)
return response.json(docs)
@app.route('/post', methods=['POST'])
@ -34,8 +34,8 @@ async def new(request):
print(doc)
db = get_db()
object_id = await db.test_col.save(doc)
return json({'object_id': str(object_id)})
return response.json({'object_id': str(object_id)})
if __name__ == "__main__":
app.run(host='127.0.0.1', port=8000)
app.run(host='0.0.0.0', port=8000, debug=True)

View File

@ -1,12 +1,12 @@
from sanic import Sanic
from sanic.response import json
from sanic import response
app = Sanic(__name__)
@app.route("/")
async def test(request):
return json({"test": True})
return response.json({"test": True})
if __name__ == '__main__':

View File

@ -2,7 +2,7 @@ import os
from sanic import Sanic
from sanic.log import log
from sanic.response import json, text, file
from sanic import response
from sanic.exceptions import ServerError
app = Sanic(__name__)
@ -10,17 +10,17 @@ app = Sanic(__name__)
@app.route("/")
async def test_async(request):
return json({"test": True})
return response.json({"test": True})
@app.route("/sync", methods=['GET', 'POST'])
def test_sync(request):
return json({"test": True})
return response.json({"test": True})
@app.route("/dynamic/<name>/<id:int>")
def test_params(request, name, id):
return text("yeehaww {} {}".format(name, id))
return response.text("yeehaww {} {}".format(name, id))
@app.route("/exception")
@ -31,11 +31,11 @@ def exception(request):
async def test_await(request):
import asyncio
await asyncio.sleep(5)
return text("I'm feeling sleepy")
return response.text("I'm feeling sleepy")
@app.route("/file")
async def test_file(request):
return await file(os.path.abspath("setup.py"))
return await response.file(os.path.abspath("setup.py"))
# ----------------------------------------------- #
@ -44,7 +44,7 @@ async def test_file(request):
@app.exception(ServerError)
async def test(request, exception):
return json({"exception": "{}".format(exception), "status": exception.status_code}, status=exception.status_code)
return response.json({"exception": "{}".format(exception), "status": exception.status_code}, status=exception.status_code)
# ----------------------------------------------- #
@ -53,17 +53,17 @@ async def test(request, exception):
@app.route("/json")
def post_json(request):
return json({"received": True, "message": request.json})
return response.json({"received": True, "message": request.json})
@app.route("/form")
def post_json(request):
return json({"received": True, "form_data": request.form, "test": request.form.get('test')})
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
@app.route("/query_string")
def query_string(request):
return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
return response.json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
# ----------------------------------------------- #

View File

@ -0,0 +1,18 @@
from sanic import Sanic
from sanic import response
app = Sanic(__name__)
@app.route('/')
async def index(request):
# generate a URL for the endpoint `post_handler`
url = app.url_for('post_handler', post_id=5)
# the URL is `/posts/5`, redirect to it
return response.redirect(url)
@app.route('/posts/<post_id>')
async def post_handler(request, post_id):
return response.text('Post - {}'.format(post_id))
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)

View File

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

View File

@ -17,7 +17,7 @@ from sanic.handlers import ErrorHandler
from sanic.log import log
from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.router import Router
from sanic.server import serve, serve_multiple, HttpProtocol
from sanic.server import serve, serve_multiple, HttpProtocol, Signal
from sanic.static import register as static_register
from sanic.testing import SanicTestClient
from sanic.views import CompositionView
@ -293,7 +293,7 @@ class Sanic:
attach_to=middleware_or_request)
# Static Files
def static(self, uri, file_or_directory, pattern='.+',
def static(self, uri, file_or_directory, pattern=r'/?.+',
use_modified_since=True, use_content_range=False):
"""Register a root to serve files from. The input can either be a
file or a directory. See
@ -687,11 +687,13 @@ class Sanic:
'port': port,
'sock': sock,
'ssl': ssl,
'signal': Signal(),
'debug': debug,
'request_handler': self.handle_request,
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'keep_alive': self.config.KEEP_ALIVE,
'loop': loop,
'register_sys_signals': register_sys_signals,
'backlog': backlog,

View File

@ -116,7 +116,7 @@ if type(_addr) is str and not os.path.exists(_addr):
class Config(dict):
def __init__(self, defaults=None, load_env=True):
def __init__(self, defaults=None, load_env=True, keep_alive=True):
super().__init__(defaults or {})
self.LOGO = """
@ -141,6 +141,7 @@ class Config(dict):
"""
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
self.REQUEST_TIMEOUT = 60 # 60 seconds
self.KEEP_ALIVE = keep_alive
if load_env:
self.load_environment_vars()
@ -208,11 +209,11 @@ class Config(dict):
self[key] = getattr(obj, key)
def load_environment_vars(self):
"""
Looks for any SANIC_ prefixed environment variables and applies
them to the configuration if present.
"""
for k, v in os.environ.items():
"""
Looks for any SANIC_ prefixed environment variables and applies
them to the configuration if present.
"""
if k.startswith(SANIC_PREFIX):
_, config_key = k.split(SANIC_PREFIX, 1)
self[config_key] = v

View File

@ -78,9 +78,10 @@ class Request(dict):
:return: token related to request
"""
auth_header = self.headers.get('Authorization')
if auth_header is not None:
return auth_header.split()[1]
return auth_header
if 'Token ' in auth_header:
return auth_header.partition('Token ')[-1]
else:
return auth_header
@property
def form(self):

View File

@ -210,7 +210,7 @@ class HTTPResponse(BaseHTTPResponse):
# 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)
status = ALL_STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
return (b'HTTP/%b %d %b\r\n'
b'Connection: %b\r\n'

View File

@ -16,6 +16,7 @@ REGEX_TYPES = {
'int': (int, r'\d+'),
'number': (float, r'[0-9\\.]+'),
'alpha': (str, r'[A-Za-z]+'),
'path': (str, r'[^/].*?'),
}
ROUTER_CACHE_SIZE = 1024
@ -71,7 +72,8 @@ class Router:
self.routes_always_check = []
self.hosts = set()
def parse_parameter_string(self, parameter_string):
@classmethod
def parse_parameter_string(cls, parameter_string):
"""Parse a parameter string into its constituent name, type, and
pattern
@ -161,10 +163,10 @@ class Router:
parameters.append(parameter)
# Mark the whole route as unhashable if it has the hash key in it
if re.search('(^|[^^]){1}/', pattern):
if re.search(r'(^|[^^]){1}/', pattern):
properties['unhashable'] = True
# Mark the route as unhashable if it matches the hash key
elif re.search(pattern, '/'):
elif re.search(r'/', pattern):
properties['unhashable'] = True
return '({})'.format(pattern)

View File

@ -73,7 +73,8 @@ class HttpProtocol(asyncio.Protocol):
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections=set(), request_timeout=60,
request_max_size=None, request_class=None, has_log=True):
request_max_size=None, request_class=None, has_log=True,
keep_alive=True):
self.loop = loop
self.transport = None
self.request = None
@ -92,10 +93,13 @@ class HttpProtocol(asyncio.Protocol):
self._timeout_handler = None
self._last_request_time = None
self._request_handler_task = None
self._keep_alive = keep_alive
@property
def keep_alive(self):
return self.parser.should_keep_alive() and not self.signal.stopped
return (self._keep_alive
and not self.signal.stopped
and self.parser.should_keep_alive())
# -------------------------------------------- #
# Connection
@ -357,7 +361,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
request_timeout=60, ssl=None, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False, connections=None,
signal=Signal(), request_class=None, has_log=True):
signal=Signal(), request_class=None, has_log=True, keep_alive=True):
signal=Signal(), request_class=None, keep_alive=True):
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
@ -406,7 +411,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
request_timeout=request_timeout,
request_max_size=request_max_size,
request_class=request_class,
has_log=has_log
has_log=has_log,
keep_alive=keep_alive,
)
server_coroutine = loop.create_server(

View File

@ -48,14 +48,18 @@ def register(app, uri, file_or_directory, pattern,
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
file_path = file_or_directory
root_path = file_path = file_or_directory
if file_uri:
file_path = path.join(
file_or_directory, sub('^[/]*', '', file_uri))
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = unquote(file_path)
file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))):
raise FileNotFound('File not found',
path=file_or_directory,
relative_url=file_uri)
try:
headers = {}
# Check if the client has been sent this file before

View File

@ -3,6 +3,7 @@ import sys
import signal
import asyncio
import logging
try:
import ssl
except ImportError:
@ -50,8 +51,8 @@ class GunicornWorker(base.Worker):
debug=is_debug,
protocol=protocol,
ssl=self.ssl_context,
run_async=True
)
run_async=True)
self._server_settings['signal'] = self.signal
self._server_settings.pop('sock')
trigger_events(self._server_settings.get('before_start', []),
self.loop)
@ -97,7 +98,6 @@ class GunicornWorker(base.Worker):
self.servers.append(await serve(
sock=sock,
connections=self.connections,
signal=self.signal,
**self._server_settings
))

View File

@ -141,6 +141,16 @@ def test_token():
return text('OK')
# uuid4 generated token.
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
headers = {
'content-type': 'application/json',
'Authorization': '{}'.format(token)
}
request, response = app.test_client.get('/', headers=headers)
assert request.token == token
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
headers = {
'content-type': 'application/json',
@ -151,6 +161,18 @@ def test_token():
assert request.token == token
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
headers = {
'content-type': 'application/json',
'Authorization': 'Bearer Token {}'.format(token)
}
request, response = app.test_client.get('/', headers=headers)
assert request.token == token
# ------------------------------------------------------------ #
# POST
# ------------------------------------------------------------ #

View File

@ -238,6 +238,30 @@ def test_dynamic_route_regex():
assert response.status == 200
def test_dynamic_route_path():
app = Sanic('test_dynamic_route_path')
@app.route('/<path:path>/info')
async def handler(request, path):
return text('OK')
request, response = app.test_client.get('/path/1/info')
assert response.status == 200
request, response = app.test_client.get('/info')
assert response.status == 404
@app.route('/<path:path>')
async def handler1(request, path):
return text('OK')
request, response = app.test_client.get('/info')
assert response.status == 200
request, response = app.test_client.get('/whatever/you/set')
assert response.status == 200
def test_dynamic_route_unhashable():
app = Sanic('test_dynamic_route_unhashable')