Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1036242064 | ||
|
|
5fd62098bd | ||
|
|
b3814ca89a | ||
|
|
b75a321e4a | ||
|
|
9caa4fec4a | ||
|
|
a0cba1aee1 | ||
|
|
97018ad62f | ||
|
|
a7d17fae44 | ||
|
|
6ce0050979 | ||
|
|
bc035fca78 | ||
|
|
df914a92e4 | ||
|
|
1b939a6823 | ||
|
|
81b6d988ec | ||
|
|
7e9b65feca | ||
|
|
6f098b3d21 | ||
|
|
5ddb0488f2 | ||
|
|
3e87314adf | ||
|
|
f6d4a06661 | ||
|
|
ff17fc95e6 | ||
|
|
c5a46f1cea | ||
|
|
0b072189c4 | ||
|
|
5b22d1486a | ||
|
|
9eb48c2b0d | ||
|
|
ff0632001c | ||
|
|
28bd09a2ea | ||
|
|
c6aaa9b09c | ||
|
|
20d9ec1fd2 | ||
|
|
2c45c2d3c0 | ||
|
|
4c66cb1854 | ||
|
|
35b92e1511 |
62
CONTRIBUTING.md
Normal file
62
CONTRIBUTING.md
Normal 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.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
41
examples/dask_distributed.py
Normal file
41
examples/dask_distributed.py
Normal 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)
|
||||
@@ -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 import response
|
||||
from jinja2 import Template
|
||||
|
||||
template = Template('Hello {{ name }}!')
|
||||
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 response.html(template.render(**data))
|
||||
rendered_template = await template.render_async(**data)
|
||||
return response.html(rendered_template)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8080, debug=True)
|
||||
@@ -1,6 +1,6 @@
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
__version__ = '0.5.1'
|
||||
__version__ = '0.5.2'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
||||
@@ -16,7 +16,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
|
||||
@@ -288,7 +288,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
|
||||
@@ -674,11 +674,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
|
||||
|
||||
@@ -6,7 +6,7 @@ SANIC_PREFIX = 'SANIC_'
|
||||
|
||||
|
||||
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 = """
|
||||
▄▄▄▄▄
|
||||
@@ -31,6 +31,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()
|
||||
@@ -98,11 +99,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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -70,7 +70,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):
|
||||
request_max_size=None, request_class=None,
|
||||
keep_alive=True):
|
||||
self.loop = loop
|
||||
self.transport = None
|
||||
self.request = None
|
||||
@@ -88,10 +89,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
|
||||
@@ -322,7 +326,7 @@ 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):
|
||||
signal=Signal(), request_class=None, keep_alive=True):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
@@ -370,6 +374,7 @@ 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,
|
||||
keep_alive=keep_alive,
|
||||
)
|
||||
|
||||
server_coroutine = loop.create_server(
|
||||
|
||||
@@ -56,7 +56,7 @@ def register(app, uri, file_or_directory, pattern,
|
||||
# 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 = path.abspath(unquote(file_path))
|
||||
if not file_path.startswith(root_path):
|
||||
if not file_path.startswith(path.abspath(unquote(root_path))):
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user