sanic/sanic/sanic.py

365 lines
13 KiB
Python
Raw Normal View History

2016-10-18 09:22:49 +01:00
from asyncio import get_event_loop
from collections import deque
from functools import partial
2016-11-19 01:06:16 +00:00
from inspect import isawaitable, stack, getmodulename
2016-10-18 09:22:49 +01:00
from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT
from time import sleep
2016-10-15 20:59:00 +01:00
from traceback import format_exc
2016-12-23 05:00:57 +00:00
import logging
2016-10-15 20:59:00 +01:00
from .config import Config
from .exceptions import Handler
2016-12-23 05:00:57 +00:00
from .log import log
2016-10-15 20:59:00 +01:00
from .response import HTTPResponse
from .router import Router
from .server import serve
from .static import register as static_register
2016-10-15 20:59:00 +01:00
from .exceptions import ServerError
class Sanic:
2016-12-23 05:00:57 +00:00
def __init__(self, name=None, router=None,
error_handler=None, logger=None):
if logger is None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s: %(levelname)s: %(message)s"
)
2016-11-19 01:06:16 +00:00
if name is None:
frame_records = stack()[1]
name = getmodulename(frame_records[1])
2016-10-15 20:59:00 +01:00
self.name = name
self.router = router or Router()
self.error_handler = error_handler or Handler(self)
self.config = Config()
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
self._blueprint_order = []
self.loop = None
self.debug = None
2016-10-15 20:59:00 +01:00
# Register alternative method names
self.go_fast = self.run
2016-10-15 20:59:00 +01:00
# -------------------------------------------------------------------- #
# Registration
# -------------------------------------------------------------------- #
# Decorator
def route(self, uri, methods=None):
"""
Decorates a function to be registered as a route
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: decorated function
"""
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith('/'):
uri = '/' + uri
2016-10-15 20:59:00 +01:00
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler)
return handler
return response
def add_route(self, handler, uri, methods=None):
"""
2016-11-25 07:29:25 +00:00
A helper method to register class instance or
functions as a handler to the application url
routes.
:param handler: function or class instance
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: function or class instance
"""
self.route(uri=uri, methods=methods)(handler)
return handler
2016-10-15 20:59:00 +01:00
# Decorator
def exception(self, *exceptions):
"""
2016-10-18 10:50:28 +01:00
Decorates a function to be registered as a handler for exceptions
:param *exceptions: exceptions
2016-10-15 20:59:00 +01:00
:return: decorated function
"""
def response(handler):
for exception in exceptions:
self.error_handler.add(exception, handler)
return handler
return response
# Decorator
def middleware(self, *args, **kwargs):
"""
Decorates and registers middleware to be called before a request
can either be called as @app.middleware or @app.middleware('request')
"""
attach_to = 'request'
def register_middleware(middleware):
if attach_to == 'request':
self.request_middleware.append(middleware)
if attach_to == 'response':
self.response_middleware.appendleft(middleware)
2016-10-15 20:59:00 +01:00
return middleware
# Detect which way this was called, @middleware or @middleware('AT')
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
return register_middleware(args[0])
else:
attach_to = args[0]
return register_middleware
# Static Files
2016-10-25 10:45:28 +01:00
def static(self, uri, file_or_directory, pattern='.+',
use_modified_since=True):
"""
Registers a root to serve files from. The input can either be a file
or a directory. See
"""
2016-10-25 10:45:28 +01:00
static_register(self, uri, file_or_directory, pattern,
use_modified_since)
def blueprint(self, blueprint, **options):
"""
Registers a blueprint on the application.
:param blueprint: Blueprint object
:param options: option dictionary with blueprint defaults
:return: Nothing
"""
if blueprint.name in self.blueprints:
assert self.blueprints[blueprint.name] is blueprint, \
'A blueprint with the name "%s" is already registered. ' \
'Blueprint names must be unique.' % \
(blueprint.name,)
else:
self.blueprints[blueprint.name] = blueprint
self._blueprint_order.append(blueprint)
blueprint.register(self, options)
def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0
log.warning("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method instead")
return self.blueprint(*args, **kwargs)
2016-10-15 20:59:00 +01:00
# -------------------------------------------------------------------- #
# Request Handling
# -------------------------------------------------------------------- #
def converted_response_type(self, response):
pass
2016-10-15 20:59:00 +01:00
async def handle_request(self, request, response_callback):
"""
2016-10-16 14:01:59 +01:00
Takes a request from the HTTP Server and returns a response object to
be sent back The HTTP Server only expects a response object, so
exception handling must be done here
2016-10-15 20:59:00 +01:00
:param request: HTTP Request object
2016-10-16 14:01:59 +01:00
:param response_callback: Response function to be called with the
response as the only argument
2016-10-15 20:59:00 +01:00
:return: Nothing
"""
try:
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
2016-10-15 20:59:00 +01:00
response = False
# The if improves speed. I don't know why
if self.request_middleware:
for middleware in self.request_middleware:
response = middleware(request)
if isawaitable(response):
response = await response
if response:
break
# No middleware results
if not response:
# -------------------------------------------- #
# Execute Handler
# -------------------------------------------- #
2016-10-15 20:59:00 +01:00
# Fetch handler from router
handler, args, kwargs = self.router.get(request)
if handler is None:
2016-10-16 14:01:59 +01:00
raise ServerError(
("'None' was returned while requesting a "
"handler from the router"))
2016-10-15 20:59:00 +01:00
# Run response handler
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
2016-10-15 20:59:00 +01:00
except Exception as e:
# -------------------------------------------- #
# Response Generation Failed
# -------------------------------------------- #
2016-10-15 20:59:00 +01:00
try:
response = self.error_handler.response(request, e)
if isawaitable(response):
response = await response
except Exception as e:
if self.debug:
2016-10-16 14:01:59 +01:00
response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format(
e, format_exc()))
2016-10-15 20:59:00 +01:00
else:
2016-10-16 14:01:59 +01:00
response = HTTPResponse(
"An error occured while handling an error")
2016-10-15 20:59:00 +01:00
response_callback(response)
# -------------------------------------------------------------------- #
# Execution
# -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, sock=None,
workers=1, loop=None):
2016-10-15 20:59:00 +01:00
"""
2016-10-16 14:01:59 +01:00
Runs the HTTP Server and listens until keyboard interrupt or term
signal. On termination, drains connections before closing.
2016-10-15 20:59:00 +01:00
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param before_start: Function to be executed before the server starts
accepting connections
2016-10-16 14:01:59 +01:00
:param after_start: Function to be executed after the server starts
accepting connections
2016-10-16 14:01:59 +01:00
:param before_stop: Function to be executed when a stop signal is
received before it is respected
:param after_stop: Function to be executed when all requests are
complete
2016-10-18 09:22:49 +01:00
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param loop: asyncio compatible event loop
2016-10-15 20:59:00 +01:00
:return: Nothing
"""
self.error_handler.debug = True
self.debug = debug
self.loop = loop
2016-10-15 20:59:00 +01:00
2016-10-18 09:22:49 +01:00
server_settings = {
'host': host,
'port': port,
'sock': sock,
'debug': debug,
'request_handler': self.handle_request,
'error_handler': self.error_handler,
2016-10-18 09:22:49 +01:00
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop
2016-10-18 09:22:49 +01:00
}
# -------------------------------------------- #
# Register start/stop events
# -------------------------------------------- #
for event_name, settings_name, args, reverse in (
("before_server_start", "before_start", before_start, False),
("after_server_start", "after_start", after_start, False),
("before_server_stop", "before_stop", before_stop, True),
("after_server_stop", "after_stop", after_stop, True),
):
listeners = []
for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name]
if args:
if type(args) is not list:
args = [args]
listeners += args
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners
2016-10-15 20:59:00 +01:00
if debug:
log.setLevel(logging.DEBUG)
log.debug(self.config.LOGO)
# Serve
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
try:
2016-10-18 09:22:49 +01:00
if workers == 1:
serve(**server_settings)
else:
log.info('Spinning up {} workers...'.format(workers))
self.serve_multiple(server_settings, workers)
2016-10-16 14:01:59 +01:00
except Exception as e:
log.exception(
2016-11-19 07:16:20 +00:00
'Experienced exception while trying to serve')
2016-10-15 20:59:00 +01:00
2016-10-18 09:22:49 +01:00
log.info("Server Stopped")
2016-10-15 20:59:00 +01:00
def stop(self):
"""
This kills the Sanic
"""
2016-10-18 09:22:49 +01:00
get_event_loop().stop()
@staticmethod
def serve_multiple(server_settings, workers, stop_event=None):
"""
Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete.
:param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch
:param stop_event: if provided, is used as a stop signal
:return:
"""
server_settings['reuse_port'] = True
# Create a stop event to be triggered by a signal
if not stop_event:
stop_event = Event()
signal(SIGINT, lambda s, f: stop_event.set())
signal(SIGTERM, lambda s, f: stop_event.set())
processes = []
for _ in range(workers):
2016-10-18 09:22:49 +01:00
process = Process(target=serve, kwargs=server_settings)
process.start()
processes.append(process)
# Infinitely wait for the stop event
try:
while not stop_event.is_set():
sleep(0.3)
except:
pass
log.info('Spinning down workers...')
for process in processes:
process.terminate()
for process in processes:
process.join()