Merge branch 'master' into worker-protocol
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
__version__ = '0.5.4'
|
||||
__version__ = '0.6.0'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from argparse import ArgumentParser
|
||||
from importlib import import_module
|
||||
|
||||
from sanic.log import log
|
||||
from sanic.log import logger
|
||||
from sanic.app import Sanic
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -36,9 +36,9 @@ if __name__ == "__main__":
|
||||
app.run(host=args.host, port=args.port,
|
||||
workers=args.workers, debug=args.debug, ssl=ssl)
|
||||
except ImportError as e:
|
||||
log.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(e.name))
|
||||
logger.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(e.name))
|
||||
except ValueError as e:
|
||||
log.error("{}".format(e))
|
||||
logger.error("{}".format(e))
|
||||
|
||||
205
sanic/app.py
205
sanic/app.py
@@ -10,11 +10,11 @@ from traceback import format_exc
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
from ssl import create_default_context, Purpose
|
||||
|
||||
from sanic.config import Config, LOGGING
|
||||
from sanic.config import Config
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import ServerError, URLBuildError, SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.log import log
|
||||
from sanic.log import logger, error_logger, LOGGING_CONFIG_DEFAULTS
|
||||
from sanic.response import HTTPResponse, StreamingHTTPResponse
|
||||
from sanic.router import Router
|
||||
from sanic.server import serve, serve_multiple, HttpProtocol, Signal
|
||||
@@ -28,43 +28,33 @@ class Sanic:
|
||||
|
||||
def __init__(self, name=None, router=None, error_handler=None,
|
||||
load_env=True, request_class=None,
|
||||
log_config=LOGGING):
|
||||
if log_config:
|
||||
logging.config.dictConfig(log_config)
|
||||
# Only set up a default log handler if the
|
||||
# end-user application didn't set anything up.
|
||||
if not (logging.root.handlers and
|
||||
log.level == logging.NOTSET and
|
||||
log_config):
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s: %(levelname)s: %(message)s")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
log.setLevel(logging.INFO)
|
||||
strict_slashes=False, log_config=None):
|
||||
|
||||
# Get name from previous stack frame
|
||||
if name is None:
|
||||
frame_records = stack()[1]
|
||||
name = getmodulename(frame_records[1])
|
||||
|
||||
# logging
|
||||
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
||||
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.request_class = request_class
|
||||
self.error_handler = error_handler or ErrorHandler()
|
||||
self.config = Config(load_env=load_env)
|
||||
self.log_config = log_config
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
self.blueprints = {}
|
||||
self._blueprint_order = []
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.strict_slashes = strict_slashes
|
||||
self.listeners = defaultdict(list)
|
||||
self.is_running = False
|
||||
self.is_request_stream = False
|
||||
self.websocket_enabled = False
|
||||
self.websocket_tasks = []
|
||||
self.websocket_tasks = set()
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
@@ -113,7 +103,7 @@ class Sanic:
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, stream=False):
|
||||
strict_slashes=None, stream=False, version=None, name=None):
|
||||
"""Decorate a function to be registered as a route
|
||||
|
||||
:param uri: path of the URL
|
||||
@@ -121,6 +111,8 @@ class Sanic:
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param stream:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
@@ -132,46 +124,64 @@ class Sanic:
|
||||
if stream:
|
||||
self.is_request_stream = True
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def response(handler):
|
||||
if stream:
|
||||
handler.is_stream = stream
|
||||
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||
host=host, strict_slashes=strict_slashes)
|
||||
host=host, strict_slashes=strict_slashes,
|
||||
version=version, name=name)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None, strict_slashes=False):
|
||||
def get(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=frozenset({"GET"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def post(self, uri, host=None, strict_slashes=False, stream=False):
|
||||
def post(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=frozenset({"POST"}), host=host,
|
||||
strict_slashes=strict_slashes, stream=stream)
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
|
||||
def put(self, uri, host=None, strict_slashes=False, stream=False):
|
||||
def put(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=frozenset({"PUT"}), host=host,
|
||||
strict_slashes=strict_slashes, stream=stream)
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
|
||||
def head(self, uri, host=None, strict_slashes=False):
|
||||
def head(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def options(self, uri, host=None, strict_slashes=False):
|
||||
def options(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def patch(self, uri, host=None, strict_slashes=False, stream=False):
|
||||
def patch(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
|
||||
strict_slashes=strict_slashes, stream=stream)
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
|
||||
def delete(self, uri, host=None, strict_slashes=False):
|
||||
def delete(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=frozenset({"DELETE"}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False):
|
||||
strict_slashes=None, version=None, name=None):
|
||||
"""A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
@@ -181,6 +191,9 @@ class Sanic:
|
||||
:param methods: list or tuple of methods allowed, these are overridden
|
||||
if using a HTTPMethodView
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:return: function or class instance
|
||||
"""
|
||||
stream = False
|
||||
@@ -203,14 +216,21 @@ class Sanic:
|
||||
stream = True
|
||||
break
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes, stream=stream)(handler)
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)(handler)
|
||||
return handler
|
||||
|
||||
# Decorator
|
||||
def websocket(self, uri, host=None, strict_slashes=False):
|
||||
def websocket(self, uri, host=None, strict_slashes=None,
|
||||
subprotocols=None, name=None):
|
||||
"""Decorate a function to be registered as a websocket route
|
||||
:param uri: path of the URL
|
||||
:param subprotocols: optional list of strings with the supported
|
||||
subprotocols
|
||||
:param host:
|
||||
:return: decorated function
|
||||
"""
|
||||
@@ -221,6 +241,9 @@ class Sanic:
|
||||
if not uri.startswith('/'):
|
||||
uri = '/' + uri
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def response(handler):
|
||||
async def websocket_handler(request, *args, **kwargs):
|
||||
request.app = self
|
||||
@@ -230,13 +253,13 @@ class Sanic:
|
||||
# On Python3.5 the Transport classes in asyncio do not
|
||||
# have a get_protocol() method as in uvloop
|
||||
protocol = request.transport._protocol
|
||||
ws = await protocol.websocket_handshake(request)
|
||||
ws = await protocol.websocket_handshake(request, subprotocols)
|
||||
|
||||
# schedule the application handler
|
||||
# its future is kept in self.websocket_tasks in case it
|
||||
# needs to be cancelled due to the server being stopped
|
||||
fut = ensure_future(handler(request, ws, *args, **kwargs))
|
||||
self.websocket_tasks.append(fut)
|
||||
self.websocket_tasks.add(fut)
|
||||
try:
|
||||
await fut
|
||||
except (CancelledError, ConnectionClosed):
|
||||
@@ -246,16 +269,19 @@ class Sanic:
|
||||
|
||||
self.router.add(uri=uri, handler=websocket_handler,
|
||||
methods=frozenset({'GET'}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, name=name)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None,
|
||||
strict_slashes=False):
|
||||
strict_slashes=None, name=None):
|
||||
"""A helper method to register a function as a websocket route."""
|
||||
return self.websocket(uri, host=host,
|
||||
strict_slashes=strict_slashes)(handler)
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
return self.websocket(uri, host=host, strict_slashes=strict_slashes,
|
||||
name=name)(handler)
|
||||
|
||||
def enable_websocket(self, enable=True):
|
||||
"""Enable or disable the support for websocket.
|
||||
@@ -319,13 +345,13 @@ class Sanic:
|
||||
# Static Files
|
||||
def static(self, uri, file_or_directory, pattern=r'/?.+',
|
||||
use_modified_since=True, use_content_range=False,
|
||||
stream_large_files=False):
|
||||
stream_large_files=False, name='static', host=None):
|
||||
"""Register 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, use_content_range,
|
||||
stream_large_files)
|
||||
stream_large_files, name, host)
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""Register a blueprint on the application.
|
||||
@@ -375,12 +401,31 @@ class Sanic:
|
||||
URLBuildError
|
||||
"""
|
||||
# find the route by the supplied view name
|
||||
uri, route = self.router.find_route_by_view_name(view_name)
|
||||
kw = {}
|
||||
# special static files url_for
|
||||
if view_name == 'static':
|
||||
kw.update(name=kwargs.pop('name', 'static'))
|
||||
elif view_name.endswith('.static'): # blueprint.static
|
||||
kwargs.pop('name', None)
|
||||
kw.update(name=view_name)
|
||||
|
||||
if not uri or not route:
|
||||
raise URLBuildError(
|
||||
'Endpoint with name `{}` was not found'.format(
|
||||
view_name))
|
||||
uri, route = self.router.find_route_by_view_name(view_name, **kw)
|
||||
if not (uri and route):
|
||||
raise URLBuildError('Endpoint with name `{}` was not found'.format(
|
||||
view_name))
|
||||
|
||||
if view_name == 'static' or view_name.endswith('.static'):
|
||||
filename = kwargs.pop('filename', None)
|
||||
# it's static folder
|
||||
if '<file_uri:' in uri:
|
||||
folder_ = uri.split('<file_uri:', 1)[0]
|
||||
if folder_.endswith('/'):
|
||||
folder_ = folder_[:-1]
|
||||
|
||||
if filename.startswith('/'):
|
||||
filename = filename[1:]
|
||||
|
||||
uri = '{}/{}'.format(folder_, filename)
|
||||
|
||||
if uri != '/' and uri.endswith('/'):
|
||||
uri = uri[:-1]
|
||||
@@ -404,6 +449,16 @@ class Sanic:
|
||||
if netloc is None and external:
|
||||
netloc = self.config.get('SERVER_NAME', '')
|
||||
|
||||
if external:
|
||||
if not scheme:
|
||||
if ':' in netloc[:8]:
|
||||
scheme = netloc[:8].split(':', 1)[0]
|
||||
else:
|
||||
scheme = 'http'
|
||||
|
||||
if '://' in netloc[:8]:
|
||||
netloc = netloc.split('://', 1)[-1]
|
||||
|
||||
for match in matched_params:
|
||||
name, _type, pattern = self.router.parse_parameter_string(
|
||||
match)
|
||||
@@ -520,7 +575,7 @@ class Sanic:
|
||||
response = await self._run_response_middleware(request,
|
||||
response)
|
||||
except:
|
||||
log.exception(
|
||||
error_logger.exception(
|
||||
'Exception occured in one of response middleware handlers'
|
||||
)
|
||||
|
||||
@@ -545,7 +600,7 @@ class Sanic:
|
||||
def run(self, host=None, port=None, debug=False, ssl=None,
|
||||
sock=None, workers=1, protocol=None,
|
||||
backlog=100, stop_event=None, register_sys_signals=True,
|
||||
log_config=None):
|
||||
access_log=True):
|
||||
"""Run the HTTP Server and listen until keyboard interrupt or term
|
||||
signal. On termination, drain connections before closing.
|
||||
|
||||
@@ -563,12 +618,10 @@ class Sanic:
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
|
||||
if sock is None:
|
||||
host, port = host or "127.0.0.1", port or 8000
|
||||
|
||||
if log_config:
|
||||
self.log_config = log_config
|
||||
logging.config.dictConfig(log_config)
|
||||
if protocol is None:
|
||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||
else HttpProtocol)
|
||||
@@ -581,7 +634,7 @@ class Sanic:
|
||||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||
workers=workers, protocol=protocol, backlog=backlog,
|
||||
register_sys_signals=register_sys_signals,
|
||||
has_log=self.log_config is not None)
|
||||
access_log=access_log)
|
||||
|
||||
try:
|
||||
self.is_running = True
|
||||
@@ -590,12 +643,12 @@ class Sanic:
|
||||
else:
|
||||
serve_multiple(server_settings, workers)
|
||||
except:
|
||||
log.exception(
|
||||
error_logger.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
raise
|
||||
finally:
|
||||
self.is_running = False
|
||||
log.info("Server Stopped")
|
||||
logger.info("Server Stopped")
|
||||
|
||||
def stop(self):
|
||||
"""This kills the Sanic"""
|
||||
@@ -608,17 +661,16 @@ class Sanic:
|
||||
async def create_server(self, host=None, port=None, debug=False,
|
||||
ssl=None, sock=None, protocol=None,
|
||||
backlog=100, stop_event=None,
|
||||
log_config=LOGGING):
|
||||
access_log=True):
|
||||
"""Asynchronous version of `run`.
|
||||
|
||||
NOTE: This does not support multiprocessing and is not the preferred
|
||||
way to run a Sanic application.
|
||||
"""
|
||||
|
||||
if sock is None:
|
||||
host, port = host or "127.0.0.1", port or 8000
|
||||
|
||||
if log_config:
|
||||
logging.config.dictConfig(log_config)
|
||||
if protocol is None:
|
||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||
else HttpProtocol)
|
||||
@@ -627,14 +679,31 @@ class Sanic:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("stop_event will be removed from future versions.",
|
||||
DeprecationWarning)
|
||||
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||
loop=get_event_loop(), protocol=protocol,
|
||||
backlog=backlog, run_async=True,
|
||||
has_log=log_config is not None)
|
||||
access_log=access_log)
|
||||
|
||||
# Trigger before_start events
|
||||
await self.trigger_events(
|
||||
server_settings.get('before_start', []),
|
||||
server_settings.get('loop')
|
||||
)
|
||||
|
||||
return await serve(**server_settings)
|
||||
|
||||
async def trigger_events(self, events, loop):
|
||||
"""Trigger events (functions or async)
|
||||
:param events: one or more sync or async functions to execute
|
||||
:param loop: event loop
|
||||
"""
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
if isawaitable(result):
|
||||
await result
|
||||
|
||||
async def _run_request_middleware(self, request):
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
@@ -660,7 +729,7 @@ class Sanic:
|
||||
def _helper(self, host=None, port=None, debug=False,
|
||||
ssl=None, sock=None, workers=1, loop=None,
|
||||
protocol=HttpProtocol, backlog=100, stop_event=None,
|
||||
register_sys_signals=True, run_async=False, has_log=True):
|
||||
register_sys_signals=True, run_async=False, access_log=True):
|
||||
"""Helper function used by `run` and `create_server`."""
|
||||
if isinstance(ssl, dict):
|
||||
# try common aliaseses
|
||||
@@ -694,12 +763,14 @@ class Sanic:
|
||||
'request_handler': self.handle_request,
|
||||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'response_timeout': self.config.RESPONSE_TIMEOUT,
|
||||
'keep_alive_timeout': self.config.KEEP_ALIVE_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,
|
||||
'has_log': has_log,
|
||||
'access_log': access_log,
|
||||
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
|
||||
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE,
|
||||
'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT
|
||||
@@ -723,9 +794,9 @@ class Sanic:
|
||||
server_settings[settings_name] = listeners
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
if self.config.LOGO is not None:
|
||||
log.debug(self.config.LOGO)
|
||||
logger.debug(self.config.LOGO)
|
||||
|
||||
if run_async:
|
||||
server_settings['run_async'] = True
|
||||
@@ -735,6 +806,6 @@ class Sanic:
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
logger.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return server_settings
|
||||
|
||||
@@ -4,8 +4,8 @@ from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
FutureRoute = namedtuple('Route',
|
||||
['handler', 'uri', 'methods',
|
||||
'host', 'strict_slashes', 'stream'])
|
||||
['handler', 'uri', 'methods', 'host',
|
||||
'strict_slashes', 'stream', 'version', 'name'])
|
||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
||||
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
||||
@@ -14,11 +14,16 @@ FutureStatic = namedtuple('Route',
|
||||
|
||||
|
||||
class Blueprint:
|
||||
def __init__(self, name, url_prefix=None, host=None):
|
||||
|
||||
def __init__(self, name,
|
||||
url_prefix=None,
|
||||
host=None, version=None,
|
||||
strict_slashes=False):
|
||||
"""Create a new blueprint
|
||||
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
:param strict_slashes: strict to trailing slash
|
||||
"""
|
||||
self.name = name
|
||||
self.url_prefix = url_prefix
|
||||
@@ -30,6 +35,8 @@ class Blueprint:
|
||||
self.listeners = defaultdict(list)
|
||||
self.middlewares = []
|
||||
self.statics = []
|
||||
self.version = version
|
||||
self.strict_slashes = strict_slashes
|
||||
|
||||
def register(self, app, options):
|
||||
"""Register the blueprint to the sanic app."""
|
||||
@@ -43,13 +50,17 @@ class Blueprint:
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.route(
|
||||
uri=uri[1:] if uri.startswith('//') else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream
|
||||
)(future.handler)
|
||||
|
||||
version = future.version or self.version
|
||||
|
||||
app.route(uri=uri[1:] if uri.startswith('//') else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
for future in self.websocket_routes:
|
||||
# attach the blueprint name to the handler so that it can be
|
||||
@@ -57,11 +68,11 @@ class Blueprint:
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.websocket(
|
||||
uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes
|
||||
)(future.handler)
|
||||
app.websocket(uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
# Middleware
|
||||
for future in self.middlewares:
|
||||
@@ -89,27 +100,35 @@ class Blueprint:
|
||||
app.listener(event)(listener)
|
||||
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, stream=False):
|
||||
strict_slashes=None, stream=False, version=None, name=None):
|
||||
"""Create a blueprint route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
"""
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(
|
||||
handler, uri, methods, host, strict_slashes, stream)
|
||||
handler, uri, methods, host, strict_slashes, stream, version,
|
||||
name)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False):
|
||||
strict_slashes=None, version=None, name=None):
|
||||
"""Create a blueprint route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
@@ -120,26 +139,36 @@ class Blueprint:
|
||||
if getattr(handler.view_class, method.lower(), None):
|
||||
methods.add(method)
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
# handle composition view differently
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes)(handler)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)(handler)
|
||||
return handler
|
||||
|
||||
def websocket(self, uri, host=None, strict_slashes=False):
|
||||
def websocket(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
"""Create a blueprint websocket route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
"""
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(handler, uri, [], host, strict_slashes, False)
|
||||
route = FutureRoute(handler, uri, [], host, strict_slashes,
|
||||
False, version, name)
|
||||
self.websocket_routes.append(route)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None):
|
||||
def add_websocket_route(self, handler, uri, host=None, version=None,
|
||||
name=None):
|
||||
"""Create a blueprint websocket route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
@@ -147,7 +176,7 @@ class Blueprint:
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:return: function or class instance
|
||||
"""
|
||||
self.websocket(uri=uri, host=host)(handler)
|
||||
self.websocket(uri=uri, host=host, version=version, name=name)(handler)
|
||||
return handler
|
||||
|
||||
def listener(self, event):
|
||||
@@ -189,34 +218,53 @@ class Blueprint:
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
name = kwargs.pop('name', 'static')
|
||||
if not name.startswith(self.name + '.'):
|
||||
name = '{}.{}'.format(self.name, name)
|
||||
|
||||
kwargs.update(name=name)
|
||||
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
||||
self.statics.append(static)
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None, strict_slashes=False):
|
||||
def get(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["GET"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def post(self, uri, host=None, strict_slashes=False, stream=False):
|
||||
def post(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["POST"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream)
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
|
||||
def put(self, uri, host=None, strict_slashes=False, stream=False):
|
||||
def put(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["PUT"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream)
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
|
||||
def head(self, uri, host=None, strict_slashes=False):
|
||||
def head(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def options(self, uri, host=None, strict_slashes=False):
|
||||
def options(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def patch(self, uri, host=None, strict_slashes=False, stream=False):
|
||||
def patch(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream)
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version, name=name)
|
||||
|
||||
def delete(self, uri, host=None, strict_slashes=False):
|
||||
def delete(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
106
sanic/config.py
106
sanic/config.py
@@ -1,102 +1,9 @@
|
||||
import os
|
||||
import sys
|
||||
import syslog
|
||||
import platform
|
||||
import types
|
||||
|
||||
from sanic.log import DefaultFilter
|
||||
|
||||
SANIC_PREFIX = 'SANIC_'
|
||||
|
||||
_address_dict = {
|
||||
'Windows': ('localhost', 514),
|
||||
'Darwin': '/var/run/syslog',
|
||||
'Linux': '/dev/log',
|
||||
'FreeBSD': '/dev/log'
|
||||
}
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'filters': {
|
||||
'accessFilter': {
|
||||
'()': DefaultFilter,
|
||||
'param': [0, 10, 20]
|
||||
},
|
||||
'errorFilter': {
|
||||
'()': DefaultFilter,
|
||||
'param': [30, 40, 50]
|
||||
}
|
||||
},
|
||||
'formatters': {
|
||||
'simple': {
|
||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
},
|
||||
'access': {
|
||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: ' +
|
||||
'%(request)s %(message)s %(status)d %(byte)d',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'internal': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'simple',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
'accessStream': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'access',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
'errorStream': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['errorFilter'],
|
||||
'formatter': 'simple',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
# before you use accessSysLog, be sure that log levels
|
||||
# 0, 10, 20 have been enabled in you syslog configuration
|
||||
# otherwise you won't be able to see the output in syslog
|
||||
# logging file.
|
||||
'accessSysLog': {
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': _address_dict.get(platform.system(),
|
||||
('localhost', 514)),
|
||||
'facility': syslog.LOG_DAEMON,
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'access'
|
||||
},
|
||||
'errorSysLog': {
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': _address_dict.get(platform.system(),
|
||||
('localhost', 514)),
|
||||
'facility': syslog.LOG_DAEMON,
|
||||
'filters': ['errorFilter'],
|
||||
'formatter': 'simple'
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'sanic': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['internal', 'errorStream']
|
||||
},
|
||||
'network': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['accessStream', 'errorStream']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# this happens when using container or systems without syslog
|
||||
# keep things in config would cause file not exists error
|
||||
_addr = LOGGING['handlers']['accessSysLog']['address']
|
||||
if type(_addr) is str and not os.path.exists(_addr):
|
||||
LOGGING['handlers'].pop('accessSysLog')
|
||||
LOGGING['handlers'].pop('errorSysLog')
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
||||
@@ -124,13 +31,16 @@ class Config(dict):
|
||||
"""
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
|
||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
self.RESPONSE_TIMEOUT = 60 # 60 seconds
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
|
||||
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
||||
self.WEBSOCKET_MAX_QUEUE = 32
|
||||
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
||||
|
||||
if load_env:
|
||||
self.load_environment_vars()
|
||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||
self.load_environment_vars(prefix=prefix)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
@@ -194,14 +104,14 @@ class Config(dict):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
|
||||
def load_environment_vars(self):
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
Looks for any SANIC_ prefixed environment variables and applies
|
||||
Looks for prefixed environment variables and applies
|
||||
them to the configuration if present.
|
||||
"""
|
||||
for k, v in os.environ.items():
|
||||
if k.startswith(SANIC_PREFIX):
|
||||
_, config_key = k.split(SANIC_PREFIX, 1)
|
||||
if k.startswith(prefix):
|
||||
_, config_key = k.split(prefix, 1)
|
||||
try:
|
||||
self[config_key] = int(v)
|
||||
except ValueError:
|
||||
|
||||
@@ -98,7 +98,8 @@ class Cookie(dict):
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self._keys:
|
||||
raise KeyError("Unknown cookie property")
|
||||
return super().__setitem__(key, value)
|
||||
if value is not False:
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def encode(self, encoding):
|
||||
output = ['%s=%s' % (self.key, _quote(self.value))]
|
||||
|
||||
@@ -155,6 +155,13 @@ class ServerError(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(503)
|
||||
class ServiceUnavailable(SanicException):
|
||||
"""The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state."""
|
||||
pass
|
||||
|
||||
|
||||
class URLBuildError(ServerError):
|
||||
pass
|
||||
|
||||
@@ -170,6 +177,13 @@ class FileNotFound(NotFound):
|
||||
|
||||
@add_status_code(408)
|
||||
class RequestTimeout(SanicException):
|
||||
"""The Web server (running the Web site) thinks that there has been too
|
||||
long an interval of time between 1) the establishment of an IP
|
||||
connection (socket) between the client and the server and
|
||||
2) the receipt of any data on that socket, so the server has dropped
|
||||
the connection. The socket connection has actually been lost - the Web
|
||||
server has 'timed out' on that particular socket connection.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -208,36 +222,57 @@ class Unauthorized(SanicException):
|
||||
"""
|
||||
Unauthorized exception (401 HTTP status code).
|
||||
|
||||
:param message: Message describing the exception.
|
||||
:param status_code: HTTP Status code.
|
||||
:param scheme: Name of the authentication scheme to be used.
|
||||
:param realm: Description of the protected area. (optional)
|
||||
:param challenge: A dict containing values to add to the WWW-Authenticate
|
||||
header that is generated. This is especially useful when dealing with the
|
||||
Digest scheme. (optional)
|
||||
|
||||
When present, kwargs is used to complete the WWW-Authentication header.
|
||||
|
||||
Examples::
|
||||
|
||||
# With a Basic auth-scheme, realm MUST be present:
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Basic",
|
||||
realm="Restricted Area")
|
||||
|
||||
# With a Digest auth-scheme, things are a bit more complicated:
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Digest",
|
||||
realm="Restricted Area",
|
||||
qop="auth, auth-int",
|
||||
algorithm="MD5",
|
||||
nonce="abcdef",
|
||||
opaque="zyxwvu")
|
||||
|
||||
# With a Bearer auth-scheme, realm is optional so you can write:
|
||||
raise Unauthorized("Auth required.", scheme="Bearer")
|
||||
|
||||
# or, if you want to specify the realm:
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Bearer",
|
||||
realm="Restricted Area")
|
||||
"""
|
||||
pass
|
||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||
super().__init__(message, status_code)
|
||||
|
||||
def __init__(self, message, scheme, realm="", challenge=None):
|
||||
super().__init__(message)
|
||||
# if auth-scheme is specified, set "WWW-Authenticate" header
|
||||
if scheme is not None:
|
||||
values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()]
|
||||
challenge = ', '.join(values)
|
||||
|
||||
adds = ""
|
||||
|
||||
if challenge is not None:
|
||||
values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()]
|
||||
adds = ', '.join(values)
|
||||
adds = ', {}'.format(adds)
|
||||
|
||||
self.headers = {
|
||||
"WWW-Authenticate": "{} realm='{}'{}".format(scheme, realm, adds)
|
||||
}
|
||||
self.headers = {
|
||||
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
|
||||
}
|
||||
|
||||
|
||||
def abort(status_code, message=None):
|
||||
"""
|
||||
Raise an exception based on SanicException. Returns the HTTP response
|
||||
message appropriate for the given status code, unless provided.
|
||||
|
||||
:param status_code: The HTTP status code to return.
|
||||
:param message: The HTTP response body. Defaults to the messages
|
||||
in response.py for the given status code.
|
||||
in response.py for the given status code.
|
||||
"""
|
||||
if message is None:
|
||||
message = COMMON_STATUS_CODES.get(status_code,
|
||||
|
||||
@@ -12,7 +12,7 @@ from sanic.exceptions import (
|
||||
TRACEBACK_WRAPPER_HTML,
|
||||
TRACEBACK_WRAPPER_INNER_HTML,
|
||||
TRACEBACK_BORDER)
|
||||
from sanic.log import log
|
||||
from sanic.log import logger
|
||||
from sanic.response import text, html
|
||||
|
||||
|
||||
@@ -86,12 +86,13 @@ class ErrorHandler:
|
||||
self.log(format_exc())
|
||||
if self.debug:
|
||||
url = getattr(request, 'url', 'unknown')
|
||||
response_message = (
|
||||
'Exception raised in exception handler "{}" '
|
||||
'for uri: "{}"\n{}').format(
|
||||
handler.__name__, url, format_exc())
|
||||
log.error(response_message)
|
||||
return text(response_message, 500)
|
||||
response_message = ('Exception raised in exception handler '
|
||||
'"%s" for uri: "%s"\n%s')
|
||||
logger.error(response_message,
|
||||
handler.__name__, url, format_exc())
|
||||
|
||||
return text(response_message % (
|
||||
handler.__name__, url, format_exc()), 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return response
|
||||
@@ -101,7 +102,7 @@ class ErrorHandler:
|
||||
Override this method in an ErrorHandler subclass to prevent
|
||||
logging exceptions.
|
||||
"""
|
||||
getattr(log, level)(message)
|
||||
getattr(logger, level)(message)
|
||||
|
||||
def default(self, request, exception):
|
||||
self.log(format_exc())
|
||||
@@ -114,10 +115,9 @@ class ErrorHandler:
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
response_message = (
|
||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||
request.url, format_exc()))
|
||||
log.error(response_message)
|
||||
response_message = ('Exception occurred while handling uri: '
|
||||
'"%s"\n%s')
|
||||
logger.error(response_message, request.url, format_exc())
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
|
||||
67
sanic/log.py
67
sanic/log.py
@@ -1,18 +1,63 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
class DefaultFilter(logging.Filter):
|
||||
LOGGING_CONFIG_DEFAULTS = dict(
|
||||
version=1,
|
||||
disable_existing_loggers=False,
|
||||
|
||||
def __init__(self, param=None):
|
||||
self.param = param
|
||||
loggers={
|
||||
"root": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"]
|
||||
},
|
||||
"sanic.error": {
|
||||
"level": "INFO",
|
||||
"handlers": ["error_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.error"
|
||||
},
|
||||
|
||||
def filter(self, record):
|
||||
if self.param is None:
|
||||
return True
|
||||
if record.levelno in self.param:
|
||||
return True
|
||||
return False
|
||||
"sanic.access": {
|
||||
"level": "INFO",
|
||||
"handlers": ["access_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.access"
|
||||
}
|
||||
},
|
||||
handlers={
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stdout
|
||||
},
|
||||
"error_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stderr
|
||||
},
|
||||
"access_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "access",
|
||||
"stream": sys.stdout
|
||||
},
|
||||
},
|
||||
formatters={
|
||||
"generic": {
|
||||
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
"class": "logging.Formatter"
|
||||
},
|
||||
"access": {
|
||||
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " +
|
||||
"%(request)s %(message)s %(status)d %(byte)d",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
"class": "logging.Formatter"
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger('sanic')
|
||||
netlog = logging.getLogger('network')
|
||||
logger = logging.getLogger('root')
|
||||
error_logger = logging.getLogger('sanic.error')
|
||||
access_logger = logging.getLogger('sanic.access')
|
||||
|
||||
@@ -17,10 +17,11 @@ except ImportError:
|
||||
json_loads = json.loads
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import log
|
||||
|
||||
from sanic.log import error_logger
|
||||
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
|
||||
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
||||
# > If the media type remains unknown, the recipient SHOULD treat it
|
||||
# > as type "application/octet-stream"
|
||||
@@ -45,7 +46,7 @@ class Request(dict):
|
||||
__slots__ = (
|
||||
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip', '_parsed_url', 'uri_template', 'stream'
|
||||
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr'
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
@@ -68,15 +69,27 @@ class Request(dict):
|
||||
self._cookies = None
|
||||
self.stream = None
|
||||
|
||||
def __repr__(self):
|
||||
if self.method is None or not self.path:
|
||||
return '<{0}>'.format(self.__class__.__name__)
|
||||
return '<{0}: {1} {2}>'.format(self.__class__.__name__,
|
||||
self.method,
|
||||
self.path)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
if self.parsed_json is None:
|
||||
try:
|
||||
self.parsed_json = json_loads(self.body)
|
||||
except Exception:
|
||||
if not self.body:
|
||||
return None
|
||||
raise InvalidUsage("Failed when parsing body as json")
|
||||
self.load_json()
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
def load_json(self, loads=json_loads):
|
||||
try:
|
||||
self.parsed_json = loads(self.body)
|
||||
except Exception:
|
||||
if not self.body:
|
||||
return None
|
||||
raise InvalidUsage("Failed when parsing body as json")
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
@@ -114,7 +127,7 @@ class Request(dict):
|
||||
self.parsed_form, self.parsed_files = (
|
||||
parse_multipart_form(self.body, boundary))
|
||||
except Exception:
|
||||
log.exception("Failed when parsing form")
|
||||
error_logger.exception("Failed when parsing form")
|
||||
|
||||
return self.parsed_form
|
||||
|
||||
@@ -142,7 +155,7 @@ class Request(dict):
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
|
||||
cookie = self.headers.get('Cookie')
|
||||
if cookie is not None:
|
||||
cookies = SimpleCookie()
|
||||
cookies.load(cookie)
|
||||
@@ -159,6 +172,25 @@ class Request(dict):
|
||||
(None, None))
|
||||
return self._ip
|
||||
|
||||
@property
|
||||
def remote_addr(self):
|
||||
"""Attempt to return the original client ip based on X-Forwarded-For.
|
||||
|
||||
:return: original client ip.
|
||||
"""
|
||||
if not hasattr(self, '_remote_addr'):
|
||||
forwarded_for = self.headers.get('X-Forwarded-For', '').split(',')
|
||||
remote_addrs = [
|
||||
addr for addr in [
|
||||
addr.strip() for addr in forwarded_for
|
||||
] if addr
|
||||
]
|
||||
if len(remote_addrs) > 0:
|
||||
self._remote_addr = remote_addrs[0]
|
||||
else:
|
||||
self._remote_addr = ''
|
||||
return self._remote_addr
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
if self.app.websocket_enabled \
|
||||
|
||||
@@ -56,6 +56,7 @@ ALL_STATUS_CODES = {
|
||||
415: b'Unsupported Media Type',
|
||||
416: b'Requested Range Not Satisfiable',
|
||||
417: b'Expectation Failed',
|
||||
418: b'I\'m a teapot',
|
||||
422: b'Unprocessable Entity',
|
||||
423: b'Locked',
|
||||
424: b'Failed Dependency',
|
||||
@@ -63,6 +64,7 @@ ALL_STATUS_CODES = {
|
||||
428: b'Precondition Required',
|
||||
429: b'Too Many Requests',
|
||||
431: b'Request Header Fields Too Large',
|
||||
451: b'Unavailable For Legal Reasons',
|
||||
500: b'Internal Server Error',
|
||||
501: b'Not Implemented',
|
||||
502: b'Bad Gateway',
|
||||
@@ -109,8 +111,9 @@ class BaseHTTPResponse:
|
||||
|
||||
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = (
|
||||
'transport', 'streaming_fn',
|
||||
'status', 'content_type', 'headers', '_cookies')
|
||||
'transport', 'streaming_fn', 'status',
|
||||
'content_type', 'headers', '_cookies'
|
||||
)
|
||||
|
||||
def __init__(self, streaming_fn, status=200, headers=None,
|
||||
content_type='text/plain'):
|
||||
@@ -234,15 +237,17 @@ class HTTPResponse(BaseHTTPResponse):
|
||||
|
||||
|
||||
def json(body, status=200, headers=None,
|
||||
content_type="application/json", **kwargs):
|
||||
content_type="application/json", dumps=json_dumps,
|
||||
**kwargs):
|
||||
"""
|
||||
Returns response object with body in json format.
|
||||
|
||||
:param body: Response data to be serialized.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param kwargs: Remaining arguments that are passed to the json encoder.
|
||||
"""
|
||||
return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
|
||||
return HTTPResponse(dumps(body, **kwargs), headers=headers,
|
||||
status=status, content_type=content_type)
|
||||
|
||||
|
||||
@@ -250,6 +255,7 @@ def text(body, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
"""
|
||||
Returns response object with body in text format.
|
||||
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
@@ -264,6 +270,7 @@ def raw(body, status=200, headers=None,
|
||||
content_type="application/octet-stream"):
|
||||
"""
|
||||
Returns response object without encoding the body.
|
||||
|
||||
:param body: Response data.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
@@ -276,6 +283,7 @@ def raw(body, status=200, headers=None,
|
||||
def html(body, status=200, headers=None):
|
||||
"""
|
||||
Returns response object with body in html format.
|
||||
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
@@ -284,15 +292,22 @@ def html(body, status=200, headers=None):
|
||||
content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
async def file(location, mime_type=None, headers=None, _range=None):
|
||||
async def file(
|
||||
location, mime_type=None, headers=None, filename=None, _range=None):
|
||||
"""Return a response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
:param _range:
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(filename))
|
||||
filename = filename or path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
if _range:
|
||||
@@ -304,24 +319,30 @@ async def file(location, mime_type=None, headers=None, _range=None):
|
||||
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)
|
||||
|
||||
|
||||
async def file_stream(location, chunk_size=4096, mime_type=None, headers=None,
|
||||
_range=None):
|
||||
async def file_stream(
|
||||
location, chunk_size=4096, mime_type=None, headers=None,
|
||||
filename=None, _range=None):
|
||||
"""Return a streaming response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
:param chunk_size: The size of each chunk in the stream (in bytes)
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
:param _range:
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(filename))
|
||||
filename = filename or path.split(location)[-1]
|
||||
|
||||
_file = await open_async(location, mode='rb')
|
||||
|
||||
|
||||
129
sanic/router.py
129
sanic/router.py
@@ -67,6 +67,8 @@ class Router:
|
||||
|
||||
def __init__(self):
|
||||
self.routes_all = {}
|
||||
self.routes_names = {}
|
||||
self.routes_static_files = {}
|
||||
self.routes_static = {}
|
||||
self.routes_dynamic = defaultdict(list)
|
||||
self.routes_always_check = []
|
||||
@@ -91,6 +93,10 @@ class Router:
|
||||
pattern = 'string'
|
||||
if ':' in parameter_string:
|
||||
name, pattern = parameter_string.split(':', 1)
|
||||
if not name:
|
||||
raise ValueError(
|
||||
"Invalid parameter syntax: {}".format(parameter_string)
|
||||
)
|
||||
|
||||
default = (str, pattern)
|
||||
# Pull from pre-configured types
|
||||
@@ -98,32 +104,8 @@ class Router:
|
||||
|
||||
return name, _type, pattern
|
||||
|
||||
def add(self, uri, methods, handler, host=None, strict_slashes=False):
|
||||
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host)
|
||||
|
||||
if strict_slashes:
|
||||
return
|
||||
|
||||
# Add versions with and without trailing /
|
||||
slash_is_missing = (
|
||||
not uri[-1] == '/'
|
||||
and not self.routes_all.get(uri + '/', False)
|
||||
)
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == '/'
|
||||
and not self.routes_all.get(uri[:-1], False)
|
||||
and not uri == '/'
|
||||
)
|
||||
# add version with trailing slash
|
||||
if slash_is_missing:
|
||||
self._add(uri + '/', methods, handler, host)
|
||||
# add version without trailing slash
|
||||
elif without_slash_is_missing:
|
||||
self._add(uri[:-1], methods, handler, host)
|
||||
|
||||
def _add(self, uri, methods, handler, host=None):
|
||||
def add(self, uri, methods, handler, host=None, strict_slashes=False,
|
||||
version=None, name=None):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: path to match
|
||||
@@ -131,6 +113,47 @@ class Router:
|
||||
provided, any method is allowed
|
||||
:param handler: request handler function.
|
||||
When executed, it should provide a response object.
|
||||
:param strict_slashes: strict to trailing slash
|
||||
:param version: current version of the route or blueprint. See
|
||||
docs for further details.
|
||||
:return: Nothing
|
||||
"""
|
||||
if version is not None:
|
||||
if uri.startswith('/'):
|
||||
uri = "/".join(["/v{}".format(str(version)), uri[1:]])
|
||||
else:
|
||||
uri = "/".join(["/v{}".format(str(version)), uri])
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host, name)
|
||||
|
||||
if strict_slashes:
|
||||
return
|
||||
|
||||
# Add versions with and without trailing /
|
||||
slash_is_missing = (
|
||||
not uri[-1] == '/' and not self.routes_all.get(uri + '/', False)
|
||||
)
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == '/' and not
|
||||
self.routes_all.get(uri[:-1], False) and not
|
||||
uri == '/'
|
||||
)
|
||||
# add version with trailing slash
|
||||
if slash_is_missing:
|
||||
self._add(uri + '/', methods, handler, host, name)
|
||||
# add version without trailing slash
|
||||
elif without_slash_is_missing:
|
||||
self._add(uri[:-1], methods, handler, host, name)
|
||||
|
||||
def _add(self, uri, methods, handler, host=None, name=None):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: path to match
|
||||
:param methods: sequence of accepted method names. If none are
|
||||
provided, any method is allowed
|
||||
:param handler: request handler function.
|
||||
When executed, it should provide a response object.
|
||||
:param name: user defined route name for url_for
|
||||
:return: Nothing
|
||||
"""
|
||||
if host is not None:
|
||||
@@ -144,7 +167,7 @@ class Router:
|
||||
"host strings, not {!r}".format(host))
|
||||
|
||||
for host_ in host:
|
||||
self.add(uri, methods, handler, host_)
|
||||
self.add(uri, methods, handler, host_, name)
|
||||
return
|
||||
|
||||
# Dict for faster lookups of if method allowed
|
||||
@@ -212,22 +235,38 @@ class Router:
|
||||
else:
|
||||
route = self.routes_all.get(uri)
|
||||
|
||||
# prefix the handler name with the blueprint name
|
||||
# if available
|
||||
# special prefix for static files
|
||||
is_static = False
|
||||
if name and name.startswith('_static_'):
|
||||
is_static = True
|
||||
name = name.split('_static_', 1)[-1]
|
||||
|
||||
if hasattr(handler, '__blueprintname__'):
|
||||
handler_name = '{}.{}'.format(
|
||||
handler.__blueprintname__, name or handler.__name__)
|
||||
else:
|
||||
handler_name = name or getattr(handler, '__name__', None)
|
||||
|
||||
if route:
|
||||
route = merge_route(route, methods, handler)
|
||||
else:
|
||||
# prefix the handler name with the blueprint name
|
||||
# if available
|
||||
if hasattr(handler, '__blueprintname__'):
|
||||
handler_name = '{}.{}'.format(
|
||||
handler.__blueprintname__, handler.__name__)
|
||||
else:
|
||||
handler_name = getattr(handler, '__name__', None)
|
||||
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters, name=handler_name, uri=uri)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if is_static:
|
||||
pair = self.routes_static_files.get(handler_name)
|
||||
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
|
||||
self.routes_static_files[handler_name] = (uri, route)
|
||||
|
||||
else:
|
||||
pair = self.routes_names.get(handler_name)
|
||||
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
|
||||
self.routes_names[handler_name] = (uri, route)
|
||||
|
||||
if properties['unhashable']:
|
||||
self.routes_always_check.append(route)
|
||||
elif parameters:
|
||||
@@ -248,6 +287,16 @@ class Router:
|
||||
uri = host + uri
|
||||
try:
|
||||
route = self.routes_all.pop(uri)
|
||||
for handler_name, pairs in self.routes_names.items():
|
||||
if pairs[0] == uri:
|
||||
self.routes_names.pop(handler_name)
|
||||
break
|
||||
|
||||
for handler_name, pairs in self.routes_static_files.items():
|
||||
if pairs[0] == uri:
|
||||
self.routes_static_files.pop(handler_name)
|
||||
break
|
||||
|
||||
except KeyError:
|
||||
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
|
||||
|
||||
@@ -263,20 +312,20 @@ class Router:
|
||||
self._get.cache_clear()
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def find_route_by_view_name(self, view_name):
|
||||
def find_route_by_view_name(self, view_name, name=None):
|
||||
"""Find a route in the router based on the specified view name.
|
||||
|
||||
:param view_name: string of view name to search by
|
||||
:param kwargs: additional params, usually for static files
|
||||
:return: tuple containing (uri, Route)
|
||||
"""
|
||||
if not view_name:
|
||||
return (None, None)
|
||||
|
||||
for uri, route in self.routes_all.items():
|
||||
if route.name == view_name:
|
||||
return uri, route
|
||||
if view_name == 'static' or view_name.endswith('.static'):
|
||||
return self.routes_static_files.get(name, (None, None))
|
||||
|
||||
return (None, None)
|
||||
return self.routes_names.get(view_name, (None, None))
|
||||
|
||||
def get(self, request):
|
||||
"""Get a request handler based on the URL of the request, or raises an
|
||||
|
||||
288
sanic/server.py
288
sanic/server.py
@@ -24,11 +24,12 @@ try:
|
||||
except ImportError:
|
||||
async_loop = asyncio
|
||||
|
||||
from sanic.log import log, netlog
|
||||
from sanic.log import logger, access_logger
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.request import Request
|
||||
from sanic.exceptions import (
|
||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
|
||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError,
|
||||
ServiceUnavailable)
|
||||
|
||||
current_time = None
|
||||
|
||||
@@ -63,17 +64,20 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# request params
|
||||
'parser', 'request', 'url', 'headers',
|
||||
# request config
|
||||
'request_handler', 'request_timeout', 'request_max_size',
|
||||
'request_class', 'is_request_stream', 'router',
|
||||
# enable or disable access log / error log purpose
|
||||
'has_log',
|
||||
'request_handler', 'request_timeout', 'response_timeout',
|
||||
'keep_alive_timeout', 'request_max_size', 'request_class',
|
||||
'is_request_stream', 'router',
|
||||
# enable or disable access log purpose
|
||||
'access_log',
|
||||
# connection management
|
||||
'_total_request_size', '_timeout_handler', '_last_communication_time',
|
||||
'_is_stream_handler')
|
||||
'_total_request_size', '_request_timeout_handler',
|
||||
'_response_timeout_handler', '_keep_alive_timeout_handler',
|
||||
'_last_request_time', '_last_response_time', '_is_stream_handler')
|
||||
|
||||
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,
|
||||
response_timeout=60, keep_alive_timeout=5,
|
||||
request_max_size=None, request_class=None, access_log=True,
|
||||
keep_alive=True, is_request_stream=False, router=None,
|
||||
state=None, debug=False, **kwargs):
|
||||
self.loop = loop
|
||||
@@ -84,18 +88,23 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.headers = None
|
||||
self.router = router
|
||||
self.signal = signal
|
||||
self.has_log = has_log
|
||||
self.access_log = access_log
|
||||
self.connections = connections
|
||||
self.request_handler = request_handler
|
||||
self.error_handler = error_handler
|
||||
self.request_timeout = request_timeout
|
||||
self.response_timeout = response_timeout
|
||||
self.keep_alive_timeout = keep_alive_timeout
|
||||
self.request_max_size = request_max_size
|
||||
self.request_class = request_class or Request
|
||||
self.is_request_stream = is_request_stream
|
||||
self._is_stream_handler = False
|
||||
self._total_request_size = 0
|
||||
self._timeout_handler = None
|
||||
self._request_timeout_handler = None
|
||||
self._response_timeout_handler = None
|
||||
self._keep_alive_timeout_handler = None
|
||||
self._last_request_time = None
|
||||
self._last_response_time = None
|
||||
self._request_handler_task = None
|
||||
self._request_stream_task = None
|
||||
self._keep_alive = keep_alive
|
||||
@@ -118,29 +127,72 @@ class HttpProtocol(asyncio.Protocol):
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.connections.add(self)
|
||||
self._timeout_handler = self.loop.call_later(
|
||||
self.request_timeout, self.connection_timeout)
|
||||
self._request_timeout_handler = self.loop.call_later(
|
||||
self.request_timeout, self.request_timeout_callback)
|
||||
self.transport = transport
|
||||
self._last_request_time = current_time
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.connections.discard(self)
|
||||
self._timeout_handler.cancel()
|
||||
if self._request_timeout_handler:
|
||||
self._request_timeout_handler.cancel()
|
||||
if self._response_timeout_handler:
|
||||
self._response_timeout_handler.cancel()
|
||||
if self._keep_alive_timeout_handler:
|
||||
self._keep_alive_timeout_handler.cancel()
|
||||
|
||||
def connection_timeout(self):
|
||||
# Check if
|
||||
def request_timeout_callback(self):
|
||||
# See the docstring in the RequestTimeout exception, to see
|
||||
# exactly what this timeout is checking for.
|
||||
# Check if elapsed time since request initiated exceeds our
|
||||
# configured maximum request timeout value
|
||||
time_elapsed = current_time - self._last_request_time
|
||||
if time_elapsed < self.request_timeout:
|
||||
time_left = self.request_timeout - time_elapsed
|
||||
self._timeout_handler = (
|
||||
self.loop.call_later(time_left, self.connection_timeout))
|
||||
self._request_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.request_timeout_callback)
|
||||
)
|
||||
else:
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
exception = RequestTimeout('Request Timeout')
|
||||
self.write_error(exception)
|
||||
try:
|
||||
raise RequestTimeout('Request Timeout')
|
||||
except RequestTimeout as exception:
|
||||
self.write_error(exception)
|
||||
|
||||
def response_timeout_callback(self):
|
||||
# Check if elapsed time since response was initiated exceeds our
|
||||
# configured maximum request timeout value
|
||||
time_elapsed = current_time - self._last_request_time
|
||||
if time_elapsed < self.response_timeout:
|
||||
time_left = self.response_timeout - time_elapsed
|
||||
self._response_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.response_timeout_callback)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
raise ServiceUnavailable('Response Timeout')
|
||||
except ServiceUnavailable as exception:
|
||||
self.write_error(exception)
|
||||
|
||||
def keep_alive_timeout_callback(self):
|
||||
# Check if elapsed time since last response exceeds our configured
|
||||
# maximum keep alive timeout value
|
||||
time_elapsed = current_time - self._last_response_time
|
||||
if time_elapsed < self.keep_alive_timeout:
|
||||
time_left = self.keep_alive_timeout - time_elapsed
|
||||
self._keep_alive_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.keep_alive_timeout_callback)
|
||||
)
|
||||
else:
|
||||
logger.info('KeepAlive Timeout. Closing connection.')
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Parsing
|
||||
@@ -187,10 +239,12 @@ class HttpProtocol(asyncio.Protocol):
|
||||
and int(value) > self.request_max_size:
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
|
||||
try:
|
||||
value = value.decode()
|
||||
except UnicodeDecodeError:
|
||||
value = value.decode('latin_1')
|
||||
self.headers.append(
|
||||
(self._header_fragment.decode().casefold(),
|
||||
value.decode()))
|
||||
(self._header_fragment.decode().casefold(), value))
|
||||
|
||||
self._header_fragment = b''
|
||||
|
||||
@@ -202,6 +256,11 @@ class HttpProtocol(asyncio.Protocol):
|
||||
method=self.parser.get_method().decode(),
|
||||
transport=self.transport
|
||||
)
|
||||
# Remove any existing KeepAlive handler here,
|
||||
# It will be recreated if required on the new request.
|
||||
if self._keep_alive_timeout_handler:
|
||||
self._keep_alive_timeout_handler.cancel()
|
||||
self._keep_alive_timeout_handler = None
|
||||
if self.is_request_stream:
|
||||
self._is_stream_handler = self.router.is_stream_handler(
|
||||
self.request)
|
||||
@@ -217,6 +276,11 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.request.body.append(body)
|
||||
|
||||
def on_message_complete(self):
|
||||
# Entire request (headers and whole body) is received.
|
||||
# We can cancel and remove the request timeout handler now.
|
||||
if self._request_timeout_handler:
|
||||
self._request_timeout_handler.cancel()
|
||||
self._request_timeout_handler = None
|
||||
if self.is_request_stream and self._is_stream_handler:
|
||||
self._request_stream_task = self.loop.create_task(
|
||||
self.request.stream.put(None))
|
||||
@@ -225,6 +289,9 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.execute_request_handler()
|
||||
|
||||
def execute_request_handler(self):
|
||||
self._response_timeout_handler = self.loop.call_later(
|
||||
self.response_timeout, self.response_timeout_callback)
|
||||
self._last_request_time = current_time
|
||||
self._request_handler_task = self.loop.create_task(
|
||||
self.request_handler(
|
||||
self.request,
|
||||
@@ -234,35 +301,52 @@ class HttpProtocol(asyncio.Protocol):
|
||||
# -------------------------------------------- #
|
||||
# Responding
|
||||
# -------------------------------------------- #
|
||||
def log_response(self, response):
|
||||
if self.access_log:
|
||||
extra = {
|
||||
'status': getattr(response, 'status', 0),
|
||||
}
|
||||
|
||||
if isinstance(response, HTTPResponse):
|
||||
extra['byte'] = len(response.body)
|
||||
else:
|
||||
extra['byte'] = -1
|
||||
|
||||
if self.request is not None:
|
||||
extra['host'] = '{0}:{1}'.format(self.request.ip[0],
|
||||
self.request.ip[1])
|
||||
extra['request'] = '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
else:
|
||||
extra['host'] = 'UNKNOWN'
|
||||
extra['request'] = 'nil'
|
||||
|
||||
access_logger.info('', extra=extra)
|
||||
|
||||
def write_response(self, response):
|
||||
"""
|
||||
Writes response content synchronously to the transport.
|
||||
"""
|
||||
if self._response_timeout_handler:
|
||||
self._response_timeout_handler.cancel()
|
||||
self._response_timeout_handler = None
|
||||
try:
|
||||
keep_alive = self.keep_alive
|
||||
self.transport.write(
|
||||
response.output(
|
||||
self.request.version, keep_alive,
|
||||
self.request_timeout))
|
||||
if self.has_log:
|
||||
netlog.info('', extra={
|
||||
'status': response.status,
|
||||
'byte': len(response.body),
|
||||
'host': '{0}:{1}'.format(self.request.ip[0],
|
||||
self.request.ip[1]),
|
||||
'request': '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
})
|
||||
self.keep_alive_timeout))
|
||||
self.log_response(response)
|
||||
except AttributeError:
|
||||
log.error(
|
||||
('Invalid response object for url {}, '
|
||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
||||
self.url, type(response)))
|
||||
logger.error('Invalid response object for url %s, '
|
||||
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before response written @ {}'.format(
|
||||
self.request.ip))
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
@@ -270,8 +354,12 @@ class HttpProtocol(asyncio.Protocol):
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._last_request_time = current_time
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._last_response_time = current_time
|
||||
self.cleanup()
|
||||
|
||||
async def stream_response(self, response):
|
||||
@@ -280,31 +368,25 @@ class HttpProtocol(asyncio.Protocol):
|
||||
the transport to the response so the response consumer can
|
||||
write to the response as needed.
|
||||
"""
|
||||
|
||||
if self._response_timeout_handler:
|
||||
self._response_timeout_handler.cancel()
|
||||
self._response_timeout_handler = None
|
||||
try:
|
||||
keep_alive = self.keep_alive
|
||||
response.transport = self.transport
|
||||
await response.stream(
|
||||
self.request.version, keep_alive, self.request_timeout)
|
||||
if self.has_log:
|
||||
netlog.info('', extra={
|
||||
'status': response.status,
|
||||
'byte': -1,
|
||||
'host': '{0}:{1}'.format(self.request.ip[0],
|
||||
self.request.ip[1]),
|
||||
'request': '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
})
|
||||
self.request.version, keep_alive, self.keep_alive_timeout)
|
||||
self.log_response(response)
|
||||
except AttributeError:
|
||||
log.error(
|
||||
('Invalid response object for url {}, '
|
||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
||||
self.url, type(response)))
|
||||
logger.error('Invalid response object for url %s, '
|
||||
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before response written @ {}'.format(
|
||||
self.request.ip))
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
@@ -312,55 +394,55 @@ class HttpProtocol(asyncio.Protocol):
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._last_request_time = current_time
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._last_response_time = current_time
|
||||
self.cleanup()
|
||||
|
||||
def write_error(self, exception):
|
||||
# An error _is_ a response.
|
||||
# Don't throw a response timeout, when a response _is_ given.
|
||||
if self._response_timeout_handler:
|
||||
self._response_timeout_handler.cancel()
|
||||
self._response_timeout_handler = None
|
||||
response = None
|
||||
try:
|
||||
response = self.error_handler.response(self.request, exception)
|
||||
version = self.request.version if self.request else '1.1'
|
||||
self.transport.write(response.output(version))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before error written @ {}'.format(
|
||||
self.request.ip if self.request else 'Unknown'))
|
||||
if self._debug:
|
||||
logger.error('Connection lost before error written @ %s',
|
||||
self.request.ip if self.request else 'Unknown')
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing error failed, connection closed {}".format(repr(e)),
|
||||
from_error=True)
|
||||
"Writing error failed, connection closed {}".format(
|
||||
repr(e)), from_error=True
|
||||
)
|
||||
finally:
|
||||
if self.has_log:
|
||||
extra = {
|
||||
'status': response.status,
|
||||
'host': '',
|
||||
'request': str(self.request) + str(self.url)
|
||||
}
|
||||
if response and isinstance(response, HTTPResponse):
|
||||
extra['byte'] = len(response.body)
|
||||
else:
|
||||
extra['byte'] = -1
|
||||
if self.request:
|
||||
extra['host'] = '%s:%d' % self.request.ip,
|
||||
extra['request'] = '%s %s' % (self.request.method,
|
||||
self.url)
|
||||
netlog.info('', extra=extra)
|
||||
if self.parser and (self.keep_alive
|
||||
or getattr(response, 'status', 0) == 408):
|
||||
self.log_response(response)
|
||||
self.transport.close()
|
||||
|
||||
def bail_out(self, message, from_error=False):
|
||||
if from_error or self.transport.is_closing():
|
||||
log.error(
|
||||
("Transport closed @ {} and exception "
|
||||
"experienced during error handling").format(
|
||||
self.transport.get_extra_info('peername')))
|
||||
log.debug(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
logger.error("Transport closed @ %s and exception "
|
||||
"experienced during error handling",
|
||||
self.transport.get_extra_info('peername'))
|
||||
logger.debug('Exception:\n%s', traceback.format_exc())
|
||||
else:
|
||||
exception = ServerError(message)
|
||||
self.write_error(exception)
|
||||
log.error(message)
|
||||
logger.error(message)
|
||||
|
||||
def cleanup(self):
|
||||
"""This is called when KeepAlive feature is used,
|
||||
it resets the connection in order for it to be able
|
||||
to handle receiving another request on the same connection."""
|
||||
self.parser = None
|
||||
self.request = None
|
||||
self.url = None
|
||||
@@ -415,12 +497,13 @@ def trigger_events(events, loop):
|
||||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
request_timeout=60, response_timeout=60, keep_alive_timeout=5,
|
||||
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, keep_alive=True,
|
||||
is_request_stream=False, router=None, websocket_max_size=None,
|
||||
websocket_max_queue=None, state=None,
|
||||
signal=Signal(), request_class=None, access_log=True,
|
||||
keep_alive=True, is_request_stream=False, router=None,
|
||||
websocket_max_size=None, websocket_max_queue=None, state=None,
|
||||
graceful_shutdown_timeout=15.0):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
@@ -440,6 +523,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
`app` instance and `loop`
|
||||
:param debug: enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
:param response_timeout: time in seconds
|
||||
:param keep_alive_timeout: time in seconds
|
||||
:param ssl: SSLContext
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param request_max_size: size in bytes, `None` for no limit
|
||||
@@ -447,7 +532,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
:param loop: asyncio compatible event loop
|
||||
:param protocol: subclass of asyncio protocol class
|
||||
:param request_class: Request class to use
|
||||
:param has_log: disable/enable access log and error log
|
||||
:param access_log: disable/enable access log
|
||||
:param is_request_stream: disable/enable Request.stream
|
||||
:param router: Router object
|
||||
:return: Nothing
|
||||
@@ -468,9 +553,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
request_handler=request_handler,
|
||||
error_handler=error_handler,
|
||||
request_timeout=request_timeout,
|
||||
response_timeout=response_timeout,
|
||||
keep_alive_timeout=keep_alive_timeout,
|
||||
request_max_size=request_max_size,
|
||||
request_class=request_class,
|
||||
has_log=has_log,
|
||||
access_log=access_log,
|
||||
keep_alive=keep_alive,
|
||||
is_request_stream=is_request_stream,
|
||||
router=router,
|
||||
@@ -502,7 +589,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
try:
|
||||
http_server = loop.run_until_complete(server_coroutine)
|
||||
except:
|
||||
log.exception("Unable to start server")
|
||||
logger.exception("Unable to start server")
|
||||
return
|
||||
|
||||
trigger_events(after_start, loop)
|
||||
@@ -513,14 +600,14 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
try:
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
except NotImplementedError:
|
||||
log.warn('Sanic tried to use loop.add_signal_handler but it is'
|
||||
' not implemented on this platform.')
|
||||
logger.warning('Sanic tried to use loop.add_signal_handler '
|
||||
'but it is not implemented on this platform.')
|
||||
pid = os.getpid()
|
||||
try:
|
||||
log.info('Starting worker [{}]'.format(pid))
|
||||
logger.info('Starting worker [%s]', pid)
|
||||
loop.run_forever()
|
||||
finally:
|
||||
log.info("Stopping worker [{}]".format(pid))
|
||||
logger.info("Stopping worker [%s]", pid)
|
||||
|
||||
# Run the on_stop function if provided
|
||||
trigger_events(before_stop, loop)
|
||||
@@ -582,8 +669,7 @@ def serve_multiple(server_settings, workers):
|
||||
server_settings['port'] = None
|
||||
|
||||
def sig_handler(signal, frame):
|
||||
log.info("Received signal {}. Shutting down.".format(
|
||||
Signals(signal).name))
|
||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||
for process in processes:
|
||||
os.kill(process.pid, SIGINT)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from sanic.response import file, file_stream, HTTPResponse
|
||||
|
||||
def register(app, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range,
|
||||
stream_large_files):
|
||||
stream_large_files, name='static', host=None):
|
||||
# TODO: Though sanic is not a file server, I feel like we should at least
|
||||
# make a good effort here. Modified-since is nice, but we could
|
||||
# also look into etags, expires, and caching
|
||||
@@ -39,6 +39,7 @@ def register(app, uri, file_or_directory, pattern,
|
||||
than the file() handler to send the file
|
||||
If this is an integer, this represents the
|
||||
threshold size to switch to file_stream()
|
||||
:param name: user defined name used for url_for
|
||||
"""
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
@@ -117,4 +118,8 @@ def register(app, uri, file_or_directory, pattern,
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
|
||||
app.route(uri, methods=['GET', 'HEAD'])(_handler)
|
||||
# special prefix for static files
|
||||
if not name.startswith('_static_'):
|
||||
name = '_static_{}'.format(name)
|
||||
|
||||
app.route(uri, methods=['GET', 'HEAD'], name=name, host=host)(_handler)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import traceback
|
||||
from json import JSONDecodeError
|
||||
|
||||
from sanic.log import log
|
||||
from sanic.log import logger
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 42101
|
||||
@@ -19,7 +19,7 @@ class SanicTestClient:
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=PORT, uri=uri)
|
||||
|
||||
log.info(url)
|
||||
logger.info(url)
|
||||
conn = aiohttp.TCPConnector(verify_ssl=False)
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, connector=conn) as session:
|
||||
@@ -61,7 +61,7 @@ class SanicTestClient:
|
||||
**request_kwargs)
|
||||
results[-1] = response
|
||||
except Exception as e:
|
||||
log.error(
|
||||
logger.error(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
exceptions.append(e)
|
||||
self.app.stop()
|
||||
|
||||
@@ -13,10 +13,18 @@ class WebSocketProtocol(HttpProtocol):
|
||||
self.websocket_max_size = websocket_max_size
|
||||
self.websocket_max_queue = websocket_max_queue
|
||||
|
||||
def connection_timeout(self):
|
||||
# timeouts make no sense for websocket routes
|
||||
# timeouts make no sense for websocket routes
|
||||
def request_timeout_callback(self):
|
||||
if self.websocket is None:
|
||||
super().connection_timeout()
|
||||
super().request_timeout_callback()
|
||||
|
||||
def response_timeout_callback(self):
|
||||
if self.websocket is None:
|
||||
super().response_timeout_callback()
|
||||
|
||||
def keep_alive_timeout_callback(self):
|
||||
if self.websocket is None:
|
||||
super().keep_alive_timeout_callback()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if self.websocket is not None:
|
||||
@@ -41,7 +49,7 @@ class WebSocketProtocol(HttpProtocol):
|
||||
else:
|
||||
super().write_response(response)
|
||||
|
||||
async def websocket_handshake(self, request):
|
||||
async def websocket_handshake(self, request, subprotocols=None):
|
||||
# let the websockets package do the handshake with the client
|
||||
headers = []
|
||||
|
||||
@@ -57,6 +65,17 @@ class WebSocketProtocol(HttpProtocol):
|
||||
except InvalidHandshake:
|
||||
raise InvalidUsage('Invalid websocket request')
|
||||
|
||||
subprotocol = None
|
||||
if subprotocols and 'Sec-Websocket-Protocol' in request.headers:
|
||||
# select a subprotocol
|
||||
client_subprotocols = [p.strip() for p in request.headers[
|
||||
'Sec-Websocket-Protocol'].split(',')]
|
||||
for p in client_subprotocols:
|
||||
if p in subprotocols:
|
||||
subprotocol = p
|
||||
set_header('Sec-Websocket-Protocol', subprotocol)
|
||||
break
|
||||
|
||||
# write the 101 response back to the client
|
||||
rv = b'HTTP/1.1 101 Switching Protocols\r\n'
|
||||
for k, v in headers:
|
||||
@@ -69,5 +88,6 @@ class WebSocketProtocol(HttpProtocol):
|
||||
max_size=self.websocket_max_size,
|
||||
max_queue=self.websocket_max_queue
|
||||
)
|
||||
self.websocket.subprotocol = subprotocol
|
||||
self.websocket.connection_made(request.transport)
|
||||
return self.websocket
|
||||
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
import signal
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
try:
|
||||
import ssl
|
||||
@@ -73,10 +74,16 @@ class GunicornWorker(base.Worker):
|
||||
trigger_events(self._server_settings.get('before_stop', []),
|
||||
self.loop)
|
||||
self.loop.run_until_complete(self.close())
|
||||
except:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
trigger_events(self._server_settings.get('after_stop', []),
|
||||
self.loop)
|
||||
self.loop.close()
|
||||
try:
|
||||
trigger_events(self._server_settings.get('after_stop', []),
|
||||
self.loop)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
self.loop.close()
|
||||
|
||||
sys.exit(self.exit_code)
|
||||
|
||||
@@ -139,8 +146,8 @@ class GunicornWorker(base.Worker):
|
||||
)
|
||||
if self.max_requests and req_count > self.max_requests:
|
||||
self.alive = False
|
||||
self.log.info(
|
||||
"Max requests exceeded, shutting down: %s", self)
|
||||
self.log.info("Max requests exceeded, shutting down: %s",
|
||||
self)
|
||||
elif pid == os.getpid() and self.ppid != os.getppid():
|
||||
self.alive = False
|
||||
self.log.info("Parent changed, shutting down: %s", self)
|
||||
|
||||
Reference in New Issue
Block a user