commit
0db49f7520
|
@ -21,12 +21,12 @@ Hello World Example
|
||||||
|
|
||||||
app = Sanic()
|
app = Sanic()
|
||||||
|
|
||||||
@app.route("/")
|
@app.route('/')
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return json({"hello": "world"})
|
return json({'hello': 'world'})
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host='0.0.0.0', port=8000)
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
|
@ -51,6 +51,73 @@ will look like:
|
||||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Blueprint groups and nesting
|
||||||
|
|
||||||
|
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
|
||||||
|
|
||||||
|
```
|
||||||
|
api/
|
||||||
|
├──content/
|
||||||
|
│ ├──authors.py
|
||||||
|
│ ├──static.py
|
||||||
|
│ └──__init__.py
|
||||||
|
├──info.py
|
||||||
|
└──__init__.py
|
||||||
|
app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Initialization of this app's blueprint hierarchy could go as follows:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# api/content/authors.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
authors = Blueprint('content_authors', url_prefix='/authors')
|
||||||
|
```
|
||||||
|
```python
|
||||||
|
# api/content/static.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
static = Blueprint('content_static', url_prefix='/static')
|
||||||
|
```
|
||||||
|
```python
|
||||||
|
# api/content/__init__.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
from .static import static
|
||||||
|
from .authors import authors
|
||||||
|
|
||||||
|
content = Blueprint.group(assets, authors, url_prefix='/content')
|
||||||
|
```
|
||||||
|
```python
|
||||||
|
# api/info.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
info = Blueprint('info', url_prefix='/info')
|
||||||
|
```
|
||||||
|
```python
|
||||||
|
# api/__init__.py
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
from .content import content
|
||||||
|
from .info import info
|
||||||
|
|
||||||
|
api = Blueprint.group(content, info, url_prefix='/api')
|
||||||
|
```
|
||||||
|
|
||||||
|
And registering these blueprints in `app.py` can now be done like so:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# app.py
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
from .api import api
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
app.blueprint(api)
|
||||||
|
```
|
||||||
|
|
||||||
## Using blueprints
|
## Using blueprints
|
||||||
|
|
||||||
Blueprints have much the same functionality as an application instance.
|
Blueprints have much the same functionality as an application instance.
|
||||||
|
|
|
@ -73,6 +73,8 @@ The following variables are accessible as properties on `Request` objects:
|
||||||
|
|
||||||
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
|
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
|
||||||
|
|
||||||
|
- `method` (str) - HTTP method of the request (ie `GET`, `POST`).
|
||||||
|
|
||||||
- `ip` (str) - IP address of the requester.
|
- `ip` (str) - IP address of the requester.
|
||||||
|
|
||||||
- `port` (str) - Port address of the requester.
|
- `port` (str) - Port address of the requester.
|
||||||
|
|
16
sanic/app.py
16
sanic/app.py
|
@ -372,10 +372,14 @@ class Sanic:
|
||||||
def blueprint(self, blueprint, **options):
|
def blueprint(self, blueprint, **options):
|
||||||
"""Register a blueprint on the application.
|
"""Register a blueprint on the application.
|
||||||
|
|
||||||
:param blueprint: Blueprint object
|
:param blueprint: Blueprint object or (list, tuple) thereof
|
||||||
:param options: option dictionary with blueprint defaults
|
:param options: option dictionary with blueprint defaults
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
if isinstance(blueprint, (list, tuple)):
|
||||||
|
for item in blueprint:
|
||||||
|
self.blueprint(item, **options)
|
||||||
|
return
|
||||||
if blueprint.name in self.blueprints:
|
if blueprint.name in self.blueprints:
|
||||||
assert self.blueprints[blueprint.name] is blueprint, \
|
assert self.blueprints[blueprint.name] is blueprint, \
|
||||||
'A blueprint with the name "%s" is already registered. ' \
|
'A blueprint with the name "%s" is already registered. ' \
|
||||||
|
@ -577,13 +581,17 @@ class Sanic:
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.debug:
|
if isinstance(e, SanicException):
|
||||||
|
response = self.error_handler.default(request=request,
|
||||||
|
exception=e)
|
||||||
|
elif self.debug:
|
||||||
response = HTTPResponse(
|
response = HTTPResponse(
|
||||||
"Error while handling error: {}\nStack: {}".format(
|
"Error while handling error: {}\nStack: {}".format(
|
||||||
e, format_exc()))
|
e, format_exc()), status=500)
|
||||||
else:
|
else:
|
||||||
response = HTTPResponse(
|
response = HTTPResponse(
|
||||||
"An error occurred while handling an error")
|
"An error occurred while handling an error",
|
||||||
|
status=500)
|
||||||
finally:
|
finally:
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Response Middleware
|
# Response Middleware
|
||||||
|
|
|
@ -14,7 +14,6 @@ FutureStatic = namedtuple('Route',
|
||||||
|
|
||||||
|
|
||||||
class Blueprint:
|
class Blueprint:
|
||||||
|
|
||||||
def __init__(self, name,
|
def __init__(self, name,
|
||||||
url_prefix=None,
|
url_prefix=None,
|
||||||
host=None, version=None,
|
host=None, version=None,
|
||||||
|
@ -38,6 +37,27 @@ class Blueprint:
|
||||||
self.version = version
|
self.version = version
|
||||||
self.strict_slashes = strict_slashes
|
self.strict_slashes = strict_slashes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def group(*blueprints, url_prefix=''):
|
||||||
|
"""Create a list of blueprints, optionally
|
||||||
|
grouping them under a general URL prefix.
|
||||||
|
|
||||||
|
:param blueprints: blueprints to be registered as a group
|
||||||
|
:param url_prefix: URL route to be prepended to all sub-prefixes
|
||||||
|
"""
|
||||||
|
def chain(nested):
|
||||||
|
"""itertools.chain() but leaves strings untouched"""
|
||||||
|
for i in nested:
|
||||||
|
if isinstance(i, (list, tuple)):
|
||||||
|
yield from chain(i)
|
||||||
|
else:
|
||||||
|
yield i
|
||||||
|
bps = []
|
||||||
|
for bp in chain(blueprints):
|
||||||
|
bp.url_prefix = url_prefix + bp.url_prefix
|
||||||
|
bps.append(bp)
|
||||||
|
return bps
|
||||||
|
|
||||||
def register(self, app, options):
|
def register(self, app, options):
|
||||||
"""Register the blueprint to the sanic app."""
|
"""Register the blueprint to the sanic app."""
|
||||||
|
|
||||||
|
|
|
@ -182,7 +182,7 @@ class Request(dict):
|
||||||
@property
|
@property
|
||||||
def socket(self):
|
def socket(self):
|
||||||
if not hasattr(self, '_socket'):
|
if not hasattr(self, '_socket'):
|
||||||
self._get_socket()
|
self._get_address()
|
||||||
return self._socket
|
return self._socket
|
||||||
|
|
||||||
def _get_address(self):
|
def _get_address(self):
|
||||||
|
|
|
@ -234,11 +234,11 @@ class Router:
|
||||||
if properties['unhashable']:
|
if properties['unhashable']:
|
||||||
routes_to_check = self.routes_always_check
|
routes_to_check = self.routes_always_check
|
||||||
ndx, route = self.check_dynamic_route_exists(
|
ndx, route = self.check_dynamic_route_exists(
|
||||||
pattern, routes_to_check)
|
pattern, routes_to_check, parameters)
|
||||||
else:
|
else:
|
||||||
routes_to_check = self.routes_dynamic[url_hash(uri)]
|
routes_to_check = self.routes_dynamic[url_hash(uri)]
|
||||||
ndx, route = self.check_dynamic_route_exists(
|
ndx, route = self.check_dynamic_route_exists(
|
||||||
pattern, routes_to_check)
|
pattern, routes_to_check, parameters)
|
||||||
if ndx != -1:
|
if ndx != -1:
|
||||||
# Pop the ndx of the route, no dups of the same route
|
# Pop the ndx of the route, no dups of the same route
|
||||||
routes_to_check.pop(ndx)
|
routes_to_check.pop(ndx)
|
||||||
|
@ -285,9 +285,9 @@ class Router:
|
||||||
self.routes_static[uri] = route
|
self.routes_static[uri] = route
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_dynamic_route_exists(pattern, routes_to_check):
|
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
|
||||||
for ndx, route in enumerate(routes_to_check):
|
for ndx, route in enumerate(routes_to_check):
|
||||||
if route.pattern == pattern:
|
if route.pattern == pattern and route.parameters == parameters:
|
||||||
return ndx, route
|
return ndx, route
|
||||||
else:
|
else:
|
||||||
return -1, None
|
return -1, None
|
||||||
|
|
|
@ -5,7 +5,7 @@ from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
from signal import (
|
from signal import (
|
||||||
SIGTERM, SIGINT,
|
SIGTERM, SIGINT, SIG_IGN,
|
||||||
signal as signal_func,
|
signal as signal_func,
|
||||||
Signals
|
Signals
|
||||||
)
|
)
|
||||||
|
@ -20,9 +20,10 @@ from httptools import HttpRequestParser
|
||||||
from httptools.parser.errors import HttpParserError
|
from httptools.parser.errors import HttpParserError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uvloop as async_loop
|
import uvloop
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
except ImportError:
|
except ImportError:
|
||||||
async_loop = asyncio
|
pass
|
||||||
|
|
||||||
from sanic.log import logger, access_logger
|
from sanic.log import logger, access_logger
|
||||||
from sanic.response import HTTPResponse
|
from sanic.response import HTTPResponse
|
||||||
|
@ -509,11 +510,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
request_timeout=60, response_timeout=60, keep_alive_timeout=5,
|
request_timeout=60, response_timeout=60, keep_alive_timeout=5,
|
||||||
ssl=None, sock=None, request_max_size=None, reuse_port=False,
|
ssl=None, sock=None, request_max_size=None, reuse_port=False,
|
||||||
loop=None, protocol=HttpProtocol, backlog=100,
|
loop=None, protocol=HttpProtocol, backlog=100,
|
||||||
register_sys_signals=True, run_async=False, connections=None,
|
register_sys_signals=True, run_multiple=False, run_async=False,
|
||||||
signal=Signal(), request_class=None, access_log=True,
|
connections=None, signal=Signal(), request_class=None,
|
||||||
keep_alive=True, is_request_stream=False, router=None,
|
access_log=True, keep_alive=True, is_request_stream=False,
|
||||||
websocket_max_size=None, websocket_max_queue=None, state=None,
|
router=None, websocket_max_size=None, websocket_max_queue=None,
|
||||||
graceful_shutdown_timeout=15.0):
|
state=None, graceful_shutdown_timeout=15.0):
|
||||||
"""Start asynchronous HTTP Server on an individual process.
|
"""Start asynchronous HTTP Server on an individual process.
|
||||||
|
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
|
@ -547,7 +548,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if not run_async:
|
if not run_async:
|
||||||
loop = async_loop.new_event_loop()
|
# create new event_loop after fork
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
|
@ -603,9 +605,14 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
|
|
||||||
trigger_events(after_start, loop)
|
trigger_events(after_start, loop)
|
||||||
|
|
||||||
|
# Ignore SIGINT when run_multiple
|
||||||
|
if run_multiple:
|
||||||
|
signal_func(SIGINT, SIG_IGN)
|
||||||
|
|
||||||
# Register signals for graceful termination
|
# Register signals for graceful termination
|
||||||
if register_sys_signals:
|
if register_sys_signals:
|
||||||
for _signal in (SIGINT, SIGTERM):
|
_singals = (SIGTERM,) if run_multiple else (SIGINT, SIGTERM)
|
||||||
|
for _signal in _singals:
|
||||||
try:
|
try:
|
||||||
loop.add_signal_handler(_signal, loop.stop)
|
loop.add_signal_handler(_signal, loop.stop)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
|
@ -668,6 +675,7 @@ def serve_multiple(server_settings, workers):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
server_settings['reuse_port'] = True
|
server_settings['reuse_port'] = True
|
||||||
|
server_settings['run_multiple'] = True
|
||||||
|
|
||||||
# Handling when custom socket is not provided.
|
# Handling when custom socket is not provided.
|
||||||
if server_settings.get('sock') is None:
|
if server_settings.get('sock') is None:
|
||||||
|
@ -682,12 +690,13 @@ def serve_multiple(server_settings, workers):
|
||||||
def sig_handler(signal, frame):
|
def sig_handler(signal, frame):
|
||||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||||
for process in processes:
|
for process in processes:
|
||||||
os.kill(process.pid, SIGINT)
|
os.kill(process.pid, SIGTERM)
|
||||||
|
|
||||||
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
|
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
|
||||||
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
|
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
|
||||||
|
|
||||||
processes = []
|
processes = []
|
||||||
|
|
||||||
for _ in range(workers):
|
for _ in range(workers):
|
||||||
process = Process(target=serve, kwargs=server_settings)
|
process = Process(target=serve, kwargs=server_settings)
|
||||||
process.daemon = True
|
process.daemon = True
|
||||||
|
|
|
@ -446,3 +446,44 @@ def test_bp_shorthand():
|
||||||
'Sec-WebSocket-Version': '13'})
|
'Sec-WebSocket-Version': '13'})
|
||||||
assert response.status == 101
|
assert response.status == 101
|
||||||
assert ev.is_set()
|
assert ev.is_set()
|
||||||
|
|
||||||
|
def test_bp_group():
|
||||||
|
app = Sanic('test_nested_bp_groups')
|
||||||
|
|
||||||
|
deep_0 = Blueprint('deep_0', url_prefix='/deep')
|
||||||
|
deep_1 = Blueprint('deep_1', url_prefix = '/deep1')
|
||||||
|
|
||||||
|
@deep_0.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return text('D0_OK')
|
||||||
|
|
||||||
|
@deep_1.route('/bottom')
|
||||||
|
def handler(request):
|
||||||
|
return text('D1B_OK')
|
||||||
|
|
||||||
|
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
|
||||||
|
mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
|
||||||
|
|
||||||
|
@mid_1.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return text('M1_OK')
|
||||||
|
|
||||||
|
top = Blueprint.group(mid_0, mid_1)
|
||||||
|
|
||||||
|
app.blueprint(top)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return text('TOP_OK')
|
||||||
|
|
||||||
|
request, response = app.test_client.get('/')
|
||||||
|
assert response.text == 'TOP_OK'
|
||||||
|
|
||||||
|
request, response = app.test_client.get('/mid1')
|
||||||
|
assert response.text == 'M1_OK'
|
||||||
|
|
||||||
|
request, response = app.test_client.get('/mid/deep')
|
||||||
|
assert response.text == 'D0_OK'
|
||||||
|
|
||||||
|
request, response = app.test_client.get('/mid/deep1/bottom')
|
||||||
|
assert response.text == 'D1B_OK'
|
||||||
|
|
|
@ -23,4 +23,3 @@ def test_multiprocessing():
|
||||||
app.run(HOST, app.test_port, workers=num_workers)
|
app.run(HOST, app.test_port, workers=num_workers)
|
||||||
|
|
||||||
assert len(process_list) == num_workers
|
assert len(process_list) == num_workers
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import asyncio
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text, json
|
||||||
from sanic.router import RouteExists, RouteDoesNotExist
|
from sanic.router import RouteExists, RouteDoesNotExist
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
|
|
||||||
|
@ -907,3 +907,27 @@ def test_unicode_routes():
|
||||||
|
|
||||||
request, response = app.test_client.get('/overload/你好')
|
request, response = app.test_client.get('/overload/你好')
|
||||||
assert response.text == 'OK2 你好'
|
assert response.text == 'OK2 你好'
|
||||||
|
|
||||||
|
|
||||||
|
def test_uri_with_different_method_and_different_params():
|
||||||
|
app = Sanic('test_uri')
|
||||||
|
|
||||||
|
@app.route('/ads/<ad_id>', methods=['GET'])
|
||||||
|
async def ad_get(request, ad_id):
|
||||||
|
return json({'ad_id': ad_id})
|
||||||
|
|
||||||
|
@app.route('/ads/<action>', methods=['POST'])
|
||||||
|
async def ad_post(request, action):
|
||||||
|
return json({'action': action})
|
||||||
|
|
||||||
|
request, response = app.test_client.get('/ads/1234')
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.json == {
|
||||||
|
'ad_id': '1234'
|
||||||
|
}
|
||||||
|
|
||||||
|
request, response = app.test_client.post('/ads/post')
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.json == {
|
||||||
|
'action': 'post'
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user