Introduce end-of-line normalization

This commit is contained in:
Michael Lavers 2016-10-15 12:59:00 -07:00
parent 1d519ff407
commit 9f824b048e
13 changed files with 912 additions and 911 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

6
.gitignore vendored
View File

@ -1,4 +1,4 @@
settings.py settings.py
*.pyc *.pyc
.idea/* .idea/*
.cache/* .cache/*

View File

@ -1,3 +1,3 @@
httptools httptools
ujson ujson
uvloop uvloop

View File

@ -1,24 +1,24 @@
class Config: class Config:
LOGO = """ LOGO = """
_______________ _______________
/ \\ / \\
| Gotta go fast! | | Gotta go fast! |
| _________________/ | _________________/
|/ |/
""" """
REQUEST_MAX_SIZE = 100000000 # 100 megababies REQUEST_MAX_SIZE = 100000000 # 100 megababies
REQUEST_TIMEOUT = 60 # 60 seconds REQUEST_TIMEOUT = 60 # 60 seconds

View File

@ -1,4 +1,4 @@
import logging import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -1,143 +1,143 @@
from cgi import parse_header from cgi import parse_header
from collections import namedtuple from collections import namedtuple
from httptools import parse_url from httptools import parse_url
from urllib.parse import parse_qs from urllib.parse import parse_qs
from ujson import loads as json_loads from ujson import loads as json_loads
from .log import log from .log import log
class RequestParameters(dict): class RequestParameters(dict):
""" """
Hosts a dict with lists as values where get returns the first Hosts a dict with lists as values where get returns the first
value of the list and getlist returns the whole shebang value of the list and getlist returns the whole shebang
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.super = super() self.super = super()
self.super.__init__(*args, **kwargs) self.super.__init__(*args, **kwargs)
def get(self, name, default=None): def get(self, name, default=None):
values = self.super.get(name) values = self.super.get(name)
return values[0] if values else default return values[0] if values else default
def getlist(self, name, default=None): def getlist(self, name, default=None):
return self.super.get(name, default) return self.super.get(name, default)
class Request: class Request:
__slots__ = ( __slots__ = (
'url', 'headers', 'version', 'method', 'url', 'headers', 'version', 'method',
'query_string', 'body', 'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
) )
def __init__(self, url_bytes, headers, version, method): def __init__(self, url_bytes, headers, version, method):
# TODO: Content-Encoding detection # TODO: Content-Encoding detection
url_parsed = parse_url(url_bytes) url_parsed = parse_url(url_bytes)
self.url = url_parsed.path.decode('utf-8') self.url = url_parsed.path.decode('utf-8')
self.headers = headers self.headers = headers
self.version = version self.version = version
self.method = method self.method = method
self.query_string = url_parsed.query.decode('utf-8') if url_parsed.query else None self.query_string = url_parsed.query.decode('utf-8') if url_parsed.query else None
# Init but do not inhale # Init but do not inhale
self.body = None self.body = None
self.parsed_json = None self.parsed_json = None
self.parsed_form = None self.parsed_form = None
self.parsed_files = None self.parsed_files = None
self.parsed_args = None self.parsed_args = None
@property @property
def json(self): def json(self):
if not self.parsed_json: if not self.parsed_json:
try: try:
self.parsed_json = json_loads(self.body) self.parsed_json = json_loads(self.body)
except: except:
pass pass
return self.parsed_json return self.parsed_json
@property @property
def form(self): def form(self):
if self.parsed_form is None: if self.parsed_form is None:
self.parsed_form = {} self.parsed_form = {}
self.parsed_files = {} self.parsed_files = {}
content_type, parameters = parse_header(self.headers.get('Content-Type')) content_type, parameters = parse_header(self.headers.get('Content-Type'))
try: try:
if content_type is None or content_type == 'application/x-www-form-urlencoded': if content_type is None or content_type == 'application/x-www-form-urlencoded':
self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8'))) self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8')))
elif content_type == 'multipart/form-data': elif content_type == 'multipart/form-data':
# TODO: Stream this instead of reading to/from memory # TODO: Stream this instead of reading to/from memory
boundary = parameters['boundary'].encode('utf-8') boundary = parameters['boundary'].encode('utf-8')
self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary) self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary)
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
pass pass
return self.parsed_form return self.parsed_form
@property @property
def files(self): def files(self):
if self.parsed_files is None: if self.parsed_files is None:
_ = self.form # compute form to get files _ = self.form # compute form to get files
return self.parsed_files return self.parsed_files
@property @property
def args(self): def args(self):
if self.parsed_args is None: if self.parsed_args is None:
if self.query_string: if self.query_string:
self.parsed_args = RequestParameters(parse_qs(self.query_string)) self.parsed_args = RequestParameters(parse_qs(self.query_string))
else: else:
self.parsed_args = {} self.parsed_args = {}
return self.parsed_args return self.parsed_args
File = namedtuple('File', ['type', 'body', 'name']) File = namedtuple('File', ['type', 'body', 'name'])
def parse_multipart_form(body, boundary): def parse_multipart_form(body, boundary):
""" """
Parses a request body and returns fields and files Parses a request body and returns fields and files
:param body: Bytes request body :param body: Bytes request body
:param boundary: Bytes multipart boundary :param boundary: Bytes multipart boundary
:return: fields (dict), files (dict) :return: fields (dict), files (dict)
""" """
files = {} files = {}
fields = {} fields = {}
form_parts = body.split(boundary) form_parts = body.split(boundary)
for form_part in form_parts[1:-1]: for form_part in form_parts[1:-1]:
file_name = None file_name = None
file_type = None file_type = None
field_name = None field_name = None
line_index = 2 line_index = 2
line_end_index = 0 line_end_index = 0
while not line_end_index == -1: while not line_end_index == -1:
line_end_index = form_part.find(b'\r\n', line_index) line_end_index = form_part.find(b'\r\n', line_index)
form_line = form_part[line_index:line_end_index].decode('utf-8') form_line = form_part[line_index:line_end_index].decode('utf-8')
line_index = line_end_index + 2 line_index = line_end_index + 2
if not form_line: if not form_line:
break break
colon_index = form_line.index(':') colon_index = form_line.index(':')
form_header_field = form_line[0:colon_index] form_header_field = form_line[0:colon_index]
form_header_value, form_parameters = parse_header(form_line[colon_index + 2:]) form_header_value, form_parameters = parse_header(form_line[colon_index + 2:])
if form_header_field == 'Content-Disposition': if form_header_field == 'Content-Disposition':
if 'filename' in form_parameters: if 'filename' in form_parameters:
file_name = form_parameters['filename'] file_name = form_parameters['filename']
field_name = form_parameters.get('name') field_name = form_parameters.get('name')
elif form_header_field == 'Content-Type': elif form_header_field == 'Content-Type':
file_type = form_header_value file_type = form_header_value
post_data = form_part[line_index:-4] post_data = form_part[line_index:-4]
if file_name or file_type: if file_name or file_type:
files[field_name] = File(type=file_type, name=file_name, body=post_data) files[field_name] = File(type=file_type, name=file_name, body=post_data)
else: else:
fields[field_name] = post_data.decode('utf-8') fields[field_name] = post_data.decode('utf-8')
return fields, files return fields, files

View File

@ -1,68 +1,68 @@
import ujson import ujson
import httptools import httptools
from ujson import loads as json_loads from ujson import loads as json_loads
from urllib.parse import parse_qs from urllib.parse import parse_qs
STATUS_CODES = { STATUS_CODES = {
200: 'OK', 200: 'OK',
400: 'Bad Request', 400: 'Bad Request',
401: 'Unauthorized', 401: 'Unauthorized',
402: 'Payment Required', 402: 'Payment Required',
403: 'Forbidden', 403: 'Forbidden',
404: 'Not Found', 404: 'Not Found',
405: 'Method Not Allowed', 405: 'Method Not Allowed',
500: 'Internal Server Error', 500: 'Internal Server Error',
501: 'Not Implemented', 501: 'Not Implemented',
502: 'Bad Gateway', 502: 'Bad Gateway',
503: 'Service Unavailable', 503: 'Service Unavailable',
504: 'Gateway Timeout', 504: 'Gateway Timeout',
} }
class HTTPResponse: class HTTPResponse:
__slots__ = ('body', 'status', 'content_type', 'headers') __slots__ = ('body', 'status', 'content_type', 'headers')
def __init__(self, body=None, status=200, headers=[], content_type='text/plain', body_bytes=b''): def __init__(self, body=None, status=200, headers=[], content_type='text/plain', body_bytes=b''):
self.content_type = content_type self.content_type = content_type
if not body is None: if not body is None:
self.body = body.encode('utf-8') self.body = body.encode('utf-8')
else: else:
self.body = body_bytes self.body = body_bytes
self.status = status self.status = status
self.headers = headers self.headers = headers
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
# This is all returned in a kind-of funky way # This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python # We tried to make this as fast as possible in pure python
additional_headers = [] additional_headers = []
if keep_alive and not keep_alive_timeout is None: if keep_alive and not keep_alive_timeout is None:
additional_headers = [b'Keep-Alive: timeout=', str(keep_alive_timeout).encode(), b's\r\n'] additional_headers = [b'Keep-Alive: timeout=', str(keep_alive_timeout).encode(), b's\r\n']
if self.headers: if self.headers:
for name, value in self.headers.items(): for name, value in self.headers.items():
additional_headers.append('{}: {}\r\n'.format(name, value).encode('utf-8')) additional_headers.append('{}: {}\r\n'.format(name, value).encode('utf-8'))
return b''.join([ return b''.join([
'HTTP/{} {} {}\r\n'.format(version, self.status, 'HTTP/{} {} {}\r\n'.format(version, self.status,
STATUS_CODES.get(self.status, 'FAIL')).encode(), STATUS_CODES.get(self.status, 'FAIL')).encode(),
b'Content-Type: ', self.content_type.encode(), b'\r\n', b'Content-Type: ', self.content_type.encode(), b'\r\n',
b'Content-Length: ', str(len(self.body)).encode(), b'\r\n', b'Content-Length: ', str(len(self.body)).encode(), b'\r\n',
b'Connection: ', ('keep-alive' if keep_alive else 'close').encode(), b'\r\n', b'Connection: ', ('keep-alive' if keep_alive else 'close').encode(), b'\r\n',
] + additional_headers + [ ] + additional_headers + [
b'\r\n', b'\r\n',
self.body, self.body,
]) ])
def json(body, status=200, headers=None): def json(body, status=200, headers=None):
return HTTPResponse(ujson.dumps(body), headers=headers, status=status, return HTTPResponse(ujson.dumps(body), headers=headers, status=status,
content_type="application/json; charset=utf-8") content_type="application/json; charset=utf-8")
def text(body, status=200, headers=None): def text(body, status=200, headers=None):
return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8") return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8")
def html(body, status=200, headers=None): def html(body, status=200, headers=None):
return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8") return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8")

View File

@ -1,128 +1,128 @@
import re import re
from collections import namedtuple from collections import namedtuple
from .exceptions import NotFound, InvalidUsage from .exceptions import NotFound, InvalidUsage
Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters'])
Parameter = namedtuple("Parameter", ['name', 'cast']) Parameter = namedtuple("Parameter", ['name', 'cast'])
class Router: class Router:
""" """
Router supports basic routing with parameters and method checks Router supports basic routing with parameters and method checks
Usage: Usage:
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...]) @sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
def my_route(request, my_parameter): def my_route(request, my_parameter):
do stuff... do stuff...
Parameters will be passed as keyword arguments to the request handling function provided Parameters will be passed as keyword arguments to the request handling function provided
Parameters can also have a type by appending :type to the <parameter>. If no type is provided, Parameters can also have a type by appending :type to the <parameter>. If no type is provided,
a string is expected. A regular expression can also be passed in as the type a string is expected. A regular expression can also be passed in as the type
TODO: TODO:
This probably needs optimization for larger sets of routes, This probably needs optimization for larger sets of routes,
since it checks every route until it finds a match which is bad and I should feel bad since it checks every route until it finds a match which is bad and I should feel bad
""" """
routes = None routes = None
regex_types = { regex_types = {
"string": (None, "\w+"), "string": (None, "\w+"),
"int": (int, "\d+"), "int": (int, "\d+"),
"number": (float, "[0-9\\.]+"), "number": (float, "[0-9\\.]+"),
"alpha": (None, "[A-Za-z]+"), "alpha": (None, "[A-Za-z]+"),
} }
def __init__(self): def __init__(self):
self.routes = [] self.routes = []
def add(self, uri, methods, handler): def add(self, uri, methods, handler):
""" """
Adds a handler to the route list Adds a handler to the route list
:param uri: Path to match :param uri: Path to match
:param methods: Array of accepted method names. If none are provided, any method is allowed :param methods: Array 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 handler: Request handler function. When executed, it should provide a response object.
:return: Nothing :return: Nothing
""" """
# Dict for faster lookups of if method allowed # Dict for faster lookups of if method allowed
methods_dict = {method: True for method in methods} if methods else None methods_dict = {method: True for method in methods} if methods else None
parameters = [] parameters = []
def add_parameter(match): def add_parameter(match):
# We could receive NAME or NAME:PATTERN # We could receive NAME or NAME:PATTERN
parts = match.group(1).split(':') parts = match.group(1).split(':')
if len(parts) == 2: if len(parts) == 2:
parameter_name, parameter_pattern = parts parameter_name, parameter_pattern = parts
else: else:
parameter_name = parts[0] parameter_name = parts[0]
parameter_pattern = 'string' parameter_pattern = 'string'
# Pull from pre-configured types # Pull from pre-configured types
parameter_regex = self.regex_types.get(parameter_pattern) parameter_regex = self.regex_types.get(parameter_pattern)
if parameter_regex: if parameter_regex:
parameter_type, parameter_pattern = parameter_regex parameter_type, parameter_pattern = parameter_regex
else: else:
parameter_type = None parameter_type = None
parameter = Parameter(name=parameter_name, cast=parameter_type) parameter = Parameter(name=parameter_name, cast=parameter_type)
parameters.append(parameter) parameters.append(parameter)
return "({})".format(parameter_pattern) return "({})".format(parameter_pattern)
pattern_string = re.sub("<(.+?)>", add_parameter, uri) pattern_string = re.sub("<(.+?)>", add_parameter, uri)
pattern = re.compile("^{}$".format(pattern_string)) pattern = re.compile("^{}$".format(pattern_string))
route = Route(handler=handler, methods=methods_dict, pattern=pattern, parameters=parameters) route = Route(handler=handler, methods=methods_dict, pattern=pattern, parameters=parameters)
self.routes.append(route) self.routes.append(route)
def get(self, request): def get(self, request):
""" """
Gets a request handler based on the URL of the request, or raises an error Gets a request handler based on the URL of the request, or raises an error
:param request: Request object :param request: Request object
:return: handler, arguments, keyword arguments :return: handler, arguments, keyword arguments
""" """
route = None route = None
args = [] args = []
kwargs = {} kwargs = {}
for _route in self.routes: for _route in self.routes:
match = _route.pattern.match(request.url) match = _route.pattern.match(request.url)
if match: if match:
for index, parameter in enumerate(_route.parameters, start=1): for index, parameter in enumerate(_route.parameters, start=1):
value = match.group(index) value = match.group(index)
kwargs[parameter.name] = parameter.cast(value) if parameter.cast is not None else value kwargs[parameter.name] = parameter.cast(value) if parameter.cast is not None else value
route = _route route = _route
break break
if route: if route:
if route.methods and not request.method in route.methods: if route.methods and not request.method in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
status_code=405) status_code=405)
return route.handler, args, kwargs return route.handler, args, kwargs
else: else:
raise NotFound("Requested URL {} not found".format(request.url)) raise NotFound("Requested URL {} not found".format(request.url))
class SimpleRouter: class SimpleRouter:
""" """
Simple router records and reads all routes from a dictionary Simple router records and reads all routes from a dictionary
It does not support parameters in routes, but is very fast It does not support parameters in routes, but is very fast
""" """
routes = None routes = None
def __init__(self): def __init__(self):
self.routes = {} self.routes = {}
def add(self, uri, methods, handler): def add(self, uri, methods, handler):
# Dict for faster lookups of method allowed # Dict for faster lookups of method allowed
methods_dict = {method: True for method in methods} if methods else None methods_dict = {method: True for method in methods} if methods else None
self.routes[uri] = Route(handler=handler, methods=methods_dict, pattern=uri, parameters=None) self.routes[uri] = Route(handler=handler, methods=methods_dict, pattern=uri, parameters=None)
def get(self, request): def get(self, request):
route = self.routes.get(request.url) route = self.routes.get(request.url)
if route: if route:
if route.methods and not request.method in route.methods: if route.methods and not request.method in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
status_code=405) status_code=405)
return route.handler, [], {} return route.handler, [], {}
else: else:
raise NotFound("Requested URL {} not found".format(request.url)) raise NotFound("Requested URL {} not found".format(request.url))

View File

@ -1,191 +1,191 @@
import asyncio import asyncio
from inspect import isawaitable from inspect import isawaitable
from traceback import format_exc from traceback import format_exc
from types import FunctionType from types import FunctionType
from .config import Config from .config import Config
from .exceptions import Handler from .exceptions import Handler
from .log import log, logging from .log import log, logging
from .middleware import Middleware from .middleware import Middleware
from .response import HTTPResponse from .response import HTTPResponse
from .router import Router from .router import Router
from .server import serve from .server import serve
from .exceptions import ServerError from .exceptions import ServerError
class Sanic: class Sanic:
def __init__(self, name, router=None, error_handler=None): def __init__(self, name, router=None, error_handler=None):
self.name = name self.name = name
self.router = router or Router() self.router = router or Router()
self.router = router or Router() self.router = router or Router()
self.error_handler = error_handler or Handler(self) self.error_handler = error_handler or Handler(self)
self.config = Config() self.config = Config()
self.request_middleware = [] self.request_middleware = []
self.response_middleware = [] self.response_middleware = []
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Registration # Registration
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Decorator # Decorator
def route(self, uri, methods=None): def route(self, uri, methods=None):
""" """
Decorates a function to be registered as a route Decorates a function to be registered as a route
:param uri: path of the URL :param uri: path of the URL
:param methods: list or tuple of methods allowed :param methods: list or tuple of methods allowed
:return: decorated function :return: decorated function
""" """
def response(handler): def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler) self.router.add(uri=uri, methods=methods, handler=handler)
return handler return handler
return response return response
# Decorator # Decorator
def exception(self, *exceptions): def exception(self, *exceptions):
""" """
Decorates a function to be registered as a route Decorates a function to be registered as a route
:param uri: path of the URL :param uri: path of the URL
:param methods: list or tuple of methods allowed :param methods: list or tuple of methods allowed
:return: decorated function :return: decorated function
""" """
def response(handler): def response(handler):
for exception in exceptions: for exception in exceptions:
self.error_handler.add(exception, handler) self.error_handler.add(exception, handler)
return handler return handler
return response return response
# Decorator # Decorator
def middleware(self, *args, **kwargs): def middleware(self, *args, **kwargs):
""" """
Decorates and registers middleware to be called before a request Decorates and registers middleware to be called before a request
can either be called as @app.middleware or @app.middleware('request') can either be called as @app.middleware or @app.middleware('request')
""" """
middleware = None middleware = None
attach_to = 'request' attach_to = 'request'
def register_middleware(middleware): def register_middleware(middleware):
if attach_to == 'request': if attach_to == 'request':
self.request_middleware.append(middleware) self.request_middleware.append(middleware)
if attach_to == 'response': if attach_to == 'response':
self.response_middleware.append(middleware) self.response_middleware.append(middleware)
return middleware return middleware
# Detect which way this was called, @middleware or @middleware('AT') # Detect which way this was called, @middleware or @middleware('AT')
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
return register_middleware(args[0]) return register_middleware(args[0])
else: else:
attach_to = args[0] attach_to = args[0]
return register_middleware return register_middleware
if isinstance(middleware, FunctionType): if isinstance(middleware, FunctionType):
middleware = Middleware(process_request=middleware) middleware = Middleware(process_request=middleware)
return middleware return middleware
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Request Handling # Request Handling
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
async def handle_request(self, request, response_callback): async def handle_request(self, request, response_callback):
""" """
Takes a request from the HTTP Server and returns a response object to be sent back 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 The HTTP Server only expects a response object, so exception handling must be done here
:param request: HTTP Request object :param request: HTTP Request object
:param response_callback: Response function to be called with the response as the only argument :param response_callback: Response function to be called with the response as the only argument
:return: Nothing :return: Nothing
""" """
try: try:
# Middleware process_request # Middleware process_request
response = False response = False
# The if improves speed. I don't know why # The if improves speed. I don't know why
if self.request_middleware: if self.request_middleware:
for middleware in self.request_middleware: for middleware in self.request_middleware:
response = middleware(request) response = middleware(request)
if isawaitable(response): if isawaitable(response):
response = await response response = await response
if response: if response:
break break
# No middleware results # No middleware results
if not response: if not response:
# Fetch handler from router # Fetch handler from router
handler, args, kwargs = self.router.get(request) handler, args, kwargs = self.router.get(request)
if handler is None: if handler is None:
raise ServerError("'None' was returned while requesting a handler from the router") raise ServerError("'None' was returned while requesting a handler from the router")
# Run response handler # Run response handler
response = handler(request, *args, **kwargs) response = handler(request, *args, **kwargs)
if isawaitable(response): if isawaitable(response):
response = await response response = await response
# Middleware process_response # Middleware process_response
if self.response_middleware: if self.response_middleware:
for middleware in self.response_middleware: for middleware in self.response_middleware:
_response = middleware(request, response) _response = middleware(request, response)
if isawaitable(_response): if isawaitable(_response):
_response = await _response _response = await _response
if _response: if _response:
response = _response response = _response
break break
except Exception as e: except Exception as e:
try: try:
response = self.error_handler.response(request, e) response = self.error_handler.response(request, e)
if isawaitable(response): if isawaitable(response):
response = await response response = await response
except Exception as e: except Exception as e:
if self.debug: if self.debug:
response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc())) response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc()))
else: else:
response = HTTPResponse("An error occured while handling an error") response = HTTPResponse("An error occured while handling an error")
response_callback(response) response_callback(response)
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Execution # Execution
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, before_stop=None): def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, before_stop=None):
""" """
Runs the HTTP Server and listens until keyboard interrupt or term signal. Runs the HTTP Server and listens until keyboard interrupt or term signal.
On termination, drains connections before closing. On termination, drains connections before closing.
:param host: Address to host on :param host: Address to host on
:param port: Port to host on :param port: Port to host on
:param debug: Enables debug output (slows server) :param debug: Enables debug output (slows server)
:param after_start: Function to be executed after the server starts listening :param after_start: Function to be executed after the server starts listening
:param before_stop: Function to be executed when a stop signal is received before it is respected :param before_stop: Function to be executed when a stop signal is received before it is respected
:return: Nothing :return: Nothing
""" """
self.error_handler.debug = True self.error_handler.debug = True
self.debug = debug self.debug = debug
if debug: if debug:
log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG)
log.debug(self.config.LOGO) log.debug(self.config.LOGO)
# Serve # Serve
log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
try: try:
serve( serve(
host=host, host=host,
port=port, port=port,
debug=debug, debug=debug,
after_start=after_start, after_start=after_start,
before_stop=before_stop, before_stop=before_stop,
request_handler=self.handle_request, request_handler=self.handle_request,
request_timeout=self.config.REQUEST_TIMEOUT, request_timeout=self.config.REQUEST_TIMEOUT,
request_max_size=self.config.REQUEST_MAX_SIZE, request_max_size=self.config.REQUEST_MAX_SIZE,
) )
except: except:
pass pass
def stop(self): def stop(self):
""" """
This kills the Sanic This kills the Sanic
""" """
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()

View File

@ -1,205 +1,205 @@
import asyncio import asyncio
from inspect import isawaitable from inspect import isawaitable
from signal import SIGINT, SIGTERM from signal import SIGINT, SIGTERM
import httptools import httptools
try: try:
import uvloop as async_loop import uvloop as async_loop
except: except:
async_loop = asyncio async_loop = asyncio
from .log import log from .log import log
from .request import Request from .request import Request
class Signal: class Signal:
stopped = False stopped = False
class HttpProtocol(asyncio.Protocol): class HttpProtocol(asyncio.Protocol):
__slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection __slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection
'parser', 'request', 'url', 'headers', # request params 'parser', 'request', 'url', 'headers', # request params
'request_handler', 'request_timeout', 'request_max_size', # request config 'request_handler', 'request_timeout', 'request_max_size', # request config
'_total_request_size', '_timeout_handler') # connection management '_total_request_size', '_timeout_handler') # connection management
def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60, def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60,
request_max_size=None): request_max_size=None):
self.loop = loop self.loop = loop
self.transport = None self.transport = None
self.request = None self.request = None
self.parser = None self.parser = None
self.url = None self.url = None
self.headers = None self.headers = None
self.signal = signal self.signal = signal
self.connections = connections self.connections = connections
self.request_handler = request_handler self.request_handler = request_handler
self.request_timeout = request_timeout self.request_timeout = request_timeout
self.request_max_size = request_max_size self.request_max_size = request_max_size
self._total_request_size = 0 self._total_request_size = 0
self._timeout_handler = None self._timeout_handler = None
# -------------------------------------------- # # -------------------------------------------- #
# Connection # Connection
# -------------------------------------------- # # -------------------------------------------- #
def connection_made(self, transport): def connection_made(self, transport):
self.connections[self] = True self.connections[self] = True
self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout) self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout)
self.transport = transport self.transport = transport
def connection_lost(self, exc): def connection_lost(self, exc):
del self.connections[self] del self.connections[self]
self._timeout_handler.cancel() self._timeout_handler.cancel()
self.cleanup() self.cleanup()
def connection_timeout(self): def connection_timeout(self):
self.bail_out("Request timed out, connection closed") self.bail_out("Request timed out, connection closed")
# -------------------------------------------- # # -------------------------------------------- #
# Parsing # Parsing
# -------------------------------------------- # # -------------------------------------------- #
def data_received(self, data): def data_received(self, data):
# Check for the request itself getting too large and exceeding memory limits # Check for the request itself getting too large and exceeding memory limits
self._total_request_size += len(data) self._total_request_size += len(data)
if self._total_request_size > self.request_max_size: if self._total_request_size > self.request_max_size:
return self.bail_out("Request too large ({}), connection closed".format(self._total_request_size)) return self.bail_out("Request too large ({}), connection closed".format(self._total_request_size))
# Create parser if this is the first time we're receiving data # Create parser if this is the first time we're receiving data
if self.parser is None: if self.parser is None:
assert self.request is None assert self.request is None
self.headers = [] self.headers = []
self.parser = httptools.HttpRequestParser(self) self.parser = httptools.HttpRequestParser(self)
# Parse request chunk or close connection # Parse request chunk or close connection
try: try:
self.parser.feed_data(data) self.parser.feed_data(data)
except httptools.parser.errors.HttpParserError as e: except httptools.parser.errors.HttpParserError as e:
self.bail_out("Invalid request data, connection closed ({})".format(e)) self.bail_out("Invalid request data, connection closed ({})".format(e))
def on_url(self, url): def on_url(self, url):
self.url = url self.url = url
def on_header(self, name, value): def on_header(self, name, value):
if name == b'Content-Length' and int(value) > self.request_max_size: if name == b'Content-Length' and int(value) > self.request_max_size:
return self.bail_out("Request body too large ({}), connection closed".format(value)) return self.bail_out("Request body too large ({}), connection closed".format(value))
self.headers.append((name.decode(), value.decode('utf-8'))) self.headers.append((name.decode(), value.decode('utf-8')))
def on_headers_complete(self): def on_headers_complete(self):
self.request = Request( self.request = Request(
url_bytes=self.url, url_bytes=self.url,
headers=dict(self.headers), headers=dict(self.headers),
version=self.parser.get_http_version(), version=self.parser.get_http_version(),
method=self.parser.get_method().decode() method=self.parser.get_method().decode()
) )
def on_body(self, body): def on_body(self, body):
self.request.body = body self.request.body = body
def on_message_complete(self): def on_message_complete(self):
self.loop.create_task(self.request_handler(self.request, self.write_response)) self.loop.create_task(self.request_handler(self.request, self.write_response))
# -------------------------------------------- # # -------------------------------------------- #
# Responding # Responding
# -------------------------------------------- # # -------------------------------------------- #
def write_response(self, response): def write_response(self, response):
try: try:
keep_alive = self.parser.should_keep_alive() and not self.signal.stopped keep_alive = self.parser.should_keep_alive() and not self.signal.stopped
self.transport.write(response.output(self.request.version, keep_alive, self.request_timeout)) self.transport.write(response.output(self.request.version, keep_alive, self.request_timeout))
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
else: else:
self.cleanup() self.cleanup()
except Exception as e: except Exception as e:
self.bail_out("Writing request failed, connection closed {}".format(e)) self.bail_out("Writing request failed, connection closed {}".format(e))
def bail_out(self, message): def bail_out(self, message):
log.error(message) log.error(message)
self.transport.close() self.transport.close()
def cleanup(self): def cleanup(self):
self.parser = None self.parser = None
self.request = None self.request = None
self.url = None self.url = None
self.headers = None self.headers = None
self._total_request_size = 0 self._total_request_size = 0
def close_if_idle(self): def close_if_idle(self):
""" """
Close the connection if a request is not being sent or received Close the connection if a request is not being sent or received
:return: boolean - True if closed, false if staying open :return: boolean - True if closed, false if staying open
""" """
if not self.parser: if not self.parser:
self.transport.close() self.transport.close()
return True return True
return False return False
def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60, def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60,
request_max_size=None): request_max_size=None):
# Create Event Loop # Create Event Loop
loop = async_loop.new_event_loop() loop = async_loop.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
# I don't think we take advantage of this # I don't think we take advantage of this
# And it slows everything waaayyy down # And it slows everything waaayyy down
# loop.set_debug(debug) # loop.set_debug(debug)
connections = {} connections = {}
signal = Signal() signal = Signal()
server_coroutine = loop.create_server(lambda: HttpProtocol( server_coroutine = loop.create_server(lambda: HttpProtocol(
loop=loop, loop=loop,
connections=connections, connections=connections,
signal=signal, signal=signal,
request_handler=request_handler, request_handler=request_handler,
request_timeout=request_timeout, request_timeout=request_timeout,
request_max_size=request_max_size, request_max_size=request_max_size,
), host, port) ), host, port)
try: try:
http_server = loop.run_until_complete(server_coroutine) http_server = loop.run_until_complete(server_coroutine)
except OSError as e: except OSError as e:
log.error("Unable to start server: {}".format(e)) log.error("Unable to start server: {}".format(e))
return return
except: except:
log.exception("Unable to start server") log.exception("Unable to start server")
return return
# Run the on_start function if provided # Run the on_start function if provided
if after_start: if after_start:
result = after_start(loop) result = after_start(loop)
if isawaitable(result): if isawaitable(result):
loop.run_until_complete(result) loop.run_until_complete(result)
# Register signals for graceful termination # Register signals for graceful termination
for _signal in (SIGINT, SIGTERM): for _signal in (SIGINT, SIGTERM):
loop.add_signal_handler(_signal, loop.stop) loop.add_signal_handler(_signal, loop.stop)
try: try:
loop.run_forever() loop.run_forever()
finally: finally:
log.info("Stop requested, draining connections...") log.info("Stop requested, draining connections...")
# Run the on_stop function if provided # Run the on_stop function if provided
if before_stop: if before_stop:
result = before_stop(loop) result = before_stop(loop)
if isawaitable(result): if isawaitable(result):
loop.run_until_complete(result) loop.run_until_complete(result)
# Wait for event loop to finish and all connections to drain # Wait for event loop to finish and all connections to drain
http_server.close() http_server.close()
loop.run_until_complete(http_server.wait_closed()) loop.run_until_complete(http_server.wait_closed())
# Complete all tasks on the loop # Complete all tasks on the loop
signal.stopped = True signal.stopped = True
for connection in connections.keys(): for connection in connections.keys():
connection.close_if_idle() connection.close_if_idle()
while connections: while connections:
loop.run_until_complete(asyncio.sleep(0.1)) loop.run_until_complete(asyncio.sleep(0.1))
loop.close() loop.close()
log.info("Server Stopped") log.info("Server Stopped")

View File

@ -1,33 +1,33 @@
import asyncpg import asyncpg
import sys import sys
import os import os
import inspect import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../') sys.path.insert(0, currentdir + '/../../../')
import timeit import timeit
from sanic.response import json from sanic.response import json
print(json({"test": True}).output()) print(json({"test": True}).output())
print("Running New 100,000 times") print("Running New 100,000 times")
times = 0 times = 0
total_time = 0 total_time = 0
for n in range(6): for n in range(6):
time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000) time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000)
print("Took {} seconds".format(time)) print("Took {} seconds".format(time))
total_time += time total_time += time
times += 1 times += 1
print("Average: {}".format(total_time / times)) print("Average: {}".format(total_time / times))
print("Running Old 100,000 times") print("Running Old 100,000 times")
times = 0 times = 0
total_time = 0 total_time = 0
for n in range(6): for n in range(6):
time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000) time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000)
print("Took {} seconds".format(time)) print("Took {} seconds".format(time))
total_time += time total_time += time
times += 1 times += 1
print("Average: {}".format(total_time / times)) print("Average: {}".format(total_time / times))

View File

@ -1,19 +1,19 @@
import sys import sys
import os import os
import inspect import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../') sys.path.insert(0, currentdir + '/../../../')
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
app = Sanic("test") app = Sanic("test")
@app.route("/") @app.route("/")
async def test(request): async def test(request):
return json({"test": True}) return json({"test": True})
app.run(host="0.0.0.0", port=sys.argv[1]) app.run(host="0.0.0.0", port=sys.argv[1])

View File

@ -1,91 +1,91 @@
import sys import sys
import os import os
import inspect import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../') sys.path.insert(0, currentdir + '/../../../')
from sanic import Sanic from sanic import Sanic
from sanic.response import json, text from sanic.response import json, text
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
app = Sanic("test") app = Sanic("test")
@app.route("/") @app.route("/")
async def test(request): async def test(request):
return json({"test": True}) return json({"test": True})
@app.route("/sync", methods=['GET', 'POST']) @app.route("/sync", methods=['GET', 'POST'])
def test(request): def test(request):
return json({"test": True}) return json({"test": True})
@app.route("/text/<name>/<butt:int>") @app.route("/text/<name>/<butt:int>")
def rtext(request, name, butt): def rtext(request, name, butt):
return text("yeehaww {} {}".format(name, butt)) return text("yeehaww {} {}".format(name, butt))
@app.route("/exception") @app.route("/exception")
def exception(request): def exception(request):
raise ServerError("yep") raise ServerError("yep")
@app.route("/exception/async") @app.route("/exception/async")
async def test(request): async def test(request):
raise ServerError("asunk") raise ServerError("asunk")
@app.route("/post_json") @app.route("/post_json")
def post_json(request): def post_json(request):
return json({"received": True, "message": request.json}) return json({"received": True, "message": request.json})
@app.route("/query_string") @app.route("/query_string")
def query_string(request): def query_string(request):
return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string}) return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
import sys import sys
app.run(host="0.0.0.0", port=sys.argv[1]) app.run(host="0.0.0.0", port=sys.argv[1])
# import asyncio_redis # import asyncio_redis
# import asyncpg # import asyncpg
# async def setup(sanic, loop): # async def setup(sanic, loop):
# sanic.conn = [] # sanic.conn = []
# sanic.redis = [] # sanic.redis = []
# for x in range(10): # for x in range(10):
# sanic.conn.append(await asyncpg.connect(user='postgres', password='zomgdev', database='postgres', host='192.168.99.100')) # sanic.conn.append(await asyncpg.connect(user='postgres', password='zomgdev', database='postgres', host='192.168.99.100'))
# for n in range(30): # for n in range(30):
# connection = await asyncio_redis.Connection.create(host='192.168.99.100', port=6379) # connection = await asyncio_redis.Connection.create(host='192.168.99.100', port=6379)
# sanic.redis.append(connection) # sanic.redis.append(connection)
# c=0 # c=0
# @app.route("/postgres") # @app.route("/postgres")
# async def postgres(request): # async def postgres(request):
# global c # global c
# values = await app.conn[c].fetch('''SELECT * FROM players''') # values = await app.conn[c].fetch('''SELECT * FROM players''')
# c += 1 # c += 1
# if c == 10: # if c == 10:
# c = 0 # c = 0
# return text("yep") # return text("yep")
# r=0 # r=0
# @app.route("/redis") # @app.route("/redis")
# async def redis(request): # async def redis(request):
# global r # global r
# try: # try:
# values = await app.redis[r].get('my_key') # values = await app.redis[r].get('my_key')
# except asyncio_redis.exceptions.ConnectionLostError: # except asyncio_redis.exceptions.ConnectionLostError:
# app.redis[r] = await asyncio_redis.Connection.create(host='127.0.0.1', port=6379) # app.redis[r] = await asyncio_redis.Connection.create(host='127.0.0.1', port=6379)
# values = await app.redis[r].get('my_key') # values = await app.redis[r].get('my_key')
# r += 1 # r += 1
# if r == 30: # if r == 30:
# r = 0 # r = 0
# return text(values) # return text(values)